1//go:build integration
2
3// todo: set up a test for dane, mta-sts, etc.
4
5package main
6
7import (
8 "bufio"
9 "crypto/tls"
10 "fmt"
11 "log/slog"
12 "net"
13 "net/http"
14 "os"
15 "os/exec"
16 "strings"
17 "testing"
18 "time"
19
20 "github.com/mjl-/mox/dns"
21 "github.com/mjl-/mox/imapclient"
22 "github.com/mjl-/mox/mlog"
23 "github.com/mjl-/mox/mox-"
24 "github.com/mjl-/mox/sasl"
25 "github.com/mjl-/mox/smtpclient"
26)
27
28func tcheck(t *testing.T, err error, errmsg string) {
29 if err != nil {
30 t.Helper()
31 t.Fatalf("%s: %s", errmsg, err)
32 }
33}
34
35func TestDeliver(t *testing.T) {
36 log := mlog.New("integration", nil)
37 mlog.Logfmt = true
38
39 hostname, err := os.Hostname()
40 tcheck(t, err, "hostname")
41 ourHostname, err := dns.ParseDomain(hostname)
42 tcheck(t, err, "parse hostname")
43
44 // Single update from IMAP IDLE.
45 type idleResponse struct {
46 untagged imapclient.Untagged
47 err error
48 }
49
50 // Deliver submits a message over submissions, and checks with imap idle if the
51 // message is received by the destination mail server.
52 deliver := func(checkTime bool, dialtls bool, imaphost, imapuser, imappassword string, send func()) {
53 t.Helper()
54
55 // Connect to IMAP, execute IDLE command, which will return on deliver message.
56 // TLS certificates work because the container has the CA certificates configured.
57 var imapconn net.Conn
58 var err error
59 if dialtls {
60 imapconn, err = tls.Dial("tcp", imaphost, nil)
61 } else {
62 imapconn, err = net.Dial("tcp", imaphost)
63 }
64 tcheck(t, err, "dial imap")
65 defer imapconn.Close()
66
67 opts := imapclient.Opts{
68 Logger: slog.Default().With("cid", mox.Cid()),
69 }
70 imapc, err := imapclient.New(imapconn, &opts)
71 tcheck(t, err, "new imapclient")
72
73 _, err = imapc.Login(imapuser, imappassword)
74 tcheck(t, err, "imap login")
75
76 _, err = imapc.Select("Inbox")
77 tcheck(t, err, "imap select inbox")
78
79 err = imapc.WriteCommandf("", "idle")
80 tcheck(t, err, "write imap idle command")
81
82 _, err = imapc.ReadContinuation()
83 tcheck(t, err, "read imap continuation")
84
85 idle := make(chan idleResponse)
86 go func() {
87 for {
88 untagged, err := imapc.ReadUntagged()
89 idle <- idleResponse{untagged, err}
90 if err != nil {
91 return
92 }
93 }
94 }()
95 defer func() {
96 err := imapc.Writelinef("done")
97 tcheck(t, err, "aborting idle")
98 }()
99
100 t0 := time.Now()
101 send()
102
103 // Wait for notification of delivery.
104 select {
105 case resp := <-idle:
106 tcheck(t, resp.err, "idle notification")
107 _, ok := resp.untagged.(imapclient.UntaggedExists)
108 if !ok {
109 t.Fatalf("got idle %#v, expected untagged exists", resp.untagged)
110 }
111 if d := time.Since(t0); checkTime && d < 1*time.Second {
112 t.Fatalf("delivery took %v, but should have taken at least 1 second, the first-time sender delay", d)
113 }
114 case <-time.After(30 * time.Second):
115 t.Fatalf("timeout after 5s waiting for IMAP IDLE notification of new message, should take about 1 second")
116 }
117 }
118
119 submit := func(dialtls bool, mailfrom, password, desthost, rcptto string) {
120 var conn net.Conn
121 var err error
122 if dialtls {
123 conn, err = tls.Dial("tcp", desthost, nil)
124 } else {
125 conn, err = net.Dial("tcp", desthost)
126 }
127 tcheck(t, err, "dial submission")
128 defer conn.Close()
129
130 msg := fmt.Sprintf(`From: <%s>
131To: <%s>
132Subject: test message
133
134This is the message.
135`, mailfrom, rcptto)
136 msg = strings.ReplaceAll(msg, "\n", "\r\n")
137 auth := func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
138 return sasl.NewClientPlain(mailfrom, password), nil
139 }
140 c, err := smtpclient.New(mox.Context, log.Logger, conn, smtpclient.TLSSkip, false, ourHostname, dns.Domain{ASCII: desthost}, smtpclient.Opts{Auth: auth})
141 tcheck(t, err, "smtp hello")
142 err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false, false)
143 tcheck(t, err, "deliver with smtp")
144 err = c.Close()
145 tcheck(t, err, "close smtpclient")
146 }
147
148 // Make sure moxacmepebble has a TLS certificate.
149 conn, err := tls.Dial("tcp", "moxacmepebble.mox1.example:465", nil)
150 tcheck(t, err, "dial submission")
151 defer conn.Close()
152
153 log.Print("submitting email to moxacmepebble, waiting for imap notification at moxmail2")
154 t0 := time.Now()
155 deliver(true, true, "moxmail2.mox2.example:993", "moxtest2@mox2.example", "accountpass4321", func() {
156 submit(true, "moxtest1@mox1.example", "accountpass1234", "moxacmepebble.mox1.example:465", "moxtest2@mox2.example")
157 })
158 log.Print("success", slog.Duration("duration", time.Since(t0)))
159
160 log.Print("submitting email to moxmail2, waiting for imap notification at moxacmepebble")
161 t0 = time.Now()
162 deliver(true, true, "moxacmepebble.mox1.example:993", "moxtest1@mox1.example", "accountpass1234", func() {
163 submit(true, "moxtest2@mox2.example", "accountpass4321", "moxmail2.mox2.example:465", "moxtest1@mox1.example")
164 })
165 log.Print("success", slog.Duration("duration", time.Since(t0)))
166
167 log.Print("submitting email to postfix, waiting for imap notification at moxacmepebble")
168 t0 = time.Now()
169 deliver(false, true, "moxacmepebble.mox1.example:993", "moxtest1@mox1.example", "accountpass1234", func() {
170 submit(true, "moxtest1@mox1.example", "accountpass1234", "moxacmepebble.mox1.example:465", "root@postfix.example")
171 })
172 log.Print("success", slog.Duration("duration", time.Since(t0)))
173
174 log.Print("submitting email to localserve")
175 t0 = time.Now()
176 deliver(false, false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
177 submit(false, "mox@localhost", "moxmoxmox", "localserve.mox1.example:1587", "moxtest1@mox1.example")
178 })
179 log.Print("success", slog.Duration("duration", time.Since(t0)))
180
181 log.Print("submitting email to localserve")
182 t0 = time.Now()
183 deliver(false, false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
184 cmd := exec.Command("go", "run", ".", "sendmail", "mox@localhost")
185 const msg = `Subject: test
186
187a message.
188`
189 cmd.Stdin = strings.NewReader(msg)
190 var out strings.Builder
191 cmd.Stdout = &out
192 err := cmd.Run()
193 log.Print("sendmail", slog.String("output", out.String()))
194 tcheck(t, err, "sendmail")
195 })
196 log.Print("success", slog.Any("duration", time.Since(t0)))
197}
198
199func expectReadAfter2s(t *testing.T, hostport string, nextproto string, expected string) {
200 tlsConfig := &tls.Config{
201 NextProtos: []string{
202 nextproto,
203 },
204 }
205
206 conn, err := tls.Dial("tcp", hostport, tlsConfig)
207 if err != nil {
208 t.Fatalf("error dialing moxacmepebblealpn 443 for %s: %v", nextproto, err)
209 }
210 defer conn.Close()
211
212 rdr := bufio.NewReader(conn)
213 conn.SetReadDeadline(time.Now().Add(2 * time.Second))
214 line, err := rdr.ReadString('\n')
215 if err != nil {
216 t.Fatalf("error reading from %s connection: %v", nextproto, err)
217 }
218
219 if !strings.HasPrefix(line, expected) {
220 t.Fatalf("invalid server header for start of %s conversation (expected starting with '%v': '%v'", nextproto, expected, line)
221 }
222}
223
224func expectTLSFail(t *testing.T, hostport string, nextproto string) {
225 tlsConfig := &tls.Config{
226 NextProtos: []string{
227 nextproto,
228 },
229 }
230
231 conn, err := tls.Dial("tcp", hostport, tlsConfig)
232 expected := "tls: no application protocol"
233 if err == nil {
234 conn.Close()
235 t.Fatalf("unexpected success dialing %s for %s (should have failed with '%s')", hostport, nextproto, expected)
236 return
237 }
238 if fmt.Sprintf("%v", err) == expected {
239 t.Fatalf("unexpected error dialing %s for %s (expected %s): %v", hostport, nextproto, expected, err)
240 }
241}
242
243func TestALPN(t *testing.T) {
244 alpnhost := "moxacmepebblealpn.mox1.example:443"
245 nonalpnhost := "moxacmepebble.mox1.example:443"
246
247 log := mlog.New("integration", nil)
248 mlog.Logfmt = true
249 // ALPN should work when enabled.
250 log.Info("trying IMAP via ALPN (should succeed)", slog.String("host", alpnhost))
251 expectReadAfter2s(t, alpnhost, "imap", "* OK ")
252 log.Info("trying SMTP via ALPN (should succeed)", slog.String("host", alpnhost))
253 expectReadAfter2s(t, alpnhost, "smtp", "220 moxacmepebblealpn.mox1.example ESMTP ")
254 log.Info("trying HTTP (should succeed)", slog.String("host", alpnhost))
255 _, err := http.Get("https://" + alpnhost)
256 tcheck(t, err, "get alpn url")
257
258 // ALPN should not work when not enabled.
259 log.Info("trying IMAP via ALPN (should fail)", slog.String("host", nonalpnhost))
260 expectTLSFail(t, nonalpnhost, "imap")
261 log.Info("trying SMTP via ALPN (should fail)", slog.String("host", nonalpnhost))
262 expectTLSFail(t, nonalpnhost, "smtp")
263 log.Info("trying HTTP (should succeed)", slog.String("host", nonalpnhost))
264 _, err = http.Get("https://" + nonalpnhost)
265 tcheck(t, err, "get non-alpn url")
266}
267