mirror of
https://github.com/axllent/mailpit.git
synced 2024-12-24 22:14:33 +02:00
Feature: Experimental Unix socket support for HTTPD & SMTPD (#373)
This commit is contained in:
parent
e2c3256f0c
commit
31ec6681a7
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -106,7 +105,7 @@ The --recent flag will only consider files with a modification date within the l
|
||||
}
|
||||
}
|
||||
|
||||
err = smtp.SendMail(sendmail.SMTPAddr, nil, returnPath, recipients, body)
|
||||
err = sendmail.Send(sendmail.SMTPAddr, returnPath, recipients, body)
|
||||
if err != nil {
|
||||
logger.Log().Errorf("error sending mail: %s (%s)", err.Error(), path)
|
||||
return nil
|
||||
|
@ -239,10 +239,10 @@ func VerifyConfig() error {
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`.*:\d+$`)
|
||||
if !re.MatchString(SMTPListen) {
|
||||
if _, _, isSocket := tools.UnixSocket(SMTPListen); !isSocket && !re.MatchString(SMTPListen) {
|
||||
return errors.New("[smtp] bind should be in the format of <ip>:<port>")
|
||||
}
|
||||
if !re.MatchString(HTTPListen) {
|
||||
if _, _, isSocket := tools.UnixSocket(HTTPListen); !isSocket && !re.MatchString(HTTPListen) {
|
||||
return errors.New("[ui] HTTP bind should be in the format of <ip>:<port>")
|
||||
}
|
||||
|
||||
|
3
go.mod
3
go.mod
@ -16,8 +16,7 @@ require (
|
||||
github.com/kovidgoyal/imaging v1.6.3
|
||||
github.com/leporo/sqlf v1.4.0
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0
|
||||
github.com/mhale/smtpd v0.8.3
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
||||
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62
|
||||
github.com/rqlite/gorqlite v0.0.0-20241013203532-4385768ae85d
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
|
6
go.sum
6
go.sum
@ -68,8 +68,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8=
|
||||
github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
|
||||
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62 h1:XMG5DklHoioVYysfYglOB7vRBg/LOUJZy2mq2QyedLg=
|
||||
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62/go.mod h1:niAM5cni0I/47IFA995xQfeK58Mkbb7FHJjacY4OGQg=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
@ -80,8 +80,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
|
||||
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
|
@ -28,13 +28,14 @@ import (
|
||||
var (
|
||||
db *sql.DB
|
||||
dbFile string
|
||||
dbIsTemp bool
|
||||
sqlDriver string
|
||||
dbLastAction time.Time
|
||||
|
||||
// zstd compression encoder & decoder
|
||||
dbEncoder, _ = zstd.NewWriter(nil)
|
||||
dbDecoder, _ = zstd.NewReader(nil)
|
||||
|
||||
temporaryFiles = []string{}
|
||||
)
|
||||
|
||||
// InitDB will initialise the database
|
||||
@ -50,7 +51,8 @@ func InitDB() error {
|
||||
// when no path is provided then we create a temporary file
|
||||
// which will get deleted on Close(), SIGINT or SIGTERM
|
||||
p = fmt.Sprintf("%s-%d.db", path.Join(os.TempDir(), "mailpit"), time.Now().UnixNano())
|
||||
dbIsTemp = true
|
||||
// delete the Unix socket file on exit
|
||||
AddTempFile(p)
|
||||
sqlDriver = "sqlite"
|
||||
dsn = p
|
||||
logger.Log().Debugf("[db] using temporary database: %s", p)
|
||||
@ -156,12 +158,8 @@ func Close() {
|
||||
// allow SQLite to finish closing DB & write WAL logs if local
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if dbIsTemp && isFile(dbFile) {
|
||||
logger.Log().Debugf("[db] deleting temporary file %s", dbFile)
|
||||
if err := os.Remove(dbFile); err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
}
|
||||
}
|
||||
// delete all temporary files
|
||||
deleteTempFiles()
|
||||
}
|
||||
|
||||
// Ping the database connection and return an error if unsuccessful
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/axllent/mailpit/internal/html2text"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
@ -18,6 +19,20 @@ var (
|
||||
StatsDeleted float64
|
||||
)
|
||||
|
||||
// AddTempFile adds a file to the slice of files to delete on exit
|
||||
func AddTempFile(s string) {
|
||||
temporaryFiles = append(temporaryFiles, s)
|
||||
}
|
||||
|
||||
// DeleteTempFiles will delete files added via AddTempFiles
|
||||
func deleteTempFiles() {
|
||||
for _, f := range temporaryFiles {
|
||||
if err := os.Remove(f); err == nil {
|
||||
logger.Log().Debugf("removed temporary file: %s", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return a header field as a []*mail.Address, or "null" is not found/empty
|
||||
func addressToSlice(env *enmime.Envelope, key string) []*mail.Address {
|
||||
data, err := env.AddressList(key)
|
||||
|
49
internal/tools/unixsocket.go
Normal file
49
internal/tools/unixsocket.go
Normal file
@ -0,0 +1,49 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// UnixSocket returns a path and a FileMode if the address is in
|
||||
// the format of unix:<path>:<permission>
|
||||
func UnixSocket(address string) (string, fs.FileMode, bool) {
|
||||
re := regexp.MustCompile(`^unix:(.*):(\d\d\d\d?)$`)
|
||||
|
||||
var f fs.FileMode
|
||||
|
||||
if !re.MatchString(address) {
|
||||
return "", f, false
|
||||
}
|
||||
|
||||
m := re.FindAllStringSubmatch(address, 1)
|
||||
|
||||
modeVal, err := strconv.ParseUint(m[0][2], 8, 32)
|
||||
|
||||
if err != nil {
|
||||
return "", f, false
|
||||
}
|
||||
|
||||
return path.Clean(m[0][1]), fs.FileMode(modeVal), true
|
||||
}
|
||||
|
||||
// PrepareSocket returns an error if an active socket file already exists
|
||||
func PrepareSocket(address string) error {
|
||||
address = path.Clean(address)
|
||||
if _, err := os.Stat(address); os.IsNotExist(err) {
|
||||
// does not exist, OK
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := net.Dial("unix", address); err == nil {
|
||||
// socket is listening
|
||||
return fmt.Errorf("socket already in use: %s", address)
|
||||
}
|
||||
|
||||
return os.Remove(address)
|
||||
}
|
@ -18,15 +18,15 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/reiver/go-telnet"
|
||||
"github.com/mneis/go-telnet"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
@ -113,14 +113,23 @@ func Run() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
socketAddr, isSocket := socketAddress(SMTPAddr)
|
||||
|
||||
// handles `sendmail -bs`
|
||||
// telnet directly to SMTP
|
||||
if UseB && UseS {
|
||||
var caller telnet.Caller = telnet.StandardCaller
|
||||
|
||||
// telnet directly to SMTP
|
||||
if err := telnet.DialToAndCall(SMTPAddr, caller); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
if isSocket {
|
||||
if err := telnet.DialToAndCallUnix(socketAddr, caller); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
if err := telnet.DialToAndCall(SMTPAddr, caller); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
@ -167,8 +176,7 @@ func Run() {
|
||||
os.Exit(11)
|
||||
}
|
||||
|
||||
err = smtp.SendMail(SMTPAddr, nil, from.Address, addresses, body)
|
||||
if err != nil {
|
||||
if err := Send(SMTPAddr, from.Address, addresses, body); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error sending mail")
|
||||
logger.Log().Fatal(err)
|
||||
}
|
||||
@ -192,3 +200,17 @@ Flags:
|
||||
-v Ignored
|
||||
`, config.Version, strings.Join(args, " "), FromAddr)
|
||||
}
|
||||
|
||||
// SocketAddress returns a path and a FileMode if the address is in
|
||||
// the format of unix:<path>
|
||||
func socketAddress(address string) (string, bool) {
|
||||
re := regexp.MustCompile(`^unix:(.*)$`)
|
||||
|
||||
if !re.MatchString(address) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
m := re.FindAllStringSubmatch(address, 1)
|
||||
|
||||
return path.Clean(m[0][1]), true
|
||||
}
|
||||
|
71
sendmail/cmd/smtp.go
Normal file
71
sendmail/cmd/smtp.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Package cmd is a wrapper library to send mail
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"os"
|
||||
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
)
|
||||
|
||||
// Send is a wrapper for smtp.SendMail() which also supports sending via unix sockets.
|
||||
// Unix sockets must be set as unix:/path/to/socket
|
||||
// It does not support authentication.
|
||||
func Send(addr string, from string, to []string, msg []byte) error {
|
||||
socketPath, isSocket := socketAddress(addr)
|
||||
|
||||
fromAddress, err := mail.ParseAddress(from)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid from address: %s", from)
|
||||
}
|
||||
|
||||
if len(to) == 0 {
|
||||
return fmt.Errorf("no To addresses specified")
|
||||
}
|
||||
|
||||
if !isSocket {
|
||||
return smtp.SendMail(addr, nil, fromAddress.Address, to, msg)
|
||||
}
|
||||
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to %s", addr)
|
||||
}
|
||||
|
||||
client, err := smtp.NewClient(conn, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the sender
|
||||
if err := client.Mail(fromAddress.Address); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error sending mail")
|
||||
logger.Log().Fatal(err)
|
||||
}
|
||||
|
||||
// Set the recipient
|
||||
for _, a := range to {
|
||||
if err := client.Rcpt(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = wc.Write(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = wc.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -161,7 +161,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := smtpd.Send(from, data.To, msg); err != nil {
|
||||
if err := smtpd.Relay(from, data.To, msg); err != nil {
|
||||
logger.Log().Errorf("[smtp] error sending message: %s", err.Error())
|
||||
httpError(w, "SMTP error: "+err.Error())
|
||||
return
|
||||
|
@ -277,5 +277,5 @@ func (d SendRequest) Send(remoteAddr string) (string, error) {
|
||||
return "", fmt.Errorf("error building message: %s", err.Error())
|
||||
}
|
||||
|
||||
return smtpd.Store(ipAddr, d.From.Email, addresses, buff.Bytes())
|
||||
return smtpd.SaveToDatabase(ipAddr, d.From.Email, addresses, buff.Bytes())
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@ -20,6 +21,7 @@ import (
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/stats"
|
||||
"github.com/axllent/mailpit/internal/storage"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/axllent/mailpit/server/apiv1"
|
||||
"github.com/axllent/mailpit/server/handlers"
|
||||
"github.com/axllent/mailpit/server/pop3"
|
||||
@ -112,11 +114,42 @@ func Listen() {
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.Log().Infof("[http] starting on %s", config.HTTPListen)
|
||||
logger.Log().Infof("[http] accessible via http://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
storage.Close()
|
||||
logger.Log().Fatal(err)
|
||||
socketAddr, perm, isSocket := tools.UnixSocket(config.HTTPListen)
|
||||
|
||||
if isSocket {
|
||||
if err := tools.PrepareSocket(socketAddr); err != nil {
|
||||
storage.Close()
|
||||
logger.Log().Fatal(err)
|
||||
}
|
||||
|
||||
// delete the Unix socket file on exit
|
||||
storage.AddTempFile(socketAddr)
|
||||
|
||||
ln, err := net.Listen("unix", socketAddr)
|
||||
if err != nil {
|
||||
storage.Close()
|
||||
logger.Log().Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(socketAddr, perm); err != nil {
|
||||
storage.Close()
|
||||
logger.Log().Fatal(err)
|
||||
}
|
||||
|
||||
logger.Log().Infof("[http] starting on %s", config.HTTPListen)
|
||||
|
||||
if err := server.Serve(ln); err != nil {
|
||||
storage.Close()
|
||||
logger.Log().Fatal(err)
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.Log().Infof("[http] starting on %s", config.HTTPListen)
|
||||
logger.Log().Infof("[http] accessible via http://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
storage.Close()
|
||||
logger.Log().Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
952
server/smtpd/lib.go
Normal file
952
server/smtpd/lib.go
Normal file
@ -0,0 +1,952 @@
|
||||
// Package smtpd implements a basic SMTP server.
|
||||
//
|
||||
// This is a modified version of https://github.com/mhale/smtpd to
|
||||
// add optional support for unix sockets.
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// Debug `true` enables verbose logging.
|
||||
Debug = false
|
||||
rcptToRE = regexp.MustCompile(`[Tt][Oo]:\s?<(.+)>`)
|
||||
mailFromRE = regexp.MustCompile(`[Ff][Rr][Oo][Mm]:\s?<(.*)>(\s(.*))?`) // Delivery Status Notifications are sent with "MAIL FROM:<>"
|
||||
mailSizeRE = regexp.MustCompile(`[Ss][Ii][Zz][Ee]=(\d+)`)
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
s.writef(s.makeEHLOResponse())
|
||||
|
||||
// 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 {
|
||||
// Validate the SIZE parameter if one was sent.
|
||||
if len(match[2]) > 0 { // A parameter is present
|
||||
sizeMatch := mailSizeRE.FindStringSubmatch(match[3])
|
||||
if sizeMatch == nil {
|
||||
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid SIZE parameter)")
|
||||
} else {
|
||||
// Enforce the maximum message size if one is set.
|
||||
size, err := strconv.Atoi(sizeMatch[1])
|
||||
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)
|
||||
s.writef(err.Error())
|
||||
} 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")
|
||||
}
|
||||
}
|
||||
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 {
|
||||
// RFC 5321 specifies support for minimum of 100 recipients is required.
|
||||
if s.srv.MaxRecipients == 0 {
|
||||
s.srv.MaxRecipients = 100
|
||||
}
|
||||
if len(to) == s.srv.MaxRecipients {
|
||||
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:
|
||||
s.writef(err.Error())
|
||||
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()) {
|
||||
s.writef(err.Error())
|
||||
} 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()) {
|
||||
s.writef(err.Error())
|
||||
} else {
|
||||
s.writef("451 4.3.5 Unable to process mail")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if msgID != "" {
|
||||
s.writef("250 2.0.0 Ok: queued as " + msgID)
|
||||
} 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
s.writef(err.Error())
|
||||
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...)
|
||||
fmt.Fprintf(s.bw, line+"\r\n")
|
||||
_ = 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 == "" {
|
||||
s.writef("334 " + base64.StdEncoding.EncodeToString([]byte("Username:")))
|
||||
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)")
|
||||
}
|
||||
|
||||
s.writef("334 " + base64.StdEncoding.EncodeToString([]byte("Password:")))
|
||||
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 + ">"
|
||||
|
||||
s.writef("334 " + base64.StdEncoding.EncodeToString([]byte(shared)))
|
||||
|
||||
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
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
@ -26,7 +30,7 @@ func autoRelayMessage(from string, to []string, data *[]byte) {
|
||||
}
|
||||
|
||||
if config.SMTPRelayAll {
|
||||
if err := Send(from, to, *data); err != nil {
|
||||
if err := Relay(from, to, *data); err != nil {
|
||||
logger.Log().Errorf("[smtp] error relaying message: %s", err.Error())
|
||||
} else {
|
||||
logger.Log().Debugf("[smtp] auto-relay message to %s from %s via %s:%d",
|
||||
@ -44,7 +48,7 @@ func autoRelayMessage(from string, to []string, data *[]byte) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := Send(from, filtered, *data); err != nil {
|
||||
if err := Relay(from, filtered, *data); err != nil {
|
||||
logger.Log().Errorf("[smtp] error relaying message: %s", err.Error())
|
||||
} else {
|
||||
logger.Log().Debugf("[smtp] auto-relay message to %s from %s via %s:%d",
|
||||
@ -52,3 +56,107 @@ func autoRelayMessage(from string, to []string, data *[]byte) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Relay will connect to a pre-configured SMTP server and send a message to one or more recipients.
|
||||
func Relay(from string, to []string, msg []byte) error {
|
||||
addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to %s: %s", addr, err.Error())
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
|
||||
if config.SMTPRelayConfig.STARTTLS {
|
||||
conf := &tls.Config{ServerName: config.SMTPRelayConfig.Host} // #nosec
|
||||
|
||||
conf.InsecureSkipVerify = config.SMTPRelayConfig.AllowInsecure
|
||||
|
||||
if err = c.StartTLS(conf); err != nil {
|
||||
return fmt.Errorf("error creating StartTLS config: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
auth := relayAuthFromConfig()
|
||||
|
||||
if auth != nil {
|
||||
if err = c.Auth(auth); err != nil {
|
||||
return fmt.Errorf("error response to AUTH command: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err = c.Mail(from); err != nil {
|
||||
return fmt.Errorf("error response to MAIL command: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
logger.Log().Warnf("error response to RCPT command for %s: %s", addr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error response to DATA command: %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
return fmt.Errorf("error sending message: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return fmt.Errorf("error closing connection: %s", err.Error())
|
||||
}
|
||||
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Return the SMTP relay authentication based on config
|
||||
func relayAuthFromConfig() smtp.Auth {
|
||||
var a smtp.Auth
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "plain" {
|
||||
a = smtp.PlainAuth("", config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password, config.SMTPRelayConfig.Host)
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "login" {
|
||||
a = LoginAuth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password)
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "cram-md5" {
|
||||
a = smtp.CRAMMD5Auth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Secret)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Custom implementation of LOGIN SMTP authentication
|
||||
// @see https://gist.github.com/andelf/5118732
|
||||
type loginAuth struct {
|
||||
username, password string
|
||||
}
|
||||
|
||||
// LoginAuth authentication
|
||||
func LoginAuth(username, password string) smtp.Auth {
|
||||
return &loginAuth{username, password}
|
||||
}
|
||||
|
||||
func (a *loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
|
||||
return "LOGIN", []byte{}, nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
switch string(fromServer) {
|
||||
case "Username:":
|
||||
return []byte(a.username), nil
|
||||
case "Password:":
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, errors.New("Unknown fromServer")
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -14,9 +14,9 @@ import (
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/stats"
|
||||
"github.com/axllent/mailpit/internal/storage"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/mhale/smtpd"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -29,11 +29,11 @@ var (
|
||||
|
||||
// MailHandler handles the incoming message to store in the database
|
||||
func mailHandler(origin net.Addr, from string, to []string, data []byte) (string, error) {
|
||||
return Store(origin, from, to, data)
|
||||
return SaveToDatabase(origin, from, to, data)
|
||||
}
|
||||
|
||||
// Store will attempt to save a message to the database
|
||||
func Store(origin net.Addr, from string, to []string, data []byte) (string, error) {
|
||||
// SaveToDatabase will attempt to save a message to the database
|
||||
func SaveToDatabase(origin net.Addr, from string, to []string, data []byte) (string, error) {
|
||||
if !config.SMTPStrictRFCHeaders {
|
||||
// replace all <CR><CR><LF> (\r\r\n) with <CR><LF> (\r\n)
|
||||
// @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153
|
||||
@ -193,24 +193,6 @@ func Listen() error {
|
||||
}
|
||||
}
|
||||
|
||||
smtpType := "no encryption"
|
||||
|
||||
if config.SMTPTLSCert != "" {
|
||||
if config.SMTPRequireSTARTTLS {
|
||||
smtpType = "STARTTLS required"
|
||||
} else if config.SMTPRequireTLS {
|
||||
smtpType = "SSL/TLS required"
|
||||
} else {
|
||||
smtpType = "STARTTLS optional"
|
||||
if !config.SMTPAuthAllowInsecure && auth.SMTPCredentials != nil {
|
||||
smtpType = "STARTTLS required"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
logger.Log().Infof("[smtpd] starting on %s (%s)", config.SMTPListen, smtpType)
|
||||
|
||||
return listenAndServe(config.SMTPListen, mailHandler, authHandler)
|
||||
}
|
||||
|
||||
@ -223,13 +205,16 @@ func verbLogTranslator(verb string) string {
|
||||
return "response"
|
||||
}
|
||||
|
||||
func listenAndServe(addr string, handler smtpd.MsgIDHandler, authHandler smtpd.AuthHandler) error {
|
||||
smtpd.Debug = true // to enable Mailpit logging
|
||||
srv := &smtpd.Server{
|
||||
func listenAndServe(addr string, handler MsgIDHandler, authHandler AuthHandler) error {
|
||||
|
||||
socketAddr, perm, isSocket := tools.UnixSocket(addr)
|
||||
|
||||
Debug = true // to enable Mailpit logging
|
||||
srv := &Server{
|
||||
Addr: addr,
|
||||
MsgIDHandler: handler,
|
||||
HandlerRcpt: handlerRcpt,
|
||||
Appname: "Mailpit",
|
||||
AppName: "Mailpit",
|
||||
Hostname: "",
|
||||
AuthHandler: nil,
|
||||
AuthRequired: false,
|
||||
@ -252,7 +237,7 @@ func listenAndServe(addr string, handler smtpd.MsgIDHandler, authHandler smtpd.A
|
||||
}
|
||||
|
||||
if config.Label != "" {
|
||||
srv.Appname = fmt.Sprintf("Mailpit (%s)", config.Label)
|
||||
srv.AppName = fmt.Sprintf("Mailpit (%s)", config.Label)
|
||||
}
|
||||
|
||||
if config.SMTPAuthAllowInsecure {
|
||||
@ -276,6 +261,39 @@ func listenAndServe(addr string, handler smtpd.MsgIDHandler, authHandler smtpd.A
|
||||
}
|
||||
}
|
||||
|
||||
if isSocket {
|
||||
srv.Addr = socketAddr
|
||||
srv.Protocol = "unix"
|
||||
srv.SocketPerm = perm
|
||||
|
||||
if err := tools.PrepareSocket(srv.Addr); err != nil {
|
||||
storage.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// delete the Unix socket file on exit
|
||||
storage.AddTempFile(srv.Addr)
|
||||
|
||||
logger.Log().Infof("[smtpd] starting on %s", config.SMTPListen)
|
||||
} else {
|
||||
smtpType := "no encryption"
|
||||
|
||||
if config.SMTPTLSCert != "" {
|
||||
if config.SMTPRequireSTARTTLS {
|
||||
smtpType = "STARTTLS required"
|
||||
} else if config.SMTPRequireTLS {
|
||||
smtpType = "SSL/TLS required"
|
||||
} else {
|
||||
smtpType = "STARTTLS optional"
|
||||
if !config.SMTPAuthAllowInsecure && auth.SMTPCredentials != nil {
|
||||
smtpType = "STARTTLS required"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log().Infof("[smtpd] starting on %s (%s)", config.SMTPListen, smtpType)
|
||||
}
|
||||
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
@ -1,115 +0,0 @@
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
)
|
||||
|
||||
// Send will connect to a pre-configured SMTP server and send a message to one or more recipients.
|
||||
func Send(from string, to []string, msg []byte) error {
|
||||
addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to %s: %s", addr, err.Error())
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
|
||||
if config.SMTPRelayConfig.STARTTLS {
|
||||
conf := &tls.Config{ServerName: config.SMTPRelayConfig.Host} // #nosec
|
||||
|
||||
conf.InsecureSkipVerify = config.SMTPRelayConfig.AllowInsecure
|
||||
|
||||
if err = c.StartTLS(conf); err != nil {
|
||||
return fmt.Errorf("error creating StartTLS config: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
auth := relayAuthFromConfig()
|
||||
|
||||
if auth != nil {
|
||||
if err = c.Auth(auth); err != nil {
|
||||
return fmt.Errorf("error response to AUTH command: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err = c.Mail(from); err != nil {
|
||||
return fmt.Errorf("error response to MAIL command: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
logger.Log().Warnf("error response to RCPT command for %s: %s", addr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error response to DATA command: %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
return fmt.Errorf("error sending message: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return fmt.Errorf("error closing connection: %s", err.Error())
|
||||
}
|
||||
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Return the SMTP relay authentication based on config
|
||||
func relayAuthFromConfig() smtp.Auth {
|
||||
var a smtp.Auth
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "plain" {
|
||||
a = smtp.PlainAuth("", config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password, config.SMTPRelayConfig.Host)
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "login" {
|
||||
a = LoginAuth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password)
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "cram-md5" {
|
||||
a = smtp.CRAMMD5Auth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Secret)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Custom implementation of LOGIN SMTP authentication
|
||||
// @see https://gist.github.com/andelf/5118732
|
||||
type loginAuth struct {
|
||||
username, password string
|
||||
}
|
||||
|
||||
// LoginAuth authentication
|
||||
func LoginAuth(username, password string) smtp.Auth {
|
||||
return &loginAuth{username, password}
|
||||
}
|
||||
|
||||
func (a *loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
|
||||
return "LOGIN", []byte{}, nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
switch string(fromServer) {
|
||||
case "Username:":
|
||||
return []byte(a.username), nil
|
||||
case "Password:":
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, errors.New("Unknown fromServer")
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user