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	"context"
 23	"math/rand"
 24	"net"
 25	"runtime/trace"
 26	"strings"
 27
 28	"github.com/emersion/go-message/textproto"
 29	"github.com/emersion/go-msgauth/authres"
 30	"github.com/emersion/go-msgauth/dmarc"
 31)
 32
 33type verifyData struct {
 34	policyDomain string
 35	fromDomain   string
 36	record       *Record
 37	recordErr    error
 38}
 39
 40// errPanic is used to propagate the panic() from the FetchRecord
 41// goroutine to the goroutine that called Apply.
 42type errPanic struct {
 43	err interface{}
 44}
 45
 46func (errPanic) Error() string {
 47	return "panic during policy fetch"
 48}
 49
 50// Verifier is the structure that wraps all state necessary to verify a
 51// single message using DMARC checks.
 52//
 53// It cannot be reused.
 54type Verifier struct {
 55	fetchCh     chan verifyData
 56	fetchCancel context.CancelFunc
 57
 58	resolver Resolver
 59
 60	// TODO(GH #206): DMARC reporting
 61	// FailureReportFunc is the callback that is called when a failure report
 62	// is generated. If it is nil - failure reports generation is disabled.
 63	// FailureReportFunc func(textproto.Header, io.Reader)
 64}
 65
 66func NewVerifier(r Resolver) *Verifier {
 67	return &Verifier{
 68		fetchCh:  make(chan verifyData, 1),
 69		resolver: r,
 70	}
 71}
 72
 73func (v *Verifier) Close() error {
 74	if v.fetchCancel != nil {
 75		v.fetchCancel()
 76	}
 77	return nil
 78}
 79
 80// FetchRecord prepares the Verifier by starting the policy lookup. Lookup is
 81// performed asynchronously to improve performance.
 82//
 83// If panic occurs in the lookup goroutine - call to Apply will panic.
 84func (v *Verifier) FetchRecord(ctx context.Context, header textproto.Header) {
 85	fromDomain, err := ExtractFromDomain(header)
 86	if err != nil {
 87		v.fetchCh <- verifyData{
 88			recordErr: err,
 89		}
 90		return
 91	}
 92
 93	ctx, v.fetchCancel = context.WithCancel(ctx)
 94	go func() {
 95		defer func() {
 96			if err := recover(); err != nil {
 97				v.fetchCh <- verifyData{
 98					recordErr: errPanic{err: err},
 99				}
100			}
101		}()
102
103		defer trace.StartRegion(ctx, "DMARC/FetchRecord").End()
104
105		policyDomain, record, err := FetchRecord(ctx, v.resolver, fromDomain)
106		v.fetchCh <- verifyData{
107			policyDomain: policyDomain,
108			fromDomain:   fromDomain,
109			record:       record,
110			recordErr:    err,
111		}
112	}()
113}
114
115// Apply actually performs all actions necessary to apply a DMARC policy to the message.
116//
117// The authRes slice should contain results for DKIM and SPF checks. FetchRecord should be
118// caled before calling this function.
119//
120// It returns the Authentication-Result field to be included in the message (as
121// a part of the EvalResult struct) and the appropriate action that should be
122// taken by the MTA. In case of PolicyReject, caller should inspect the
123// Result.Value to determine whether to use a temporary or permanent error code
124// as Apply implements the 'fail closed' strategy for handling of temporary
125// errors.
126//
127// Additionally, it relies on the math/rand default source to be initialized to determine
128// whether to apply a policy with the pct key.
129func (v *Verifier) Apply(authRes []authres.Result) (EvalResult, Policy) {
130	data := <-v.fetchCh
131	if data.recordErr != nil {
132		result := authres.DMARCResult{
133			Value:  authres.ResultPermError,
134			Reason: "Policy lookup failed: " + data.recordErr.Error(),
135			// If may be empty, but it is fine (it will not be included in the field then).
136			From: data.fromDomain,
137		}
138		if dnsErr, ok := data.recordErr.(*net.DNSError); ok && dnsErr.Temporary() {
139			result.Value = authres.ResultTempError
140			// 'fail closed' behavior, reject the message if a temporary error
141			// occurs.
142			return EvalResult{
143				Authres: result,
144			}, dmarc.PolicyReject
145		}
146		return EvalResult{
147			Authres: result,
148		}, dmarc.PolicyNone
149	}
150	if data.record == nil {
151		return EvalResult{
152			Authres: authres.DMARCResult{
153				Value: authres.ResultNone,
154				From:  data.fromDomain,
155			},
156		}, dmarc.PolicyNone
157	}
158
159	result := EvaluateAlignment(data.fromDomain, data.record, authRes)
160	if result.Authres.Value == authres.ResultPass || result.Authres.Value == authres.ResultNone {
161		return result, dmarc.PolicyNone
162	}
163
164	if data.record.Percent != nil && rand.Int31n(100) > int32(*data.record.Percent) {
165		return result, dmarc.PolicyNone
166	}
167
168	policy := data.record.Policy
169	if !strings.EqualFold(data.policyDomain, data.fromDomain) && data.record.SubdomainPolicy != "" {
170		policy = data.record.SubdomainPolicy
171	}
172
173	return result, policy
174}