1//go:build integration
2
3// todo: set up a test for dane, mta-sts, etc.
4
5package main
6
7import (
8 "crypto/tls"
9 "fmt"
10 "log/slog"
11 "net"
12 "os"
13 "os/exec"
14 "strings"
15 "testing"
16 "time"
17
18 "github.com/mjl-/mox/dns"
19 "github.com/mjl-/mox/imapclient"
20 "github.com/mjl-/mox/mlog"
21 "github.com/mjl-/mox/mox-"
22 "github.com/mjl-/mox/sasl"
23 "github.com/mjl-/mox/smtpclient"
24)
25
26func tcheck(t *testing.T, err error, errmsg string) {
27 if err != nil {
28 t.Helper()
29 t.Fatalf("%s: %s", errmsg, err)
30 }
31}
32
33func TestDeliver(t *testing.T) {
34 log := mlog.New("integration", nil)
35 mlog.Logfmt = true
36
37 hostname, err := os.Hostname()
38 tcheck(t, err, "hostname")
39 ourHostname, err := dns.ParseDomain(hostname)
40 tcheck(t, err, "parse hostname")
41
42 // Single update from IMAP IDLE.
43 type idleResponse struct {
44 untagged imapclient.Untagged
45 err error
46 }
47
48 // Deliver submits a message over submissions, and checks with imap idle if the
49 // message is received by the destination mail server.
50 deliver := func(checkTime bool, dialtls bool, imaphost, imapuser, imappassword string, send func()) {
51 t.Helper()
52
53 // Connect to IMAP, execute IDLE command, which will return on deliver message.
54 // TLS certificates work because the container has the CA certificates configured.
55 var imapconn net.Conn
56 var err error
57 if dialtls {
58 imapconn, err = tls.Dial("tcp", imaphost, nil)
59 } else {
60 imapconn, err = net.Dial("tcp", imaphost)
61 }
62 tcheck(t, err, "dial imap")
63 defer imapconn.Close()
64
65 imapc, err := imapclient.New(imapconn, false)
66 tcheck(t, err, "new imapclient")
67
68 _, _, err = imapc.Login(imapuser, imappassword)
69 tcheck(t, err, "imap login")
70
71 _, _, err = imapc.Select("Inbox")
72 tcheck(t, err, "imap select inbox")
73
74 err = imapc.Commandf("", "idle")
75 tcheck(t, err, "write imap idle command")
76
77 _, _, _, err = imapc.ReadContinuation()
78 tcheck(t, err, "read imap continuation")
79
80 idle := make(chan idleResponse)
81 go func() {
82 for {
83 untagged, err := imapc.ReadUntagged()
84 idle <- idleResponse{untagged, err}
85 if err != nil {
86 return
87 }
88 }
89 }()
90 defer func() {
91 err := imapc.Writelinef("done")
92 tcheck(t, err, "aborting idle")
93 }()
94
95 t0 := time.Now()
96 send()
97
98 // Wait for notification of delivery.
99 select {
100 case resp := <-idle:
101 tcheck(t, resp.err, "idle notification")
102 _, ok := resp.untagged.(imapclient.UntaggedExists)
103 if !ok {
104 t.Fatalf("got idle %#v, expected untagged exists", resp.untagged)
105 }
106 if d := time.Since(t0); checkTime && d < 1*time.Second {
107 t.Fatalf("delivery took %v, but should have taken at least 1 second, the first-time sender delay", d)
108 }
109 case <-time.After(30 * time.Second):
110 t.Fatalf("timeout after 5s waiting for IMAP IDLE notification of new message, should take about 1 second")
111 }
112 }
113
114 submit := func(dialtls bool, mailfrom, password, desthost, rcptto string) {
115 var conn net.Conn
116 var err error
117 if dialtls {
118 conn, err = tls.Dial("tcp", desthost, nil)
119 } else {
120 conn, err = net.Dial("tcp", desthost)
121 }
122 tcheck(t, err, "dial submission")
123 defer conn.Close()
124
125 msg := fmt.Sprintf(`From: <%s>
126To: <%s>
127Subject: test message
128
129This is the message.
130`, mailfrom, rcptto)
131 msg = strings.ReplaceAll(msg, "\n", "\r\n")
132 auth := func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
133 return sasl.NewClientPlain(mailfrom, password), nil
134 }
135 c, err := smtpclient.New(mox.Context, log.Logger, conn, smtpclient.TLSSkip, false, ourHostname, dns.Domain{ASCII: desthost}, smtpclient.Opts{Auth: auth})
136 tcheck(t, err, "smtp hello")
137 err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false, false)
138 tcheck(t, err, "deliver with smtp")
139 err = c.Close()
140 tcheck(t, err, "close smtpclient")
141 }
142
143 // Make sure moxacmepebble has a TLS certificate.
144 conn, err := tls.Dial("tcp", "moxacmepebble.mox1.example:465", nil)
145 tcheck(t, err, "dial submission")
146 defer conn.Close()
147
148 log.Print("submitting email to moxacmepebble, waiting for imap notification at moxmail2")
149 t0 := time.Now()
150 deliver(true, true, "moxmail2.mox2.example:993", "moxtest2@mox2.example", "accountpass4321", func() {
151 submit(true, "moxtest1@mox1.example", "accountpass1234", "moxacmepebble.mox1.example:465", "moxtest2@mox2.example")
152 })
153 log.Print("success", slog.Duration("duration", time.Since(t0)))
154
155 log.Print("submitting email to moxmail2, waiting for imap notification at moxacmepebble")
156 t0 = time.Now()
157 deliver(true, true, "moxacmepebble.mox1.example:993", "moxtest1@mox1.example", "accountpass1234", func() {
158 submit(true, "moxtest2@mox2.example", "accountpass4321", "moxmail2.mox2.example:465", "moxtest1@mox1.example")
159 })
160 log.Print("success", slog.Duration("duration", time.Since(t0)))
161
162 log.Print("submitting email to postfix, waiting for imap notification at moxacmepebble")
163 t0 = time.Now()
164 deliver(false, true, "moxacmepebble.mox1.example:993", "moxtest1@mox1.example", "accountpass1234", func() {
165 submit(true, "moxtest1@mox1.example", "accountpass1234", "moxacmepebble.mox1.example:465", "root@postfix.example")
166 })
167 log.Print("success", slog.Duration("duration", time.Since(t0)))
168
169 log.Print("submitting email to localserve")
170 t0 = time.Now()
171 deliver(false, false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
172 submit(false, "mox@localhost", "moxmoxmox", "localserve.mox1.example:1587", "moxtest1@mox1.example")
173 })
174 log.Print("success", slog.Duration("duration", time.Since(t0)))
175
176 log.Print("submitting email to localserve")
177 t0 = time.Now()
178 deliver(false, false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
179 cmd := exec.Command("go", "run", ".", "sendmail", "mox@localhost")
180 const msg = `Subject: test
181
182a message.
183`
184 cmd.Stdin = strings.NewReader(msg)
185 var out strings.Builder
186 cmd.Stdout = &out
187 err := cmd.Run()
188 log.Print("sendmail", slog.String("output", out.String()))
189 tcheck(t, err, "sendmail")
190 })
191 log.Print("success", slog.Any("duration", time.Since(t0)))
192}
193