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 msgpipeline2021import (22 "bufio"23 "context"24 "crypto/sha1"25 "encoding/hex"26 "errors"27 "net"28 "strings"29 "testing"3031 "github.com/emersion/go-message/textproto"32 "github.com/emersion/go-msgauth/authres"33 "github.com/emersion/go-smtp"34 "github.com/foxcpp/go-mockdns"35 "github.com/foxcpp/maddy/framework/buffer"36 "github.com/foxcpp/maddy/framework/exterrors"37 "github.com/foxcpp/maddy/framework/module"38 "github.com/foxcpp/maddy/internal/testutils"39)4041func doTestDelivery(t *testing.T, tgt module.DeliveryTarget, from string, to []string, hdr string) (string, error) {42 t.Helper()4344 IDRaw := sha1.Sum([]byte(t.Name()))45 encodedID := hex.EncodeToString(IDRaw[:])4647 body := buffer.MemoryBuffer{Slice: []byte("foobar")}48 ctx := module.MsgMetadata{49 DontTraceSender: true,50 ID: encodedID,51 }5253 hdrParsed, err := textproto.ReadHeader(bufio.NewReader(strings.NewReader(hdr)))54 if err != nil {55 panic(err)56 }5758 delivery, err := tgt.Start(context.Background(), &ctx, from)59 if err != nil {60 return encodedID, err61 }62 for _, rcpt := range to {63 if err := delivery.AddRcpt(context.Background(), rcpt, smtp.RcptOptions{}); err != nil {64 if err := delivery.Abort(context.Background()); err != nil {65 t.Log("delivery.Abort:", err)66 }67 return encodedID, err68 }69 }70 if err := delivery.Body(context.Background(), hdrParsed, body); err != nil {71 if err := delivery.Abort(context.Background()); err != nil {72 t.Log("delivery.Abort:", err)73 }74 return encodedID, err75 }76 if err := delivery.Commit(context.Background()); err != nil {77 return encodedID, err78 }7980 return encodedID, err81}8283func dmarcResult(t *testing.T, hdr textproto.Header) authres.ResultValue {84 field := hdr.Get("Authentication-Results")85 if field == "" {86 t.Fatalf("No results field")87 }8889 _, results, err := authres.Parse(field)90 if err != nil {91 t.Fatalf("Field parse err: %v", err)92 }9394 for _, res := range results {95 dmarcRes, ok := res.(*authres.DMARCResult)96 if ok {97 return dmarcRes.Value98 }99 }100101 t.Fatalf("No DMARC authres found")102 return ""103}104105func TestDMARC(t *testing.T) {106 test := func(zones map[string]mockdns.Zone, hdr string, authres []authres.Result, reject, quarantine bool, dmarcRes authres.ResultValue) {107 t.Helper()108109 tgt := testutils.Target{}110 p := MsgPipeline{111 msgpipelineCfg: msgpipelineCfg{112 globalChecks: []module.Check{113 &testutils.Check{114 BodyRes: module.CheckResult{115 AuthResult: authres,116 },117 },118 },119 perSource: map[string]sourceBlock{},120 defaultSource: sourceBlock{121 perRcpt: map[string]*rcptBlock{},122 defaultRcpt: &rcptBlock{123 targets: []module.DeliveryTarget{&tgt},124 },125 },126 doDMARC: true,127 },128 Log: testutils.Logger(t, "pipeline"),129 Resolver: &mockdns.Resolver{Zones: zones},130 }131132 _, err := doTestDelivery(t, &p, "test@example.org", []string{"test@example.com"}, hdr)133 if reject {134 if err == nil {135 t.Errorf("expected message to be rejected")136 return137 }138 t.Log(err, exterrors.Fields(err))139 return140 }141 if err != nil {142 t.Errorf("unexpected error: %v %+v", err, exterrors.Fields(err))143 return144 }145146 if len(tgt.Messages) != 1 {147 t.Errorf("got %d messages", len(tgt.Messages))148 return149 }150 msg := tgt.Messages[0]151152 if msg.MsgMeta.Quarantine != quarantine {153 t.Errorf("msg.MsgMeta.Quarantine (%v) != quarantine (%v)", msg.MsgMeta.Quarantine, quarantine)154 return155 }156157 res := dmarcResult(t, msg.Header)158 if res != dmarcRes {159 t.Errorf("expected DMARC result to be '%v', got '%v'", dmarcRes, res)160 return161 }162 }163164 // No policy => DMARC 'none'165 test(map[string]mockdns.Zone{}, "From: hello@example.org\r\n\r\n", []authres.Result{166 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},167 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},168 }, false, false, authres.ResultNone)169170 // Policy present & identifiers align => DMARC 'pass'171 test(map[string]mockdns.Zone{172 "_dmarc.example.org.": {173 TXT: []string{"v=DMARC1; p=none"},174 },175 }, "From: hello@example.org\r\n\r\n", []authres.Result{176 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},177 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},178 }, false, false, authres.ResultPass)179180 // Policy fetch error => DMARC 'permerror' but the message181 // is accepted.182 test(map[string]mockdns.Zone{183 "_dmarc.example.com.": {184 Err: errors.New("the dns server is going insane"),185 },186 }, "From: hello@example.com\r\n\r\n", []authres.Result{187 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},188 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},189 }, false, false, authres.ResultPermError)190191 // Policy fetch error => DMARC 'temperror' but the message192 // is rejected ("fail closed")193 test(map[string]mockdns.Zone{194 "_dmarc.example.com.": {195 Err: &net.DNSError{196 Err: "the dns server is going insane, temporary",197 IsTemporary: true,198 },199 },200 }, "From: hello@example.com\r\n\r\n", []authres.Result{201 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},202 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},203 }, true, false, authres.ResultTempError)204205 // Misaligned From vs DKIM => DMARC 'fail', policy says to reject206 test(map[string]mockdns.Zone{207 "_dmarc.example.com.": {208 TXT: []string{"v=DMARC1; p=reject"},209 },210 }, "From: hello@example.com\r\n\r\n", []authres.Result{211 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},212 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},213 }, true, false, "")214215 // Misaligned From vs DKIM => DMARC 'fail', policy says to quarantine.216 test(map[string]mockdns.Zone{217 "_dmarc.example.com.": {218 TXT: []string{"v=DMARC1; p=quarantine"},219 },220 }, "From: hello@example.com\r\n\r\n", []authres.Result{221 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},222 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},223 }, false, true, authres.ResultFail)224}