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*/1819// Package smtpconn contains the code shared between target.smtp and20// remote modules.21//22// It implements the wrapper over the SMTP connection (go-smtp.Client) object23// 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 smtpconn2930import (31 "context"32 "crypto/tls"33 "errors"34 "fmt"35 "io"36 "net"37 "runtime/trace"38 "time"3940 "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)4748// The C object represents the SMTP connection and is a wrapper around49// 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.Dialer54 // DialContext by New.55 Dialer func(ctx context.Context, network, addr string) (net.Conn, error)5657 // Timeout for most session commands (EHLO, MAIL, RCPT, DATA, STARTTLS).58 // Set to 5 mins by New.59 CommandTimeout time.Duration6061 // Timeout for the initial TCP connection establishment.62 ConnectTimeout time.Duration6364 // Timeout for the final dot. Set to 12 mins by New.65 // (see go-smtp source for explanation of used defaults).66 SubmissionTimeout time.Duration6768 // Hostname to sent in the EHLO/HELO command. Set to69 // 'localhost.localdomain' by New. Expected to be encoded in ACE form.70 Hostname string7172 // tls.Config to use. Can be nil if no special changes are required.73 TLSConfig *tls.Config7475 // Logger to use for debug log and certain errors.76 Log log.Logger7778 // Include the remote server address in SMTP status messages in the form79 // "ADDRESS said: ..."80 AddrInSMTPMsg bool8182 conn net.Conn83 serverName string84 cl *smtp.Client85 rcpts []string86 lmtp bool87}8889// New creates the new instance of the C object, populating the required fields90// 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}101102func (c *C) wrapClientErr(err error, serverName string) error {103 if err == nil {104 return nil105 }106107 switch err := err.(type) {108 case TLSError:109 return err110 case *exterrors.SMTPError:111 return err112 case *smtp.SMTPError:113 msg := err.Message114 if c.AddrInSMTPMsg {115 msg = serverName + " said: " + err.Message116 }117118 if err.Code == 552 {119 err.Code = 452120 err.EnhancedCode[0] = 4121 c.Log.Msg("SMTP code 552 rewritten to 452 per RFC 5321 Section 4.5.3.1.10")122 }123124 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.Addr137 misc["io_op"] = err.Op138 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}163164// 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 }171172 c.serverName = endp.Host173 c.cl = cl174 c.conn = conn175176 c.Log.DebugMsg("connected", "remote_server", c.serverName,177 "local_addr", c.LocalAddr(), "remote_addr", c.RemoteAddr())178179 return didTLS, nil180}181182// ConnectLMTP estabilishes the network connection with the remote host and183// 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 }189190 c.serverName = endp.Host191 c.cl = cl192 c.conn = conn193194 c.Log.DebugMsg("connected", "remote_server", c.serverName,195 "local_addr", c.LocalAddr(), "remote_addr", c.RemoteAddr())196197 return didTLS, nil198}199200// TLSError is returned by Connect to indicate the error during STARTTLS201// command execution.202//203// If the endpoint uses Implicit TLS, TLS errors are threated as connection204// errors and thus are not returned as TLSError.205type TLSError struct {206 Err error207}208209func (err TLSError) Error() string {210 return "smtpconn: " + err.Err.Error()211}212213func (err TLSError) Unwrap() error {214 return err.Err215}216217func (c *C) LocalAddr() net.Addr {218 if c.conn == nil {219 return nil220 }221 return c.conn.LocalAddr()222}223224func (c *C) RemoteAddr() net.Addr {225 if c.conn == nil {226 return nil227 }228 return c.conn.RemoteAddr()229}230231func (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, err237 }238239 if endp.IsTLS() {240 cfg := tlsConfig.Clone()241 cfg.ServerName = endp.Host242 conn = tls.Client(conn, cfg)243 }244245 c.lmtp = lmtp246 // 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 }252253 cl.CommandTimeout = c.CommandTimeout254 cl.SubmissionTimeout = c.SubmissionTimeout255256 // 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, err260 }261262 if !starttls {263 return false, cl, conn, nil264 }265266 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 }272273 cfg := tlsConfig.Clone()274 cfg.ServerName = endp.Host275 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 happened278 // *after* the handshake (e.g. PKI verification fail), we don't log the error in279 // this case though.280 if err := cl.Quit(); err != nil {281 cl.Close()282 }283284 return false, nil, nil, TLSError{err}285 }286287 // Re-do HELO using our hostname instead of localhost.288 if err := cl.Hello(c.Hostname); err != nil {289 cl.Close()290291 var tlsErr *tls.CertificateVerificationError292 if errors.As(err, &tlsErr) {293 return false, nil, nil, TLSError{Err: tlsErr}294 }295296 return false, nil, nil, err297 }298299 return true, cl, conn, nil300}301302// 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 not306// supported - attempt will be done to convert addresses to the ASCII form, if307// 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()310311 outOpts := smtp.MailOptions{312 // Future extensions may add additional fields that should not be313 // copied blindly. So we copy only fields we know should be handled314 // this way.315316 Size: opts.Size,317 RequireTLS: opts.RequireTLS,318 }319320 // INTERNATIONALIZATION: Use SMTPUTF8 is possible, attempt to convert addresses otherwise.321322 // There is no way we can accept a message with non-ASCII addresses without SMTPUTF8323 // this is enforced by endpoint/smtp.324 if opts.UTF8 {325 if ok, _ := c.cl.Extension("SMTPUTF8"); ok {326 outOpts.UTF8 = true327 } else {328 var err error329 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 }343344 if err := c.cl.Mail(from, &outOpts); err != nil {345 return c.wrapClientErr(err, c.serverName)346 }347348 return nil349}350351// Rcpts returns the list of recipients that were accepted by the remote server.352func (c *C) Rcpts() []string {353 return c.rcpts354}355356func (c *C) ServerName() string {357 return c.serverName358}359360func (c *C) Client() *smtp.Client {361 return c.cl362}363364func (c *C) IsLMTP() bool {365 return c.lmtp366}367368// 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 remote371// 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()374375 outOpts := &smtp.RcptOptions{376 // TODO: DSN support377 }378379 // If necessary, the extension flag is enabled in Start.380 if ok, _ := c.cl.Extension("SMTPUTF8"); !address.IsASCII(to) && !ok {381 var err error382 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 }395396 if err := c.cl.Rcpt(to, outOpts); err != nil {397 return c.wrapClientErr(err, c.serverName)398 }399400 c.rcpts = append(c.rcpts, to)401402 return nil403}404405type lmtpError map[string]*smtp.SMTPError406407func (l lmtpError) SetStatus(rcptTo string, err *smtp.SMTPError) {408 l[rcptTo] = err409}410411func (l lmtpError) singleError() *smtp.SMTPError {412 nonNils := 0413 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 err422 }423 }424 }425 return nil426}427428func (l lmtpError) Unwrap() error {429 if err := l.singleError(); err != nil {430 return err431 }432 return nil433}434435func (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}441442func (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 err446 }447 hasAnyFailures := false448 for _, err := range statusCb {449 if err != nil {450 hasAnyFailures = true451 }452 }453 if hasAnyFailures {454 return statusCb455 }456 return nil457}458459// Data sends the DATA command to the remote server and then sends the message header460// and body.461//462// If the Data command fails, the connection may be in a unclean state (e.g. in463// 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()466467 if c.IsLMTP() {468 return c.smtpToLMTPData(ctx, hdr, body)469 }470471 wc, err := c.cl.Data()472 if err != nil {473 return c.wrapClientErr(err, c.serverName)474 }475476 if err := textproto.WriteHeader(wc, hdr); err != nil {477 return c.wrapClientErr(err, c.serverName)478 }479480 if _, err := io.Copy(wc, body); err != nil {481 return c.wrapClientErr(err, c.serverName)482 }483484 if err := wc.Close(); err != nil {485 return c.wrapClientErr(err, c.serverName)486 }487488 return nil489}490491func (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()493494 wc, err := c.cl.LMTPData(statusCb)495 if err != nil {496 return c.wrapClientErr(err, c.serverName)497 }498499 if err := textproto.WriteHeader(wc, hdr); err != nil {500 return c.wrapClientErr(err, c.serverName)501 }502503 if _, err := io.Copy(wc, body); err != nil {504 return c.wrapClientErr(err, c.serverName)505 }506507 if err := wc.Close(); err != nil {508 return c.wrapClientErr(err, c.serverName)509 }510511 return nil512}513514func (c *C) Noop() error {515 if c.cl == nil {516 return errors.New("smtpconn: not connected")517 }518519 return c.cl.Noop()520}521522// Close sends the QUIT command, if it fails - it directly closes the523// connection.524func (c *C) Close() error {525 c.cl.CommandTimeout = 5 * time.Second526527 if err := c.cl.Quit(); err != nil {528 var smtpErr *smtp.SMTPError529 var netErr *net.OpError530 if errors.As(err, &smtpErr) && smtpErr.Code == 421 {531 // 421 "Service not available" is typically sent532 // 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 }541542 return c.cl.Close()543 }544545 c.cl = nil546 c.serverName = ""547548 return nil549}550551// DirectClose closes the underlying connection without sending the QUIT552// command.553func (c *C) DirectClose() error {554 c.cl.Close()555 c.cl = nil556 c.serverName = ""557 return nil558}