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 dkim2021import (22 "bytes"23 "context"24 "errors"25 "io"26 nettextproto "net/textproto"27 "runtime/trace"28 "strings"2930 "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)4243type Check struct {44 instName string45 log log.Logger4647 requiredFields map[string]struct{}48 brokenSigAction modconfig.FailAction49 noSigAction modconfig.FailAction50 failOpen bool5152 resolver dns.Resolver53}5455func 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 }, nil64}6566func (c *Check) Init(cfg *config.Map) error {67 var requiredFields []string6869 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{}, nil75 }, modconfig.FailActionDirective, &c.brokenSigAction)76 cfg.Custom("no_sig_action", false, false,77 func() (interface{}, error) {78 return modconfig.FailAction{}, nil79 }, modconfig.FailActionDirective, &c.noSigAction)80 _, err := cfg.Process()81 if err != nil {82 return err83 }8485 c.requiredFields = make(map[string]struct{})86 for _, field := range requiredFields {87 c.requiredFields[nettextproto.CanonicalMIMEHeaderKey(field)] = struct{}{}88 }8990 return nil91}9293func (c *Check) Name() string {94 return "check.dkim"95}9697func (c *Check) InstanceName() string {98 return c.instName99}100101type dkimCheckState struct {102 c *Check103 msgMeta *module.MsgMetadata104 log log.Logger105}106107func (d *dkimCheckState) CheckConnection(ctx context.Context) module.CheckResult {108 return module.CheckResult{}109}110111func (d *dkimCheckState) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {112 return module.CheckResult{}113}114115func (d *dkimCheckState) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {116 return module.CheckResult{}117}118119func (d *dkimCheckState) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {120 defer trace.StartRegion(ctx, "check.dkim/CheckBody").End()121122 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 }142143 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 }158159 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 }176177 goodSigs := false178179 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.ResultFail185186 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.ResultPermError192 }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.ResultTempError207 }208209 res.AuthResult = append(res.AuthResult, &authres.DKIMResult{210 Value: val,211 Reason: reason,212 Domain: verif.Domain,213 Identifier: verif.Identifier,214 })215 continue216 }217218 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.ResultPermError225 reason = "some header fields are not signed"226 }227 }228229 if val == authres.ResultPass {230 goodSigs = true231 d.log.DebugMsg("good signature", "domain", verif.Domain, "identifier", verif.Identifier)232 }233234 res.AuthResult = append(res.AuthResult, &authres.DKIMResult{235 Value: val,236 Reason: reason,237 Domain: verif.Domain,238 Identifier: verif.Identifier,239 })240 }241242 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 res252}253254func (d *dkimCheckState) Name() string {255 return "check.dkim"256}257258func (d *dkimCheckState) Close() error {259 return nil260}261262func (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 }, nil268}269270func init() {271 module.Register("check.dkim", New)272}