Bump the version to 2.1.0.
[mailpopbox.git] / smtp_test.go
1 // mailpopbox
2 // Copyright 2020 Blue Static <https://www.bluestatic.org>
3 // This program is free software licensed under the GNU General Public License,
4 // version 3.0. The full text of the license can be found in LICENSE.txt.
5 // SPDX-License-Identifier: GPL-3.0-only
6
7 package main
8
9 import (
10 "bytes"
11 "fmt"
12 "io/ioutil"
13 "net/mail"
14 "os"
15 "path/filepath"
16 "strings"
17 "testing"
18
19 "go.uber.org/zap"
20
21 "src.bluestatic.org/mailpopbox/smtp"
22 )
23
24 func TestVerifyAddress(t *testing.T) {
25 dir, err := ioutil.TempDir("", "maildrop")
26 if err != nil {
27 t.Errorf("Failed to create temp dir: %v", err)
28 return
29 }
30 defer os.RemoveAll(dir)
31
32 s := smtpServer{
33 config: Config{
34 Hostname: "mx.example.com",
35 Servers: []Server{
36 {
37 Domain: "example.com",
38 MaildropPath: dir,
39 BlockedAddresses: []string{
40 "blocked@example.com",
41 },
42 },
43 },
44 },
45 log: zap.NewNop(),
46 }
47
48 if s.VerifyAddress(mail.Address{Address: "example@example.com"}) != smtp.ReplyOK {
49 t.Errorf("Valid mailbox is not reported to be valid")
50 }
51 if s.VerifyAddress(mail.Address{Address: "mailbox@example.com"}) != smtp.ReplyOK {
52 t.Errorf("Valid mailbox is not reported to be valid")
53 }
54 if s.VerifyAddress(mail.Address{Address: "hello@other.net"}) == smtp.ReplyOK {
55 t.Errorf("Invalid mailbox reports to be valid")
56 }
57 if s.VerifyAddress(mail.Address{Address: "hello@mx.example.com"}) == smtp.ReplyOK {
58 t.Errorf("Invalid mailbox reports to be valid")
59 }
60 if s.VerifyAddress(mail.Address{Address: "unknown"}) == smtp.ReplyOK {
61 t.Errorf("Invalid mailbox reports to be valid")
62 }
63 if s.VerifyAddress(mail.Address{Address: "blocked@example.com"}) == smtp.ReplyOK {
64 t.Errorf("Blocked mailbox reports to be valid")
65 }
66 }
67
68 func TestMessageDelivery(t *testing.T) {
69 dir, err := ioutil.TempDir("", "maildrop")
70 if err != nil {
71 t.Errorf("Failed to create temp dir: %v", err)
72 return
73 }
74 defer os.RemoveAll(dir)
75
76 s := smtpServer{
77 config: Config{
78 Hostname: "mx.example.com",
79 Servers: []Server{
80 {
81 Domain: "example.com",
82 MaildropPath: dir,
83 },
84 },
85 },
86 log: zap.NewNop(),
87 }
88
89 env := smtp.Envelope{
90 MailFrom: mail.Address{Address: "sender@mail.net"},
91 RcptTo: []mail.Address{{Address: "receive@example.com"}},
92 Data: []byte("Hello, world"),
93 ID: "msgid",
94 }
95
96 if rl := s.DeliverMessage(env); rl != nil {
97 t.Errorf("Failed to deliver message: %v", rl)
98 }
99
100 f, err := os.Open(filepath.Join(dir, "msgid.msg"))
101 if err != nil {
102 t.Errorf("Failed to open delivered message: %v", err)
103 }
104 defer f.Close()
105
106 data, err := ioutil.ReadAll(f)
107 if err != nil {
108 t.Errorf("Failed to read message: %v", err)
109 }
110
111 if !bytes.Contains(data, env.Data) {
112 t.Errorf("Could not find expected data in message")
113 }
114 }
115
116 func TestAuthenticate(t *testing.T) {
117 server := smtpServer{
118 config: Config{
119 Servers: []Server{
120 Server{
121 Domain: "domain1.net",
122 MailboxPassword: "d1",
123 },
124 Server{
125 Domain: "domain2.xyz",
126 MailboxPassword: "d2",
127 },
128 },
129 },
130 }
131
132 authTests := []struct {
133 authz, authc, passwd string
134 ok bool
135 }{
136 {"foo@domain1.net", "mailbox@domain1.net", "d1", true},
137 {"", "mailbox@domain1.net", "d1", true},
138 {"foo@domain2.xyz", "mailbox@domain1.xyz", "d1", false},
139 {"foo@domain2.xyz", "mailbox@domain1.xyz", "d2", false},
140 {"foo@domain2.xyz", "mailbox@domain2.xyz", "d2", true},
141 {"invalid", "mailbox@domain2.xyz", "d2", false},
142 {"", "mailbox@domain2.xyz", "d2", true},
143 {"", "", "", false},
144 }
145
146 for i, test := range authTests {
147 actual := server.Authenticate(test.authz, test.authc, test.passwd)
148 if actual != test.ok {
149 t.Errorf("Test %d, got %v, expected %v", i, actual, test.ok)
150 }
151 }
152 }
153
154 type testMTA struct {
155 relayed chan smtp.Envelope
156 }
157
158 func (m *testMTA) RelayMessage(en smtp.Envelope) {
159 m.relayed <- en
160 }
161
162 func newTestMTA() *testMTA {
163 return &testMTA{
164 relayed: make(chan smtp.Envelope),
165 }
166 }
167
168 func TestBasicRelay(t *testing.T) {
169 mta := newTestMTA()
170 server := smtpServer{
171 mta: mta,
172 log: zap.NewNop(),
173 }
174
175 buf := new(bytes.Buffer)
176 fmt.Fprintln(buf, "From: <mailbox@example.com>\r")
177 fmt.Fprintln(buf, "To: <dest@another.net>\r")
178 fmt.Fprintf(buf, "Subject: Basic relay\n\n")
179 fmt.Fprintln(buf, "This is a basic relay message")
180
181 en := smtp.Envelope{
182 MailFrom: mail.Address{Address: "mailbox@example.com"},
183 RcptTo: []mail.Address{{Address: "dest@another.com"}},
184 Data: buf.Bytes(),
185 ID: "id1",
186 }
187
188 server.RelayMessage(en, en.MailFrom.Address)
189
190 relayed := <-mta.relayed
191
192 if !bytes.Equal(relayed.Data, en.Data) {
193 t.Errorf("Relayed message data does not match")
194 }
195 }
196
197 func TestSendAsRelay(t *testing.T) {
198 mta := newTestMTA()
199 server := smtpServer{
200 mta: mta,
201 log: zap.NewNop(),
202 }
203
204 buf := new(bytes.Buffer)
205 fmt.Fprintln(buf, "Received: msg from wherever")
206 fmt.Fprintln(buf, "From: <mailbox@example.com>")
207 fmt.Fprintln(buf, "To: <valid@dest.xyz>")
208 fmt.Fprintf(buf, "Subject: Send-as relay [sendas:source]\n\n")
209 fmt.Fprintln(buf, "We've switched the senders!")
210
211 en := smtp.Envelope{
212 MailFrom: mail.Address{Address: "mailbox@example.com"},
213 RcptTo: []mail.Address{{Address: "valid@dest.xyz"}},
214 Data: buf.Bytes(),
215 ID: "id1",
216 }
217
218 server.RelayMessage(en, en.MailFrom.Address)
219
220 relayed := <-mta.relayed
221
222 replaced := "source@example.com"
223 original := "mailbox@example.com"
224
225 if want, got := replaced, relayed.MailFrom.Address; want != got {
226 t.Errorf("Want mail to be from %q, got %q", want, got)
227 }
228
229 if want, got := 1, len(relayed.RcptTo); want != got {
230 t.Errorf("Want %d recipient, got %d", want, got)
231 }
232 if want, got := "valid@dest.xyz", relayed.RcptTo[0].Address; want != got {
233 t.Errorf("Unexpected RcptTo %q", got)
234 }
235
236 msg := string(relayed.Data)
237
238 if strings.Index(msg, original) != -1 {
239 t.Errorf("Should not find %q in message %q", original, msg)
240 }
241
242 if strings.Index(msg, "\nFrom: <source@example.com>\n") == -1 {
243 t.Errorf("Could not find From: header in message %q", msg)
244 }
245
246 if strings.Index(msg, "\nSubject: Send-as relay \n") == -1 {
247 t.Errorf("Could not find modified Subject: header in message %q", msg)
248 }
249 }