maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1//go:build integration
  2// +build integration
  3
  4/*
  5Maddy Mail Server - Composable all-in-one email server.
  6Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  7
  8This program is free software: you can redistribute it and/or modify
  9it under the terms of the GNU General Public License as published by
 10the Free Software Foundation, either version 3 of the License, or
 11(at your option) any later version.
 12
 13This program is distributed in the hope that it will be useful,
 14but WITHOUT ANY WARRANTY; without even the implied warranty of
 15MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16GNU General Public License for more details.
 17
 18You should have received a copy of the GNU General Public License
 19along with this program.  If not, see <https://www.gnu.org/licenses/>.
 20*/
 21
 22package tests_test
 23
 24import (
 25	"errors"
 26	"fmt"
 27	"io/ioutil"
 28	"path/filepath"
 29	"strings"
 30	"testing"
 31
 32	"github.com/foxcpp/go-mockdns"
 33	"github.com/foxcpp/maddy/tests"
 34)
 35
 36func TestCheckRequireTLS(tt *testing.T) {
 37	tt.Parallel()
 38	t := tests.NewT(tt)
 39	t.DNS(nil)
 40	t.Port("smtp")
 41	t.Config(`
 42		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
 43			hostname mx.maddy.test
 44			tls self_signed
 45
 46			defer_sender_reject no
 47
 48			check {
 49				require_tls
 50			}
 51			deliver_to dummy
 52		}
 53	`)
 54	t.Run(1)
 55	defer t.Close()
 56
 57	conn := t.Conn("smtp")
 58	defer conn.Close()
 59	conn.SMTPNegotation("localhost", nil, nil)
 60	conn.Writeln("MAIL FROM:<testing@two.maddy.test>")
 61	conn.ExpectPattern("550 5.7.1 *")
 62	conn.Writeln("STARTTLS")
 63	conn.ExpectPattern("220 *")
 64	conn.TLS()
 65	conn.SMTPNegotation("localhost", nil, nil)
 66	conn.Writeln("MAIL FROM:<testing@two.maddy.test>")
 67	conn.ExpectPattern("250 *")
 68	conn.Writeln("QUIT")
 69	conn.ExpectPattern("221 *")
 70}
 71
 72func TestProxyProtocolTrustedSource(tt *testing.T) {
 73	tt.Parallel()
 74	t := tests.NewT(tt)
 75	t.DNS(map[string]mockdns.Zone{
 76		"one.maddy.test.": {
 77			TXT: []string{"v=spf1 ip4:127.0.0.17 -all"},
 78		},
 79	})
 80	t.Port("smtp")
 81	t.Config(`
 82		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
 83			hostname mx.maddy.test
 84			tls off
 85
 86			proxy_protocol {
 87				trust ` + tests.DefaultSourceIP.String() + ` ::1/128
 88				tls off
 89			}
 90
 91			defer_sender_reject no
 92
 93			check {
 94				spf {
 95					enforce_early yes
 96					fail_action reject
 97				}
 98			}
 99
100			deliver_to dummy
101		}
102	`)
103	t.Run(1)
104	defer t.Close()
105
106	conn := t.Conn("smtp")
107	defer conn.Close()
108	conn.Writeln(fmt.Sprintf("PROXY TCP4 127.0.0.17 %s 12345 %d", tests.DefaultSourceIP.String(), t.Port("smtp")))
109	conn.SMTPNegotation("localhost", nil, nil)
110	conn.Writeln("MAIL FROM:<testing@one.maddy.test>")
111	conn.ExpectPattern("250 *")
112	conn.Writeln("QUIT")
113	conn.ExpectPattern("221 *")
114}
115
116func TestProxyProtocolUntrustedSource(tt *testing.T) {
117	tt.Parallel()
118	t := tests.NewT(tt)
119	t.DNS(map[string]mockdns.Zone{
120		"one.maddy.test.": {
121			TXT: []string{"v=spf1 ip4:127.0.0.17 -all"},
122		},
123	})
124	t.Port("smtp")
125	t.Config(`
126		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
127			hostname mx.maddy.test
128			tls off
129
130			proxy_protocol {
131				trust fe80::bad/128
132				tls off
133			}
134
135			defer_sender_reject no
136
137			check {
138				spf {
139					enforce_early yes
140					fail_action reject
141				}
142			}
143
144			deliver_to dummy
145		}
146	`)
147	t.Run(1)
148	defer t.Close()
149
150	conn := t.Conn("smtp")
151	defer conn.Close()
152	conn.Writeln(fmt.Sprintf("PROXY TCP4 127.0.0.17 %s 12345 %d", tests.DefaultSourceIP.String(), t.Port("smtp")))
153	conn.SMTPNegotation("localhost", nil, nil)
154	conn.Writeln("MAIL FROM:<testing@one.maddy.test>")
155	conn.ExpectPattern("550 *")
156	conn.Writeln("QUIT")
157	conn.ExpectPattern("221 *")
158}
159
160func TestCheckSPF(tt *testing.T) {
161	tt.Parallel()
162	t := tests.NewT(tt)
163	t.DNS(map[string]mockdns.Zone{
164		"none.maddy.test.": {
165			TXT: []string{},
166		},
167		"pass.maddy.test.": {
168			TXT: []string{"v=spf1 +all"},
169		},
170		"neutral.maddy.test.": {
171			TXT: []string{"v=spf1 ?all"},
172		},
173		"fail.maddy.test.": {
174			TXT: []string{"v=spf1 -all"},
175		},
176		"softfail.maddy.test.": {
177			TXT: []string{"v=spf1 ~all"},
178		},
179		"permerr.maddy.test.": {
180			TXT: []string{"v=spf1 something_clever"},
181		},
182		"temperr.maddy.test.": {
183			Err: errors.New("IANA forgot to resign the root zone"),
184		},
185	})
186	t.Port("smtp")
187	t.Config(`
188		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
189			hostname mx.maddy.test
190			tls off
191
192			defer_sender_reject no
193
194			check {
195				spf {
196					enforce_early yes
197
198					none_action reject 551
199					neutral_action reject
200					fail_action reject 552
201					softfail_action reject 553
202					permerr_action reject 554
203					temperr_action reject 455
204				}
205			}
206			deliver_to dummy
207		}
208	`)
209	t.Run(1)
210	defer t.Close()
211
212	conn := t.Conn("smtp")
213	defer conn.Close()
214	conn.SMTPNegotation("fail.maddy.test", nil, nil)
215
216	conn.Writeln("MAIL FROM:<testing@pass.maddy.test>")
217	conn.ExpectPattern("250 *")
218	conn.Writeln("RSET")
219	conn.ExpectPattern("250 *")
220
221	// Actually checks fail.maddy.test.
222	conn.Writeln("MAIL FROM:<>")
223	conn.ExpectPattern("552 5.7.0 *")
224
225	conn.SMTPNegotation("pass.maddy.test", nil, nil)
226
227	conn.Writeln("MAIL FROM:<>")
228	conn.ExpectPattern("250 *")
229
230	conn.Writeln("MAIL FROM:<testing@none.maddy.test>")
231	conn.ExpectPattern("551 5.7.0 *")
232
233	// Also check the default enhanced code is meaningful.
234	conn.Writeln("MAIL FROM:<testing@neutral.maddy.test>")
235	conn.ExpectPattern("550 5.7.23 *")
236
237	conn.Writeln("MAIL FROM:<testing@fail.maddy.test>")
238	conn.ExpectPattern("552 5.7.0 *")
239
240	conn.Writeln("MAIL FROM:<testing@softfail.maddy.test>")
241	conn.ExpectPattern("553 5.7.0 *")
242
243	conn.Writeln("MAIL FROM:<testing@permerr.maddy.test>")
244	conn.ExpectPattern("554 5.7.0 *")
245
246	conn.Writeln("MAIL FROM:<testing@temperr.maddy.test>")
247	conn.ExpectPattern("455 4.7.0 *")
248
249	conn.Writeln("QUIT")
250	conn.ExpectPattern("221 *")
251}
252
253func TestSPF_DMARCDefer(tt *testing.T) {
254	tt.Parallel()
255	t := tests.NewT(tt)
256	t.DNS(map[string]mockdns.Zone{
257		"subdomain.maddy-dmarc.test.": {
258			TXT: []string{"v=spf1 -all"},
259		},
260		"maddy-dmarc.test.": {
261			TXT: []string{"v=spf1 -all"},
262		},
263		"_dmarc.maddy-dmarc.test.": {
264			TXT: []string{"v=DMARC1; p=reject; sp=none"},
265		},
266		"subdomain.maddy-dmarc2.test.": {
267			TXT: []string{"v=spf1 -all"},
268		},
269		"maddy-dmarc2.test.": {
270			TXT: []string{"v=spf1 -all"},
271		},
272		"_dmarc.maddy-dmarc2.test.": {
273			TXT: []string{"v=DMARC1; p=reject"},
274		},
275		"maddy-no-dmarc.test.": {
276			TXT: []string{"v=spf1 -all"},
277		},
278		"maddy-dmarc-lookup-fail.test.": {
279			TXT: []string{"v=spf1 -all"},
280		},
281		"_dmarc.maddy-dmarc-lookup-fail.test.": {
282			Err: errors.New("nop"),
283		},
284	})
285	t.Port("smtp")
286	t.Config(`
287		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
288			hostname mx.maddy.test
289			tls off
290
291			defer_sender_reject no
292
293			check {
294				spf {
295					enforce_early no
296
297					none_action ignore
298					neutral_action reject
299					fail_action reject
300					softfail_action reject
301					permerr_action reject
302					temperr_action reject
303				}
304			}
305			deliver_to dummy
306		}
307	`)
308	t.Run(1)
309	defer t.Close()
310
311	conn := t.Conn("smtp")
312	defer conn.Close()
313	conn.SMTPNegotation("localhost", nil, nil)
314
315	msg := func(fromEnv, fromHdr, bodyRespPattern string) {
316		tt.Helper()
317
318		conn.Writeln("MAIL FROM:<" + fromEnv + ">")
319		conn.ExpectPattern("250 *")
320		conn.Writeln("RCPT TO:<testing@maddy.test>")
321		conn.ExpectPattern("250 *")
322		conn.Writeln("DATA")
323		conn.ExpectPattern("354 *")
324		conn.Writeln("From: <" + fromHdr + ">")
325		conn.Writeln("")
326		conn.Writeln("Heya!")
327		conn.Writeln(".")
328		conn.ExpectPattern(bodyRespPattern)
329	}
330
331	msg("test@subdomain.maddy-dmarc.test", "test@subdomain.maddy-dmarc.test", "550 *")
332
333	// Malformed From domain, DMARC cannot work so use only SPF.
334	msg("test@subdomain.maddy-dmarc.test", "", "550 *")
335
336	msg("test@subdomain.maddy-dmarc.test", "maddy-dmarc-lookup-fail.test", "550 *")
337
338	// No actual DMARC check is done but SPF check results are not applied.
339	msg("test@maddy-dmarc.test", "test@maddy-dmarc.test", "250 *")
340	msg("test@maddy-dmarc2.test", "test@maddy-dmarc2.test", "250 *")
341
342	msg("test@maddy-no-dmarc.test", "test@maddy-no-dmarc.test", "550 *")
343
344	conn.Writeln("QUIT")
345	conn.ExpectPattern("221 *")
346}
347
348func TestDNSBLConfig(tt *testing.T) {
349	tt.Parallel()
350	t := tests.NewT(tt)
351	t.DNS(map[string]mockdns.Zone{
352		tests.DefaultSourceIPRev + ".dnsbl.test.": {
353			A: []string{"127.0.0.127"},
354		},
355		"sender.test.dnsbl.test.": {
356			A: []string{"127.0.0.127"},
357		},
358	})
359	t.Port("smtp")
360	t.Config(`
361		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
362			hostname mx.maddy.test
363			tls off
364
365			defer_sender_reject no
366
367			check {
368				dnsbl {
369					reject_threshold 1
370
371					dnsbl.test {
372						client_ipv4
373						mailfrom
374					}
375				}
376			}
377			deliver_to dummy
378		}
379	`)
380	t.Run(1)
381	defer t.Close()
382
383	conn := t.Conn("smtp")
384	defer conn.Close()
385	conn.SMTPNegotation("localhost", nil, nil)
386
387	conn.Writeln("MAIL FROM:<testing@sender.test>")
388	conn.ExpectPattern("554 5.7.0 Client identity is listed in the used DNSBL *")
389
390	conn.Writeln("MAIL FROM:<testing@misc.test>")
391	conn.ExpectPattern("554 5.7.0 Client identity is listed in the used DNSBL *")
392
393	conn.Writeln("QUIT")
394	conn.ExpectPattern("221 *")
395}
396
397func TestDNSBLConfig2(tt *testing.T) {
398	tt.Parallel()
399	t := tests.NewT(tt)
400	t.DNS(map[string]mockdns.Zone{
401		tests.DefaultSourceIPRev + ".dnsbl2.test.": {
402			A: []string{"127.0.0.127"},
403		},
404		"sender.test.dnsbl.test.": {
405			A: []string{"127.0.0.127"},
406		},
407	})
408	t.Port("smtp")
409	t.Config(`
410		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
411			hostname mx.maddy.test
412			tls off
413
414			defer_sender_reject no
415
416			check {
417				dnsbl {
418					reject_threshold 1
419
420					dnsbl.test {
421						mailfrom
422					}
423					dnsbl2.test {
424						client_ipv4
425						score -1
426					}
427				}
428			}
429			deliver_to dummy
430		}
431	`)
432	t.Run(1)
433	defer t.Close()
434
435	conn := t.Conn("smtp")
436	defer conn.Close()
437	conn.SMTPNegotation("localhost", nil, nil)
438
439	conn.Writeln("MAIL FROM:<testing@sender.test>")
440	conn.ExpectPattern("250 *")
441
442	conn.Writeln("QUIT")
443	conn.ExpectPattern("221 *")
444}
445
446func TestCheckAuthorizeSender(tt *testing.T) {
447	tt.Parallel()
448	t := tests.NewT(tt)
449	t.DNS(nil)
450	t.Port("smtp")
451	t.Config(`
452		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
453			hostname mx.maddy.test
454			tls off
455
456			auth dummy
457			defer_sender_reject off
458
459			source example1.org {
460				check {
461					authorize_sender {
462						auth_normalize precis_casefold
463						user_to_email static {
464							entry "test-user1" "test@example1.org"
465							entry "test-user2" "é@example1.org"
466						}
467					}
468				}
469				deliver_to dummy
470			}
471			source example2.org {
472				check {
473					authorize_sender {
474						auth_normalize precis_casefold
475						prepare_email static {
476							entry "alias-to-test@example2.org" "test@example2.org"
477						}
478						user_to_email static {
479							entry "test-user1" "test@example2.org"
480							entry "test-user2" "test2@example2.org"
481						}
482					}
483				}
484				deliver_to dummy
485			}
486
487			default_source {
488				reject
489			}
490		}`)
491	t.Run(1)
492	defer t.Close()
493
494	c := t.Conn("smtp")
495	c.SMTPNegotation("client.maddy.test", nil, nil)
496	c.SMTPPlainAuth("test-user2", "1", true)
497	c.Writeln("MAIL FROM:<test@example1.org>")
498	c.ExpectPattern("5*") // rejected - user is not test-user1
499	c.Writeln("MAIL FROM:<test3@example1.org>")
500	c.ExpectPattern("5*") // rejected - unknown email
501	c.Writeln("MAIL FROM:<E\u0301@EXAMPLE1.org> SMTPUTF8")
502	c.ExpectPattern("2*") // OK - é@example1.org belongs to test-user2
503	c.Close()
504
505	c = t.Conn("smtp")
506	c.SMTPNegotation("client.maddy.test", nil, nil)
507	c.SMTPPlainAuth("test-user1", "1", true)
508	c.Writeln("MAIL FROM:<test2@example2.org>")
509	c.ExpectPattern("5*") // rejected - user is not test-user2
510	c.Writeln("MAIL FROM:<test@example2.org>")
511	c.ExpectPattern("2*") // OK - test@example2.org belongs to test-user
512	c.Writeln("MAIL FROM:<alias-to-test@example2.org>")
513	c.ExpectPattern("2*") // OK - test@example2.org belongs to test-user
514	c.Close()
515}
516
517func TestCheckCommand(tt *testing.T) {
518	tt.Parallel()
519	t := tests.NewT(tt)
520	t.DNS(nil)
521	t.Port("smtp")
522	t.Config(`
523		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
524			hostname mx.maddy.test
525			tls off
526
527			check {
528				command {env:TEST_PWD}/testdata/check_command.sh {sender} {
529					code 12 reject
530				}
531			}
532			deliver_to dummy
533		}
534	`)
535	t.Run(1)
536	defer t.Close()
537
538	conn := t.Conn("smtp")
539	defer conn.Close()
540	conn.SMTPNegotation("localhost", nil, nil)
541
542	// Note: Internally, messages are handled using LF line endings, being
543	// converted CRLF only when transfered over Internet protocols.
544	expectedMsg := "From: <testing@sender.test>\n" +
545		"To: <testing@maddy.test>\n" +
546		"Subject: Hi there!\n" +
547		"\n" +
548		"Nice to meet you!\n"
549	submitMsg := func(conn *tests.Conn, from string) {
550		// Fairly trivial SMTP transaction.
551		conn.Writeln("MAIL FROM:<" + from + ">")
552		conn.ExpectPattern("250 *")
553		conn.Writeln("RCPT TO:<testing@maddy.test>")
554		conn.ExpectPattern("250 *")
555		conn.Writeln("DATA")
556		conn.ExpectPattern("354 *")
557		conn.Writeln("From: <testing@sender.test>")
558		conn.Writeln("To: <testing@maddy.test>")
559		conn.Writeln("Subject: Hi there!")
560		conn.Writeln("")
561		conn.Writeln("Nice to meet you!")
562		conn.Writeln(".")
563	}
564
565	t.Subtest("Message dump", func(t *tests.T) {
566		conn := conn.Rebind(t)
567
568		submitMsg(conn, "testing@maddy.test")
569		conn.ExpectPattern("250 *")
570
571		msgPath := filepath.Join(t.StateDir(), "msg")
572		msgContents, err := ioutil.ReadFile(msgPath)
573		if err != nil {
574			t.Fatal(err)
575		}
576
577		if string(msgContents) != expectedMsg {
578			t.Log("Wrong message contents received by check script!")
579			t.Log("Actual:")
580			t.Log(msgContents)
581			t.Log("Expected:")
582			t.Log(expectedMsg)
583		}
584	})
585	t.Subtest("Message dump + Add header", func(t *tests.T) {
586		conn := conn.Rebind(t)
587
588		submitMsg(conn, "testing+addHeader@maddy.test")
589		conn.ExpectPattern("250 *")
590
591		msgPath := filepath.Join(t.StateDir(), "msg")
592		msgContents, err := ioutil.ReadFile(msgPath)
593		if err != nil {
594			t.Fatal(err)
595		}
596
597		expectedMsg := "X-Added-Header: 1\n" + expectedMsg
598		if string(msgContents) != expectedMsg {
599			t.Log("Wrong message contents received by check script!")
600			t.Log("Actual:")
601			t.Log(msgContents)
602			t.Log("Expected:")
603			t.Log(expectedMsg)
604		}
605	})
606	t.Subtest("Body reject", func(t *tests.T) {
607		conn := conn.Rebind(t)
608
609		submitMsg(conn, "testing+reject@maddy.test")
610		conn.ExpectPattern("550 *")
611
612		msgPath := filepath.Join(t.StateDir(), "msg")
613		msgContents, err := ioutil.ReadFile(msgPath)
614		if err != nil {
615			t.Fatal(err)
616		}
617
618		if string(msgContents) != expectedMsg {
619			t.Log("Wrong message contents received by check script!")
620			t.Log("Actual:")
621			t.Log(msgContents)
622			t.Log("Expected:")
623			t.Log([]byte(expectedMsg))
624		}
625	})
626
627	conn.Writeln("QUIT")
628	conn.ExpectPattern("221 *")
629}
630
631func TestHeaderSizeConstraint(tt *testing.T) {
632	tt.Parallel()
633	t := tests.NewT(tt)
634	t.DNS(nil)
635	t.Port("smtp")
636	t.Config(`
637		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
638			hostname mx.maddy.test
639			tls off
640			deliver_to dummy
641			max_header_size 1K
642		}
643	`)
644	t.Run(1)
645	defer t.Close()
646
647	conn := t.Conn("smtp")
648	defer conn.Close()
649	conn.SMTPNegotation("localhost", nil, nil)
650	conn.Writeln("MAIL FROM:<testsender@maddy.test>")
651	conn.ExpectPattern("250 *")
652	conn.Writeln("RCPT TO:<testing@maddy.test>")
653	conn.ExpectPattern("250 *")
654	conn.Writeln("DATA")
655	conn.ExpectPattern("354 *")
656	conn.Writeln("From: <testing@sender.test>")
657	conn.Writeln("To: <testing@maddy.test>")
658	conn.Writeln("Subject: " + strings.Repeat("A", 2*1024))
659	conn.Writeln("")
660	conn.Writeln("Hi")
661	conn.Writeln(".")
662
663	conn.ExpectPattern("552 5.3.4 Message header size exceeds limit *")
664
665	conn.Writeln("QUIT")
666	conn.ExpectPattern("221 *")
667}