1//go:build integration2// +build integration34/*5Maddy Mail Server - Composable all-in-one email server.6Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors78This program is free software: you can redistribute it and/or modify9it under the terms of the GNU General Public License as published by10the Free Software Foundation, either version 3 of the License, or11(at your option) any later version.1213This program is distributed in the hope that it will be useful,14but WITHOUT ANY WARRANTY; without even the implied warranty of15MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the16GNU General Public License for more details.1718You should have received a copy of the GNU General Public License19along with this program. If not, see <https://www.gnu.org/licenses/>.20*/2122package tests_test2324import (25 "errors"26 "fmt"27 "io/ioutil"28 "path/filepath"29 "strings"30 "testing"3132 "github.com/foxcpp/go-mockdns"33 "github.com/foxcpp/maddy/tests"34)3536func 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.test44 tls self_signed4546 defer_sender_reject no4748 check {49 require_tls50 }51 deliver_to dummy52 }53 `)54 t.Run(1)55 defer t.Close()5657 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}7172func 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.test84 tls off8586 proxy_protocol {87 trust ` + tests.DefaultSourceIP.String() + ` ::1/12888 tls off89 }9091 defer_sender_reject no9293 check {94 spf {95 enforce_early yes96 fail_action reject97 }98 }99100 deliver_to dummy101 }102 `)103 t.Run(1)104 defer t.Close()105106 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}115116func 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.test128 tls off129130 proxy_protocol {131 trust fe80::bad/128132 tls off133 }134135 defer_sender_reject no136137 check {138 spf {139 enforce_early yes140 fail_action reject141 }142 }143144 deliver_to dummy145 }146 `)147 t.Run(1)148 defer t.Close()149150 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}159160func 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.test190 tls off191192 defer_sender_reject no193194 check {195 spf {196 enforce_early yes197198 none_action reject 551199 neutral_action reject200 fail_action reject 552201 softfail_action reject 553202 permerr_action reject 554203 temperr_action reject 455204 }205 }206 deliver_to dummy207 }208 `)209 t.Run(1)210 defer t.Close()211212 conn := t.Conn("smtp")213 defer conn.Close()214 conn.SMTPNegotation("fail.maddy.test", nil, nil)215216 conn.Writeln("MAIL FROM:<testing@pass.maddy.test>")217 conn.ExpectPattern("250 *")218 conn.Writeln("RSET")219 conn.ExpectPattern("250 *")220221 // Actually checks fail.maddy.test.222 conn.Writeln("MAIL FROM:<>")223 conn.ExpectPattern("552 5.7.0 *")224225 conn.SMTPNegotation("pass.maddy.test", nil, nil)226227 conn.Writeln("MAIL FROM:<>")228 conn.ExpectPattern("250 *")229230 conn.Writeln("MAIL FROM:<testing@none.maddy.test>")231 conn.ExpectPattern("551 5.7.0 *")232233 // Also check the default enhanced code is meaningful.234 conn.Writeln("MAIL FROM:<testing@neutral.maddy.test>")235 conn.ExpectPattern("550 5.7.23 *")236237 conn.Writeln("MAIL FROM:<testing@fail.maddy.test>")238 conn.ExpectPattern("552 5.7.0 *")239240 conn.Writeln("MAIL FROM:<testing@softfail.maddy.test>")241 conn.ExpectPattern("553 5.7.0 *")242243 conn.Writeln("MAIL FROM:<testing@permerr.maddy.test>")244 conn.ExpectPattern("554 5.7.0 *")245246 conn.Writeln("MAIL FROM:<testing@temperr.maddy.test>")247 conn.ExpectPattern("455 4.7.0 *")248249 conn.Writeln("QUIT")250 conn.ExpectPattern("221 *")251}252253func 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.test289 tls off290291 defer_sender_reject no292293 check {294 spf {295 enforce_early no296297 none_action ignore298 neutral_action reject299 fail_action reject300 softfail_action reject301 permerr_action reject302 temperr_action reject303 }304 }305 deliver_to dummy306 }307 `)308 t.Run(1)309 defer t.Close()310311 conn := t.Conn("smtp")312 defer conn.Close()313 conn.SMTPNegotation("localhost", nil, nil)314315 msg := func(fromEnv, fromHdr, bodyRespPattern string) {316 tt.Helper()317318 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 }330331 msg("test@subdomain.maddy-dmarc.test", "test@subdomain.maddy-dmarc.test", "550 *")332333 // Malformed From domain, DMARC cannot work so use only SPF.334 msg("test@subdomain.maddy-dmarc.test", "", "550 *")335336 msg("test@subdomain.maddy-dmarc.test", "maddy-dmarc-lookup-fail.test", "550 *")337338 // 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 *")341342 msg("test@maddy-no-dmarc.test", "test@maddy-no-dmarc.test", "550 *")343344 conn.Writeln("QUIT")345 conn.ExpectPattern("221 *")346}347348func 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.test363 tls off364365 defer_sender_reject no366367 check {368 dnsbl {369 reject_threshold 1370371 dnsbl.test {372 client_ipv4373 mailfrom374 }375 }376 }377 deliver_to dummy378 }379 `)380 t.Run(1)381 defer t.Close()382383 conn := t.Conn("smtp")384 defer conn.Close()385 conn.SMTPNegotation("localhost", nil, nil)386387 conn.Writeln("MAIL FROM:<testing@sender.test>")388 conn.ExpectPattern("554 5.7.0 Client identity is listed in the used DNSBL *")389390 conn.Writeln("MAIL FROM:<testing@misc.test>")391 conn.ExpectPattern("554 5.7.0 Client identity is listed in the used DNSBL *")392393 conn.Writeln("QUIT")394 conn.ExpectPattern("221 *")395}396397func 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.test412 tls off413414 defer_sender_reject no415416 check {417 dnsbl {418 reject_threshold 1419420 dnsbl.test {421 mailfrom422 }423 dnsbl2.test {424 client_ipv4425 score -1426 }427 }428 }429 deliver_to dummy430 }431 `)432 t.Run(1)433 defer t.Close()434435 conn := t.Conn("smtp")436 defer conn.Close()437 conn.SMTPNegotation("localhost", nil, nil)438439 conn.Writeln("MAIL FROM:<testing@sender.test>")440 conn.ExpectPattern("250 *")441442 conn.Writeln("QUIT")443 conn.ExpectPattern("221 *")444}445446func 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.test454 tls off455456 auth dummy457 defer_sender_reject off458459 source example1.org {460 check {461 authorize_sender {462 auth_normalize precis_casefold463 user_to_email static {464 entry "test-user1" "test@example1.org"465 entry "test-user2" "é@example1.org"466 }467 }468 }469 deliver_to dummy470 }471 source example2.org {472 check {473 authorize_sender {474 auth_normalize precis_casefold475 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 dummy485 }486487 default_source {488 reject489 }490 }`)491 t.Run(1)492 defer t.Close()493494 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-user1499 c.Writeln("MAIL FROM:<test3@example1.org>")500 c.ExpectPattern("5*") // rejected - unknown email501 c.Writeln("MAIL FROM:<E\u0301@EXAMPLE1.org> SMTPUTF8")502 c.ExpectPattern("2*") // OK - é@example1.org belongs to test-user2503 c.Close()504505 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-user2510 c.Writeln("MAIL FROM:<test@example2.org>")511 c.ExpectPattern("2*") // OK - test@example2.org belongs to test-user512 c.Writeln("MAIL FROM:<alias-to-test@example2.org>")513 c.ExpectPattern("2*") // OK - test@example2.org belongs to test-user514 c.Close()515}516517func 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.test525 tls off526527 check {528 command {env:TEST_PWD}/testdata/check_command.sh {sender} {529 code 12 reject530 }531 }532 deliver_to dummy533 }534 `)535 t.Run(1)536 defer t.Close()537538 conn := t.Conn("smtp")539 defer conn.Close()540 conn.SMTPNegotation("localhost", nil, nil)541542 // Note: Internally, messages are handled using LF line endings, being543 // 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 }564565 t.Subtest("Message dump", func(t *tests.T) {566 conn := conn.Rebind(t)567568 submitMsg(conn, "testing@maddy.test")569 conn.ExpectPattern("250 *")570571 msgPath := filepath.Join(t.StateDir(), "msg")572 msgContents, err := ioutil.ReadFile(msgPath)573 if err != nil {574 t.Fatal(err)575 }576577 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)587588 submitMsg(conn, "testing+addHeader@maddy.test")589 conn.ExpectPattern("250 *")590591 msgPath := filepath.Join(t.StateDir(), "msg")592 msgContents, err := ioutil.ReadFile(msgPath)593 if err != nil {594 t.Fatal(err)595 }596597 expectedMsg := "X-Added-Header: 1\n" + expectedMsg598 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)608609 submitMsg(conn, "testing+reject@maddy.test")610 conn.ExpectPattern("550 *")611612 msgPath := filepath.Join(t.StateDir(), "msg")613 msgContents, err := ioutil.ReadFile(msgPath)614 if err != nil {615 t.Fatal(err)616 }617618 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 })626627 conn.Writeln("QUIT")628 conn.ExpectPattern("221 *")629}630631func 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.test639 tls off640 deliver_to dummy641 max_header_size 1K642 }643 `)644 t.Run(1)645 defer t.Close()646647 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(".")662663 conn.ExpectPattern("552 5.3.4 Message header size exceeds limit *")664665 conn.Writeln("QUIT")666 conn.ExpectPattern("221 *")667}