2024-10-24 23:12:34 +13:00
// Package smtpd implements a basic SMTP server.
//
// This is a modified version of https://github.com/mhale/smtpd to
2025-01-25 12:17:15 +13:00
// add support for unix sockets and Mailpit Chaos.
2024-10-24 23:12:34 +13:00
package smtpd
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io/fs"
"log"
"net"
"os"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
2025-01-25 12:17:15 +13:00
"github.com/axllent/mailpit/internal/smtpd/chaos"
2024-10-24 23:12:34 +13:00
)
var (
// Debug `true` enables verbose logging.
Debug = false
2024-12-14 19:56:20 +01:00
rcptToRE = regexp . MustCompile ( ` (?i)TO: ?<([^<>\v]+)>( |$)(.*)? ` )
mailFromRE = regexp . MustCompile ( ` (?i)FROM: ?<(|[^<>\v]+)>( |$)(.*)? ` ) // Delivery Status Notifications are sent with "MAIL FROM:<>"
2024-12-13 15:40:11 +13:00
// extract mail size from 'MAIL FROM' parameter
mailFromSizeRE = regexp . MustCompile ( ` (?U)(^| |,)[Ss][Ii][Zz][Ee]=(.*)($|,| ) ` )
2024-10-24 23:12:34 +13:00
)
// Handler function called upon successful receipt of an email.
// Results in a "250 2.0.0 Ok: queued" response.
type Handler func ( remoteAddr net . Addr , from string , to [ ] string , data [ ] byte ) error
// MsgIDHandler function called upon successful receipt of an email. Returns a message ID.
// Results in a "250 2.0.0 Ok: queued as <message-id>" response.
type MsgIDHandler func ( remoteAddr net . Addr , from string , to [ ] string , data [ ] byte ) ( string , error )
// HandlerRcpt function called on RCPT. Return accept status.
type HandlerRcpt func ( remoteAddr net . Addr , from string , to string ) bool
// AuthHandler function called when a login attempt is performed. Returns true if credentials are correct.
type AuthHandler func ( remoteAddr net . Addr , mechanism string , username [ ] byte , password [ ] byte , shared [ ] byte ) ( bool , error )
// ErrServerClosed is the default message when a server closes a connection
var ErrServerClosed = errors . New ( "Server has been closed" )
// ListenAndServe listens on the TCP network address addr
// and then calls Serve with handler to handle requests
// on incoming connections.
func ListenAndServe ( addr string , handler Handler , appName string , hostname string ) error {
srv := & Server { Addr : addr , Handler : handler , AppName : appName , Hostname : hostname }
return srv . ListenAndServe ( )
}
// ListenAndServeTLS listens on the TCP network address addr
// and then calls Serve with handler to handle requests
// on incoming connections. Connections may be upgraded to TLS if the client requests it.
func ListenAndServeTLS ( addr string , certFile string , keyFile string , handler Handler , appName string , hostname string ) error {
srv := & Server { Addr : addr , Handler : handler , AppName : appName , Hostname : hostname }
err := srv . ConfigureTLS ( certFile , keyFile )
if err != nil {
return err
}
return srv . ListenAndServe ( )
}
type maxSizeExceededError struct {
limit int
}
func maxSizeExceeded ( limit int ) maxSizeExceededError {
return maxSizeExceededError { limit }
}
// Error uses the RFC 5321 response message in preference to RFC 1870.
// RFC 3463 defines enhanced status code x.3.4 as "Message too big for system".
func ( err maxSizeExceededError ) Error ( ) string {
return fmt . Sprintf ( "552 5.3.4 Requested mail action aborted: exceeded storage allocation (%d)" , err . limit )
}
// LogFunc is a function capable of logging the client-server communication.
type LogFunc func ( remoteIP , verb , line string )
// Server is an SMTP server.
type Server struct {
Addr string // TCP address to listen on, defaults to ":25" (all addresses, port 25) if empty
AppName string
AuthHandler AuthHandler
AuthMechs map [ string ] bool // Override list of allowed authentication mechanisms. Currently supported: LOGIN, PLAIN, CRAM-MD5. Enabling LOGIN and PLAIN will reduce RFC 4954 compliance.
AuthRequired bool // Require authentication for every command except AUTH, EHLO, HELO, NOOP, RSET or QUIT as per RFC 4954. Ignored if AuthHandler is not configured.
DisableReverseDNS bool // Disable reverse DNS lookups, enforces "unknown" hostname
Handler Handler
HandlerRcpt HandlerRcpt
Hostname string
LogRead LogFunc
LogWrite LogFunc
MaxSize int // Maximum message size allowed, in bytes
MaxRecipients int // Maximum number of recipients, defaults to 100.
MsgIDHandler MsgIDHandler
Timeout time . Duration
TLSConfig * tls . Config
TLSListener bool // Listen for incoming TLS connections only (not recommended as it may reduce compatibility). Ignored if TLS is not configured.
TLSRequired bool // Require TLS for every command except NOOP, EHLO, STARTTLS, or QUIT as per RFC 3207. Ignored if TLS is not configured.
Protocol string // Default tcp, supports unix
SocketPerm fs . FileMode // if using Unix socket, socket permissions
inShutdown int32 // server was closed or shutdown
openSessions int32 // count of open sessions
mu sync . Mutex
shutdownChan chan struct { } // let the sessions know we are shutting down
XClientAllowed [ ] string // List of XCLIENT allowed IP addresses
}
// ConfigureTLS creates a TLS configuration from certificate and key files.
func ( srv * Server ) ConfigureTLS ( certFile string , keyFile string ) error {
cert , err := tls . LoadX509KeyPair ( certFile , keyFile )
if err != nil {
return err
}
srv . TLSConfig = & tls . Config { Certificates : [ ] tls . Certificate { cert } } // #nosec
return nil
}
// // ConfigureTLSWithPassphrase creates a TLS configuration from a certificate,
// // an encrypted key file and the associated passphrase:
// func (srv *Server) ConfigureTLSWithPassphrase(
// certFile string,
// keyFile string,
// passphrase string,
// ) error {
// certPEMBlock, err := os.ReadFile(certFile)
// if err != nil {
// return err
// }
// keyPEMBlock, err := os.ReadFile(keyFile)
// if err != nil {
// return err
// }
// keyDERBlock, _ := pem.Decode(keyPEMBlock)
// keyPEMDecrypted, err := x509.DecryptPEMBlock(keyDERBlock, []byte(passphrase))
// if err != nil {
// return err
// }
// var pemBlock pem.Block
// pemBlock.Type = keyDERBlock.Type
// pemBlock.Bytes = keyPEMDecrypted
// keyPEMBlock = pem.EncodeToMemory(&pemBlock)
// cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
// if err != nil {
// return err
// }
// srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
// return nil
// }
// ListenAndServe listens on the either a TCP network address srv.Addr or
// alternatively a Unix socket. and then calls Serve to handle requests on
// incoming connections. If srv.Addr is blank, ":25" is used.
func ( srv * Server ) ListenAndServe ( ) error {
if atomic . LoadInt32 ( & srv . inShutdown ) != 0 {
return ErrServerClosed
}
if srv . Addr == "" {
srv . Addr = ":25"
}
if srv . AppName == "" {
srv . AppName = "smtpd"
}
if srv . Hostname == "" {
srv . Hostname , _ = os . Hostname ( )
}
if srv . Timeout == 0 {
srv . Timeout = 5 * time . Minute
}
if srv . Protocol == "" {
srv . Protocol = "tcp"
}
var ln net . Listener
var err error
// If TLSListener is enabled, listen for TLS connections only.
if srv . TLSConfig != nil && srv . TLSListener {
ln , err = tls . Listen ( srv . Protocol , srv . Addr , srv . TLSConfig )
} else {
ln , err = net . Listen ( srv . Protocol , srv . Addr )
}
if err != nil {
return err
}
if srv . Protocol == "unix" {
// set permissions
if err := os . Chmod ( srv . Addr , srv . SocketPerm ) ; err != nil {
return err
}
}
return srv . Serve ( ln )
}
// Serve creates a new SMTP session after a network connection is established.
func ( srv * Server ) Serve ( ln net . Listener ) error {
if atomic . LoadInt32 ( & srv . inShutdown ) != 0 {
return ErrServerClosed
}
defer ln . Close ( )
for {
// if we are shutting down, don't accept new connections
select {
case <- srv . getShutdownChan ( ) :
return ErrServerClosed
default :
}
conn , err := ln . Accept ( )
if err != nil {
if netErr , ok := err . ( net . Error ) ; ok && netErr . Temporary ( ) {
continue
}
return err
}
session := srv . newSession ( conn )
atomic . AddInt32 ( & srv . openSessions , 1 )
go session . serve ( )
}
}
type session struct {
srv * Server
conn net . Conn
br * bufio . Reader
bw * bufio . Writer
remoteIP string // Remote IP address
remoteHost string // Remote hostname according to reverse DNS lookup
remoteName string // Remote hostname as supplied with EHLO
xClient string // Information string as supplied with XCLIENT
xClientADDR string // Information string as supplied with XCLIENT ADDR
xClientNAME string // Information string as supplied with XCLIENT NAME
xClientTrust bool // Trust XCLIENT from current IP address
tls bool
authenticated bool
}
// Create new session from connection.
func ( srv * Server ) newSession ( conn net . Conn ) ( s * session ) {
s = & session {
srv : srv ,
conn : conn ,
br : bufio . NewReader ( conn ) ,
bw : bufio . NewWriter ( conn ) ,
}
// Get remote end info for the Received header.
s . remoteIP , _ , _ = net . SplitHostPort ( s . conn . RemoteAddr ( ) . String ( ) )
if s . remoteIP == "" {
s . remoteIP = "127.0.0.1"
}
if ! s . srv . DisableReverseDNS {
names , err := net . LookupAddr ( s . remoteIP )
if err == nil && len ( names ) > 0 {
s . remoteHost = names [ 0 ]
} else {
s . remoteHost = "unknown"
}
} else {
s . remoteHost = "unknown"
}
// Set tls = true if TLS is already in use.
_ , s . tls = s . conn . ( * tls . Conn )
for _ , checkIP := range srv . XClientAllowed {
if s . remoteIP == checkIP {
s . xClientTrust = true
}
}
return
}
func ( srv * Server ) getShutdownChan ( ) <- chan struct { } {
srv . mu . Lock ( )
defer srv . mu . Unlock ( )
if srv . shutdownChan == nil {
srv . shutdownChan = make ( chan struct { } )
}
return srv . shutdownChan
}
func ( srv * Server ) closeShutdownChan ( ) {
srv . mu . Lock ( )
defer srv . mu . Unlock ( )
if srv . shutdownChan == nil {
srv . shutdownChan = make ( chan struct { } )
}
select {
case <- srv . shutdownChan :
default :
close ( srv . shutdownChan )
}
}
// Close - closes the connection without waiting
func ( srv * Server ) Close ( ) error {
atomic . StoreInt32 ( & srv . inShutdown , 1 )
srv . closeShutdownChan ( )
return nil
}
// Shutdown - waits for current sessions to complete before closing
func ( srv * Server ) Shutdown ( ctx context . Context ) error {
atomic . StoreInt32 ( & srv . inShutdown , 1 )
srv . closeShutdownChan ( )
// wait for up to 30 seconds to allow the current sessions to
// end
timer := time . NewTimer ( 100 * time . Millisecond )
defer timer . Stop ( )
for i := 0 ; i < 300 ; i ++ {
// wait for open sessions to close
if atomic . LoadInt32 ( & srv . openSessions ) == 0 {
break
}
select {
case <- timer . C :
timer . Reset ( 100 * time . Millisecond )
case <- ctx . Done ( ) :
return ctx . Err ( )
default :
}
}
return nil
}
// Function called to handle connection requests.
func ( s * session ) serve ( ) {
defer atomic . AddInt32 ( & s . srv . openSessions , - 1 )
defer s . conn . Close ( )
var from string
var gotFrom bool
var to [ ] string
var buffer bytes . Buffer
2025-02-15 22:57:25 +13:00
// RFC 5321 specifies support for minimum of 100 recipients is required.
if s . srv . MaxRecipients == 0 {
s . srv . MaxRecipients = 100
}
2024-10-24 23:12:34 +13:00
// Send banner.
s . writef ( "220 %s %s ESMTP Service ready" , s . srv . Hostname , s . srv . AppName )
loop :
for {
// Attempt to read a line from the socket.
// On timeout, send a timeout message and return from serve().
// On error, assume the client has gone away i.e. return from serve().
line , err := s . readLine ( )
if err != nil {
if netErr , ok := err . ( net . Error ) ; ok && netErr . Timeout ( ) {
s . writef ( "421 4.4.2 %s %s ESMTP Service closing transmission channel after timeout exceeded" , s . srv . Hostname , s . srv . AppName )
}
break
}
verb , args := s . parseLine ( line )
switch verb {
case "HELO" :
s . remoteName = args
s . writef ( "250 %s greets %s" , s . srv . Hostname , s . remoteName )
// RFC 2821 section 4.1.4 specifies that EHLO has the same effect as RSET, so reset for HELO too.
from = ""
gotFrom = false
to = nil
buffer . Reset ( )
case "EHLO" :
s . remoteName = args
2025-01-25 11:57:32 +13:00
s . writef ( "%s" , s . makeEHLOResponse ( ) )
2024-10-24 23:12:34 +13:00
// RFC 2821 section 4.1.4 specifies that EHLO has the same effect as RSET.
from = ""
gotFrom = false
to = nil
buffer . Reset ( )
case "MAIL" :
if s . srv . TLSConfig != nil && s . srv . TLSRequired && ! s . tls {
s . writef ( "530 5.7.0 Must issue a STARTTLS command first" )
break
}
if s . srv . AuthHandler != nil && s . srv . AuthRequired && ! s . authenticated {
s . writef ( "530 5.7.0 Authentication required" )
break
}
match := mailFromRE . FindStringSubmatch ( args )
if match == nil {
s . writef ( "501 5.5.4 Syntax error in parameters or arguments (invalid FROM parameter)" )
} else {
2025-01-25 12:17:15 +13:00
// Mailpit Chaos
if fail , code := chaos . Config . Sender . Trigger ( ) ; fail {
s . writef ( "%d Chaos sender error" , code )
break
}
2024-10-24 23:12:34 +13:00
// Validate the SIZE parameter if one was sent.
if len ( match [ 2 ] ) > 0 { // A parameter is present
2024-12-13 15:40:11 +13:00
sizeMatch := mailFromSizeRE . FindStringSubmatch ( match [ 3 ] )
2024-10-24 23:12:34 +13:00
if sizeMatch == nil {
2024-12-13 15:40:11 +13:00
// ignore other parameter
2024-12-13 20:05:30 +13:00
from = match [ 1 ]
gotFrom = true
2024-12-13 15:40:11 +13:00
s . writef ( "250 2.1.0 Ok" )
2024-10-24 23:12:34 +13:00
} else {
// Enforce the maximum message size if one is set.
2024-12-13 15:40:11 +13:00
size , err := strconv . Atoi ( sizeMatch [ 2 ] )
2024-10-24 23:12:34 +13:00
if err != nil { // Bad SIZE parameter
s . writef ( "501 5.5.4 Syntax error in parameters or arguments (invalid SIZE parameter)" )
} else if s . srv . MaxSize > 0 && size > s . srv . MaxSize { // SIZE above maximum size, if set
err = maxSizeExceeded ( s . srv . MaxSize )
2025-01-18 14:47:20 +13:00
s . writef ( "%s" , err . Error ( ) )
2024-10-24 23:12:34 +13:00
} else { // SIZE ok
from = match [ 1 ]
gotFrom = true
s . writef ( "250 2.1.0 Ok" )
}
}
} else { // No parameters after FROM
from = match [ 1 ]
gotFrom = true
s . writef ( "250 2.1.0 Ok" )
}
}
2025-01-25 12:17:15 +13:00
2024-10-24 23:12:34 +13:00
to = nil
buffer . Reset ( )
case "RCPT" :
if s . srv . TLSConfig != nil && s . srv . TLSRequired && ! s . tls {
s . writef ( "530 5.7.0 Must issue a STARTTLS command first" )
break
}
if s . srv . AuthHandler != nil && s . srv . AuthRequired && ! s . authenticated {
s . writef ( "530 5.7.0 Authentication required" )
break
}
if ! gotFrom {
s . writef ( "503 5.5.1 Bad sequence of commands (MAIL required before RCPT)" )
break
}
match := rcptToRE . FindStringSubmatch ( args )
if match == nil {
s . writef ( "501 5.5.4 Syntax error in parameters or arguments (invalid TO parameter)" )
} else {
2025-01-25 12:17:15 +13:00
// Mailpit Chaos
if fail , code := chaos . Config . Recipient . Trigger ( ) ; fail {
s . writef ( "%d Chaos recipient error" , code )
break
}
2025-02-15 22:57:25 +13:00
if len ( to ) >= s . srv . MaxRecipients {
2024-10-24 23:12:34 +13:00
s . writef ( "452 4.5.3 Too many recipients" )
} else {
accept := true
if s . srv . HandlerRcpt != nil {
accept = s . srv . HandlerRcpt ( s . conn . RemoteAddr ( ) , from , match [ 1 ] )
}
if accept {
to = append ( to , match [ 1 ] )
s . writef ( "250 2.1.5 Ok" )
} else {
s . writef ( "550 5.1.0 Requested action not taken: mailbox unavailable" )
}
}
}
case "DATA" :
if s . srv . TLSConfig != nil && s . srv . TLSRequired && ! s . tls {
s . writef ( "530 5.7.0 Must issue a STARTTLS command first" )
break
}
if s . srv . AuthHandler != nil && s . srv . AuthRequired && ! s . authenticated {
s . writef ( "530 5.7.0 Authentication required" )
break
}
if ! gotFrom || len ( to ) == 0 {
s . writef ( "503 5.5.1 Bad sequence of commands (MAIL & RCPT required before DATA)" )
break
}
s . writef ( "354 Start mail input; end with <CR><LF>.<CR><LF>" )
// Attempt to read message body from the socket.
// On timeout, send a timeout message and return from serve().
// On net.Error, assume the client has gone away i.e. return from serve().
// On other errors, allow the client to try again.
data , err := s . readData ( )
if err != nil {
switch err . ( type ) {
case net . Error :
if err . ( net . Error ) . Timeout ( ) {
s . writef ( "421 4.4.2 %s %s ESMTP Service closing transmission channel after timeout exceeded" , s . srv . Hostname , s . srv . AppName )
}
break loop
case maxSizeExceededError :
2025-01-18 14:47:20 +13:00
s . writef ( "%s" , err . Error ( ) )
2024-10-24 23:12:34 +13:00
continue
default :
s . writef ( "451 4.3.0 Requested action aborted: local error in processing" )
continue
}
}
// Create Received header & write message body into buffer.
buffer . Reset ( )
buffer . Write ( s . makeHeaders ( to ) )
buffer . Write ( data )
// Pass mail on to handler.
if s . srv . Handler != nil {
err := s . srv . Handler ( s . conn . RemoteAddr ( ) , from , to , buffer . Bytes ( ) )
if err != nil {
checkErrFormat := regexp . MustCompile ( ` ^([2-5][0-9] { 2})[\s\-](.+)$ ` )
if checkErrFormat . MatchString ( err . Error ( ) ) {
2025-01-18 14:47:20 +13:00
s . writef ( "%s" , err . Error ( ) )
2024-10-24 23:12:34 +13:00
} else {
s . writef ( "451 4.3.5 Unable to process mail" )
}
break
}
s . writef ( "250 2.0.0 Ok: queued" )
} else if s . srv . MsgIDHandler != nil {
msgID , err := s . srv . MsgIDHandler ( s . conn . RemoteAddr ( ) , from , to , buffer . Bytes ( ) )
if err != nil {
checkErrFormat := regexp . MustCompile ( ` ^([2-5][0-9] { 2})[\s\-](.+)$ ` )
if checkErrFormat . MatchString ( err . Error ( ) ) {
2025-01-18 14:47:20 +13:00
s . writef ( "%s" , err . Error ( ) )
2024-10-24 23:12:34 +13:00
} else {
s . writef ( "451 4.3.5 Unable to process mail" )
}
break
}
if msgID != "" {
2025-01-18 14:47:20 +13:00
s . writef ( "250 2.0.0 Ok: queued as %s" , msgID )
2024-10-24 23:12:34 +13:00
} else {
s . writef ( "250 2.0.0 Ok: queued" )
}
} else {
s . writef ( "250 2.0.0 Ok: queued" )
}
// Reset for next mail.
from = ""
gotFrom = false
to = nil
buffer . Reset ( )
case "QUIT" :
s . writef ( "221 2.0.0 %s %s ESMTP Service closing transmission channel" , s . srv . Hostname , s . srv . AppName )
break loop
case "RSET" :
if s . srv . TLSConfig != nil && s . srv . TLSRequired && ! s . tls {
s . writef ( "530 5.7.0 Must issue a STARTTLS command first" )
break
}
s . writef ( "250 2.0.0 Ok" )
from = ""
gotFrom = false
to = nil
buffer . Reset ( )
case "NOOP" :
s . writef ( "250 2.0.0 Ok" )
case "XCLIENT" :
s . xClient = args
if s . xClientTrust {
xCArgs := strings . Split ( args , " " )
for _ , xCArg := range xCArgs {
xCParse := strings . Split ( strings . TrimSpace ( xCArg ) , "=" )
if strings . ToUpper ( xCParse [ 0 ] ) == "ADDR" && ( net . ParseIP ( xCParse [ 1 ] ) != nil ) {
s . xClientADDR = xCParse [ 1 ]
}
if strings . ToUpper ( xCParse [ 0 ] ) == "NAME" && len ( xCParse [ 1 ] ) > 0 {
if xCParse [ 1 ] != "[UNAVAILABLE]" {
s . xClientNAME = xCParse [ 1 ]
}
}
}
if len ( s . xClientADDR ) > 7 {
s . remoteIP = s . xClientADDR
if len ( s . xClientNAME ) > 4 {
s . remoteHost = s . xClientNAME
} else {
names , err := net . LookupAddr ( s . remoteIP )
if err == nil && len ( names ) > 0 {
s . remoteHost = names [ 0 ]
} else {
s . remoteHost = "unknown"
}
}
}
}
s . writef ( "250 2.0.0 Ok" )
case "HELP" , "VRFY" , "EXPN" :
// See RFC 5321 section 4.2.4 for usage of 500 & 502 response codes.
s . writef ( "502 5.5.1 Command not implemented" )
case "STARTTLS" :
// Parameters are not allowed (RFC 3207 section 4).
if args != "" {
s . writef ( "501 5.5.2 Syntax error (no parameters allowed)" )
break
}
// Handle case where TLS is requested but not configured (and therefore not listed as a service extension).
if s . srv . TLSConfig == nil {
s . writef ( "502 5.5.1 Command not implemented" )
break
}
// Handle case where STARTTLS is received when TLS is already in use.
if s . tls {
s . writef ( "503 5.5.1 Bad sequence of commands (TLS already in use)" )
break
}
s . writef ( "220 2.0.0 Ready to start TLS" )
// Establish a TLS connection with the client.
tlsConn := tls . Server ( s . conn , s . srv . TLSConfig )
err := tlsConn . Handshake ( )
if err != nil {
s . writef ( "403 4.7.0 TLS handshake failed" )
break
}
// TLS handshake succeeded, switch to using the TLS connection.
s . conn = tlsConn
s . br = bufio . NewReader ( s . conn )
s . bw = bufio . NewWriter ( s . conn )
s . tls = true
// RFC 3207 specifies that the server must discard any prior knowledge obtained from the client.
s . remoteName = ""
from = ""
gotFrom = false
to = nil
buffer . Reset ( )
case "AUTH" :
if s . srv . TLSConfig != nil && s . srv . TLSRequired && ! s . tls {
s . writef ( "530 5.7.0 Must issue a STARTTLS command first" )
break
}
// Handle case where AUTH is requested but not configured (and therefore not listed as a service extension).
if s . srv . AuthHandler == nil {
s . writef ( "502 5.5.1 Command not implemented" )
break
}
// Handle case where AUTH is received when already authenticated.
if s . authenticated {
s . writef ( "503 5.5.1 Bad sequence of commands (already authenticated for this session)" )
break
}
// RFC 4954 specifies that AUTH is not permitted during mail transactions.
if gotFrom || len ( to ) > 0 {
s . writef ( "503 5.5.1 Bad sequence of commands (AUTH not permitted during mail transaction)" )
break
}
// RFC 4954 requires a mechanism parameter.
authType , authArgs := s . parseLine ( args )
if authType == "" {
s . writef ( "501 5.5.4 Malformed AUTH input (argument required)" )
break
}
// RFC 4954 requires rejecting unsupported authentication mechanisms with a 504 response.
allowedAuth := s . authMechs ( )
if allowed , found := allowedAuth [ authType ] ; ! found || ! allowed {
s . writef ( "504 5.5.4 Unrecognized authentication type" )
break
}
2025-01-25 12:17:15 +13:00
// Mailpit Chaos
if fail , code := chaos . Config . Authentication . Trigger ( ) ; fail {
s . writef ( "%d Chaos authentication error" , code )
break
}
2024-10-24 23:12:34 +13:00
// RFC 4954 also specifies that ESMTP code 5.5.4 ("Invalid command arguments") should be returned
// when attempting to use an unsupported authentication type.
// Many servers return 5.7.4 ("Security features not supported") instead.
switch authType {
case "PLAIN" :
s . authenticated , err = s . handleAuthPlain ( authArgs )
case "LOGIN" :
s . authenticated , err = s . handleAuthLogin ( authArgs )
case "CRAM-MD5" :
s . authenticated , err = s . handleAuthCramMD5 ( )
}
if err != nil {
if netErr , ok := err . ( net . Error ) ; ok && netErr . Timeout ( ) {
s . writef ( "421 4.4.2 %s %s ESMTP Service closing transmission channel after timeout exceeded" , s . srv . Hostname , s . srv . AppName )
break loop
}
2025-01-18 14:47:20 +13:00
s . writef ( "%s" , err . Error ( ) )
2024-10-24 23:12:34 +13:00
break
}
if s . authenticated {
s . writef ( "235 2.7.0 Authentication successful" )
} else {
s . writef ( "535 5.7.8 Authentication credentials invalid" )
}
default :
// See RFC 5321 section 4.2.4 for usage of 500 & 502 response codes.
s . writef ( "500 5.5.2 Syntax error, command unrecognized" )
}
}
}
// Wrapper function for writing a complete line to the socket.
func ( s * session ) writef ( format string , args ... interface { } ) {
if s . srv . Timeout > 0 {
_ = s . conn . SetWriteDeadline ( time . Now ( ) . Add ( s . srv . Timeout ) )
}
line := fmt . Sprintf ( format , args ... )
2025-01-18 14:47:20 +13:00
fmt . Fprintf ( s . bw , "%s\r\n" , line )
2024-10-24 23:12:34 +13:00
_ = s . bw . Flush ( )
if Debug {
verb := "WROTE"
if s . srv . LogWrite != nil {
s . srv . LogWrite ( s . remoteIP , verb , line )
} else {
log . Println ( s . remoteIP , verb , line )
}
}
}
// Read a complete line from the socket.
func ( s * session ) readLine ( ) ( string , error ) {
if s . srv . Timeout > 0 {
_ = s . conn . SetReadDeadline ( time . Now ( ) . Add ( s . srv . Timeout ) )
}
line , err := s . br . ReadString ( '\n' )
if err != nil {
return "" , err
}
line = strings . TrimSpace ( line ) // Strip trailing \r\n
if Debug {
verb := "READ"
if s . srv . LogRead != nil {
s . srv . LogRead ( s . remoteIP , verb , line )
} else {
log . Println ( s . remoteIP , verb , line )
}
}
return line , err
}
// Parse a line read from the socket.
func ( s * session ) parseLine ( line string ) ( verb string , args string ) {
if idx := strings . Index ( line , " " ) ; idx != - 1 {
verb = strings . ToUpper ( line [ : idx ] )
args = strings . TrimSpace ( line [ idx + 1 : ] )
} else {
verb = strings . ToUpper ( line )
args = ""
}
return verb , args
}
// Read the message data following a DATA command.
func ( s * session ) readData ( ) ( [ ] byte , error ) {
var data [ ] byte
for {
if s . srv . Timeout > 0 {
_ = s . conn . SetReadDeadline ( time . Now ( ) . Add ( s . srv . Timeout ) )
}
line , err := s . br . ReadBytes ( '\n' )
if err != nil {
return nil , err
}
// Handle end of data denoted by lone period (\r\n.\r\n)
if bytes . Equal ( line , [ ] byte ( ".\r\n" ) ) {
break
}
// Remove leading period (RFC 5321 section 4.5.2)
if line [ 0 ] == '.' {
line = line [ 1 : ]
}
// Enforce the maximum message size limit.
if s . srv . MaxSize > 0 {
if len ( data ) + len ( line ) > s . srv . MaxSize {
_ , _ = s . br . Discard ( s . br . Buffered ( ) ) // Discard the buffer remnants.
return nil , maxSizeExceeded ( s . srv . MaxSize )
}
}
data = append ( data , line ... )
}
return data , nil
}
// Create the Received header to comply with RFC 2821 section 3.8.2.
// TODO: Work out what to do with multiple to addresses.
func ( s * session ) makeHeaders ( to [ ] string ) [ ] byte {
var buffer bytes . Buffer
now := time . Now ( ) . Format ( "Mon, 2 Jan 2006 15:04:05 -0700 (MST)" )
buffer . WriteString ( fmt . Sprintf ( "Received: from %s (%s [%s])\r\n" , s . remoteName , s . remoteHost , s . remoteIP ) )
buffer . WriteString ( fmt . Sprintf ( " by %s (%s) with SMTP\r\n" , s . srv . Hostname , s . srv . AppName ) )
buffer . WriteString ( fmt . Sprintf ( " for <%s>; %s\r\n" , to [ 0 ] , now ) )
return buffer . Bytes ( )
}
// Determine allowed authentication mechanisms.
// RFC 4954 specifies that plaintext authentication mechanisms such as LOGIN and PLAIN require a TLS connection.
// This can be explicitly overridden e.g. setting s.srv.AuthMechs["LOGIN"] = true.
func ( s * session ) authMechs ( ) ( mechs map [ string ] bool ) {
mechs = map [ string ] bool { "LOGIN" : s . tls , "PLAIN" : s . tls , "CRAM-MD5" : true }
for mech := range mechs {
allowed , found := s . srv . AuthMechs [ mech ]
if found {
mechs [ mech ] = allowed
}
}
return
}
// Create the greeting string sent in response to an EHLO command.
func ( s * session ) makeEHLOResponse ( ) ( response string ) {
response = fmt . Sprintf ( "250-%s greets %s\r\n" , s . srv . Hostname , s . remoteName )
// RFC 1870 specifies that "SIZE 0" indicates no maximum size is in force.
response += fmt . Sprintf ( "250-SIZE %d\r\n" , s . srv . MaxSize )
// Only list STARTTLS if TLS is configured, but not currently in use.
if s . srv . TLSConfig != nil && ! s . tls {
response += "250-STARTTLS\r\n"
}
// Only list AUTH if an AuthHandler is configured and at least one mechanism is allowed.
if s . srv . AuthHandler != nil {
var mechs [ ] string
for mech , allowed := range s . authMechs ( ) {
if allowed {
mechs = append ( mechs , mech )
}
}
if len ( mechs ) > 0 {
response += "250-AUTH " + strings . Join ( mechs , " " ) + "\r\n"
}
}
response += "250 ENHANCEDSTATUSCODES"
return
}
func ( s * session ) handleAuthLogin ( arg string ) ( bool , error ) {
var err error
if arg == "" {
2025-01-18 14:47:20 +13:00
s . writef ( "334 %s" , base64 . StdEncoding . EncodeToString ( [ ] byte ( "Username:" ) ) )
2024-10-24 23:12:34 +13:00
arg , err = s . readLine ( )
if err != nil {
return false , err
}
}
username , err := base64 . StdEncoding . DecodeString ( arg )
if err != nil {
return false , errors . New ( "501 5.5.2 Syntax error (unable to decode)" )
}
2025-01-18 14:47:20 +13:00
s . writef ( "334 %s" , base64 . StdEncoding . EncodeToString ( [ ] byte ( "Password:" ) ) )
2024-10-24 23:12:34 +13:00
line , err := s . readLine ( )
if err != nil {
return false , err
}
password , err := base64 . StdEncoding . DecodeString ( line )
if err != nil {
return false , errors . New ( "501 5.5.2 Syntax error (unable to decode)" )
}
// Validate credentials.
authenticated , err := s . srv . AuthHandler ( s . conn . RemoteAddr ( ) , "LOGIN" , username , password , nil )
return authenticated , err
}
func ( s * session ) handleAuthPlain ( arg string ) ( bool , error ) {
var err error
// If fast mode (AUTH PLAIN [arg]) is not used, prompt for credentials.
if arg == "" {
s . writef ( "334 " )
arg , err = s . readLine ( )
if err != nil {
return false , err
}
}
data , err := base64 . StdEncoding . DecodeString ( arg )
if err != nil {
return false , errors . New ( "501 5.5.2 Syntax error (unable to decode)" )
}
parts := bytes . Split ( data , [ ] byte { 0 } )
if len ( parts ) != 3 {
return false , errors . New ( "501 5.5.2 Syntax error (unable to parse)" )
}
// Validate credentials.
authenticated , err := s . srv . AuthHandler ( s . conn . RemoteAddr ( ) , "PLAIN" , parts [ 1 ] , parts [ 2 ] , nil )
return authenticated , err
}
func ( s * session ) handleAuthCramMD5 ( ) ( bool , error ) {
shared := "<" + strconv . Itoa ( os . Getpid ( ) ) + "." + strconv . Itoa ( time . Now ( ) . Nanosecond ( ) ) + "@" + s . srv . Hostname + ">"
2025-01-18 14:47:20 +13:00
s . writef ( "334 %s" , base64 . StdEncoding . EncodeToString ( [ ] byte ( shared ) ) )
2024-10-24 23:12:34 +13:00
data , err := s . readLine ( )
if err != nil {
return false , err
}
if data == "*" {
return false , errors . New ( "501 5.7.0 Authentication cancelled" )
}
buf , err := base64 . StdEncoding . DecodeString ( data )
if err != nil {
return false , errors . New ( "501 5.5.2 Syntax error (unable to decode)" )
}
fields := strings . Split ( string ( buf ) , " " )
if len ( fields ) < 2 {
return false , errors . New ( "501 5.5.2 Syntax error (unable to parse)" )
}
// Validate credentials.
authenticated , err := s . srv . AuthHandler ( s . conn . RemoteAddr ( ) , "CRAM-MD5" , [ ] byte ( fields [ 0 ] ) , [ ] byte ( fields [ 1 ] ) , [ ] byte ( shared ) )
return authenticated , err
}