1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package smtp2021import (22 "flag"23 "math/rand"24 "net"25 "os"26 "strconv"27 "strings"28 "testing"29 "time"3031 "github.com/emersion/go-sasl"32 "github.com/emersion/go-smtp"33 "github.com/foxcpp/go-mockdns"34 "github.com/foxcpp/maddy/framework/config"35 "github.com/foxcpp/maddy/framework/exterrors"36 "github.com/foxcpp/maddy/framework/module"37 "github.com/foxcpp/maddy/internal/auth"38 "github.com/foxcpp/maddy/internal/msgpipeline"39 "github.com/foxcpp/maddy/internal/testutils"40)4142var testPort string4344const testMsg = "From: <sender@example.org>\r\n" +45 "Subject: Hello there!\r\n" +46 "\r\n" +47 "foobar\r\n"4849func testEndpoint(t *testing.T, modName string, authMod module.PlainAuth, tgt module.DeliveryTarget, checks []module.Check, cfg []config.Node) *Endpoint {50 t.Helper()5152 mod, err := New(modName, []string{"tcp://127.0.0.1:" + testPort})53 if err != nil {54 t.Fatal(err)55 }56 endp := mod.(*Endpoint)5758 endp.resolver = &mockdns.Resolver{59 Zones: map[string]mockdns.Zone{60 "mx.example.org.": {61 A: []string{"127.0.0.1"},62 },63 "1.0.0.127.in-addr.arpa.": {64 PTR: []string{"mx.example.org"},65 },66 },67 }68 endp.Log = testutils.Logger(t, "smtp")6970 cfg = append(cfg,71 config.Node{72 Name: "hostname",73 Args: []string{"mx.example.com"},74 },75 config.Node{76 Name: "tls",77 Args: []string{"off"},78 },79 config.Node{ // To make it succeed, pipeline is actually replaced below.80 Name: "deliver_to",81 Args: []string{"dummy"},82 },83 )8485 if authMod != nil {86 cfg = append(cfg, config.Node{87 Name: "auth",88 Args: []string{"dummy"},89 })90 }9192 err = endp.Init(config.NewMap(nil, config.Node{93 Children: cfg,94 }))95 if err != nil {96 t.Fatal(err)97 }9899 endp.saslAuth = auth.SASLAuth{100 Log: testutils.Logger(t, "smtp/saslauth"),101 Plain: []module.PlainAuth{authMod},102 }103104 endp.pipeline = msgpipeline.Mock(tgt, checks)105 endp.pipeline.Hostname = "mx.example.com"106 endp.pipeline.Resolver = endp.resolver107 endp.pipeline.FirstPipeline = true108 endp.pipeline.Log = testutils.Logger(t, "smtp/pipeline")109110 return endp111}112113func submitMsg(t *testing.T, cl *smtp.Client, from string, rcpts []string, msg string) error {114 return submitMsgOpts(t, cl, from, rcpts, nil, msg)115}116117func submitMsgOpts(t *testing.T, cl *smtp.Client, from string, rcpts []string, opts *smtp.MailOptions, msg string) error {118 t.Helper()119120 // Error for this one is ignored because it fails if EHLO was already sent121 // and submitMsg can happen multiple times.122 _ = cl.Hello("mx.example.org")123 if err := cl.Mail(from, opts); err != nil {124 return err125 }126 for _, rcpt := range rcpts {127 if err := cl.Rcpt(rcpt, &smtp.RcptOptions{}); err != nil {128 return err129 }130 }131 data, err := cl.Data()132 if err != nil {133 return err134 }135 if _, err := data.Write([]byte(msg)); err != nil {136 return err137 }138139 return data.Close()140}141142func TestSMTPDelivery(t *testing.T) {143 tgt := testutils.Target{}144 endp := testEndpoint(t, "smtp", nil, &tgt, nil, nil)145 defer endp.Close()146147 cl, err := smtp.Dial("127.0.0.1:" + testPort)148 if err != nil {149 t.Fatal(err)150 }151 defer cl.Close()152153 err = submitMsg(t, cl, "sender@example.org", []string{"rcpt1@example.com", "rcpt2@example.com"}, testMsg)154 if err != nil {155 t.Fatal(err)156 }157158 if len(tgt.Messages) != 1 {159 t.Fatal("Expected a message, got", len(tgt.Messages))160 }161 msg := tgt.Messages[0]162 msgID := testutils.CheckMsgID(t, &msg, "sender@example.org", []string{"rcpt1@example.com", "rcpt2@example.com"}, "")163164 receivedPrefix := `from mx.example.org (mx.example.org [127.0.0.1]) by mx.example.com (envelope-sender <sender@example.org>) with ESMTP id ` + msgID165166 if !strings.HasPrefix(msg.Header.Get("Received"), receivedPrefix) {167 t.Error("Wrong Received contents:", msg.Header.Get("Received"))168 }169170 if msg.MsgMeta.Conn.Proto != "ESMTP" {171 t.Error("Wrong SrcProto:", msg.MsgMeta.Conn.Proto)172 }173174 rdnsName, _ := msg.MsgMeta.Conn.RDNSName.Get()175 if rdnsName, _ := rdnsName.(string); rdnsName != "mx.example.org" {176 t.Error("Wrong rDNS name:", rdnsName)177 }178}179180func TestSMTPDelivery_rDNSError(t *testing.T) {181 tgt := testutils.Target{}182 endp := testEndpoint(t, "smtp", nil, &tgt, nil, nil)183 defer endp.Close()184185 endp.resolver.(*mockdns.Resolver).Zones["1.0.0.127.in-addr.arpa."] = mockdns.Zone{186 Err: &net.DNSError{187 Name: "1.0.0.127.in-addr.arpa.",188 Server: "127.0.0.1:53",189 Err: "bad",190 IsNotFound: false,191 },192 }193194 cl, err := smtp.Dial("127.0.0.1:" + testPort)195 if err != nil {196 t.Fatal(err)197 }198 defer cl.Close()199200 err = submitMsg(t, cl, "sender@example.org", []string{"rcpt1@example.com", "rcpt2@example.com"}, testMsg)201 if err != nil {202 t.Fatal(err)203 }204205 if len(tgt.Messages) != 1 {206 t.Fatal("Expected a message, got", len(tgt.Messages))207 }208 msg := tgt.Messages[0]209 testutils.CheckMsgID(t, &msg, "sender@example.org", []string{"rcpt1@example.com", "rcpt2@example.com"}, "")210211 rdnsName, err := msg.MsgMeta.Conn.RDNSName.Get()212 if rdnsName != nil || err == nil {213 t.Errorf("Wrong rDNS result: %#+v (%v)", rdnsName, err)214 }215}216217func TestSMTPDelivery_EarlyCheck_Fail(t *testing.T) {218 tgt := testutils.Target{}219 endp := testEndpoint(t, "smtp", nil, &tgt, []module.Check{220 &testutils.Check{221 EarlyErr: &exterrors.SMTPError{222 Code: 523,223 Message: "Hey",224 },225 },226 }, nil)227 defer endp.Close()228229 cl, err := smtp.Dial("127.0.0.1:" + testPort)230 if err != nil {231 t.Fatal(err)232 }233 defer cl.Close()234235 err = cl.Mail("sender@example.org", nil)236 if err == nil {237 t.Fatal("Expected an error, got none")238 }239240 smtpErr, ok := err.(*smtp.SMTPError)241 if !ok {242 t.Fatal("Non-SMTPError returned")243 }244245 if smtpErr.Code != 523 {246 t.Fatal("Wrong SMTP code:", smtpErr.Code)247 }248 if smtpErr.Message != "Hey" {249 t.Fatal("Wrong SMTP message:", smtpErr.Message)250 }251}252253func TestSMTPDeliver_CheckError(t *testing.T) {254 tgt := testutils.Target{}255 endp := testEndpoint(t, "smtp", nil, &tgt, []module.Check{256 &testutils.Check{257 ConnRes: module.CheckResult{258 Reason: &exterrors.SMTPError{259 Code: 523,260 Message: "Hey",261 },262 Reject: true,263 },264 },265 }, nil)266 endp.deferServerReject = false267 defer endp.Close()268269 cl, err := smtp.Dial("127.0.0.1:" + testPort)270 if err != nil {271 t.Fatal(err)272 }273 defer cl.Close()274275 err = cl.Mail("sender@example.org", nil)276 if err == nil {277 t.Fatal("Expected an error, got none")278 }279 smtpErr, ok := err.(*smtp.SMTPError)280 if !ok {281 t.Fatal("Non-SMTPError returned")282 }283284 if smtpErr.Code != 523 {285 t.Fatal("Wrong SMTP code:", smtpErr.Code)286 }287 if !strings.HasPrefix(smtpErr.Message, "Hey") {288 t.Fatal("Wrong SMTP message:", smtpErr.Message)289 }290}291292func TestSMTPDeliver_CheckError_Deferred(t *testing.T) {293 tgt := testutils.Target{}294 endp := testEndpoint(t, "smtp", nil, &tgt, []module.Check{295 &testutils.Check{296 ConnRes: module.CheckResult{297 Reason: &exterrors.SMTPError{298 Code: 523,299 Message: "Hey",300 },301 Reject: true,302 },303 },304 }, nil)305 endp.deferServerReject = true306 defer endp.Close()307308 cl, err := smtp.Dial("127.0.0.1:" + testPort)309 if err != nil {310 t.Fatal(err)311 }312 defer cl.Close()313314 err = cl.Mail("sender@example.org", nil)315 if err != nil {316 t.Fatal(err)317 }318319 checkErr := func(err error) {320 if err == nil {321 t.Fatal("Expected an error, got none")322 }323 smtpErr, ok := err.(*smtp.SMTPError)324 if !ok {325 t.Error("Non-SMTPError returned")326 return327 }328329 if smtpErr.Code != 523 {330 t.Error("Wrong SMTP code:", smtpErr.Code)331 }332 if !strings.HasPrefix(smtpErr.Message, "Hey") {333 t.Error("Wrong SMTP message:", smtpErr.Message)334 }335 }336337 checkErr(cl.Rcpt("test1@example.org", &smtp.RcptOptions{}))338 checkErr(cl.Rcpt("test1@example.org", &smtp.RcptOptions{}))339 checkErr(cl.Rcpt("test2@example.org", &smtp.RcptOptions{}))340}341342func TestSMTPDelivery_Multi(t *testing.T) {343 tgt := testutils.Target{}344 endp := testEndpoint(t, "smtp", nil, &tgt, nil, nil)345 defer endp.Close()346347 cl, err := smtp.Dial("127.0.0.1:" + testPort)348 if err != nil {349 t.Fatal(err)350 }351 defer cl.Close()352353 err = submitMsg(t, cl, "sender1@example.org", []string{"rcpt1@example.com", "rcpt2@example.com"}, testMsg)354 if err != nil {355 t.Fatal(err)356 }357 err = submitMsg(t, cl, "sender2@example.org", []string{"rcpt3@example.com", "rcpt4@example.com"}, testMsg)358 if err != nil {359 t.Fatal(err)360 }361362 if len(tgt.Messages) != 2 {363 t.Fatal("Expected two messages, got", len(tgt.Messages))364 }365 msg := tgt.Messages[0]366 msgID := testutils.CheckMsgID(t, &msg, "sender1@example.org", []string{"rcpt1@example.com", "rcpt2@example.com"}, "")367 receivedPrefix := `from mx.example.org (mx.example.org [127.0.0.1]) by mx.example.com (envelope-sender <sender1@example.org>) with ESMTP id ` + msgID368 if !strings.HasPrefix(msg.Header.Get("Received"), receivedPrefix) {369 t.Error("Wrong Received contents:", msg.Header.Get("Received"))370 }371372 msg = tgt.Messages[1]373 msgID = testutils.CheckMsgID(t, &msg, "sender2@example.org", []string{"rcpt3@example.com", "rcpt4@example.com"}, "")374 receivedPrefix = `from mx.example.org (mx.example.org [127.0.0.1]) by mx.example.com (envelope-sender <sender2@example.org>) with ESMTP id ` + msgID375 if !strings.HasPrefix(msg.Header.Get("Received"), receivedPrefix) {376 t.Error("Wrong Received contents:", msg.Header.Get("Received"))377 }378}379380func TestSMTPDelivery_AbortData(t *testing.T) {381 tgt := testutils.Target{}382 endp := testEndpoint(t, "smtp", nil, &tgt, nil, nil)383 defer endp.Close()384385 cl, err := smtp.Dial("127.0.0.1:" + testPort)386 if err != nil {387 t.Fatal(err)388 }389 defer cl.Close()390391 if err := cl.Hello("mx.example.org"); err != nil {392 t.Fatal(err)393 }394 if err := cl.Mail("sender@example.org", nil); err != nil {395 t.Fatal(err)396 }397 if err := cl.Rcpt("test@example.com", &smtp.RcptOptions{}); err != nil {398 t.Fatal(err)399 }400 data, err := cl.Data()401 if err != nil {402 t.Fatal(err)403 }404 if _, err := data.Write([]byte(testMsg)); err != nil {405 t.Fatal(err)406 }407408 // Then.. Suddenly, close the connection without sending the final dot.409 cl.Close()410411 time.Sleep(250 * time.Millisecond)412413 if len(tgt.Messages) != 0 {414 t.Fatal("Expected no messages, got", len(tgt.Messages))415 }416}417418func TestSMTPDelivery_EmptyMessage(t *testing.T) {419 tgt := testutils.Target{}420 endp := testEndpoint(t, "smtp", nil, &tgt, nil, nil)421 defer endp.Close()422423 cl, err := smtp.Dial("127.0.0.1:" + testPort)424 if err != nil {425 t.Fatal(err)426 }427 defer cl.Close()428429 if err := cl.Hello("mx.example.org"); err != nil {430 t.Fatal(err)431 }432 if err := cl.Mail("sender@example.org", nil); err != nil {433 t.Fatal(err)434 }435 if err := cl.Rcpt("test@example.com", &smtp.RcptOptions{}); err != nil {436 t.Fatal(err)437 }438 data, err := cl.Data()439 if err != nil {440 t.Fatal(err)441 }442 if err := data.Close(); err != nil {443 t.Fatal(err)444 }445446 time.Sleep(250 * time.Millisecond)447448 if len(tgt.Messages) != 1 {449 t.Fatal("Expected 1 message, got", len(tgt.Messages))450 }451 msg := tgt.Messages[0]452 if len(msg.Body) != 0 {453 t.Fatal("Expected an empty body, got", len(msg.Body))454 }455}456457func TestSMTPDelivery_AbortLogout(t *testing.T) {458 tgt := testutils.Target{}459 endp := testEndpoint(t, "smtp", nil, &tgt, nil, nil)460 defer endp.Close()461462 cl, err := smtp.Dial("127.0.0.1:" + testPort)463 if err != nil {464 t.Fatal(err)465 }466 defer cl.Close()467468 if err := cl.Hello("mx.example.org"); err != nil {469 t.Fatal(err)470 }471 if err := cl.Mail("sender@example.org", nil); err != nil {472 t.Fatal(err)473 }474 if err := cl.Rcpt("test@example.com", &smtp.RcptOptions{}); err != nil {475 t.Fatal(err)476 }477478 // Then.. Suddenly, close the connection.479 cl.Close()480481 time.Sleep(250 * time.Millisecond)482483 if len(tgt.Messages) != 0 {484 t.Fatal("Expected no messages, got", len(tgt.Messages))485 }486}487488func TestSMTPDelivery_Reset(t *testing.T) {489 tgt := testutils.Target{}490 endp := testEndpoint(t, "smtp", nil, &tgt, nil, nil)491 defer endp.Close()492493 cl, err := smtp.Dial("127.0.0.1:" + testPort)494 if err != nil {495 t.Fatal(err)496 }497 defer cl.Close()498499 if err := cl.Mail("from-garbage@example.org", nil); err != nil {500 t.Fatal(err)501 }502 if err := cl.Rcpt("to-garbage@example.org", &smtp.RcptOptions{}); err != nil {503 t.Fatal(err)504 }505 if err := cl.Reset(); err != nil {506 t.Fatal(err)507 }508509 // then submit the message as if nothing happened.510511 err = submitMsg(t, cl, "sender@example.org", []string{"rcpt1@example.com", "rcpt2@example.com"}, testMsg)512 if err != nil {513 t.Fatal(err)514 }515516 if len(tgt.Messages) != 1 {517 t.Fatal("Expected a message, got", len(tgt.Messages))518 }519 msg := tgt.Messages[0]520 testutils.CheckMsgID(t, &msg, "sender@example.org", []string{"rcpt1@example.com", "rcpt2@example.com"}, "")521}522523func TestSMTPDelivery_SubmissionAuthRequire(t *testing.T) {524 tgt := testutils.Target{}525 endp := testEndpoint(t, "submission", &module.Dummy{}, &tgt, nil, nil)526 defer endp.Close()527528 cl, err := smtp.Dial("127.0.0.1:" + testPort)529 if err != nil {530 t.Fatal(err)531 }532 defer cl.Close()533534 if err := cl.Mail("from-garbage@example.org", nil); err == nil {535 t.Fatal("Expected an error, got none")536 }537}538539func TestSMTPDelivery_SubmissionAuthOK(t *testing.T) {540 tgt := testutils.Target{}541 endp := testEndpoint(t, "submission", &module.Dummy{}, &tgt, nil, nil)542 defer endp.Close()543544 cl, err := smtp.Dial("127.0.0.1:" + testPort)545 if err != nil {546 t.Fatal(err)547 }548 defer cl.Close()549550 if err := cl.Auth(sasl.NewPlainClient("", "user", "password")); err != nil {551 t.Fatal(err)552 }553554 if err := submitMsg(t, cl, "sender@example.org", []string{"rcpt@example.org"}, testMsg); err != nil {555 t.Fatal(err)556 }557558 if len(tgt.Messages) != 1 {559 t.Fatal("Expected a message, got", len(tgt.Messages))560 }561 msg := tgt.Messages[0]562 msgID := testutils.CheckMsgID(t, &msg, "sender@example.org", []string{"rcpt@example.org"}, "")563564 if msg.MsgMeta.Conn.AuthUser != "user" {565 t.Error("Wrong AuthUser:", msg.MsgMeta.Conn.AuthUser)566 }567 if msg.MsgMeta.Conn.AuthPassword != "password" {568 t.Error("Wrong AuthPassword:", msg.MsgMeta.Conn.AuthPassword)569 }570571 receivedPrefix := `by mx.example.com (envelope-sender <sender@example.org>) with ESMTP id ` + msgID572 if !strings.HasPrefix(msg.Header.Get("Received"), receivedPrefix) {573 t.Error("Wrong Received contents:", msg.Header.Get("Received"))574 }575576 if msg.Header.Get("Message-ID") == "" {577 t.Error("No submissionPrepare run")578 }579}580581func TestMain(m *testing.M) {582 remoteSmtpPort := flag.String("test.smtpport", "random", "(maddy) SMTP port to use for connections in tests")583 flag.Parse()584585 if *remoteSmtpPort == "random" {586 *remoteSmtpPort = strconv.Itoa(rand.Intn(65536-10000) + 10000)587 }588589 testPort = *remoteSmtpPort590 os.Exit(m.Run())591}