1package smtpserver
2
3import (
4 "errors"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "github.com/mjl-/bstore"
10
11 "github.com/mjl-/mox/dns"
12 "github.com/mjl-/mox/smtp"
13 "github.com/mjl-/mox/smtpclient"
14 "github.com/mjl-/mox/store"
15)
16
17// Check user can submit message with message From address they are member of.
18func TestAliasSubmitMsgFrom(t *testing.T) {
19 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
20 defer ts.close()
21
22 ts.submission = true
23 ts.user = "mjl@mox.example"
24 ts.pass = password0
25
26 var msg = strings.ReplaceAll(`From: <public@mox.example>
27To: <public@mox.example>
28Subject: test
29
30test email
31`, "\n", "\r\n")
32
33 ts.run(func(err error, client *smtpclient.Client) {
34 t.Helper()
35 mailFrom := "mjl@mox.example"
36 rcptTo := "public@mox.example"
37 if err == nil {
38 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
39 }
40 ts.smtpErr(err, nil)
41 })
42
43 msg = strings.ReplaceAll(`From: <private@mox.example>
44To: <private@mox.example>
45Subject: test
46
47test email
48`, "\n", "\r\n")
49
50 ts.run(func(err error, client *smtpclient.Client) {
51 t.Helper()
52 mailFrom := "mjl@mox.example"
53 rcptTo := "private@mox.example"
54 if err == nil {
55 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
56 }
57 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
58 })
59}
60
61// Non-member cannot submit as alias that allows it for members.
62func TestAliasSubmitMsgFromDenied(t *testing.T) {
63 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
64 defer ts.close()
65
66 // Trying to open account by alias should result in proper error.
67 _, _, err := store.OpenEmail(pkglog, "public@mox.example")
68 if err == nil || !errors.Is(err, store.ErrUnknownCredentials) {
69 t.Fatalf("opening alias, got err %v, expected store.ErrUnknownCredentials", err)
70 }
71
72 acc, err := store.OpenAccount(pkglog, "☺")
73 tcheck(t, err, "open account")
74 err = acc.SetPassword(pkglog, password0)
75 tcheck(t, err, "set password")
76 err = acc.Close()
77 tcheck(t, err, "close account")
78 acc.CheckClosed()
79
80 ts.submission = true
81 ts.user = "☺@mox.example"
82 ts.pass = password0
83
84 var msg = strings.ReplaceAll(`From: <public@mox.example>
85To: <public@mox.example>
86Subject: test
87
88test email
89`, "\n", "\r\n")
90
91 ts.run(func(err error, client *smtpclient.Client) {
92 t.Helper()
93 mailFrom := "☺@mox.example"
94 rcptTo := "public@mox.example"
95 if err == nil {
96 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, true, false)
97 }
98 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7DeliveryUnauth1})
99 })
100}
101
102// Non-member can deliver to public list, not to private list.
103func TestAliasDeliverNonMember(t *testing.T) {
104 resolver := dns.MockResolver{
105 A: map[string][]string{
106 "example.org.": {"127.0.0.10"}, // For mx check.
107 },
108 PTR: map[string][]string{
109 "127.0.0.10": {"example.org."}, // To get passed junk filter.
110 },
111 }
112 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
113 defer ts.close()
114
115 var msg = strings.ReplaceAll(`From: <other@example.org>
116To: <private@mox.example>
117
118test email
119`, "\n", "\r\n")
120
121 ts.run(func(err error, client *smtpclient.Client) {
122 t.Helper()
123 mailFrom := "other@example.org"
124 rcptTo := "private@mox.example"
125 if err == nil {
126 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
127 }
128 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7ExpnProhibited2})
129 })
130
131 msg = strings.ReplaceAll(`From: <private@mox.example>
132To: <private@mox.example>
133
134test email
135`, "\n", "\r\n")
136
137 ts.run(func(err error, client *smtpclient.Client) {
138 t.Helper()
139 mailFrom := "private@example.org"
140 rcptTo := "private@mox.example"
141 if err == nil {
142 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
143 }
144 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7ExpnProhibited2})
145 })
146
147 msg = strings.ReplaceAll(`From: <other@example.org>
148To: <public@mox.example>
149Subject: test
150
151test email
152`, "\n", "\r\n")
153
154 ts.run(func(err error, client *smtpclient.Client) {
155 t.Helper()
156 mailFrom := "other@example.org"
157 rcptTo := "public@mox.example"
158 if err == nil {
159 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
160 }
161 ts.smtpErr(err, nil)
162
163 ts.checkCount("Inbox", 2) // Receiving for both mjl@ and móx@.
164 })
165}
166
167// Member can deliver to private list, but still not with alias address as message
168// from. Message with alias from address as message from is allowed.
169func TestAliasDeliverMember(t *testing.T) {
170 resolver := dns.MockResolver{
171 A: map[string][]string{
172 "mox.example.": {"127.0.0.10"}, // For mx check.
173 },
174 PTR: map[string][]string{
175 "127.0.0.10": {"mox.example."}, // To get passed junk filter.
176 },
177 TXT: map[string][]string{
178 "mox.example.": {"v=spf1 ip4:127.0.0.10 -all"}, // To allow multiple recipients in transaction.
179 },
180 }
181 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
182 defer ts.close()
183
184 var msg = strings.ReplaceAll(`From: <mjl@mox.example>
185To: <private@mox.example>
186
187test email
188`, "\n", "\r\n")
189
190 ts.run(func(err error, client *smtpclient.Client) {
191 t.Helper()
192 mailFrom := "mjl@mox.example"
193 rcptTo := []string{"private@mox.example", "móx@mox.example"}
194 if err == nil {
195 _, err = client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), true, true, false)
196 // assuming there wasn't a per-recipient error
197 }
198 ts.smtpErr(err, nil)
199
200 ts.checkCount("Inbox", 0) // Not receiving for mjl@ due to msgfrom, and not móx@ due to rcpt to.
201 })
202
203 ts.run(func(err error, client *smtpclient.Client) {
204 t.Helper()
205 mailFrom := "mjl@mox.example"
206 rcptTo := "private@mox.example"
207 if err == nil {
208 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
209 }
210 ts.smtpErr(err, nil)
211
212 ts.checkCount("Inbox", 1) // Only receiving for móx@mox.example, not mjl@.
213 })
214
215 msg = strings.ReplaceAll(`From: <private@mox.example>
216To: <private@mox.example>
217Subject: test
218
219test email
220`, "\n", "\r\n")
221
222 ts.run(func(err error, client *smtpclient.Client) {
223 t.Helper()
224 mailFrom := "other@mox.example"
225 rcptTo := "private@mox.example"
226 if err == nil {
227 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
228 }
229 ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7ExpnProhibited2})
230 })
231
232 msg = strings.ReplaceAll(`From: <public@mox.example>
233To: <public@mox.example>
234Subject: test
235
236test email
237`, "\n", "\r\n")
238
239 ts.run(func(err error, client *smtpclient.Client) {
240 t.Helper()
241 mailFrom := "mjl@mox.example"
242 rcptTo := "public@mox.example"
243 if err == nil {
244 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
245 }
246 ts.smtpErr(err, nil)
247 })
248}
249
250// Message is rejected if no member accepts it.
251func TestAliasDeliverReject(t *testing.T) {
252 resolver := dns.MockResolver{
253 A: map[string][]string{
254 "mox.example.": {"127.0.0.10"}, // For mx check.
255 },
256 PTR: map[string][]string{
257 "127.0.0.10": {"mox.example."}, // To get passed junk filter.
258 },
259 TXT: map[string][]string{
260 "mox.example.": {"v=spf1 ip4:127.0.0.10 -all"}, // To allow multiple recipients in transaction.
261 },
262 }
263 ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
264 defer ts.close()
265
266 var msg = strings.ReplaceAll(`From: <mjl@mox.example>
267To: <private@mox.example>
268
269test email
270`, "\n", "\r\n")
271
272 ts.run(func(err error, client *smtpclient.Client) {
273 t.Helper()
274 mailFrom := "mjl@mox.example"
275 rcptTo := "private@mox.example"
276 if err == nil {
277 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
278 }
279 ts.smtpErr(err, nil)
280
281 ts.checkCount("Inbox", 1) // Only receiving for móx@mox.example, not mjl@.
282 })
283
284 // Mark message as junk.
285 q := bstore.QueryDB[store.Message](ctxbg, ts.acc.DB)
286 n, err := q.UpdateFields(map[string]any{"Junk": true})
287 tcheck(t, err, "mark as junk")
288 tcompare(t, n, 1)
289
290 ts.run(func(err error, client *smtpclient.Client) {
291 t.Helper()
292 mailFrom := "mjl@mox.example"
293 rcptTo := "private@mox.example"
294 if err == nil {
295 err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
296 }
297 ts.smtpErr(err, &smtpclient.Error{Code: smtp.C451LocalErr, Secode: smtp.SeSys3Other0})
298 })
299}
300