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 dmarc2021import (22 "bufio"23 "context"24 "errors"25 "net"26 "strings"27 "testing"2829 "github.com/emersion/go-message/textproto"30 "github.com/emersion/go-msgauth/authres"31 "github.com/foxcpp/go-mockdns"32)3334func TestDMARC(t *testing.T) {35 test := func(zones map[string]mockdns.Zone, hdr string, authres []authres.Result, policyApplied Policy, dmarcRes authres.ResultValue) {36 t.Helper()37 v := NewVerifier(&mockdns.Resolver{Zones: zones})38 defer v.Close()3940 hdrParsed, err := textproto.ReadHeader(bufio.NewReader(strings.NewReader(hdr)))41 if err != nil {42 panic(err)43 }44 v.FetchRecord(context.Background(), hdrParsed)45 evalRes, policy := v.Apply(authres)4647 if policy != policyApplied {48 t.Errorf("expected applied policy to be '%v', got '%v'", policyApplied, policy)49 }50 if evalRes.Authres.Value != dmarcRes {51 t.Errorf("expected DMARC result to be '%v', got '%v'", dmarcRes, evalRes.Authres.Value)52 }53 }5455 // No policy => DMARC 'none'56 test(map[string]mockdns.Zone{}, "From: hello@example.org\r\n\r\n", []authres.Result{57 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},58 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},59 }, PolicyNone, authres.ResultNone)6061 // Policy present & identifiers align => DMARC 'pass'62 test(map[string]mockdns.Zone{63 "_dmarc.example.org.": {64 TXT: []string{"v=DMARC1; p=none"},65 },66 }, "From: hello@example.org\r\n\r\n", []authres.Result{67 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},68 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},69 }, PolicyNone, authres.ResultPass)7071 // No SPF check run => DMARC 'none', no action taken72 test(map[string]mockdns.Zone{73 "_dmarc.example.org.": {74 TXT: []string{"v=DMARC1; p=reject"},75 },76 }, "From: hello@example.org\r\n\r\n", []authres.Result{77 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},78 }, PolicyNone, authres.ResultNone)7980 // No DKIM check run => DMARC 'none', no action taken81 test(map[string]mockdns.Zone{82 "_dmarc.example.org.": {83 TXT: []string{"v=DMARC1; p=reject"},84 },85 }, "From: hello@example.org\r\n\r\n", []authres.Result{86 &authres.SPFResult{Value: authres.ResultPass, From: "example.org", Helo: "mx.example.org"},87 }, PolicyNone, authres.ResultNone)8889 // Check org. domain and from domain, prefer from domain.90 // https://tools.ietf.org/html/rfc7489#section-6.6.391 test(map[string]mockdns.Zone{92 "_dmarc.example.org.": {93 TXT: []string{"v=DMARC1; p=none"},94 },95 }, "From: hello@sub.example.org\r\n\r\n", []authres.Result{96 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},97 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},98 }, PolicyNone, authres.ResultPass)99 test(map[string]mockdns.Zone{100 "_dmarc.sub.example.org.": {101 TXT: []string{"v=DMARC1; p=none"},102 },103 }, "From: hello@sub.example.org\r\n\r\n", []authres.Result{104 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},105 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},106 }, PolicyNone, authres.ResultPass)107 test(map[string]mockdns.Zone{108 "_dmarc.sub.example.org.": {109 TXT: []string{"v=DMARC1; p=none"},110 },111 "_dmarc.example.org.": {112 TXT: []string{"v=malformed"},113 },114 }, "From: hello@sub.example.org\r\n\r\n", []authres.Result{115 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},116 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},117 }, PolicyNone, authres.ResultPass)118119 // Non-DMARC records are ignored.120 // https://tools.ietf.org/html/rfc7489#section-6.6.3121 test(map[string]mockdns.Zone{122 "_dmarc.example.org.": {123 TXT: []string{"ignore", "v=DMARC1; p=none"},124 },125 }, "From: hello@sub.example.org\r\n\r\n", []authres.Result{126 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},127 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},128 }, PolicyNone, authres.ResultPass)129130 // Multiple policies => no policy.131 // https://tools.ietf.org/html/rfc7489#section-6.6.3132 test(map[string]mockdns.Zone{133 "_dmarc.example.org.": {134 TXT: []string{"v=DMARC1; p=reject", "v=DMARC1; p=none"},135 },136 }, "From: hello@sub.example.org\r\n\r\n", []authres.Result{137 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},138 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},139 }, PolicyNone, authres.ResultNone)140141 // Malformed policy => no policy142 test(map[string]mockdns.Zone{143 "_dmarc.example.com.": {144 TXT: []string{"v=aaaa"},145 },146 }, "From: hello@example.com\r\n\r\n", []authres.Result{147 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},148 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},149 }, PolicyNone, authres.ResultNone)150151 // Policy fetch error => DMARC 'permerror' but the message152 // is accepted.153 test(map[string]mockdns.Zone{154 "_dmarc.example.com.": {155 Err: errors.New("the dns server is going insane"),156 },157 }, "From: hello@example.com\r\n\r\n", []authres.Result{158 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},159 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},160 }, PolicyNone, authres.ResultPermError)161162 // Policy fetch error => DMARC 'temperror' but the message163 // is accepted ("fail closed")164 test(map[string]mockdns.Zone{165 "_dmarc.example.com.": {166 Err: &net.DNSError{167 Err: "the dns server is going insane, temporary",168 IsTemporary: true,169 },170 },171 }, "From: hello@example.com\r\n\r\n", []authres.Result{172 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},173 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},174 }, PolicyReject, authres.ResultTempError)175176 // Misaligned From vs DKIM => DMARC 'fail'.177 // Side note: More comprehensive tests for alignment evaluation178 // can be found in check/dmarc/evaluate_test.go. This test merely checks179 // that the correct action is taken based on the policy.180 test(map[string]mockdns.Zone{181 "_dmarc.example.com.": {182 TXT: []string{"v=DMARC1; p=none"},183 },184 }, "From: hello@example.com\r\n\r\n", []authres.Result{185 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},186 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},187 }, PolicyNone, authres.ResultFail)188189 // Misaligned From vs DKIM => DMARC 'fail', policy says to reject190 test(map[string]mockdns.Zone{191 "_dmarc.example.com.": {192 TXT: []string{"v=DMARC1; p=reject"},193 },194 }, "From: hello@example.com\r\n\r\n", []authres.Result{195 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},196 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},197 }, PolicyReject, authres.ResultFail)198199 // Misaligned From vs DKIM => DMARC 'fail'200 // Subdomain policy requests no action, main domain policy says to reject.201 test(map[string]mockdns.Zone{202 "_dmarc.example.com.": {203 TXT: []string{"v=DMARC1; sp=none; p=reject"},204 },205 }, "From: hello@sub.example.com\r\n\r\n", []authres.Result{206 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},207 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},208 }, PolicyNone, authres.ResultFail)209210 // Misaligned From vs DKIM => DMARC 'fail', policy says to quarantine.211 test(map[string]mockdns.Zone{212 "_dmarc.example.com.": {213 TXT: []string{"v=DMARC1; p=quarantine"},214 },215 }, "From: hello@example.com\r\n\r\n", []authres.Result{216 &authres.DKIMResult{Value: authres.ResultPass, Domain: "example.org"},217 &authres.SPFResult{Value: authres.ResultNone, From: "example.org", Helo: "mx.example.org"},218 }, PolicyQuarantine, authres.ResultFail)219}