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 dkim
 20
 21import (
 22	"bytes"
 23	"context"
 24	"errors"
 25	"io"
 26	nettextproto "net/textproto"
 27	"runtime/trace"
 28	"strings"
 29
 30	"github.com/emersion/go-message/textproto"
 31	"github.com/emersion/go-msgauth/authres"
 32	"github.com/emersion/go-msgauth/dkim"
 33	"github.com/foxcpp/maddy/framework/buffer"
 34	"github.com/foxcpp/maddy/framework/config"
 35	modconfig "github.com/foxcpp/maddy/framework/config/module"
 36	"github.com/foxcpp/maddy/framework/dns"
 37	"github.com/foxcpp/maddy/framework/exterrors"
 38	"github.com/foxcpp/maddy/framework/log"
 39	"github.com/foxcpp/maddy/framework/module"
 40	"github.com/foxcpp/maddy/internal/target"
 41)
 42
 43type Check struct {
 44	instName string
 45	log      log.Logger
 46
 47	requiredFields  map[string]struct{}
 48	brokenSigAction modconfig.FailAction
 49	noSigAction     modconfig.FailAction
 50	failOpen        bool
 51
 52	resolver dns.Resolver
 53}
 54
 55func New(_, instName string, _, inlineArgs []string) (module.Module, error) {
 56	if len(inlineArgs) != 0 {
 57		return nil, errors.New("check.dkim: inline arguments are not used")
 58	}
 59	return &Check{
 60		instName: instName,
 61		log:      log.Logger{Name: "check.dkim"},
 62		resolver: dns.DefaultResolver(),
 63	}, nil
 64}
 65
 66func (c *Check) Init(cfg *config.Map) error {
 67	var requiredFields []string
 68
 69	cfg.Bool("debug", true, false, &c.log.Debug)
 70	cfg.StringList("required_fields", false, false, []string{"From", "Subject"}, &requiredFields)
 71	cfg.Bool("fail_open", false, false, &c.failOpen)
 72	cfg.Custom("broken_sig_action", false, false,
 73		func() (interface{}, error) {
 74			return modconfig.FailAction{}, nil
 75		}, modconfig.FailActionDirective, &c.brokenSigAction)
 76	cfg.Custom("no_sig_action", false, false,
 77		func() (interface{}, error) {
 78			return modconfig.FailAction{}, nil
 79		}, modconfig.FailActionDirective, &c.noSigAction)
 80	_, err := cfg.Process()
 81	if err != nil {
 82		return err
 83	}
 84
 85	c.requiredFields = make(map[string]struct{})
 86	for _, field := range requiredFields {
 87		c.requiredFields[nettextproto.CanonicalMIMEHeaderKey(field)] = struct{}{}
 88	}
 89
 90	return nil
 91}
 92
 93func (c *Check) Name() string {
 94	return "check.dkim"
 95}
 96
 97func (c *Check) InstanceName() string {
 98	return c.instName
 99}
100
101type dkimCheckState struct {
102	c       *Check
103	msgMeta *module.MsgMetadata
104	log     log.Logger
105}
106
107func (d *dkimCheckState) CheckConnection(ctx context.Context) module.CheckResult {
108	return module.CheckResult{}
109}
110
111func (d *dkimCheckState) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {
112	return module.CheckResult{}
113}
114
115func (d *dkimCheckState) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {
116	return module.CheckResult{}
117}
118
119func (d *dkimCheckState) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {
120	defer trace.StartRegion(ctx, "check.dkim/CheckBody").End()
121
122	if !header.Has("DKIM-Signature") {
123		if d.c.noSigAction.Reject || d.c.noSigAction.Quarantine {
124			d.log.Printf("no signatures present")
125		} else {
126			d.log.Debugf("no signatures present")
127		}
128		return d.c.noSigAction.Apply(module.CheckResult{
129			Reason: &exterrors.SMTPError{
130				Code:         550,
131				EnhancedCode: exterrors.EnhancedCode{5, 7, 20},
132				Message:      "No DKIM signatures",
133				CheckName:    "check.dkim",
134			},
135			AuthResult: []authres.Result{
136				&authres.DKIMResult{
137					Value: authres.ResultNone,
138				},
139			},
140		})
141	}
142
143	b := bytes.Buffer{}
144	_ = textproto.WriteHeader(&b, header)
145	bodyRdr, err := body.Open()
146	if err != nil {
147		return module.CheckResult{
148			Reject: true,
149			Reason: exterrors.WithTemporary(
150				exterrors.WithFields(err, map[string]interface{}{
151					"check":    "check.dkim",
152					"smtp_msg": "Internal I/O error",
153				}),
154				true,
155			),
156		}
157	}
158
159	verifications, err := dkim.VerifyWithOptions(io.MultiReader(&b, bodyRdr), &dkim.VerifyOptions{
160		LookupTXT: func(domain string) ([]string, error) {
161			return d.c.resolver.LookupTXT(ctx, domain)
162		},
163	})
164	if err != nil {
165		return module.CheckResult{
166			Reject: true,
167			Reason: exterrors.WithTemporary(
168				exterrors.WithFields(err, map[string]interface{}{
169					"check":    "check.dkim",
170					"smtp_msg": "Internal error during policy check",
171				}),
172				true,
173			),
174		}
175	}
176
177	goodSigs := false
178
179	res := module.CheckResult{AuthResult: make([]authres.Result, 0, len(verifications))}
180	for _, verif := range verifications {
181		val := authres.ResultValue(authres.ResultPass)
182		reason := ""
183		if verif.Err != nil {
184			val = authres.ResultFail
185
186			reason = strings.TrimPrefix(verif.Err.Error(), "dkim: ")
187			if !d.c.brokenSigAction.Reject || !d.c.brokenSigAction.Quarantine {
188				d.log.DebugMsg("bad signature", "domain", verif.Domain, "identifier", verif.Identifier)
189			}
190			if dkim.IsPermFail(verif.Err) {
191				val = authres.ResultPermError
192			}
193			if dkim.IsTempFail(verif.Err) {
194				if !d.c.failOpen {
195					return module.CheckResult{
196						Reject: true,
197						Reason: &exterrors.SMTPError{
198							Code:         421,
199							EnhancedCode: exterrors.EnhancedCode{4, 7, 20},
200							Message:      "Temporary error during DKIM verification",
201							CheckName:    "check.dkim",
202							Err:          verif.Err,
203						},
204					}
205				}
206				val = authres.ResultTempError
207			}
208
209			res.AuthResult = append(res.AuthResult, &authres.DKIMResult{
210				Value:      val,
211				Reason:     reason,
212				Domain:     verif.Domain,
213				Identifier: verif.Identifier,
214			})
215			continue
216		}
217
218		signedFields := make(map[string]struct{}, len(verif.HeaderKeys))
219		for _, field := range verif.HeaderKeys {
220			signedFields[nettextproto.CanonicalMIMEHeaderKey(field)] = struct{}{}
221		}
222		for field := range d.c.requiredFields {
223			if _, ok := signedFields[field]; !ok {
224				val = authres.ResultPermError
225				reason = "some header fields are not signed"
226			}
227		}
228
229		if val == authres.ResultPass {
230			goodSigs = true
231			d.log.DebugMsg("good signature", "domain", verif.Domain, "identifier", verif.Identifier)
232		}
233
234		res.AuthResult = append(res.AuthResult, &authres.DKIMResult{
235			Value:      val,
236			Reason:     reason,
237			Domain:     verif.Domain,
238			Identifier: verif.Identifier,
239		})
240	}
241
242	if !goodSigs {
243		res.Reason = &exterrors.SMTPError{
244			Code:         550,
245			EnhancedCode: exterrors.EnhancedCode{5, 7, 20},
246			Message:      "No passing DKIM signatures",
247			CheckName:    "check.dkim",
248		}
249		return d.c.brokenSigAction.Apply(res)
250	}
251	return res
252}
253
254func (d *dkimCheckState) Name() string {
255	return "check.dkim"
256}
257
258func (d *dkimCheckState) Close() error {
259	return nil
260}
261
262func (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {
263	return &dkimCheckState{
264		c:       c,
265		msgMeta: msgMeta,
266		log:     target.DeliveryLogger(c.log, msgMeta),
267	}, nil
268}
269
270func init() {
271	module.Register("check.dkim", New)
272}