2024-11-21 19:34:13 +00:00
package smtp
import (
"errors"
"fmt"
"io"
"net"
"strconv"
"strings"
2024-11-25 15:50:16 +00:00
"time"
2024-11-21 19:34:13 +00:00
"crypto/tls"
"net/textproto"
"git.ailur.dev/ailur/spf"
)
var (
defaultCapabilities = [ ] string {
"250-8BITMIME" ,
"250-ENHANCEDSTATUSCODES" ,
"250-SMTPUTF8" ,
"250 BINARYMIME" ,
}
2024-11-25 15:50:16 +00:00
queue = make ( map [ time . Time ] * MailQueueItem )
2024-11-21 19:34:13 +00:00
)
// MailQueueItem is a struct that represents an item in the mail queue
type MailQueueItem struct {
From * Address
To [ ] * Address
Host string
}
// ViewMailQueue returns the current mail queue
2024-11-25 15:50:16 +00:00
func ViewMailQueue ( ) map [ time . Time ] * MailQueueItem {
2024-11-21 19:34:13 +00:00
return queue
}
// Address is a struct that represents an email address
type Address struct {
Name string
Address string
}
// Mail is a struct that represents an email
type Mail struct {
From * Address
To [ ] * Address
Data [ ] byte
}
// DatabaseBackend is a struct that represents a database backend
type DatabaseBackend struct {
CheckUser func ( * Address ) ( bool , error )
2024-11-25 15:50:16 +00:00
WriteMail func ( * Mail ) error
2024-11-21 19:34:13 +00:00
}
// AuthenticationBackend is a struct that represents an authentication backend
type AuthenticationBackend struct {
2024-12-12 20:02:36 +00:00
Authenticate func ( initial string , conn * textproto . Conn ) ( CheckAddress , error )
SupportedMechanisms [ ] string
2024-11-21 19:34:13 +00:00
}
2024-12-08 15:13:33 +00:00
type CheckAddress func ( * Address ) ( bool , error )
2024-11-21 19:34:13 +00:00
func readMultilineCodeResponse ( conn * textproto . Conn ) ( int , string , error ) {
var lines strings . Builder
for {
line , err := conn . ReadLine ( )
if err != nil {
return 0 , "" , err
}
lines . WriteString ( line )
code , err := strconv . Atoi ( line [ : 3 ] )
if err != nil {
return 0 , "" , err
}
if line [ 3 ] != '-' {
return code , lines . String ( ) , nil
}
}
}
func systemError ( err error , receiver * Address , database DatabaseBackend ) {
2024-11-25 15:50:16 +00:00
_ = database . WriteMail ( & Mail {
2024-11-21 19:34:13 +00:00
From : & Address {
Name : "EMail System" ,
Address : "system" ,
} ,
To : [ ] * Address { receiver } ,
Data : [ ] byte ( fmt . Sprintf ( "Hello there. This is the EMail system.\n We're sorry, but an error occurred while trying to send your email. The error was: %s. The email has not been sent." , err . Error ( ) ) ) ,
} )
}
2024-11-25 15:50:16 +00:00
func sendEmail ( args SenderArgs , mail * Mail , database DatabaseBackend , queueID time . Time ) {
2024-11-21 19:34:13 +00:00
mxs , err := net . LookupMX ( mail . To [ 0 ] . Address )
if err != nil {
systemError ( err , queue [ queueID ] . From , database )
delete ( queue , queueID )
return
}
ips , err := net . LookupIP ( mxs [ 0 ] . Host )
if err != nil {
systemError ( err , queue [ queueID ] . From , database )
delete ( queue , queueID )
return
}
conn , err := net . Dial ( "tcp" , ips [ 0 ] . String ( ) + ":25" )
if err != nil {
systemError ( err , queue [ queueID ] . From , database )
delete ( queue , queueID )
return
}
err = Send ( args , mail , conn , mxs [ 0 ] . Host )
if err != nil {
systemError ( err , queue [ queueID ] . From , database )
delete ( queue , queueID )
return
}
err = conn . Close ( )
if err != nil {
systemError ( err , queue [ queueID ] . From , database )
delete ( queue , queueID )
return
}
delete ( queue , queueID )
}
func speakMultiLine ( conn * textproto . Conn , lines [ ] string ) error {
for _ , line := range lines {
err := conn . PrintfLine ( line )
if err != nil {
return err
}
}
return nil
}
// Receiver is a struct that represents an SMTP receiver
type Receiver struct {
underlyingListener net . Listener
hostname string
2024-12-08 15:13:33 +00:00
ownedDomains map [ string ] struct { }
2024-11-21 19:34:13 +00:00
enforceTLS bool
tlsConfig * tls . Config
database DatabaseBackend
auth AuthenticationBackend
}
// NewReceiver creates a new Receiver
func NewReceiver ( conn net . Listener , hostname string , ownedDomains [ ] string , enforceTLS bool , database DatabaseBackend , authentication AuthenticationBackend , tlsConfig * tls . Config ) * Receiver {
2024-12-08 15:13:33 +00:00
var ownedDomainsMap = make ( map [ string ] struct { } )
2024-11-21 19:34:13 +00:00
for _ , domain := range ownedDomains {
2024-12-08 15:13:33 +00:00
ownedDomainsMap [ domain ] = struct { } { }
2024-11-21 19:34:13 +00:00
}
return & Receiver {
underlyingListener : conn ,
hostname : hostname ,
ownedDomains : ownedDomainsMap ,
enforceTLS : enforceTLS ,
tlsConfig : tlsConfig ,
database : database ,
auth : authentication ,
}
}
// Close closes the connection to the Receiver
func ( fr * Receiver ) Close ( ) error {
return fr . underlyingListener . Close ( )
}
// Serve serves the Receiver. It will always return a non-nil error
func ( fr * Receiver ) Serve ( ) error {
for {
conn , err := fr . underlyingListener . Accept ( )
if err != nil {
return err
}
go fr . handleConnection ( conn )
}
}
func ( fr * Receiver ) handleConnection ( conn net . Conn ) {
var state struct {
HELO bool
2024-12-08 15:13:33 +00:00
AUTH CheckAddress
2024-11-21 19:34:13 +00:00
TLS bool
FROM * Address
RCPT [ ] * Address
DATA [ ] byte
}
submissionSlice := strings . Split ( conn . LocalAddr ( ) . String ( ) , ":" )
isSubmission := submissionSlice [ len ( submissionSlice ) - 1 ] != "25"
textProto := textproto . NewConn ( conn )
err := textProto . PrintfLine ( "220 %s ESMTP At your service" , fr . hostname )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
for {
line , err := textProto . ReadLine ( )
2024-12-12 20:02:36 +00:00
fmt . Println ( line )
2024-11-21 19:34:13 +00:00
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
switch {
case strings . HasPrefix ( line , "QUIT" ) :
err = textProto . PrintfLine ( "221 2.0.0 See you soon" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
}
_ = conn . Close ( )
return
case strings . HasPrefix ( line , "RSET" ) :
state . HELO = false
state . AUTH = nil
state . FROM = nil
state . RCPT = nil
state . DATA = nil
err = textProto . PrintfLine ( "250 2.0.0 Connection reset" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
case strings . HasPrefix ( line , "NOOP" ) :
err = textProto . PrintfLine ( "250 2.0.0 Take your time" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
case strings . HasPrefix ( line , "HELO" ) :
if state . HELO {
err = textProto . PrintfLine ( "503 5.5.1 HELO already called" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
} else {
state . HELO = true
err = textProto . PrintfLine ( "250 %s, ready to receive mail" , fr . hostname )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
}
case strings . HasPrefix ( line , "EHLO" ) :
if state . HELO {
err = textProto . PrintfLine ( "503 5.5.1 EHLO already called" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
} else {
var capabilities [ ] string
if fr . tlsConfig != nil {
capabilities = append ( capabilities , "250-STARTTLS" )
}
if fr . enforceTLS {
capabilities = append ( capabilities , "250-REQUIRETLS" )
}
2024-12-12 20:02:36 +00:00
if fr . auth . SupportedMechanisms != nil {
capabilities = append ( capabilities , "250-AUTH " + strings . Join ( fr . auth . SupportedMechanisms , " " ) )
}
2024-11-21 19:34:13 +00:00
capabilities = append ( capabilities , defaultCapabilities ... )
state . HELO = true
err = speakMultiLine ( textProto , capabilities )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
}
case strings . HasPrefix ( line , "AUTH" ) :
2024-12-12 20:02:36 +00:00
fmt . Println ( "It's about time" )
2024-11-21 19:34:13 +00:00
if ! isSubmission {
err = textProto . PrintfLine ( "503 5.5.1 AUTH only allowed on submission port" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
if ! state . TLS && fr . enforceTLS {
err = textProto . PrintfLine ( "530 5.7.0 Must issue a STARTTLS command first" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
if state . AUTH != nil {
err = textProto . PrintfLine ( "503 5.5.1 AUTH already called" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
} else {
2024-12-08 15:13:33 +00:00
checkAddress , err := fr . auth . Authenticate ( strings . TrimPrefix ( line , "AUTH " ) , textProto )
2024-11-21 19:34:13 +00:00
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
2024-12-08 15:13:33 +00:00
if checkAddress == nil {
2024-11-21 19:34:13 +00:00
err = textProto . PrintfLine ( "535 5.7.8 Authentication failed" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
} else {
2024-12-08 15:13:33 +00:00
state . AUTH = checkAddress
2024-11-21 19:34:13 +00:00
err = textProto . PrintfLine ( "235 2.7.0 Authentication successful" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
}
}
case strings . HasPrefix ( line , "STARTTLS" ) :
if state . TLS {
err = textProto . PrintfLine ( "503 5.5.1 STARTTLS already called" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
} else {
err = textProto . PrintfLine ( "220 2.0.0 Ready to start TLS" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
tlsConn := tls . Server ( conn , fr . tlsConfig )
textProto = textproto . NewConn ( tlsConn )
state . HELO = false
state . AUTH = nil
state . FROM = nil
state . RCPT = nil
state . DATA = nil
state . TLS = true
}
case strings . HasPrefix ( line , "MAIL FROM" ) :
if ! state . HELO {
err = textProto . PrintfLine ( "503 5.5.1 HELO required" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
if ! state . TLS && fr . enforceTLS {
err = textProto . PrintfLine ( "530 5.7.0 Must issue a STARTTLS command first" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
addressSlice := strings . Split ( strings . TrimPrefix ( strings . TrimSuffix ( line , ">" ) , "MAIL FROM:<" ) , "@" )
address := & Address {
Name : addressSlice [ 0 ] ,
Address : addressSlice [ 1 ] ,
}
if isSubmission {
if state . AUTH == nil {
err = textProto . PrintfLine ( "503 5.5.1 AUTH required" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
2024-12-08 15:13:33 +00:00
ok , err := state . AUTH ( address )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
if ! ok {
2024-11-21 19:34:13 +00:00
err = textProto . PrintfLine ( "535 5.7.8 Authenticated wrong user" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
} else {
err := spf . CheckIP ( strings . Split ( conn . RemoteAddr ( ) . String ( ) , ":" ) [ 0 ] , address . Address )
if err != nil {
if err . Type ( ) == spf . ErrTypeInternal {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
} else {
err := textProto . PrintfLine ( "550 5.7.1 SPF check failed" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
}
}
state . FROM = address
err = textProto . PrintfLine ( "250 2.1.0 Sender OK" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
case strings . HasPrefix ( line , "RCPT TO" ) :
if ! state . TLS && fr . enforceTLS {
err = textProto . PrintfLine ( "530 5.7.0 Must issue a STARTTLS command first" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
addressSlice := strings . Split ( strings . TrimPrefix ( strings . TrimSuffix ( line , ">" ) , "RCPT TO:<" ) , "@" )
address := & Address {
Name : addressSlice [ 0 ] ,
Address : addressSlice [ 1 ] ,
}
if isSubmission {
if state . AUTH == nil {
err = textProto . PrintfLine ( "503 5.5.1 AUTH required" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
} else {
_ , ok := fr . ownedDomains [ address . Address ]
if ! ok {
err = textProto . PrintfLine ( "503 5.5.1 Relaying not allowed" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
ok , err := fr . database . CheckUser ( address )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
if ! ok {
err = textProto . PrintfLine ( "550 5.1.1 User not found" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
}
state . RCPT = append ( state . RCPT , address )
err = textProto . PrintfLine ( "250 2.1.5 Recipient OK" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
case strings . HasPrefix ( line , "DATA" ) :
if ! state . TLS && fr . enforceTLS {
err = textProto . PrintfLine ( "530 5.7.0 Must issue a STARTTLS command first" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
if state . FROM == nil {
err = textProto . PrintfLine ( "503 5.5.1 MAIL FROM required" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
if len ( state . RCPT ) == 0 {
err = textProto . PrintfLine ( "503 5.5.1 RCPT TO required" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
continue
}
err = textProto . PrintfLine ( "354 2.0.0 Start mail input; end with <CRLF>.<CRLF>" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
state . DATA , err = io . ReadAll ( textProto . DotReader ( ) )
mail := & Mail {
From : state . FROM ,
To : state . RCPT ,
Data : state . DATA ,
}
if ! isSubmission {
2024-11-25 15:50:16 +00:00
err := fr . database . WriteMail ( mail )
2024-11-21 19:34:13 +00:00
if err != nil {
_ = textProto . PrintfLine ( err . Error ( ) )
_ = conn . Close ( )
return
}
err = textProto . PrintfLine ( "250 2.0.0 Message accepted for delivery" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
} else {
2024-11-25 15:50:16 +00:00
queueID := time . Now ( )
2024-11-21 19:34:13 +00:00
queue [ queueID ] = & MailQueueItem {
From : state . FROM ,
To : state . RCPT ,
Host : strings . Split ( conn . RemoteAddr ( ) . String ( ) , ":" ) [ 0 ] ,
}
go sendEmail ( SenderArgs {
Hostname : fr . hostname ,
EnforceTLS : fr . enforceTLS ,
} , mail , fr . database , queueID )
err = textProto . PrintfLine ( "250 2.0.0 Message queued for delivery" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
state . DATA = nil
state . FROM = nil
state . RCPT = nil
}
default :
err = textProto . PrintfLine ( "500 5.5.2 Command not recognized" )
if err != nil {
_ = textProto . PrintfLine ( "421 4.7.0 Temporary server error" )
_ = conn . Close ( )
return
}
}
}
}
// SenderArgs is a struct that represents the arguments for the Sender
type SenderArgs struct {
Hostname string
EnforceTLS bool
}
// Send sends an email to another server
func Send ( args SenderArgs , mail * Mail , conn net . Conn , mxHost string ) ( err error ) {
textConn := textproto . NewConn ( conn )
err = textConn . PrintfLine ( "RSET" )
if err != nil {
return err
}
code , line , err := textConn . ReadCodeLine ( 0 )
if err != nil {
return err
}
if code != 220 {
return errors . New ( "unexpected RSET response - " + line )
}
code , line , err = textConn . ReadCodeLine ( 0 )
if err != nil {
return err
}
if code != 250 {
return errors . New ( "unexpected greeting - " + line )
}
err = textConn . PrintfLine ( "EHLO %s" , args . Hostname )
if err != nil {
return err
}
code , lines , err := readMultilineCodeResponse ( textConn )
if err != nil {
return err
}
if code != 250 {
return errors . New ( "unexpected EHLO response - " + lines )
}
if strings . Contains ( lines , "STARTTLS" ) {
err = textConn . PrintfLine ( "STARTTLS" )
if err != nil {
return err
}
code , line , err := textConn . ReadCodeLine ( 0 )
if err != nil {
return err
}
if code != 220 {
return errors . New ( "unexpected STARTTLS response - " + line )
}
tlsConn := tls . Client ( conn , & tls . Config {
ServerName : mxHost ,
InsecureSkipVerify : false ,
} )
err = tlsConn . Handshake ( )
if err != nil {
return err
}
textConn = textproto . NewConn ( tlsConn )
// Just use HELO, no point using EHLO when we already have all the capabilities
// This also gets us out of using readMultilineCodeResponse
err = textConn . PrintfLine ( "HELO %s" , args . Hostname )
if err != nil {
return err
}
code , line , err = textConn . ReadCodeLine ( 0 )
if err != nil {
return err
}
if code != 250 {
return errors . New ( "unexpected STARTTLS HELO response - " + line )
}
} else if args . EnforceTLS {
return errors . New ( "STARTTLS not supported" )
}
err = textConn . PrintfLine ( "MAIL FROM:<%s@%s>" , mail . From . Name , mail . From . Address )
if err != nil {
return err
}
code , line , err = textConn . ReadCodeLine ( 0 )
if err != nil {
return err
}
if code != 250 {
return errors . New ( "unexpected MAIL FROM response - " + line )
}
for _ , recipient := range mail . To {
err = textConn . PrintfLine ( "RCPT TO:<%s@%s>" , recipient . Name , recipient . Address )
if err != nil {
return err
}
code , line , err = textConn . ReadCodeLine ( 0 )
if err != nil {
return err
}
if code != 250 {
return errors . New ( "unexpected RCPT TO response - " + line )
}
}
err = textConn . PrintfLine ( "DATA" )
if err != nil {
return err
}
code , line , err = textConn . ReadCodeLine ( 0 )
if err != nil {
return err
}
if code != 354 {
return errors . New ( "unexpected DATA response - " + line )
}
writer := textConn . DotWriter ( )
_ , err = writer . Write ( mail . Data )
if err != nil {
return err
}
err = writer . Close ( )
if err != nil {
return errors . New ( "failed to close data writer - " + err . Error ( ) )
}
code , line , err = textConn . ReadCodeLine ( 0 )
if err != nil {
return err
}
if code != 250 {
return errors . New ( "unexpected DATA finish response - " + line + ", your message may have been sent, but it is not guaranteed" )
}
err = textConn . PrintfLine ( "QUIT" )
if err != nil {
return err
}
code , line , err = textConn . ReadCodeLine ( 0 )
if err != nil {
return err
}
if code != 221 {
return errors . New ( "unexpected QUIT response - " + line + ", your message may have been sent, but it is not guaranteed" )
}
return
}