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 imapc, err := imapclient.New(mox.Cid(), imapconn, false)
68 tcheck(t, err, "new imapclient")
69
70 _, _, err = imapc.Login(imapuser, imappassword)
71 tcheck(t, err, "imap login")
72
73 _, _, err = imapc.Select("Inbox")
74 tcheck(t, err, "imap select inbox")
75
76 err = imapc.Commandf("", "idle")
77 tcheck(t, err, "write imap idle command")
78
79 _, _, _, err = imapc.ReadContinuation()
80 tcheck(t, err, "read imap continuation")
81
82 idle := make(chan idleResponse)
83 go func() {
84 for {
85 untagged, err := imapc.ReadUntagged()
86 idle <- idleResponse{untagged, err}
87 if err != nil {
88 return
89 }
90 }
91 }()
92 defer func() {
93 err := imapc.Writelinef("done")
94 tcheck(t, err, "aborting idle")
95 }()
96
97 t0 := time.Now()
98 send()
99
100 // Wait for notification of delivery.
101 select {
102 case resp := <-idle:
103 tcheck(t, resp.err, "idle notification")
104 _, ok := resp.untagged.(imapclient.UntaggedExists)
105 if !ok {
106 t.Fatalf("got idle %#v, expected untagged exists", resp.untagged)
107 }
108 if d := time.Since(t0); checkTime && d < 1*time.Second {
109 t.Fatalf("delivery took %v, but should have taken at least 1 second, the first-time sender delay", d)
110 }
111 case <-time.After(30 * time.Second):
112 t.Fatalf("timeout after 5s waiting for IMAP IDLE notification of new message, should take about 1 second")
113 }
114 }
115
116 submit := func(dialtls bool, mailfrom, password, desthost, rcptto string) {
117 var conn net.Conn
118 var err error
119 if dialtls {
120 conn, err = tls.Dial("tcp", desthost, nil)
121 } else {
122 conn, err = net.Dial("tcp", desthost)
123 }
124 tcheck(t, err, "dial submission")
125 defer conn.Close()
126
127 msg := fmt.Sprintf(`From: <%s>
128To: <%s>
129Subject: test message
130
131This is the message.
132`, mailfrom, rcptto)
133 msg = strings.ReplaceAll(msg, "\n", "\r\n")
134 auth := func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
135 return sasl.NewClientPlain(mailfrom, password), nil
136 }
137 c, err := smtpclient.New(mox.Context, log.Logger, conn, smtpclient.TLSSkip, false, ourHostname, dns.Domain{ASCII: desthost}, smtpclient.Opts{Auth: auth})
138 tcheck(t, err, "smtp hello")
139 err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false, false)
140 tcheck(t, err, "deliver with smtp")
141 err = c.Close()
142 tcheck(t, err, "close smtpclient")
143 }
144
145 // Make sure moxacmepebble has a TLS certificate.
146 conn, err := tls.Dial("tcp", "moxacmepebble.mox1.example:465", nil)
147 tcheck(t, err, "dial submission")
148 defer conn.Close()
149
150 log.Print("submitting email to moxacmepebble, waiting for imap notification at moxmail2")
151 t0 := time.Now()
152 deliver(true, true, "moxmail2.mox2.example:993", "moxtest2@mox2.example", "accountpass4321", func() {
153 submit(true, "moxtest1@mox1.example", "accountpass1234", "moxacmepebble.mox1.example:465", "moxtest2@mox2.example")
154 })
155 log.Print("success", slog.Duration("duration", time.Since(t0)))
156
157 log.Print("submitting email to moxmail2, waiting for imap notification at moxacmepebble")
158 t0 = time.Now()
159 deliver(true, true, "moxacmepebble.mox1.example:993", "moxtest1@mox1.example", "accountpass1234", func() {
160 submit(true, "moxtest2@mox2.example", "accountpass4321", "moxmail2.mox2.example:465", "moxtest1@mox1.example")
161 })
162 log.Print("success", slog.Duration("duration", time.Since(t0)))
163
164 log.Print("submitting email to postfix, waiting for imap notification at moxacmepebble")
165 t0 = time.Now()
166 deliver(false, true, "moxacmepebble.mox1.example:993", "moxtest1@mox1.example", "accountpass1234", func() {
167 submit(true, "moxtest1@mox1.example", "accountpass1234", "moxacmepebble.mox1.example:465", "root@postfix.example")
168 })
169 log.Print("success", slog.Duration("duration", time.Since(t0)))
170
171 log.Print("submitting email to localserve")
172 t0 = time.Now()
173 deliver(false, false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
174 submit(false, "mox@localhost", "moxmoxmox", "localserve.mox1.example:1587", "moxtest1@mox1.example")
175 })
176 log.Print("success", slog.Duration("duration", time.Since(t0)))
177
178 log.Print("submitting email to localserve")
179 t0 = time.Now()
180 deliver(false, false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
181 cmd := exec.Command("go", "run", ".", "sendmail", "mox@localhost")
182 const msg = `Subject: test
183
184a message.
185`
186 cmd.Stdin = strings.NewReader(msg)
187 var out strings.Builder
188 cmd.Stdout = &out
189 err := cmd.Run()
190 log.Print("sendmail", slog.String("output", out.String()))
191 tcheck(t, err, "sendmail")
192 })
193 log.Print("success", slog.Any("duration", time.Since(t0)))
194}
195
196func expectReadAfter2s(t *testing.T, hostport string, nextproto string, expected string) {
197 tlsConfig := &tls.Config{
198 NextProtos: []string{
199 nextproto,
200 },
201 }
202
203 conn, err := tls.Dial("tcp", hostport, tlsConfig)
204 if err != nil {
205 t.Fatalf("error dialing moxacmepebblealpn 443 for %s: %v", nextproto, err)
206 }
207 defer conn.Close()
208
209 rdr := bufio.NewReader(conn)
210 conn.SetReadDeadline(time.Now().Add(2 * time.Second))
211 line, err := rdr.ReadString('\n')
212 if err != nil {
213 t.Fatalf("error reading from %s connection: %v", nextproto, err)
214 }
215
216 if !strings.HasPrefix(line, expected) {
217 t.Fatalf("invalid server header for start of %s conversation (expected starting with '%v': '%v'", nextproto, expected, line)
218 }
219}
220
221func expectTLSFail(t *testing.T, hostport string, nextproto string) {
222 tlsConfig := &tls.Config{
223 NextProtos: []string{
224 nextproto,
225 },
226 }
227
228 conn, err := tls.Dial("tcp", hostport, tlsConfig)
229 expected := "tls: no application protocol"
230 if err == nil {
231 conn.Close()
232 t.Fatalf("unexpected success dialing %s for %s (should have failed with '%s')", hostport, nextproto, expected)
233 return
234 }
235 if fmt.Sprintf("%v", err) == expected {
236 t.Fatalf("unexpected error dialing %s for %s (expected %s): %v", hostport, nextproto, expected, err)
237 }
238}
239
240func TestALPN(t *testing.T) {
241 alpnhost := "moxacmepebblealpn.mox1.example:443"
242 nonalpnhost := "moxacmepebble.mox1.example:443"
243
244 log := mlog.New("integration", nil)
245 mlog.Logfmt = true
246 // ALPN should work when enabled.
247 log.Info("trying IMAP via ALPN (should succeed)", slog.String("host", alpnhost))
248 expectReadAfter2s(t, alpnhost, "imap", "* OK ")
249 log.Info("trying SMTP via ALPN (should succeed)", slog.String("host", alpnhost))
250 expectReadAfter2s(t, alpnhost, "smtp", "220 moxacmepebblealpn.mox1.example ESMTP ")
251 log.Info("trying HTTP (should succeed)", slog.String("host", alpnhost))
252 _, err := http.Get("https://" + alpnhost)
253 tcheck(t, err, "get alpn url")
254
255 // ALPN should not work when not enabled.
256 log.Info("trying IMAP via ALPN (should fail)", slog.String("host", nonalpnhost))
257 expectTLSFail(t, nonalpnhost, "imap")
258 log.Info("trying SMTP via ALPN (should fail)", slog.String("host", nonalpnhost))
259 expectTLSFail(t, nonalpnhost, "smtp")
260 log.Info("trying HTTP (should succeed)", slog.String("host", nonalpnhost))
261 _, err = http.Get("https://" + nonalpnhost)
262 tcheck(t, err, "get non-alpn url")
263}
264