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 smtp
 20
 21import (
 22	"bufio"
 23	"context"
 24	"errors"
 25	"fmt"
 26	"io"
 27	"net"
 28	"runtime/trace"
 29	"strconv"
 30	"strings"
 31	"sync"
 32
 33	"github.com/emersion/go-message/textproto"
 34	"github.com/emersion/go-sasl"
 35	"github.com/emersion/go-smtp"
 36	"github.com/foxcpp/maddy/framework/address"
 37	"github.com/foxcpp/maddy/framework/buffer"
 38	"github.com/foxcpp/maddy/framework/dns"
 39	"github.com/foxcpp/maddy/framework/exterrors"
 40	"github.com/foxcpp/maddy/framework/log"
 41	"github.com/foxcpp/maddy/framework/module"
 42	"github.com/foxcpp/maddy/internal/auth"
 43)
 44
 45func limitReader(r io.Reader, n int64, err error) *limitedReader {
 46	return &limitedReader{R: r, N: n, E: err, Enabled: true}
 47}
 48
 49type limitedReader struct {
 50	R       io.Reader
 51	N       int64
 52	E       error
 53	Enabled bool
 54}
 55
 56// same as io.LimitedReader.Read except returning the custom error and the option
 57// to be disabled
 58func (l *limitedReader) Read(p []byte) (n int, err error) {
 59	if !l.Enabled {
 60		return l.R.Read(p)
 61	}
 62	if l.N <= 0 {
 63		return 0, l.E
 64	}
 65	if int64(len(p)) > l.N {
 66		p = p[0:l.N]
 67	}
 68	n, err = l.R.Read(p)
 69	l.N -= int64(n)
 70	return
 71}
 72
 73type Session struct {
 74	endp *Endpoint
 75
 76	// Specific for this session.
 77	// sessionCtx is not used for cancellation or timeouts, only for tracing.
 78	sessionCtx       context.Context
 79	cancelRDNS       func()
 80	connState        module.ConnState
 81	repeatedMailErrs int
 82	loggedRcptErrors int
 83
 84	// Specific for the currently handled message.
 85	// msgCtx is not used for cancellation or timeouts, only for tracing.
 86	// It is the subcontext of sessionCtx.
 87	// Mutex is used to prevent Close from accessing inconsistent state when it
 88	// is called asynchronously to any SMTP command.
 89	msgLock     sync.Mutex
 90	msgCtx      context.Context
 91	msgTask     *trace.Task
 92	mailFrom    string
 93	opts        smtp.MailOptions
 94	msgMeta     *module.MsgMetadata
 95	delivery    module.Delivery
 96	deliveryErr error
 97
 98	log log.Logger
 99}
100
101func (s *Session) AuthMechanisms() []string {
102	return s.endp.saslAuth.SASLMechanisms()
103}
104
105func (s *Session) Auth(mech string) (sasl.Server, error) {
106	return s.endp.saslAuth.CreateSASL(mech, s.connState.RemoteAddr, func(identity string, data auth.ContextData) error {
107		s.connState.AuthUser = identity
108		s.connState.AuthPassword = data.Password
109		return nil
110	}), nil
111}
112
113func (s *Session) Reset() {
114	s.msgLock.Lock()
115	defer s.msgLock.Unlock()
116
117	if s.delivery != nil {
118		s.abort(s.msgCtx)
119	}
120	s.endp.Log.DebugMsg("reset")
121}
122
123func (s *Session) releaseLimits() {
124	domain := ""
125	if s.mailFrom != "" {
126		var err error
127		_, domain, err = address.Split(s.mailFrom)
128		if err != nil {
129			return
130		}
131	}
132
133	addr, ok := s.msgMeta.Conn.RemoteAddr.(*net.TCPAddr)
134	if !ok {
135		addr = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)}
136	}
137	s.endp.limits.ReleaseMsg(addr.IP, domain)
138}
139
140func (s *Session) abort(ctx context.Context) {
141	if err := s.delivery.Abort(ctx); err != nil {
142		s.endp.Log.Error("delivery abort failed", err)
143	}
144	s.log.Msg("aborted", "msg_id", s.msgMeta.ID)
145	abortedSMTPTransactions.WithLabelValues(s.endp.name).Inc()
146	s.cleanSession()
147}
148
149func (s *Session) cleanSession() {
150	s.releaseLimits()
151
152	s.mailFrom = ""
153	s.opts = smtp.MailOptions{}
154	s.msgMeta = nil
155	s.delivery = nil
156	s.deliveryErr = nil
157	s.msgCtx = nil
158	s.msgTask.End()
159}
160
161func (s *Session) AuthPlain(username, password string) error {
162	// Executed before authentication and session initialization.
163	if err := s.endp.pipeline.RunEarlyChecks(context.TODO(), &s.connState); err != nil {
164		return s.endp.wrapErr("", true, "AUTH", err)
165	}
166
167	// saslAuth will handle AuthMap and AuthNormalize.
168	err := s.endp.saslAuth.AuthPlain(username, password)
169	if err != nil {
170		s.endp.Log.Error("authentication failed", err, "username", username, "src_ip", s.connState.RemoteAddr)
171
172		failedLogins.WithLabelValues(s.endp.name).Inc()
173
174		if exterrors.IsTemporary(err) {
175			return &smtp.SMTPError{
176				Code:         454,
177				EnhancedCode: smtp.EnhancedCode{4, 7, 0},
178				Message:      "Temporary authentication failure",
179			}
180		}
181
182		return &smtp.SMTPError{
183			Code:         535,
184			EnhancedCode: smtp.EnhancedCode{5, 7, 8},
185			Message:      "Invalid credentials",
186		}
187	}
188
189	s.connState.AuthUser = username
190	s.connState.AuthPassword = password
191
192	return nil
193}
194
195func (s *Session) startDelivery(ctx context.Context, from string, opts smtp.MailOptions) (string, error) {
196	var err error
197	msgMeta := &module.MsgMetadata{
198		Conn:     &s.connState,
199		SMTPOpts: opts,
200	}
201	msgMeta.ID, err = module.GenerateMsgID()
202	if err != nil {
203		return "", err
204	}
205
206	if s.connState.AuthUser != "" {
207		s.log.Msg("incoming message",
208			"src_host", msgMeta.Conn.Hostname,
209			"src_ip", msgMeta.Conn.RemoteAddr.String(),
210			"sender", from,
211			"msg_id", msgMeta.ID,
212			"username", s.connState.AuthUser,
213		)
214	} else {
215		s.log.Msg("incoming message",
216			"src_host", msgMeta.Conn.Hostname,
217			"src_ip", msgMeta.Conn.RemoteAddr.String(),
218			"sender", from,
219			"msg_id", msgMeta.ID,
220		)
221	}
222
223	// INTERNATIONALIZATION: Do not permit non-ASCII addresses unless SMTPUTF8 is
224	// used.
225	if !opts.UTF8 {
226		for _, ch := range from {
227			if ch > 128 {
228				return "", &exterrors.SMTPError{
229					Code:         550,
230					EnhancedCode: exterrors.EnhancedCode{5, 6, 7},
231					Message:      "SMTPUTF8 is required for non-ASCII senders",
232				}
233			}
234		}
235	}
236
237	// Decode punycode, normalize to NFC and case-fold address.
238	cleanFrom := from
239	if from != "" {
240		cleanFrom, err = address.CleanDomain(from)
241		if err != nil {
242			return "", &exterrors.SMTPError{
243				Code:         553,
244				EnhancedCode: exterrors.EnhancedCode{5, 1, 7},
245				Message:      "Unable to normalize the sender address",
246			}
247		}
248	}
249
250	msgMeta.OriginalFrom = from
251
252	domain := ""
253	if cleanFrom != "" {
254		_, domain, err = address.Split(cleanFrom)
255		if err != nil {
256			return "", err
257		}
258	}
259	remoteIP, ok := msgMeta.Conn.RemoteAddr.(*net.TCPAddr)
260	if !ok {
261		remoteIP = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)}
262	}
263	if err := s.endp.limits.TakeMsg(context.Background(), remoteIP.IP, domain); err != nil {
264		return "", err
265	}
266
267	s.msgCtx, s.msgTask = trace.NewTask(ctx, "Incoming Message")
268
269	mailCtx, mailTask := trace.NewTask(s.msgCtx, "MAIL FROM")
270	defer mailTask.End()
271
272	delivery, err := s.endp.pipeline.Start(mailCtx, msgMeta, cleanFrom)
273	if err != nil {
274		s.msgCtx = nil
275		s.msgTask.End()
276		s.endp.limits.ReleaseMsg(remoteIP.IP, domain)
277		return msgMeta.ID, err
278	}
279
280	startedSMTPTransactions.WithLabelValues(s.endp.name).Inc()
281
282	s.msgMeta = msgMeta
283	s.mailFrom = cleanFrom
284	s.delivery = delivery
285
286	return msgMeta.ID, nil
287}
288
289func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
290	if s.endp.authAlwaysRequired && s.connState.AuthUser == "" {
291		return smtp.ErrAuthRequired
292	}
293
294	s.msgLock.Lock()
295	defer s.msgLock.Unlock()
296
297	if !s.endp.deferServerReject {
298		// Will initialize s.msgCtx.
299		msgID, err := s.startDelivery(s.sessionCtx, from, *opts)
300		if err != nil {
301			if !errors.Is(err, context.DeadlineExceeded) {
302				s.log.Error("MAIL FROM error", err, "msg_id", msgID)
303			}
304			return s.endp.wrapErr(msgID, !opts.UTF8, "MAIL", err)
305		}
306	}
307
308	// Keep the MAIL FROM argument for deferred startDelivery.
309	s.mailFrom = from
310	s.opts = *opts
311
312	return nil
313}
314
315func (s *Session) fetchRDNSName(ctx context.Context) {
316	defer trace.StartRegion(ctx, "rDNS fetch").End()
317
318	tcpAddr, ok := s.connState.RemoteAddr.(*net.TCPAddr)
319	if !ok {
320		s.connState.RDNSName.Set(nil, nil)
321		return
322	}
323
324	name, err := dns.LookupAddr(ctx, s.endp.resolver, tcpAddr.IP)
325	if err != nil {
326		dnsErr, ok := err.(*net.DNSError)
327		if ok && dnsErr.IsNotFound {
328			s.connState.RDNSName.Set(nil, nil)
329			return
330		}
331
332		if !errors.Is(err, context.Canceled) {
333			// Often occurs when transaction completes before rDNS lookup and
334			// rDNS name was not actually needed. So do not log cancelation
335			// error if that's the case.
336
337			reason, misc := exterrors.UnwrapDNSErr(err)
338			misc["reason"] = reason
339			s.log.Error("rDNS error", exterrors.WithFields(err, misc), "src_ip", s.connState.RemoteAddr)
340		}
341		s.connState.RDNSName.Set(nil, err)
342		return
343	}
344
345	s.connState.RDNSName.Set(name, nil)
346}
347
348func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
349	s.msgLock.Lock()
350	defer s.msgLock.Unlock()
351
352	// deferServerReject = true and this is the first RCPT TO command.
353	if s.delivery == nil {
354		// If we already attempted to initialize the delivery -
355		// fail again.
356		if s.deliveryErr != nil {
357			s.repeatedMailErrs++
358			// The deliveryErr is already wrapped.
359			return s.deliveryErr
360		}
361
362		// It will initialize s.msgCtx.
363		msgID, err := s.startDelivery(s.sessionCtx, s.mailFrom, s.opts)
364		if err != nil {
365			if !errors.Is(err, context.DeadlineExceeded) {
366				s.log.Error("MAIL FROM error (deferred)", err, "rcpt", to, "msg_id", msgID)
367			}
368			s.deliveryErr = s.endp.wrapErr(msgID, !s.opts.UTF8, "RCPT", err)
369			return s.deliveryErr
370		}
371	}
372
373	rcptCtx, rcptTask := trace.NewTask(s.msgCtx, "RCPT TO")
374	defer rcptTask.End()
375
376	if err := s.rcpt(rcptCtx, to, opts); err != nil {
377		if s.loggedRcptErrors < s.endp.maxLoggedRcptErrors {
378			s.log.Error("RCPT error", err, "rcpt", to, "msg_id", s.msgMeta.ID)
379			s.loggedRcptErrors++
380			if s.loggedRcptErrors == s.endp.maxLoggedRcptErrors {
381				s.log.Msg("too many RCPT errors, possible dictonary attack", "src_ip", s.connState.RemoteAddr, "msg_id", s.msgMeta.ID)
382			}
383		}
384		return s.endp.wrapErr(s.msgMeta.ID, !s.opts.UTF8, "RCPT", err)
385	}
386	s.endp.Log.Msg("RCPT ok", "rcpt", to, "msg_id", s.msgMeta.ID)
387	return nil
388}
389
390func (s *Session) rcpt(ctx context.Context, to string, opts *smtp.RcptOptions) error {
391	// INTERNATIONALIZATION: Do not permit non-ASCII addresses unless SMTPUTF8 is
392	// used.
393	if !address.IsASCII(to) && !s.opts.UTF8 {
394		return &exterrors.SMTPError{
395			Code:         553,
396			EnhancedCode: exterrors.EnhancedCode{5, 6, 7},
397			Message:      "SMTPUTF8 is required for non-ASCII recipients",
398		}
399	}
400	cleanTo, err := address.CleanDomain(to)
401	if err != nil {
402		return &exterrors.SMTPError{
403			Code:         501,
404			EnhancedCode: exterrors.EnhancedCode{5, 1, 2},
405			Message:      "Unable to normalize the recipient address",
406		}
407	}
408
409	return s.delivery.AddRcpt(ctx, cleanTo, *opts)
410}
411
412func (s *Session) Logout() error {
413	s.msgLock.Lock()
414	defer s.msgLock.Unlock()
415
416	if s.delivery != nil {
417		s.abort(s.msgCtx)
418
419		if s.repeatedMailErrs > s.endp.maxLoggedRcptErrors {
420			s.log.Msg("MAIL FROM repeated error a lot of times, possible dictonary attack", "count", s.repeatedMailErrs, "src_ip", s.connState.RemoteAddr)
421		}
422	}
423	if s.cancelRDNS != nil {
424		s.cancelRDNS()
425	}
426
427	s.endp.sessionCnt.Add(-1)
428
429	return nil
430}
431
432func (s *Session) prepareBody(r io.Reader) (textproto.Header, buffer.Buffer, error) {
433	limitr := limitReader(r, s.endp.maxHeaderBytes, &exterrors.SMTPError{
434		Code:         552,
435		EnhancedCode: exterrors.EnhancedCode{5, 3, 4},
436		Message:      "Message header size exceeds limit",
437	})
438
439	bufr := bufio.NewReader(limitr)
440	header, err := textproto.ReadHeader(bufr)
441	if err != nil {
442		return textproto.Header{}, nil, fmt.Errorf("I/O error while parsing header: %w", err)
443	}
444
445	if s.endp.submission {
446		// The MsgMetadata is passed by pointer all the way down.
447		if err := s.submissionPrepare(s.msgMeta, &header); err != nil {
448			return textproto.Header{}, nil, err
449		}
450	}
451
452	// the header size check is done. The message size will be checked by go-smtp
453	limitr.Enabled = false
454
455	buf, err := s.endp.buffer(bufr)
456	if err != nil {
457		return textproto.Header{}, nil, fmt.Errorf("I/O error while writing buffer: %w", err)
458	}
459
460	return header, buf, nil
461}
462
463func (s *Session) Data(r io.Reader) error {
464	s.msgLock.Lock()
465	defer s.msgLock.Unlock()
466
467	bodyCtx, bodyTask := trace.NewTask(s.msgCtx, "DATA")
468	defer bodyTask.End()
469
470	wrapErr := func(err error) error {
471		s.log.Error("DATA error", err, "msg_id", s.msgMeta.ID)
472		return s.endp.wrapErr(s.msgMeta.ID, !s.opts.UTF8, "DATA", err)
473	}
474
475	header, buf, err := s.prepareBody(r)
476	if err != nil {
477		return wrapErr(err)
478	}
479	defer func() {
480		if err := buf.Remove(); err != nil {
481			s.log.Error("failed to remove buffered body", err)
482		}
483
484		// go-smtp will call Reset, but it will call Abort if delivery is non-nil.
485		s.cleanSession()
486	}()
487
488	if err := s.checkRoutingLoops(header); err != nil {
489		return wrapErr(err)
490	}
491
492	if strings.EqualFold(header.Get("TLS-Required"), "No") {
493		s.msgMeta.TLSRequireOverride = true
494	}
495
496	if err := s.delivery.Body(bodyCtx, header, buf); err != nil {
497		return wrapErr(err)
498	}
499
500	if err := s.delivery.Commit(bodyCtx); err != nil {
501		return wrapErr(err)
502	}
503
504	s.log.Msg("accepted", "msg_id", s.msgMeta.ID)
505
506	return nil
507}
508
509type statusWrapper struct {
510	sc smtp.StatusCollector
511	s  *Session
512}
513
514func (sw statusWrapper) SetStatus(rcpt string, err error) {
515	sw.sc.SetStatus(rcpt, sw.s.endp.wrapErr(sw.s.msgMeta.ID, !sw.s.opts.UTF8, "DATA", err))
516}
517
518func (s *Session) LMTPData(r io.Reader, sc smtp.StatusCollector) error {
519	s.msgLock.Lock()
520	defer s.msgLock.Unlock()
521
522	bodyCtx, bodyTask := trace.NewTask(s.msgCtx, "DATA")
523	defer bodyTask.End()
524
525	wrapErr := func(err error) error {
526		s.log.Error("DATA error", err, "msg_id", s.msgMeta.ID)
527		return s.endp.wrapErr(s.msgMeta.ID, !s.opts.UTF8, "DATA", err)
528	}
529
530	header, buf, err := s.prepareBody(r)
531	if err != nil {
532		return wrapErr(err)
533	}
534	defer func() {
535		if err := buf.Remove(); err != nil {
536			s.log.Error("failed to remove buffered body", err)
537		}
538
539		// go-smtp will call Reset, but it will call Abort if delivery is non-nil.
540		s.cleanSession()
541	}()
542
543	if strings.EqualFold(header.Get("TLS-Required"), "No") {
544		s.msgMeta.TLSRequireOverride = true
545	}
546
547	if err := s.checkRoutingLoops(header); err != nil {
548		return wrapErr(err)
549	}
550
551	s.delivery.(module.PartialDelivery).BodyNonAtomic(bodyCtx, statusWrapper{sc, s}, header, buf)
552
553	// We can't really tell whether it is failed completely or succeeded
554	// so always commit. Should be harmless, anyway.
555	if err := s.delivery.Commit(bodyCtx); err != nil {
556		return wrapErr(err)
557	}
558
559	s.log.Msg("accepted", "msg_id", s.msgMeta.ID)
560
561	return nil
562}
563
564func (s *Session) checkRoutingLoops(header textproto.Header) error {
565	// RFC 5321 Section 6.3:
566	// >Simple counting of the number of "Received:" header fields in a
567	// >message has proven to be an effective, although rarely optimal,
568	// >method of detecting loops in mail systems.
569	receivedCount := 0
570	for f := header.FieldsByKey("Received"); f.Next(); {
571		receivedCount++
572	}
573	if receivedCount > s.endp.maxReceived {
574		return &exterrors.SMTPError{
575			Code:         554,
576			EnhancedCode: exterrors.EnhancedCode{5, 4, 6},
577			Message:      fmt.Sprintf("Too many Received header fields (%d), possible forwarding loop", receivedCount),
578		}
579	}
580
581	return nil
582}
583
584func (endp *Endpoint) wrapErr(msgId string, mangleUTF8 bool, command string, err error) error {
585	if err == nil {
586		return nil
587	}
588
589	if errors.Is(err, context.DeadlineExceeded) {
590		return &smtp.SMTPError{
591			Code:         451,
592			EnhancedCode: smtp.EnhancedCode{4, 4, 5},
593			Message:      "High load, try again later",
594		}
595	}
596
597	res := &smtp.SMTPError{
598		Code:         554,
599		EnhancedCode: smtp.EnhancedCodeNotSet,
600		// Err on the side of caution if the error lacks SMTP annotations. If
601		// we just pass the error text through, we might accidenetally disclose
602		// details of server configuration.
603		Message: "Internal server error",
604	}
605
606	if exterrors.IsTemporary(err) {
607		res.Code = 451
608	}
609
610	ctxInfo := exterrors.Fields(err)
611	ctxCode, ok := ctxInfo["smtp_code"].(int)
612	if ok {
613		res.Code = ctxCode
614	}
615	ctxEnchCode, ok := ctxInfo["smtp_enchcode"].(exterrors.EnhancedCode)
616	if ok {
617		res.EnhancedCode = smtp.EnhancedCode(ctxEnchCode)
618	}
619	ctxMsg, ok := ctxInfo["smtp_msg"].(string)
620	if ok {
621		res.Message = ctxMsg
622	}
623
624	if smtpErr, ok := err.(*smtp.SMTPError); ok {
625		endp.Log.Printf("plain SMTP error returned, this is deprecated")
626		res.Code = smtpErr.Code
627		res.EnhancedCode = smtpErr.EnhancedCode
628		res.Message = smtpErr.Message
629	}
630
631	if msgId != "" {
632		res.Message += " (msg ID = " + msgId + ")"
633	}
634
635	failedCmds.WithLabelValues(endp.name, command, strconv.Itoa(res.Code),
636		fmt.Sprintf("%d.%d.%d",
637			res.EnhancedCode[0],
638			res.EnhancedCode[1],
639			res.EnhancedCode[2])).Inc()
640
641	// INTERNATIONALIZATION: See RFC 6531 Section 3.7.4.1.
642	if mangleUTF8 {
643		b := strings.Builder{}
644		b.Grow(len(res.Message))
645		for _, ch := range res.Message {
646			if ch > 128 {
647				b.WriteRune('?')
648			} else {
649				b.WriteRune(ch)
650			}
651		}
652		res.Message = b.String()
653	}
654
655	return res
656}