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 "context"23 "math/rand"24 "net"25 "runtime/trace"26 "strings"2728 "github.com/emersion/go-message/textproto"29 "github.com/emersion/go-msgauth/authres"30 "github.com/emersion/go-msgauth/dmarc"31)3233type verifyData struct {34 policyDomain string35 fromDomain string36 record *Record37 recordErr error38}3940// errPanic is used to propagate the panic() from the FetchRecord41// goroutine to the goroutine that called Apply.42type errPanic struct {43 err interface{}44}4546func (errPanic) Error() string {47 return "panic during policy fetch"48}4950// Verifier is the structure that wraps all state necessary to verify a51// single message using DMARC checks.52//53// It cannot be reused.54type Verifier struct {55 fetchCh chan verifyData56 fetchCancel context.CancelFunc5758 resolver Resolver5960 // TODO(GH #206): DMARC reporting61 // FailureReportFunc is the callback that is called when a failure report62 // is generated. If it is nil - failure reports generation is disabled.63 // FailureReportFunc func(textproto.Header, io.Reader)64}6566func NewVerifier(r Resolver) *Verifier {67 return &Verifier{68 fetchCh: make(chan verifyData, 1),69 resolver: r,70 }71}7273func (v *Verifier) Close() error {74 if v.fetchCancel != nil {75 v.fetchCancel()76 }77 return nil78}7980// FetchRecord prepares the Verifier by starting the policy lookup. Lookup is81// 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 return91 }9293 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 }()102103 defer trace.StartRegion(ctx, "DMARC/FetchRecord").End()104105 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}114115// 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 be118// caled before calling this function.119//120// It returns the Authentication-Result field to be included in the message (as121// a part of the EvalResult struct) and the appropriate action that should be122// taken by the MTA. In case of PolicyReject, caller should inspect the123// Result.Value to determine whether to use a temporary or permanent error code124// as Apply implements the 'fail closed' strategy for handling of temporary125// errors.126//127// Additionally, it relies on the math/rand default source to be initialized to determine128// whether to apply a policy with the pct key.129func (v *Verifier) Apply(authRes []authres.Result) (EvalResult, Policy) {130 data := <-v.fetchCh131 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.ResultTempError140 // 'fail closed' behavior, reject the message if a temporary error141 // occurs.142 return EvalResult{143 Authres: result,144 }, dmarc.PolicyReject145 }146 return EvalResult{147 Authres: result,148 }, dmarc.PolicyNone149 }150 if data.record == nil {151 return EvalResult{152 Authres: authres.DMARCResult{153 Value: authres.ResultNone,154 From: data.fromDomain,155 },156 }, dmarc.PolicyNone157 }158159 result := EvaluateAlignment(data.fromDomain, data.record, authRes)160 if result.Authres.Value == authres.ResultPass || result.Authres.Value == authres.ResultNone {161 return result, dmarc.PolicyNone162 }163164 if data.record.Percent != nil && rand.Int31n(100) > int32(*data.record.Percent) {165 return result, dmarc.PolicyNone166 }167168 policy := data.record.Policy169 if !strings.EqualFold(data.policyDomain, data.fromDomain) && data.record.SubdomainPolicy != "" {170 policy = data.record.SubdomainPolicy171 }172173 return result, policy174}