mirror of
https://github.com/wiggin77/mailrelay.git
synced 2025-07-01 23:11:03 +02:00
support STARTTLS
This commit is contained in:
17
README.md
17
README.md
@ -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",
|
||||||
|
30
client.go
30
client.go
@ -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("################################")
|
|
||||||
}
|
|
||||||
|
@ -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
110
main.go
@ -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()
|
||||||
|
}
|
||||||
|
18
server.go
18
server.go
@ -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)
|
||||||
|
Reference in New Issue
Block a user