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
 19// Package smtpconn contains the code shared between target.smtp and
 20// remote modules.
 21//
 22// It implements the wrapper over the SMTP connection (go-smtp.Client) object
 23// with the following features added:
 24// - Logging of certain errors (e.g. QUIT command errors)
 25// - Wrapping of returned errors using the exterrors package.
 26// - SMTPUTF8/IDNA support.
 27// - TLS support mode (don't use, attempt, require).
 28package smtpconn
 29
 30import (
 31	"context"
 32	"crypto/tls"
 33	"errors"
 34	"fmt"
 35	"io"
 36	"net"
 37	"runtime/trace"
 38	"time"
 39
 40	"github.com/emersion/go-message/textproto"
 41	"github.com/emersion/go-smtp"
 42	"github.com/foxcpp/maddy/framework/address"
 43	"github.com/foxcpp/maddy/framework/config"
 44	"github.com/foxcpp/maddy/framework/exterrors"
 45	"github.com/foxcpp/maddy/framework/log"
 46)
 47
 48// The C object represents the SMTP connection and is a wrapper around
 49// go-smtp.Client with additional maddy-specific logic.
 50//
 51// Currently, the C object represents one session and cannot be reused.
 52type C struct {
 53	// Dialer to use to estabilish new network connections. Set to net.Dialer
 54	// DialContext by New.
 55	Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
 56
 57	// Timeout for most session commands (EHLO, MAIL, RCPT, DATA, STARTTLS).
 58	// Set to 5 mins by New.
 59	CommandTimeout time.Duration
 60
 61	// Timeout for the initial TCP connection establishment.
 62	ConnectTimeout time.Duration
 63
 64	// Timeout for the final dot. Set to 12 mins by New.
 65	// (see go-smtp source for explanation of used defaults).
 66	SubmissionTimeout time.Duration
 67
 68	// Hostname to sent in the EHLO/HELO command. Set to
 69	// 'localhost.localdomain' by New. Expected to be encoded in ACE form.
 70	Hostname string
 71
 72	// tls.Config to use. Can be nil if no special changes are required.
 73	TLSConfig *tls.Config
 74
 75	// Logger to use for debug log and certain errors.
 76	Log log.Logger
 77
 78	// Include the remote server address in SMTP status messages in the form
 79	// "ADDRESS said: ..."
 80	AddrInSMTPMsg bool
 81
 82	conn       net.Conn
 83	serverName string
 84	cl         *smtp.Client
 85	rcpts      []string
 86	lmtp       bool
 87}
 88
 89// New creates the new instance of the C object, populating the required fields
 90// with resonable default values.
 91func New() *C {
 92	return &C{
 93		Dialer:            (&net.Dialer{}).DialContext,
 94		ConnectTimeout:    5 * time.Minute,
 95		CommandTimeout:    5 * time.Minute,
 96		SubmissionTimeout: 12 * time.Minute,
 97		TLSConfig:         &tls.Config{},
 98		Hostname:          "localhost.localdomain",
 99	}
100}
101
102func (c *C) wrapClientErr(err error, serverName string) error {
103	if err == nil {
104		return nil
105	}
106
107	switch err := err.(type) {
108	case TLSError:
109		return err
110	case *exterrors.SMTPError:
111		return err
112	case *smtp.SMTPError:
113		msg := err.Message
114		if c.AddrInSMTPMsg {
115			msg = serverName + " said: " + err.Message
116		}
117
118		if err.Code == 552 {
119			err.Code = 452
120			err.EnhancedCode[0] = 4
121			c.Log.Msg("SMTP code 552 rewritten to 452 per RFC 5321 Section 4.5.3.1.10")
122		}
123
124		return &exterrors.SMTPError{
125			Code:         err.Code,
126			EnhancedCode: exterrors.EnhancedCode(err.EnhancedCode),
127			Message:      msg,
128			Misc: map[string]interface{}{
129				"remote_server": serverName,
130			},
131			Err: err,
132		}
133	case *net.OpError:
134		if _, ok := err.Err.(*net.DNSError); ok {
135			reason, misc := exterrors.UnwrapDNSErr(err)
136			misc["remote_server"] = err.Addr
137			misc["io_op"] = err.Op
138			return &exterrors.SMTPError{
139				Code:         exterrors.SMTPCode(err, 450, 550),
140				EnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 4, 4}),
141				Message:      "DNS error",
142				Err:          err,
143				Reason:       reason,
144				Misc:         misc,
145			}
146		}
147		return &exterrors.SMTPError{
148			Code:         450,
149			EnhancedCode: exterrors.EnhancedCode{4, 4, 2},
150			Message:      "Network I/O error",
151			Err:          err,
152			Misc: map[string]interface{}{
153				"remote_addr": err.Addr,
154				"io_op":       err.Op,
155			},
156		}
157	default:
158		return exterrors.WithFields(err, map[string]interface{}{
159			"remote_server": serverName,
160		})
161	}
162}
163
164// Connect actually estabilishes the network connection with the remote host,
165// executes HELO/EHLO and optionally STARTTLS command.
166func (c *C) Connect(ctx context.Context, endp config.Endpoint, starttls bool, tlsConfig *tls.Config) (didTLS bool, err error) {
167	didTLS, cl, conn, err := c.attemptConnect(ctx, false, endp, starttls, tlsConfig)
168	if err != nil {
169		return false, c.wrapClientErr(err, endp.Host)
170	}
171
172	c.serverName = endp.Host
173	c.cl = cl
174	c.conn = conn
175
176	c.Log.DebugMsg("connected", "remote_server", c.serverName,
177		"local_addr", c.LocalAddr(), "remote_addr", c.RemoteAddr())
178
179	return didTLS, nil
180}
181
182// ConnectLMTP estabilishes the network connection with the remote host and
183// sends LHLO command, negotiating LMTP use.
184func (c *C) ConnectLMTP(ctx context.Context, endp config.Endpoint, starttls bool, tlsConfig *tls.Config) (didTLS bool, err error) {
185	didTLS, cl, conn, err := c.attemptConnect(ctx, true, endp, starttls, tlsConfig)
186	if err != nil {
187		return false, c.wrapClientErr(err, endp.Host)
188	}
189
190	c.serverName = endp.Host
191	c.cl = cl
192	c.conn = conn
193
194	c.Log.DebugMsg("connected", "remote_server", c.serverName,
195		"local_addr", c.LocalAddr(), "remote_addr", c.RemoteAddr())
196
197	return didTLS, nil
198}
199
200// TLSError is returned by Connect to indicate the error during STARTTLS
201// command execution.
202//
203// If the endpoint uses Implicit TLS, TLS errors are threated as connection
204// errors and thus are not returned as TLSError.
205type TLSError struct {
206	Err error
207}
208
209func (err TLSError) Error() string {
210	return "smtpconn: " + err.Err.Error()
211}
212
213func (err TLSError) Unwrap() error {
214	return err.Err
215}
216
217func (c *C) LocalAddr() net.Addr {
218	if c.conn == nil {
219		return nil
220	}
221	return c.conn.LocalAddr()
222}
223
224func (c *C) RemoteAddr() net.Addr {
225	if c.conn == nil {
226		return nil
227	}
228	return c.conn.RemoteAddr()
229}
230
231func (c *C) attemptConnect(ctx context.Context, lmtp bool, endp config.Endpoint, starttls bool, tlsConfig *tls.Config) (didTLS bool, cl *smtp.Client, conn net.Conn, err error) {
232	dialCtx, cancel := context.WithTimeout(ctx, c.ConnectTimeout)
233	conn, err = c.Dialer(dialCtx, endp.Network(), endp.Address())
234	cancel()
235	if err != nil {
236		return false, nil, nil, err
237	}
238
239	if endp.IsTLS() {
240		cfg := tlsConfig.Clone()
241		cfg.ServerName = endp.Host
242		conn = tls.Client(conn, cfg)
243	}
244
245	c.lmtp = lmtp
246	// This uses initial greeting timeout of 5 minutes (hardcoded).
247	if lmtp {
248		cl = smtp.NewClientLMTP(conn)
249	} else {
250		cl = smtp.NewClient(conn)
251	}
252
253	cl.CommandTimeout = c.CommandTimeout
254	cl.SubmissionTimeout = c.SubmissionTimeout
255
256	// i18n: hostname is already expected to be in A-labels form.
257	if err := cl.Hello(c.Hostname); err != nil {
258		cl.Close()
259		return false, nil, nil, err
260	}
261
262	if !starttls {
263		return false, cl, conn, nil
264	}
265
266	if ok, _ := cl.Extension("STARTTLS"); !ok {
267		if err := cl.Quit(); err != nil {
268			cl.Close()
269		}
270		return false, nil, nil, fmt.Errorf("TLS required but unsupported by downstream")
271	}
272
273	cfg := tlsConfig.Clone()
274	cfg.ServerName = endp.Host
275	if err := cl.StartTLS(cfg); err != nil {
276		// After the handshake failure, the connection may be in a bad state.
277		// We attempt to send the proper QUIT command though, in case the error happened
278		// *after* the handshake (e.g. PKI verification fail), we don't log the error in
279		// this case though.
280		if err := cl.Quit(); err != nil {
281			cl.Close()
282		}
283
284		return false, nil, nil, TLSError{err}
285	}
286
287	// Re-do HELO using our hostname instead of localhost.
288	if err := cl.Hello(c.Hostname); err != nil {
289		cl.Close()
290
291		var tlsErr *tls.CertificateVerificationError
292		if errors.As(err, &tlsErr) {
293			return false, nil, nil, TLSError{Err: tlsErr}
294		}
295
296		return false, nil, nil, err
297	}
298
299	return true, cl, conn, nil
300}
301
302// Mail sends the MAIL FROM command to the remote server.
303//
304// SIZE and REQUIRETLS options are forwarded to the remote server as-is.
305// SMTPUTF8 is forwarded if supported by the remote server, if it is not
306// supported - attempt will be done to convert addresses to the ASCII form, if
307// this is not possible, the corresponding method (Mail or Rcpt) will fail.
308func (c *C) Mail(ctx context.Context, from string, opts smtp.MailOptions) error {
309	defer trace.StartRegion(ctx, "smtpconn/MAIL FROM").End()
310
311	outOpts := smtp.MailOptions{
312		// Future extensions may add additional fields that should not be
313		// copied blindly. So we copy only fields we know should be handled
314		// this way.
315
316		Size:       opts.Size,
317		RequireTLS: opts.RequireTLS,
318	}
319
320	// INTERNATIONALIZATION: Use SMTPUTF8 is possible, attempt to convert addresses otherwise.
321
322	// There is no way we can accept a message with non-ASCII addresses without SMTPUTF8
323	// this is enforced by endpoint/smtp.
324	if opts.UTF8 {
325		if ok, _ := c.cl.Extension("SMTPUTF8"); ok {
326			outOpts.UTF8 = true
327		} else {
328			var err error
329			from, err = address.ToASCII(from)
330			if err != nil {
331				return &exterrors.SMTPError{
332					Code:         550,
333					EnhancedCode: exterrors.EnhancedCode{5, 6, 7},
334					Message:      "SMTPUTF8 is unsupported, cannot convert sender address",
335					Misc: map[string]interface{}{
336						"remote_server": c.serverName,
337					},
338					Err: err,
339				}
340			}
341		}
342	}
343
344	if err := c.cl.Mail(from, &outOpts); err != nil {
345		return c.wrapClientErr(err, c.serverName)
346	}
347
348	return nil
349}
350
351// Rcpts returns the list of recipients that were accepted by the remote server.
352func (c *C) Rcpts() []string {
353	return c.rcpts
354}
355
356func (c *C) ServerName() string {
357	return c.serverName
358}
359
360func (c *C) Client() *smtp.Client {
361	return c.cl
362}
363
364func (c *C) IsLMTP() bool {
365	return c.lmtp
366}
367
368// Rcpt sends the RCPT TO command to the remote server.
369//
370// If the address is non-ASCII and cannot be converted to ASCII and the remote
371// server does not support SMTPUTF8, error will be returned.
372func (c *C) Rcpt(ctx context.Context, to string, opts smtp.RcptOptions) error {
373	defer trace.StartRegion(ctx, "smtpconn/RCPT TO").End()
374
375	outOpts := &smtp.RcptOptions{
376		// TODO: DSN support
377	}
378
379	// If necessary, the extension flag is enabled in Start.
380	if ok, _ := c.cl.Extension("SMTPUTF8"); !address.IsASCII(to) && !ok {
381		var err error
382		to, err = address.ToASCII(to)
383		if err != nil {
384			return &exterrors.SMTPError{
385				Code:         553,
386				EnhancedCode: exterrors.EnhancedCode{5, 6, 7},
387				Message:      "SMTPUTF8 is unsupported, cannot convert recipient address",
388				Misc: map[string]interface{}{
389					"remote_server": c.serverName,
390				},
391				Err: err,
392			}
393		}
394	}
395
396	if err := c.cl.Rcpt(to, outOpts); err != nil {
397		return c.wrapClientErr(err, c.serverName)
398	}
399
400	c.rcpts = append(c.rcpts, to)
401
402	return nil
403}
404
405type lmtpError map[string]*smtp.SMTPError
406
407func (l lmtpError) SetStatus(rcptTo string, err *smtp.SMTPError) {
408	l[rcptTo] = err
409}
410
411func (l lmtpError) singleError() *smtp.SMTPError {
412	nonNils := 0
413	for _, e := range l {
414		if e != nil {
415			nonNils++
416		}
417	}
418	if nonNils == 1 {
419		for _, err := range l {
420			if err != nil {
421				return err
422			}
423		}
424	}
425	return nil
426}
427
428func (l lmtpError) Unwrap() error {
429	if err := l.singleError(); err != nil {
430		return err
431	}
432	return nil
433}
434
435func (l lmtpError) Error() string {
436	if err := l.singleError(); err != nil {
437		return err.Error()
438	}
439	return fmt.Sprintf("multiple errors reported by LMTP downstream: %v", map[string]*smtp.SMTPError(l))
440}
441
442func (c *C) smtpToLMTPData(ctx context.Context, hdr textproto.Header, body io.Reader) error {
443	statusCb := lmtpError{}
444	if err := c.LMTPData(ctx, hdr, body, statusCb.SetStatus); err != nil {
445		return err
446	}
447	hasAnyFailures := false
448	for _, err := range statusCb {
449		if err != nil {
450			hasAnyFailures = true
451		}
452	}
453	if hasAnyFailures {
454		return statusCb
455	}
456	return nil
457}
458
459// Data sends the DATA command to the remote server and then sends the message header
460// and body.
461//
462// If the Data command fails, the connection may be in a unclean state (e.g. in
463// the middle of message data stream). It is not safe to continue using it.
464func (c *C) Data(ctx context.Context, hdr textproto.Header, body io.Reader) error {
465	defer trace.StartRegion(ctx, "smtpconn/DATA").End()
466
467	if c.IsLMTP() {
468		return c.smtpToLMTPData(ctx, hdr, body)
469	}
470
471	wc, err := c.cl.Data()
472	if err != nil {
473		return c.wrapClientErr(err, c.serverName)
474	}
475
476	if err := textproto.WriteHeader(wc, hdr); err != nil {
477		return c.wrapClientErr(err, c.serverName)
478	}
479
480	if _, err := io.Copy(wc, body); err != nil {
481		return c.wrapClientErr(err, c.serverName)
482	}
483
484	if err := wc.Close(); err != nil {
485		return c.wrapClientErr(err, c.serverName)
486	}
487
488	return nil
489}
490
491func (c *C) LMTPData(ctx context.Context, hdr textproto.Header, body io.Reader, statusCb func(string, *smtp.SMTPError)) error {
492	defer trace.StartRegion(ctx, "smtpconn/LMTPDATA").End()
493
494	wc, err := c.cl.LMTPData(statusCb)
495	if err != nil {
496		return c.wrapClientErr(err, c.serverName)
497	}
498
499	if err := textproto.WriteHeader(wc, hdr); err != nil {
500		return c.wrapClientErr(err, c.serverName)
501	}
502
503	if _, err := io.Copy(wc, body); err != nil {
504		return c.wrapClientErr(err, c.serverName)
505	}
506
507	if err := wc.Close(); err != nil {
508		return c.wrapClientErr(err, c.serverName)
509	}
510
511	return nil
512}
513
514func (c *C) Noop() error {
515	if c.cl == nil {
516		return errors.New("smtpconn: not connected")
517	}
518
519	return c.cl.Noop()
520}
521
522// Close sends the QUIT command, if it fails - it directly closes the
523// connection.
524func (c *C) Close() error {
525	c.cl.CommandTimeout = 5 * time.Second
526
527	if err := c.cl.Quit(); err != nil {
528		var smtpErr *smtp.SMTPError
529		var netErr *net.OpError
530		if errors.As(err, &smtpErr) && smtpErr.Code == 421 {
531			// 421 "Service not available" is typically sent
532			// when idle timeout happens.
533			c.Log.DebugMsg("QUIT error", "reason", c.wrapClientErr(err, c.serverName))
534		} else if errors.As(err, &netErr) &&
535			(netErr.Timeout() || netErr.Err.Error() == "write: broken pipe" || netErr.Err.Error() == "read: connection reset") {
536			// The case for silently closed connections.
537			c.Log.DebugMsg("QUIT error", "reason", c.wrapClientErr(err, c.serverName))
538		} else {
539			c.Log.Error("QUIT error", c.wrapClientErr(err, c.serverName))
540		}
541
542		return c.cl.Close()
543	}
544
545	c.cl = nil
546	c.serverName = ""
547
548	return nil
549}
550
551// DirectClose closes the underlying connection without sending the QUIT
552// command.
553func (c *C) DirectClose() error {
554	c.cl.Close()
555	c.cl = nil
556	c.serverName = ""
557	return nil
558}