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