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.
## 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)
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_port": 465,
"smtp_starttls": false,
"smtp_username": "username@fastmail.com",
"smtp_password": "secretAppPassword",
"local_listen_ip": "0.0.0.0",

View File

@ -5,6 +5,7 @@ import (
"crypto/tls"
"fmt"
"io"
"net"
"net/smtp"
"net/textproto"
@ -25,10 +26,9 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
msg.Write(e.Data.Bytes())
msg.WriteString("\r\n")
fmt.Println("==== Starting email send ====")
defer fmt.Println("==== Finished email send ====")
Logger.Infof("starting email send -- from:%s, starttls:%t", e.MailFrom.String(), config.STARTTLS)
var err error
var conn *tls.Conn
var conn net.Conn
var client *smtp.Client
var writer io.WriteCloser
@ -37,8 +37,14 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
ServerName: config.SMTPServer,
}
if conn, err = tls.Dial("tcp", server, tlsconfig); err != nil {
return errors.Wrap(err, "dial error")
if config.STARTTLS {
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 {
@ -52,6 +58,12 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
}
}(&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)
if err = client.Auth(auth); err != nil {
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
// from getting to `client.Quit`
shouldCloseClient = false
Logger.Info("email sent with no errors.")
return nil
}
@ -114,10 +127,3 @@ func getTo(e *mail.Envelope) []string {
}
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_port": 465,
"smtp_starttls": false,
"smtp_username": "username@fastmail.com",
"smtp_password": "secret_app_password",
"local_listen_port": 2525,

110
main.go
View File

@ -4,57 +4,85 @@ import (
"encoding/json"
"flag"
"fmt"
"log"
"net/smtp"
"os"
"os/signal"
"syscall"
log "github.com/flashmob/go-guerrilla/log"
)
type loggerLevels struct {
Debug *log.Logger
Error *log.Logger
}
var Logger log.Logger
type mailRelayConfig struct {
SMTPServer string `json:"smtp_server"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
LocalListenIP string `json:"local_listen_ip"`
LocalListenPort int `json:"local_listen_port"`
AllowedHosts []string `json:"allowed_hosts"`
SMTPServer string `json:"smtp_server"`
SMTPPort int `json:"smtp_port"`
SMTPStartTLS bool `json:"smtp_starttls"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
LocalListenIP string `json:"local_listen_ip"`
LocalListenPort int `json:"local_listen_port"`
AllowedHosts []string `json:"allowed_hosts"`
}
// Logger provides application logging.
var Logger loggerLevels
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)
Logger.Error = log.New(os.Stderr, "error: ", log.Ldate|log.Ltime|log.Lshortfile)
func run() error {
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.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()
appConfig, err := loadConfig(configFile)
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 {
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
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
signal.Notify(c, os.Kill)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// Block until a signal is received.
s := <-c
fmt.Println("Got signal:", s)
os.Exit(0)
<-c
return nil
}
func loadConfig(path string) (*mailRelayConfig, error) {
@ -71,3 +99,33 @@ func loadConfig(path string) (*mailRelayConfig, error) {
}
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.
func Start(appConfig *mailRelayConfig) (err error) {
func Start(appConfig *mailRelayConfig, verbose bool) (err error) {
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{
ListenInterface: listen,
IsEnabled: true,
@ -30,6 +39,7 @@ func Start(appConfig *mailRelayConfig) (err error) {
"smtp_password": appConfig.SMTPPassword,
"smtp_server": appConfig.SMTPServer,
"smtp_port": appConfig.SMTPPort,
"smtp_starttls": appConfig.SMTPStartTLS,
}
cfg.BackendConfig = bcfg
@ -42,6 +52,7 @@ func Start(appConfig *mailRelayConfig) (err error) {
type relayConfig struct {
SMTPServer string `json:"smtp_server"`
SMTPPort int `json:"smtp_port"`
STARTTLS bool `json:"smtp_starttls"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
}
@ -67,8 +78,7 @@ var mailRelayProcessor = func() backends.Decorator {
err := sendMail(e, config)
if err != nil {
fmt.Printf("!!! %v\n", err)
return backends.NewResult(fmt.Sprintf("554 Error: %s", err)), err
return backends.NewResult(err.Error()), err
}
return p.Process(e, task)