maddy

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

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

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package dmarc
 20
 21import (
 22	"bufio"
 23	"context"
 24	"errors"
 25	"net"
 26	"strings"
 27	"testing"
 28
 29	"github.com/emersion/go-message/textproto"
 30	"github.com/emersion/go-msgauth/authres"
 31	"github.com/foxcpp/go-mockdns"
 32)
 33
 34func 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()
 39
 40		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)
 46
 47		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	}
 54
 55	// 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)
 60
 61	// 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)
 70
 71	// No SPF check run => DMARC 'none', no action taken
 72	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)
 79
 80	// No DKIM check run => DMARC 'none', no action taken
 81	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)
 88
 89	// Check org. domain and from domain, prefer from domain.
 90	// https://tools.ietf.org/html/rfc7489#section-6.6.3
 91	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)
118
119	// Non-DMARC records are ignored.
120	// https://tools.ietf.org/html/rfc7489#section-6.6.3
121	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)
129
130	// Multiple policies => no policy.
131	// https://tools.ietf.org/html/rfc7489#section-6.6.3
132	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)
140
141	// Malformed policy => no policy
142	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)
150
151	// Policy fetch error => DMARC 'permerror' but the message
152	// 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)
161
162	// Policy fetch error => DMARC 'temperror' but the message
163	// 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)
175
176	// Misaligned From vs DKIM => DMARC 'fail'.
177	// Side note: More comprehensive tests for alignment evaluation
178	// can be found in check/dmarc/evaluate_test.go. This test merely checks
179	// 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)
188
189	// Misaligned From vs DKIM => DMARC 'fail', policy says to reject
190	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)
198
199	// 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)
209
210	// 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}