Compare commits

...

8 commits
v1.0.0 ... main

3 changed files with 90 additions and 41 deletions

56
examples/main.go Normal file
View file

@ -0,0 +1,56 @@
package main
import (
"fmt"
"net"
"net/textproto"
"git.ailur.dev/ailur/smtp"
)
// DatabaseBackend is a smtp.DatabaseBackend implementation that always returns true for CheckUser and prints the mail data to stdout.
var DatabaseBackend = smtp.DatabaseBackend{
CheckUser: func(address *smtp.Address) (bool, error) {
return true, nil
},
WriteMail: func(mail *smtp.Mail) error {
fmt.Println(string(mail.Data))
return nil
},
}
// AuthenticationBackend is a smtp.AuthenticationBackend implementation that always returns a fixed address for Authenticate.
var AuthenticationBackend = smtp.AuthenticationBackend{
Authenticate: func(initial string, conn *textproto.Conn) (smtp.CheckAddress, error) {
return func(address *smtp.Address) (bool, error) {
return true, nil
}, nil
},
}
func main() {
go func() {
// Serve on the server-to-server port
listener, err := net.Listen("tcp", ":25")
if err != nil {
panic(err)
}
receiver := smtp.NewReceiver(listener, "localhost", []string{"localhost", "127.0.0.1", "0.0.0.0", "example.org", "192.168.1.253"}, false, DatabaseBackend, AuthenticationBackend, nil)
err = receiver.Serve()
panic(err)
}()
go func() {
// Serve on the submission port
listener, err := net.Listen("tcp", ":587")
if err != nil {
panic(err)
}
receiver := smtp.NewReceiver(listener, "localhost", []string{"localhost", "127.0.0.1", "0.0.0.0", "cta.social"}, false, DatabaseBackend, AuthenticationBackend, nil)
err = receiver.Serve()
panic(err)
}()
// Block forever
select {}
}

2
go.mod
View file

@ -1,4 +1,4 @@
module smtp
module git.ailur.dev/ailur/smtp
go 1.23

73
smtp.go
View file

@ -7,12 +7,12 @@ import (
"net"
"strconv"
"strings"
"time"
"crypto/tls"
"net/textproto"
"git.ailur.dev/ailur/spf"
"github.com/google/uuid"
)
var (
@ -22,7 +22,7 @@ var (
"250-SMTPUTF8",
"250 BINARYMIME",
}
queue = make(map[uuid.UUID]*MailQueueItem)
queue = make(map[time.Time]*MailQueueItem)
)
// MailQueueItem is a struct that represents an item in the mail queue
@ -33,7 +33,7 @@ type MailQueueItem struct {
}
// ViewMailQueue returns the current mail queue
func ViewMailQueue() map[uuid.UUID]*MailQueueItem {
func ViewMailQueue() map[time.Time]*MailQueueItem {
return queue
}
@ -53,14 +53,17 @@ type Mail struct {
// DatabaseBackend is a struct that represents a database backend
type DatabaseBackend struct {
CheckUser func(*Address) (bool, error)
WriteMail func(*Mail) (uuid.UUID, error)
WriteMail func(*Mail) error
}
// AuthenticationBackend is a struct that represents an authentication backend
type AuthenticationBackend struct {
Authenticate func(string) (*Address, error)
Authenticate func(initial string, conn *textproto.Conn) (CheckAddress, error)
SupportedMechanisms []string
}
type CheckAddress func(*Address) (bool, error)
func readMultilineCodeResponse(conn *textproto.Conn) (int, string, error) {
var lines strings.Builder
for {
@ -83,7 +86,7 @@ func readMultilineCodeResponse(conn *textproto.Conn) (int, string, error) {
}
func systemError(err error, receiver *Address, database DatabaseBackend) {
_, _ = database.WriteMail(&Mail{
_ = database.WriteMail(&Mail{
From: &Address{
Name: "EMail System",
Address: "system",
@ -93,7 +96,7 @@ func systemError(err error, receiver *Address, database DatabaseBackend) {
})
}
func sendEmail(args SenderArgs, mail *Mail, database DatabaseBackend, queueID uuid.UUID) {
func sendEmail(args SenderArgs, mail *Mail, database DatabaseBackend, queueID time.Time) {
mxs, err := net.LookupMX(mail.To[0].Address)
if err != nil {
systemError(err, queue[queueID].From, database)
@ -147,7 +150,7 @@ func speakMultiLine(conn *textproto.Conn, lines []string) error {
type Receiver struct {
underlyingListener net.Listener
hostname string
ownedDomains map[string]any
ownedDomains map[string]struct{}
enforceTLS bool
tlsConfig *tls.Config
database DatabaseBackend
@ -156,9 +159,9 @@ type Receiver struct {
// NewReceiver creates a new Receiver
func NewReceiver(conn net.Listener, hostname string, ownedDomains []string, enforceTLS bool, database DatabaseBackend, authentication AuthenticationBackend, tlsConfig *tls.Config) *Receiver {
var ownedDomainsMap = make(map[string]any)
var ownedDomainsMap = make(map[string]struct{})
for _, domain := range ownedDomains {
ownedDomainsMap[domain] = nil
ownedDomainsMap[domain] = struct{}{}
}
return &Receiver{
underlyingListener: conn,
@ -191,7 +194,7 @@ func (fr *Receiver) Serve() error {
func (fr *Receiver) handleConnection(conn net.Conn) {
var state struct {
HELO bool
AUTH *Address
AUTH CheckAddress
TLS bool
FROM *Address
RCPT []*Address
@ -209,8 +212,6 @@ func (fr *Receiver) handleConnection(conn net.Conn) {
return
}
fmt.Println("Connection from", conn.RemoteAddr().String())
for {
line, err := textProto.ReadLine()
if err != nil {
@ -281,6 +282,9 @@ func (fr *Receiver) handleConnection(conn net.Conn) {
if fr.enforceTLS {
capabilities = append(capabilities, "250-REQUIRETLS")
}
if fr.auth.SupportedMechanisms != nil {
capabilities = append(capabilities, "250-AUTH "+strings.Join(fr.auth.SupportedMechanisms, " "))
}
capabilities = append(capabilities, defaultCapabilities...)
state.HELO = true
err = speakMultiLine(textProto, capabilities)
@ -320,14 +324,14 @@ func (fr *Receiver) handleConnection(conn net.Conn) {
}
continue
} else {
address, err := fr.auth.Authenticate(line)
checkAddress, err := fr.auth.Authenticate(strings.TrimPrefix(line, "AUTH "), textProto)
if err != nil {
_ = textProto.PrintfLine("421 4.7.0 Temporary server error")
_ = textProto.PrintfLine(err.Error())
_ = conn.Close()
return
}
if address == nil {
if checkAddress == nil {
err = textProto.PrintfLine("535 5.7.8 Authentication failed")
if err != nil {
_ = textProto.PrintfLine("421 4.7.0 Temporary server error")
@ -335,7 +339,7 @@ func (fr *Receiver) handleConnection(conn net.Conn) {
return
}
} else {
state.AUTH = address
state.AUTH = checkAddress
err = textProto.PrintfLine("235 2.7.0 Authentication successful")
if err != nil {
_ = textProto.PrintfLine("421 4.7.0 Temporary server error")
@ -410,7 +414,14 @@ func (fr *Receiver) handleConnection(conn net.Conn) {
continue
}
if *address != *state.AUTH {
ok, err := state.AUTH(address)
if err != nil {
_ = textProto.PrintfLine("421 4.7.0 Temporary server error")
_ = conn.Close()
return
}
if !ok {
err = textProto.PrintfLine("535 5.7.8 Authenticated wrong user")
if err != nil {
_ = textProto.PrintfLine("421 4.7.0 Temporary server error")
@ -557,7 +568,7 @@ func (fr *Receiver) handleConnection(conn net.Conn) {
}
if !isSubmission {
_, err := fr.database.WriteMail(mail)
err := fr.database.WriteMail(mail)
if err != nil {
_ = textProto.PrintfLine(err.Error())
_ = conn.Close()
@ -571,7 +582,7 @@ func (fr *Receiver) handleConnection(conn net.Conn) {
return
}
} else {
queueID := uuid.New()
queueID := time.Now()
queue[queueID] = &MailQueueItem{
From: state.FROM,
@ -579,7 +590,6 @@ func (fr *Receiver) handleConnection(conn net.Conn) {
Host: strings.Split(conn.RemoteAddr().String(), ":")[0],
}
go sendEmail(SenderArgs{
Hostname: fr.hostname,
EnforceTLS: fr.enforceTLS,
}, mail, fr.database, queueID)
@ -608,7 +618,6 @@ func (fr *Receiver) handleConnection(conn net.Conn) {
// SenderArgs is a struct that represents the arguments for the Sender
type SenderArgs struct {
Hostname string
EnforceTLS bool
}
@ -639,7 +648,7 @@ func Send(args SenderArgs, mail *Mail, conn net.Conn, mxHost string) (err error)
return errors.New("unexpected greeting - " + line)
}
err = textConn.PrintfLine("EHLO %s", args.Hostname)
err = textConn.PrintfLine("EHLO %s", mxHost)
if err != nil {
return err
}
@ -673,16 +682,11 @@ func Send(args SenderArgs, mail *Mail, conn net.Conn, mxHost string) (err error)
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)
err = textConn.PrintfLine("HELO %s", mxHost)
if err != nil {
return err
}
@ -705,10 +709,7 @@ func Send(args SenderArgs, mail *Mail, conn net.Conn, mxHost string) (err error)
}
code, line, err = textConn.ReadCodeLine(0)
fmt.Println(code, line, err)
if err != nil {
// For some reason the EHLO stuff ends up here
fmt.Println("5")
return err
}
@ -723,9 +724,7 @@ func Send(args SenderArgs, mail *Mail, conn net.Conn, mxHost string) (err error)
}
code, line, err = textConn.ReadCodeLine(0)
fmt.Println(code, line, err)
if err != nil {
fmt.Println("6")
return err
}
@ -740,9 +739,7 @@ func Send(args SenderArgs, mail *Mail, conn net.Conn, mxHost string) (err error)
}
code, line, err = textConn.ReadCodeLine(0)
fmt.Println(code, line, err)
if err != nil {
fmt.Println("7")
return err
}
@ -762,9 +759,7 @@ func Send(args SenderArgs, mail *Mail, conn net.Conn, mxHost string) (err error)
}
code, line, err = textConn.ReadCodeLine(0)
fmt.Println(code, line, err)
if err != nil {
fmt.Println("8")
return err
}
@ -778,9 +773,7 @@ func Send(args SenderArgs, mail *Mail, conn net.Conn, mxHost string) (err error)
}
code, line, err = textConn.ReadCodeLine(0)
fmt.Println(code, line, err)
if err != nil {
fmt.Println("9")
return err
}