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 check
 20
 21import (
 22	"context"
 23	"fmt"
 24	"runtime/trace"
 25
 26	"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)
 35
 36type (
 37	StatelessCheckContext struct {
 38		// Embedded context.Context value, used for tracing, cancellation and
 39		// timeouts.
 40		context.Context
 41
 42		// Resolver that should be used by the check for DNS queries.
 43		Resolver dns.Resolver
 44
 45		MsgMeta *module.MsgMetadata
 46
 47		// Logger that should be used by the check for logging, note that it is
 48		// already wrapped to append Msg ID to all messages so check code
 49		// should not do the same.
 50		Logger log.Logger
 51	}
 52	FuncConnCheck   func(checkContext StatelessCheckContext) module.CheckResult
 53	FuncSenderCheck func(checkContext StatelessCheckContext, mailFrom string) module.CheckResult
 54	FuncRcptCheck   func(checkContext StatelessCheckContext, rcptTo string) module.CheckResult
 55	FuncBodyCheck   func(checkContext StatelessCheckContext, header textproto.Header, body buffer.Buffer) module.CheckResult
 56)
 57
 58type statelessCheck struct {
 59	modName  string
 60	instName string
 61	resolver dns.Resolver
 62	logger   log.Logger
 63
 64	// One used by Init if config option is not passed by a user.
 65	defaultFailAction modconfig.FailAction
 66	// The actual fail action that should be applied.
 67	failAction modconfig.FailAction
 68
 69	connCheck   FuncConnCheck
 70	senderCheck FuncSenderCheck
 71	rcptCheck   FuncRcptCheck
 72	bodyCheck   FuncBodyCheck
 73}
 74
 75type statelessCheckState struct {
 76	c       *statelessCheck
 77	msgMeta *module.MsgMetadata
 78}
 79
 80func (s *statelessCheckState) String() string {
 81	return s.c.modName + ":" + s.c.instName
 82}
 83
 84func (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()
 89
 90	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}
 98
 99func (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()
104
105	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}
113
114func (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()
119
120	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}
128
129func (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()
134
135	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}
143
144func (s *statelessCheckState) Close() error {
145	return nil
146}
147
148func (c *statelessCheck) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {
149	return &statelessCheckState{
150		c:       c,
151		msgMeta: msgMeta,
152	}, nil
153}
154
155func (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, nil
160		}, modconfig.FailActionDirective, &c.failAction)
161	_, err := cfg.Process()
162	return err
163}
164
165func (c *statelessCheck) Name() string {
166	return c.modName
167}
168
169func (c *statelessCheck) InstanceName() string {
170	return c.instName
171}
172
173// RegisterStatelessCheck is helper function to create stateless message check modules
174// 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 interface
177// 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 check
181// code doesn't need to know about it. It should assume that it is always "Reject" and hence it should
182// 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},
193
194			defaultFailAction: defaultFailAction,
195
196			connCheck:   connCheck,
197			senderCheck: senderCheck,
198			rcptCheck:   rcptCheck,
199			bodyCheck:   bodyCheck,
200		}, nil
201	})
202}