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 check2021import (22 "context"23 "fmt"24 "runtime/trace"2526 "github.com/emersion/go-message/textproto"27 "github.com/foxcpp/maddy/framework/buffer"28 "github.com/foxcpp/maddy/framework/config"29 modconfig "github.com/foxcpp/maddy/framework/config/module"30 "github.com/foxcpp/maddy/framework/dns"31 "github.com/foxcpp/maddy/framework/log"32 "github.com/foxcpp/maddy/framework/module"33 "github.com/foxcpp/maddy/internal/target"34)3536type (37 StatelessCheckContext struct {38 // Embedded context.Context value, used for tracing, cancellation and39 // timeouts.40 context.Context4142 // Resolver that should be used by the check for DNS queries.43 Resolver dns.Resolver4445 MsgMeta *module.MsgMetadata4647 // Logger that should be used by the check for logging, note that it is48 // already wrapped to append Msg ID to all messages so check code49 // should not do the same.50 Logger log.Logger51 }52 FuncConnCheck func(checkContext StatelessCheckContext) module.CheckResult53 FuncSenderCheck func(checkContext StatelessCheckContext, mailFrom string) module.CheckResult54 FuncRcptCheck func(checkContext StatelessCheckContext, rcptTo string) module.CheckResult55 FuncBodyCheck func(checkContext StatelessCheckContext, header textproto.Header, body buffer.Buffer) module.CheckResult56)5758type statelessCheck struct {59 modName string60 instName string61 resolver dns.Resolver62 logger log.Logger6364 // One used by Init if config option is not passed by a user.65 defaultFailAction modconfig.FailAction66 // The actual fail action that should be applied.67 failAction modconfig.FailAction6869 connCheck FuncConnCheck70 senderCheck FuncSenderCheck71 rcptCheck FuncRcptCheck72 bodyCheck FuncBodyCheck73}7475type statelessCheckState struct {76 c *statelessCheck77 msgMeta *module.MsgMetadata78}7980func (s *statelessCheckState) String() string {81 return s.c.modName + ":" + s.c.instName82}8384func (s *statelessCheckState) CheckConnection(ctx context.Context) module.CheckResult {85 if s.c.connCheck == nil {86 return module.CheckResult{}87 }88 defer trace.StartRegion(ctx, s.c.modName+"/CheckConnection").End()8990 originalRes := s.c.connCheck(StatelessCheckContext{91 Context: ctx,92 Resolver: s.c.resolver,93 MsgMeta: s.msgMeta,94 Logger: target.DeliveryLogger(s.c.logger, s.msgMeta),95 })96 return s.c.failAction.Apply(originalRes)97}9899func (s *statelessCheckState) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {100 if s.c.senderCheck == nil {101 return module.CheckResult{}102 }103 defer trace.StartRegion(ctx, s.c.modName+"/CheckSender").End()104105 originalRes := s.c.senderCheck(StatelessCheckContext{106 Context: ctx,107 Resolver: s.c.resolver,108 MsgMeta: s.msgMeta,109 Logger: target.DeliveryLogger(s.c.logger, s.msgMeta),110 }, mailFrom)111 return s.c.failAction.Apply(originalRes)112}113114func (s *statelessCheckState) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {115 if s.c.rcptCheck == nil {116 return module.CheckResult{}117 }118 defer trace.StartRegion(ctx, s.c.modName+"/CheckRcpt").End()119120 originalRes := s.c.rcptCheck(StatelessCheckContext{121 Context: ctx,122 Resolver: s.c.resolver,123 MsgMeta: s.msgMeta,124 Logger: target.DeliveryLogger(s.c.logger, s.msgMeta),125 }, rcptTo)126 return s.c.failAction.Apply(originalRes)127}128129func (s *statelessCheckState) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {130 if s.c.bodyCheck == nil {131 return module.CheckResult{}132 }133 defer trace.StartRegion(ctx, s.c.modName+"/CheckBody").End()134135 originalRes := s.c.bodyCheck(StatelessCheckContext{136 Context: ctx,137 Resolver: s.c.resolver,138 MsgMeta: s.msgMeta,139 Logger: target.DeliveryLogger(s.c.logger, s.msgMeta),140 }, header, body)141 return s.c.failAction.Apply(originalRes)142}143144func (s *statelessCheckState) Close() error {145 return nil146}147148func (c *statelessCheck) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {149 return &statelessCheckState{150 c: c,151 msgMeta: msgMeta,152 }, nil153}154155func (c *statelessCheck) Init(cfg *config.Map) error {156 cfg.Bool("debug", true, false, &c.logger.Debug)157 cfg.Custom("fail_action", false, false,158 func() (interface{}, error) {159 return c.defaultFailAction, nil160 }, modconfig.FailActionDirective, &c.failAction)161 _, err := cfg.Process()162 return err163}164165func (c *statelessCheck) Name() string {166 return c.modName167}168169func (c *statelessCheck) InstanceName() string {170 return c.instName171}172173// RegisterStatelessCheck is helper function to create stateless message check modules174// that run one simple check during one stage.175//176// It creates the module and its instance with the specified name that implement module.Check interface177// and runs passed functions when corresponding module.CheckState methods are called.178//179// Note about CheckResult that is returned by the functions:180// StatelessCheck supports different action types based on the user configuration, but the particular check181// code doesn't need to know about it. It should assume that it is always "Reject" and hence it should182// populate Reason field of the result object with the relevant error description.183func RegisterStatelessCheck(name string, defaultFailAction modconfig.FailAction, connCheck FuncConnCheck, senderCheck FuncSenderCheck, rcptCheck FuncRcptCheck, bodyCheck FuncBodyCheck) {184 module.Register(name, func(modName, instName string, aliases, inlineArgs []string) (module.Module, error) {185 if len(inlineArgs) != 0 {186 return nil, fmt.Errorf("%s: inline arguments are not used", modName)187 }188 return &statelessCheck{189 modName: modName,190 instName: instName,191 resolver: dns.DefaultResolver(),192 logger: log.Logger{Name: modName},193194 defaultFailAction: defaultFailAction,195196 connCheck: connCheck,197 senderCheck: senderCheck,198 rcptCheck: rcptCheck,199 bodyCheck: bodyCheck,200 }, nil201 })202}