support STARTTLS

This commit is contained in:
wiggin77
2020-05-08 21:29:59 -04:00
parent a84781bb3f
commit a2c76abff0
5 changed files with 134 additions and 42 deletions

View File

@ -12,6 +12,22 @@ Run `mailrelay` on a local PC and set your device (e.g. scanner) to send mail to
`mailrelay` is written in Go, and can be compiled for any Go supported platform including Linux, MacOS, Windows. `mailrelay` is written in Go, and can be compiled for any Go supported platform including Linux, MacOS, Windows.
## Encryption
`mailrelay` uses TLS to connect to your SMTP provider. By default implicit TLS connections are assumed, meaning the connection is established
using TLS at the socket level. This is in accordance with [RFC 8314 section 3](https://tools.ietf.org/html/rfc8314#section-3). These connections usually use port 465.
However, some providers do not adhere to this recommendation (I'm looking at you Office365!) and only support the legacy STARTTLS command, which expects a non-encrypted socket connection at first, which is then upgraded to TLS. To enable this, set `smtp_starttls` to `true` in your config.
These connections usually use port 587.
## Testing your configuration
You can send a test email using the `-test` flag. A email will be sent using the SMTP provider specified in your `mailrelay.json` configuration.
```bash
./mailrelay -config=./mailrelay.json -test -sender=dlauder@warpmail.net -rcpt=ender.wiggin@warpmail.net
```
## Example (Linux) ## Example (Linux)
On local PC (192.168.1.54) create file `/etc/mailrelay.json` with contents: On local PC (192.168.1.54) create file `/etc/mailrelay.json` with contents:
@ -22,6 +38,7 @@ On local PC (192.168.1.54) create file `/etc/mailrelay.json` with contents:
{ {
"smtp_server": "smtp.fastmail.com", "smtp_server": "smtp.fastmail.com",
"smtp_port": 465, "smtp_port": 465,
"smtp_starttls": false,
"smtp_username": "username@fastmail.com", "smtp_username": "username@fastmail.com",
"smtp_password": "secretAppPassword", "smtp_password": "secretAppPassword",
"local_listen_ip": "0.0.0.0", "local_listen_ip": "0.0.0.0",

View File

@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
"net"
"net/smtp" "net/smtp"
"net/textproto" "net/textproto"
@ -25,10 +26,9 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
msg.Write(e.Data.Bytes()) msg.Write(e.Data.Bytes())
msg.WriteString("\r\n") msg.WriteString("\r\n")
fmt.Println("==== Starting email send ====") Logger.Infof("starting email send -- from:%s, starttls:%t", e.MailFrom.String(), config.STARTTLS)
defer fmt.Println("==== Finished email send ====")
var err error var err error
var conn *tls.Conn var conn net.Conn
var client *smtp.Client var client *smtp.Client
var writer io.WriteCloser var writer io.WriteCloser
@ -37,8 +37,14 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
ServerName: config.SMTPServer, ServerName: config.SMTPServer,
} }
if conn, err = tls.Dial("tcp", server, tlsconfig); err != nil { if config.STARTTLS {
return errors.Wrap(err, "dial error") if conn, err = net.Dial("tcp", server); err != nil {
return errors.Wrap(err, "dial error")
}
} else {
if conn, err = tls.Dial("tcp", server, tlsconfig); err != nil {
return errors.Wrap(err, "TLS dial error")
}
} }
if client, err = smtp.NewClient(conn, config.SMTPServer); err != nil { if client, err = smtp.NewClient(conn, config.SMTPServer); err != nil {
@ -52,6 +58,12 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
} }
}(&shouldCloseClient) }(&shouldCloseClient)
if config.STARTTLS {
if err = client.StartTLS(tlsconfig); err != nil {
return errors.Wrap(err, "starttls error")
}
}
auth := smtp.PlainAuth("", config.SMTPUsername, config.SMTPPassword, config.SMTPServer) auth := smtp.PlainAuth("", config.SMTPUsername, config.SMTPPassword, config.SMTPServer)
if err = client.Auth(auth); err != nil { if err = client.Auth(auth); err != nil {
return errors.Wrap(err, "auth error") return errors.Wrap(err, "auth error")
@ -82,6 +94,7 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
// We only need to close client if some other error prevented us // We only need to close client if some other error prevented us
// from getting to `client.Quit` // from getting to `client.Quit`
shouldCloseClient = false shouldCloseClient = false
Logger.Info("email sent with no errors.")
return nil return nil
} }
@ -114,10 +127,3 @@ func getTo(e *mail.Envelope) []string {
} }
return ret return ret
} }
func display(b []byte) {
s := string(b)
fmt.Println("################################")
fmt.Printf("%s\n", s)
fmt.Println("################################")
}

View File

@ -1,6 +1,7 @@
{ {
"smtp_server": "smtp.fastmail.com", "smtp_server": "smtp.fastmail.com",
"smtp_port": 465, "smtp_port": 465,
"smtp_starttls": false,
"smtp_username": "username@fastmail.com", "smtp_username": "username@fastmail.com",
"smtp_password": "secret_app_password", "smtp_password": "secret_app_password",
"local_listen_port": 2525, "local_listen_port": 2525,

110
main.go
View File

@ -4,57 +4,85 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"log" "net/smtp"
"os" "os"
"os/signal" "os/signal"
"syscall"
log "github.com/flashmob/go-guerrilla/log"
) )
type loggerLevels struct { var Logger log.Logger
Debug *log.Logger
Error *log.Logger
}
type mailRelayConfig struct { type mailRelayConfig struct {
SMTPServer string `json:"smtp_server"` SMTPServer string `json:"smtp_server"`
SMTPPort int `json:"smtp_port"` SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"` SMTPStartTLS bool `json:"smtp_starttls"`
SMTPPassword string `json:"smtp_password"` SMTPUsername string `json:"smtp_username"`
LocalListenIP string `json:"local_listen_ip"` SMTPPassword string `json:"smtp_password"`
LocalListenPort int `json:"local_listen_port"` LocalListenIP string `json:"local_listen_ip"`
AllowedHosts []string `json:"allowed_hosts"` LocalListenPort int `json:"local_listen_port"`
AllowedHosts []string `json:"allowed_hosts"`
} }
// Logger provides application logging.
var Logger loggerLevels
func main() { func main() {
err := run()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
os.Exit(0)
}
Logger.Debug = log.New(os.Stdout, "debug: ", log.Ldate|log.Ltime|log.Lshortfile) func run() error {
Logger.Error = log.New(os.Stderr, "error: ", log.Ldate|log.Ltime|log.Lshortfile)
var configFile string var configFile string
var test bool
var testsender string
var testrcpt string
var verbose bool
flag.StringVar(&configFile, "config", "/etc/mailrelay.json", "specifies JSON config file") flag.StringVar(&configFile, "config", "/etc/mailrelay.json", "specifies JSON config file")
flag.BoolVar(&test, "test", false, "sends a test message to SMTP server")
flag.StringVar(&testsender, "sender", "", "used with 'test' to specify sender email address")
flag.StringVar(&testrcpt, "rcpt", "", "used with 'test' to specify recipient email address")
flag.BoolVar(&verbose, "verbose", false, "verbose output")
flag.Parse() flag.Parse()
appConfig, err := loadConfig(configFile) appConfig, err := loadConfig(configFile)
if err != nil { if err != nil {
Logger.Error.Fatalf("loading config: %v", err) flag.Usage()
return fmt.Errorf("loading config: %w", err)
} }
err = Start(appConfig) err = Start(appConfig, verbose)
if err != nil { if err != nil {
Logger.Error.Fatalf("starting server: %v", err) flag.Usage()
return fmt.Errorf("starting server: %w", err)
}
logLevel := "info"
if verbose {
logLevel = "debug"
}
Logger, err = log.GetLogger("stdout", logLevel)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
if test {
err = sendTest(testsender, testrcpt, appConfig.LocalListenPort)
if err != nil {
return fmt.Errorf("sending test message: %w", err)
}
return nil
} }
// Wait for SIGINT // Wait for SIGINT
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
signal.Notify(c, os.Kill)
// Block until a signal is received. // Block until a signal is received.
s := <-c <-c
fmt.Println("Got signal:", s) return nil
os.Exit(0)
} }
func loadConfig(path string) (*mailRelayConfig, error) { func loadConfig(path string) (*mailRelayConfig, error) {
@ -71,3 +99,33 @@ func loadConfig(path string) (*mailRelayConfig, error) {
} }
return &cfg, nil return &cfg, nil
} }
// sendTest sends a test message to the SMTP server specified in mailrelay.json
func sendTest(sender string, rcpt string, port int) error {
conn, err := smtp.Dial(fmt.Sprintf("localhost:%d", port))
if err != nil {
return err
}
if err := conn.Mail(sender); err != nil {
return err
}
if err := conn.Rcpt(rcpt); err != nil {
return err
}
writeBody := func(c *smtp.Client) error {
wc, err := conn.Data()
if err != nil {
return err
}
defer wc.Close()
_, err = fmt.Fprintf(wc, "From: %s\nSubject: Test message\n\nThis is a test email from mailrelay.\n", sender)
return err
}
if err := writeBody(conn); err != nil {
return err
}
return conn.Quit()
}

View File

@ -10,11 +10,20 @@ import (
) )
// Start starts the server. // Start starts the server.
func Start(appConfig *mailRelayConfig) (err error) { func Start(appConfig *mailRelayConfig, verbose bool) (err error) {
listen := fmt.Sprintf("%s:%d", appConfig.LocalListenIP, appConfig.LocalListenPort) listen := fmt.Sprintf("%s:%d", appConfig.LocalListenIP, appConfig.LocalListenPort)
cfg := &guerrilla.AppConfig{LogFile: log.OutputStdout.String(), AllowedHosts: appConfig.AllowedHosts} logLevel := "info"
if verbose {
logLevel = "debug"
}
cfg := &guerrilla.AppConfig{
LogFile: log.OutputStdout.String(),
AllowedHosts: appConfig.AllowedHosts,
LogLevel: logLevel,
}
sc := guerrilla.ServerConfig{ sc := guerrilla.ServerConfig{
ListenInterface: listen, ListenInterface: listen,
IsEnabled: true, IsEnabled: true,
@ -30,6 +39,7 @@ func Start(appConfig *mailRelayConfig) (err error) {
"smtp_password": appConfig.SMTPPassword, "smtp_password": appConfig.SMTPPassword,
"smtp_server": appConfig.SMTPServer, "smtp_server": appConfig.SMTPServer,
"smtp_port": appConfig.SMTPPort, "smtp_port": appConfig.SMTPPort,
"smtp_starttls": appConfig.SMTPStartTLS,
} }
cfg.BackendConfig = bcfg cfg.BackendConfig = bcfg
@ -42,6 +52,7 @@ func Start(appConfig *mailRelayConfig) (err error) {
type relayConfig struct { type relayConfig struct {
SMTPServer string `json:"smtp_server"` SMTPServer string `json:"smtp_server"`
SMTPPort int `json:"smtp_port"` SMTPPort int `json:"smtp_port"`
STARTTLS bool `json:"smtp_starttls"`
SMTPUsername string `json:"smtp_username"` SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"` SMTPPassword string `json:"smtp_password"`
} }
@ -67,8 +78,7 @@ var mailRelayProcessor = func() backends.Decorator {
err := sendMail(e, config) err := sendMail(e, config)
if err != nil { if err != nil {
fmt.Printf("!!! %v\n", err) return backends.NewResult(err.Error()), err
return backends.NewResult(fmt.Sprintf("554 Error: %s", err)), err
} }
return p.Process(e, task) return p.Process(e, task)