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 milter
 20
 21import (
 22	"context"
 23	"crypto/tls"
 24	"errors"
 25	"fmt"
 26	"net"
 27	"time"
 28
 29	"github.com/emersion/go-message/textproto"
 30	"github.com/emersion/go-milter"
 31	"github.com/foxcpp/maddy/framework/buffer"
 32	"github.com/foxcpp/maddy/framework/config"
 33	"github.com/foxcpp/maddy/framework/exterrors"
 34	"github.com/foxcpp/maddy/framework/log"
 35	"github.com/foxcpp/maddy/framework/module"
 36	"github.com/foxcpp/maddy/internal/target"
 37)
 38
 39const modName = "check.milter"
 40
 41type Check struct {
 42	cl        *milter.Client
 43	milterUrl string
 44	failOpen  bool
 45	instName  string
 46	log       log.Logger
 47}
 48
 49func New(_, instName string, _, inlineArgs []string) (module.Module, error) {
 50	c := &Check{
 51		instName: instName,
 52		log:      log.Logger{Name: modName, Debug: log.DefaultLogger.Debug},
 53	}
 54	switch len(inlineArgs) {
 55	case 1:
 56		c.milterUrl = inlineArgs[0]
 57	case 0:
 58	default:
 59		return nil, fmt.Errorf("%s: unexpected amount of arguments, want 1 or 0", modName)
 60	}
 61	return c, nil
 62}
 63
 64func (c *Check) Name() string {
 65	return modName
 66}
 67
 68func (c *Check) InstanceName() string {
 69	return c.instName
 70}
 71
 72func (c *Check) Init(cfg *config.Map) error {
 73	cfg.String("endpoint", false, false, c.milterUrl, &c.milterUrl)
 74	cfg.Bool("fail_open", false, false, &c.failOpen)
 75	if _, err := cfg.Process(); err != nil {
 76		return err
 77	}
 78
 79	if c.milterUrl == "" {
 80		return fmt.Errorf("%s: milter endpoint is not set", modName)
 81	}
 82
 83	endp, err := config.ParseEndpoint(c.milterUrl)
 84	if err != nil {
 85		return fmt.Errorf("%s: %v", modName, err)
 86	}
 87
 88	switch endp.Scheme {
 89	case "tcp", "unix":
 90	default:
 91		return fmt.Errorf("%s: scheme unsupported: %v", modName, endp.Scheme)
 92	}
 93
 94	c.cl = milter.NewClientWithOptions(endp.Network(), endp.Address(), milter.ClientOptions{
 95		Dialer: &net.Dialer{
 96			Timeout: 10 * time.Second,
 97		},
 98		ReadTimeout:  10 * time.Second,
 99		WriteTimeout: 10 * time.Second,
100		ActionMask:   milter.OptAddHeader | milter.OptQuarantine,
101		ProtocolMask: 0,
102	})
103
104	return nil
105}
106
107type state struct {
108	c          *Check
109	session    *milter.ClientSession
110	msgMeta    *module.MsgMetadata
111	skipChecks bool
112	log        log.Logger
113}
114
115func (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {
116	session, err := c.cl.Session()
117	if err != nil {
118		return nil, err
119	}
120	return &state{
121		c:       c,
122		session: session,
123		msgMeta: msgMeta,
124		log:     target.DeliveryLogger(c.log, msgMeta),
125	}, nil
126}
127
128func (s *state) handleAction(act *milter.Action) module.CheckResult {
129	switch act.Code {
130	case milter.ActAccept:
131		s.skipChecks = true
132		return module.CheckResult{}
133	case milter.ActContinue:
134		return module.CheckResult{}
135	case milter.ActReplyCode:
136		return module.CheckResult{
137			Reject: true,
138			Reason: &exterrors.SMTPError{
139				Code:         act.SMTPCode,
140				EnhancedCode: exterrors.EnhancedCode{5, 7, 1},
141				Message:      "Message rejected due to local policy",
142				Reason:       "reply code action",
143				CheckName:    "milter",
144				Misc: map[string]interface{}{
145					"milter": s.c.milterUrl,
146				},
147			},
148		}
149	case milter.ActDiscard:
150		s.log.Msg("silent discard is not supported, rejecting message")
151		fallthrough
152	case milter.ActTempFail:
153		return module.CheckResult{
154			Reject: true,
155			Reason: &exterrors.SMTPError{
156				Code:         450,
157				EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
158				Message:      "Message rejected due to local policy",
159				Reason:       "reject action",
160				CheckName:    "milter",
161				Misc: map[string]interface{}{
162					"milter": s.c.milterUrl,
163				},
164			},
165		}
166	case milter.ActReject:
167		return module.CheckResult{
168			Reject: true,
169			Reason: &exterrors.SMTPError{
170				Code:         550,
171				EnhancedCode: exterrors.EnhancedCode{5, 7, 1},
172				Message:      "Message rejected due to local policy",
173				Reason:       "reject action",
174				CheckName:    "milter",
175				Misc: map[string]interface{}{
176					"milter": s.c.milterUrl,
177				},
178			},
179		}
180	default:
181		s.log.Msg("unknown action code ignored", "code", act.Code, "milter", s.c.milterUrl)
182		return module.CheckResult{}
183	}
184}
185
186// apply applies the modification actions returned by milter to the check results object.
187func (s *state) apply(modifyActs []milter.ModifyAction, res module.CheckResult) module.CheckResult {
188	out := res
189	for _, act := range modifyActs {
190		switch act.Code {
191		case milter.ActAddRcpt, milter.ActDelRcpt:
192			s.log.Msg("envelope changes are not supported", "rcpt", act.Rcpt, "code", act.Code, "milter", s.c.milterUrl)
193		case milter.ActChangeFrom:
194			s.log.Msg("envelope changes are not supported", "from", act.From, "code", act.Code, "milter", s.c.milterUrl)
195		case milter.ActChangeHeader:
196			s.log.Msg("header field changes are not supported", "field", act.HeaderName, "milter", s.c.milterUrl)
197		case milter.ActInsertHeader:
198			if act.HeaderIndex != 1 {
199				s.log.Msg("header inserting not on top is not supported, prepending instead", "field", act.HeaderName, "milter", s.c.milterUrl)
200			}
201			fallthrough
202		case milter.ActAddHeader:
203			// Header field might be arbitarly folded by the caller and we want
204			// to preserve that exact format in case it is important (DKIM
205			// signature is added by milter).
206			field := make([]byte, 0, len(act.HeaderName)+2+len(act.HeaderValue)+2)
207			field = append(field, act.HeaderName...)
208			field = append(field, ':', ' ')
209			field = append(field, act.HeaderValue...)
210			field = append(field, '\r', '\n')
211			out.Header.AddRaw(field)
212		case milter.ActQuarantine:
213			out.Quarantine = true
214			out.Reason = exterrors.WithFields(errors.New("milter quarantine action"), map[string]interface{}{
215				"check":  "milter",
216				"milter": s.c.milterUrl,
217				"reason": act.Reason,
218			})
219		}
220	}
221	return out
222}
223
224func (s *state) CheckConnection(ctx context.Context) module.CheckResult {
225	if s.msgMeta.Conn == nil {
226		// Submit some dummy values as the message is likely generated locally.
227
228		act, err := s.session.Conn("localhost", milter.FamilyInet, 25, "127.0.0.1")
229		if err != nil {
230			return s.ioError(err)
231		}
232		if act.Code != milter.ActContinue {
233			return s.handleAction(act)
234		}
235
236		act, err = s.session.Helo("localhost")
237		if err != nil {
238			return s.ioError(err)
239		}
240		return s.handleAction(act)
241	}
242
243	if !s.session.ProtocolOption(milter.OptNoConnect) {
244		if err := s.session.Macros(milter.CodeConn,
245			"daemon_name", "maddy",
246			"if_name", "unknown",
247			"if_addr", "0.0.0.0",
248			// TODO: $j
249			// TODO: $_
250		); err != nil {
251			return s.ioError(err)
252		}
253
254		var (
255			protoFamily milter.ProtoFamily
256			port        uint16
257			addr        string
258		)
259		switch rAddr := s.msgMeta.Conn.RemoteAddr.(type) {
260		case *net.TCPAddr:
261			port = uint16(rAddr.Port)
262			if v4 := rAddr.IP.To4(); v4 != nil {
263				// Make sure to not accidentally send IPv6-mapped IPv4 address.
264				protoFamily = milter.FamilyInet
265				addr = v4.String()
266			} else {
267				protoFamily = milter.FamilyInet6
268				addr = rAddr.IP.String()
269			}
270		case *net.UnixAddr:
271			protoFamily = milter.FamilyUnix
272			addr = rAddr.Name
273		default:
274			protoFamily = milter.FamilyUnknown
275		}
276
277		act, err := s.session.Conn(s.msgMeta.Conn.Hostname, protoFamily, port, addr)
278		if err != nil {
279			return s.ioError(err)
280		}
281		if act.Code != milter.ActContinue {
282			return s.handleAction(act)
283		}
284	}
285
286	if !s.session.ProtocolOption(milter.OptNoHelo) {
287		if s.msgMeta.Conn.TLS.HandshakeComplete {
288			fields := make([]string, 0, 4*2)
289			tlsState := s.msgMeta.Conn.TLS
290
291			switch tlsState.Version {
292			case tls.VersionTLS10:
293				fields = append(fields, "tls_version", "TLSv1")
294			case tls.VersionTLS11:
295				fields = append(fields, "tls_version", "TLSv1.1")
296			case tls.VersionTLS12:
297				fields = append(fields, "tls_version", "TLSv1.2")
298			case tls.VersionTLS13:
299				fields = append(fields, "tls_version", "TLSv1.3")
300			}
301			fields = append(fields, "cipher", tls.CipherSuiteName(tlsState.CipherSuite))
302
303			if len(tlsState.PeerCertificates) != 0 {
304				fields = append(fields, "cert_subject",
305					tlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Subject.String())
306				fields = append(fields, "cert_issuer",
307					tlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Issuer.String())
308			}
309
310			if err := s.session.Macros(milter.CodeHelo, fields...); err != nil {
311				return s.ioError(err)
312			}
313		}
314		act, err := s.session.Helo(s.msgMeta.Conn.Hostname)
315		if err != nil {
316			return s.ioError(err)
317		}
318		return s.handleAction(act)
319	}
320
321	return module.CheckResult{}
322}
323
324func (s *state) ioError(err error) module.CheckResult {
325	if s.c.failOpen {
326		s.skipChecks = true // silently permit processing to continue
327		s.c.log.Error("I/O error", err)
328		return module.CheckResult{}
329	}
330
331	return module.CheckResult{
332		Reject: true,
333		Reason: &exterrors.SMTPError{
334			Code:         451,
335			EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
336			Message:      "I/O error during policy check",
337			Err:          err,
338			CheckName:    "milter",
339			Misc: map[string]interface{}{
340				"milter": s.c.milterUrl,
341			},
342		},
343	}
344}
345
346func (s *state) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {
347	if s.skipChecks || s.session.ProtocolOption(milter.OptNoMailFrom) {
348		return module.CheckResult{}
349	}
350
351	fields := make([]string, 0, 2)
352	fields = append(fields, "i", s.msgMeta.ID)
353	// TODO: fields = append(fields, "auth_type", s.msgMeta.???)
354	if s.msgMeta.Conn.AuthUser != "" {
355		fields = append(fields, "auth_authen", s.msgMeta.Conn.AuthUser)
356	}
357	if err := s.session.Macros(milter.CodeMail, fields...); err != nil {
358		return s.ioError(err)
359	}
360
361	esmtpArgs := make([]string, 0, 2)
362	if s.msgMeta.SMTPOpts.UTF8 {
363		esmtpArgs = append(esmtpArgs, "SMTPUTF8")
364	}
365
366	act, err := s.session.Mail(mailFrom, esmtpArgs)
367	if err != nil {
368		return s.ioError(err)
369	}
370	return s.handleAction(act)
371}
372
373func (s *state) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {
374	if s.skipChecks {
375		return module.CheckResult{}
376	}
377
378	act, err := s.session.Rcpt(rcptTo, nil)
379	if err != nil {
380		return s.ioError(err)
381	}
382	return s.handleAction(act)
383}
384
385func (s *state) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {
386	if s.skipChecks {
387		return module.CheckResult{}
388	}
389
390	act, err := s.session.Header(header)
391	if err != nil {
392		return s.ioError(err)
393	}
394	if act.Code != milter.ActContinue {
395		return s.handleAction(act)
396	}
397
398	var modifyAct []milter.ModifyAction
399
400	if !s.session.ProtocolOption(milter.OptNoBody) {
401		// body.Open can be expensive for on-disk buffering.
402		r, err := body.Open()
403		if err != nil {
404			// Not ioError(err) because fail_open directive is applied only for external I/O.
405			return module.CheckResult{
406				Reject: true,
407				Reason: &exterrors.SMTPError{
408					Code:         451,
409					EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
410					Message:      "Internal error during policy check",
411					Err:          err,
412					CheckName:    "milter",
413					Misc: map[string]interface{}{
414						"milter": s.c.milterUrl,
415					},
416				},
417			}
418		}
419
420		modifyAct, act, err = s.session.BodyReadFrom(r)
421		if err != nil {
422			return s.ioError(err)
423		}
424	} else {
425		modifyAct, act, err = s.session.End()
426		if err != nil {
427			return s.ioError(err)
428		}
429	}
430
431	result := s.handleAction(act)
432	return s.apply(modifyAct, result)
433}
434
435func (s *state) Close() error {
436	return s.session.Close()
437}
438
439var (
440	_ module.Check      = &Check{}
441	_ module.CheckState = &state{}
442)
443
444func init() {
445	module.Register(modName, New)
446}