41 Commits

Author SHA1 Message Date
Bernhard Froehlich
21b597f351 Bump version to 1.3.0 2019-09-07 11:31:28 +00:00
Bernhard Froehlich
5f82b4736c Update dependencies 2019-09-07 11:31:05 +00:00
Bernhard Fröhlich
de430286b3 Merge branch 'remote-sender' of beppler/smtprelay into master 2019-09-07 06:20:28 +00:00
Carlos Alberto Costa Beppler
769193ea4d Adjust the description of remote_sender parameter.
It represents the e-mail address used while sending message to the outgoing SMTP server.
2019-09-06 21:00:35 -03:00
Carlos Alberto Costa Beppler
0b65e904d8 Allows specify the sender used on SMTP conversation with outgoing server. 2019-09-06 17:07:37 -03:00
Bernhard Froehlich
2c9645ac68 Add drone config 2019-05-09 08:17:45 +00:00
Bernhard Froehlich
770e819e2b Improve error checking 2019-02-21 08:29:49 +00:00
Bernhard Froehlich
d11f8d81ea Fix formatting 2019-02-21 08:27:12 +00:00
Bernhard Froehlich
6270d75571 Improve TLS Config to prefer server ciphers, remove 3DES ciphers and require TLS 1.1 or higher 2019-01-08 15:09:29 +00:00
Bernhard Froehlich
92be537b25 Bump version to 1.2.0 2019-01-07 12:03:27 +00:00
Bernhard Froehlich
b9d1663a18 Fixes for new authentication code 2019-01-07 11:52:25 +00:00
Bernhard Froehlich
3a96014c70 Revert package renaming 2018-12-29 13:08:10 +00:00
Bernhard Froehlich
ade7cbca36 Rename to smtprelay 2018-12-29 12:39:56 +00:00
Bernhard Froehlich
ab341c697d Code refactoring and rename package 2018-12-29 12:24:32 +00:00
Bernhard Froehlich
ab850e8765 Use proper hostname instead of "localhost" for outgoing mails 2018-12-28 15:40:43 +00:00
Bernhard Froehlich
a82b0faf96 Check sender email against auth file when user is authenticated 2018-12-28 15:30:55 +00:00
Bernhard Froehlich
76a04a2001 Authentication checker converted to store passwords as bcrypt hashes 2018-12-28 15:18:50 +00:00
Bernhard Froehlich
0df376e54a Bump to 1.1.1-dev 2018-12-26 21:20:51 +00:00
Bernhard Froehlich
c05e5779a8 Bump version to 1.1.0 2018-12-26 21:07:01 +00:00
Bernhard Froehlich
3c1d683edc Update documentation for SMTPS support (outgoing) 2018-12-26 21:05:59 +00:00
Bernhard Froehlich
83b558239a Implement SMTPS support 2018-12-26 20:58:13 +00:00
Bernhard Froehlich
571e9ea942 Fork net/smtp package 2018-12-26 20:10:19 +00:00
Bernhard Froehlich
38aa14ddbf Ensure that authentication was successfull before we relay mails 2018-12-26 19:35:38 +00:00
Bernhard Froehlich
72dad9fb70 Handle error cases for user supplied regexp 2018-12-21 14:10:34 +00:00
Bernhard Froehlich
e01895f25a Update README 2018-12-21 10:16:26 +00:00
Bernhard Froehlich
a0e357c0de Add log message for successful delivery 2018-12-21 09:09:41 +00:00
Bernhard Froehlich
3d648a2ce7 Adjust SMTP error codes and messages to be aligned with the RFCs 2018-12-21 09:06:22 +00:00
Bernhard Froehlich
66fb86be7a Improve logging for received emails 2018-12-20 14:16:39 +00:00
Bernhard Froehlich
1d1c617161 Remove debug logging from smtpd 2018-12-20 13:53:06 +00:00
Bernhard Froehlich
e87b1e7168 Implement authentication checker against a plaintext file 2018-12-20 13:52:19 +00:00
Bernhard Froehlich
926c681bdf Implement Sender and Recipient checker against regular expressions 2018-12-20 11:36:26 +00:00
Bernhard Froehlich
dbe4c3bc50 More formatting fixes 2018-12-20 11:13:05 +00:00
Bernhard Froehlich
6427f0ec23 Fix whitespace 2018-12-20 11:11:28 +00:00
Bernhard Froehlich
a5f3c2bd3f Implement connection checker to restrict which networks are allowed to send mails to us 2018-12-20 11:09:06 +00:00
Bernhard Froehlich
b03ecbb02d Introduce VERSION to be able to identify ourselves 2018-12-20 09:58:04 +00:00
Bernhard Froehlich
3b643b3927 Improve logging and make sure smtpd is also logging 2018-12-20 09:44:41 +00:00
Bernhard Froehlich
88d85458dc Implement logfile and print on stdout and the logfile per default 2018-12-20 09:17:40 +00:00
Bernhard Froehlich
58d124b20d Switch dependency back to chrj/smtpd since my fixes were merged 2018-12-20 08:45:12 +00:00
Bernhard Froehlich
646103d157 Update smtpd dependency to latest commit 2018-12-14 13:37:22 +00:00
Bernhard Froehlich
5004734404 Temporary switch to my own fork of smtpd which contains a few fixes 2018-12-14 12:48:23 +00:00
Bernhard Froehlich
72fe85dea9 Add Received line to comply with RFC 5321 2018-12-14 11:26:48 +00:00
9 changed files with 828 additions and 35 deletions

8
.drone.yml Normal file
View File

@@ -0,0 +1,8 @@
kind: pipeline
name: default
steps:
- name: build
image: golang
commands:
- go build

View File

@@ -1,10 +1,29 @@
# smtp-proxy # smtprelay
Simple Golang based SMTP relay/proxy server that accepts mail via SMTP
and forwards it directly to another SMTP server.
## Why another SMTP server?
Outgoing mails are usually send via SMTP to an MTA (Mail Transfer Agent)
which is one of Postfix, Exim, Sendmail or OpenSMTPD on UNIX/Linux in most
cases. You really don't want to setup and maintain any of those full blown
kitchensinks yourself because they are complex, fragile and hard to
configure.
My use case is simple. I need to send automatically generated mails from
cron via msmtp/sSMTP/dma, mails from various services and network printers
to GMail without giving away my GMail credentials to each device which
produces mail.
Simple Go SMTP relay/proxy server that accepts mails via SMTP
and forwards directly to another SMTP server.
## Main features ## Main features
* STARTTLS/TLS support * Supports SMTPS/TLS (465), STARTTLS (587) and unencrypted SMTP (25)
* Checks for sender, receiver, client IP
* Authentication support with file (LOGIN, PLAIN)
* Enforce encryption for authentication
* Forwards all mail to a smarthost (GMail, MailGun or any other SMTP server)
* Small codebase
* IPv6 support * IPv6 support
* Forward to GMail, MailGun or any other SMTP server

67
auth.go Normal file
View File

@@ -0,0 +1,67 @@
package main
import (
"bufio"
"errors"
"os"
"strings"
"golang.org/x/crypto/bcrypt"
)
var (
filename string
)
func AuthLoadFile(file string) error {
f, err := os.Open(file)
if err != nil {
return err
}
f.Close()
filename = file
return nil
}
func AuthReady() bool {
return (filename != "")
}
func AuthFetch(username string) (string, string, error) {
if !AuthReady() {
return "", "", errors.New("Authentication file not specified. Call LoadFile() first")
}
file, err := os.Open(filename)
if err != nil {
return "", "", err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
parts := strings.Fields(scanner.Text())
if len(parts) != 3 {
continue
}
if strings.ToLower(username) == strings.ToLower(parts[0]) {
return parts[1], parts[2], nil
}
}
return "", "", errors.New("User not found")
}
func AuthCheckPassword(username string, secret string) error {
hash, _, err := AuthFetch(username)
if err != nil {
return err
}
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(secret)) == nil {
return nil
}
return errors.New("Password invalid")
}

34
config.go Normal file
View File

@@ -0,0 +1,34 @@
package main
import (
"flag"
"github.com/vharitonsky/iniflags"
)
const (
VERSION = "1.3.0"
)
var (
logFile = flag.String("logfile", "/var/log/smtprelay.log", "Path to logfile")
hostName = flag.String("hostname", "localhost.localdomain", "Server hostname")
welcomeMsg = flag.String("welcome_msg", "", "Welcome message for SMTP session")
listen = flag.String("listen", "127.0.0.1:25 [::1]:25", "Address and port to listen for incoming SMTP")
localCert = flag.String("local_cert", "", "SSL certificate for STARTTLS/TLS")
localKey = flag.String("local_key", "", "SSL private key for STARTTLS/TLS")
localForceTLS = flag.Bool("local_forcetls", false, "Force STARTTLS (needs local_cert and local_key)")
allowedNets = flag.String("allowed_nets", "127.0.0.1/8 ::1/128", "Networks allowed to send mails")
allowedSender = flag.String("allowed_sender", "", "Regular expression for valid FROM EMail adresses")
allowedRecipients = flag.String("allowed_recipients", "", "Regular expression for valid TO EMail adresses")
allowedUsers = flag.String("allowed_users", "", "Path to file with valid users/passwords")
remoteHost = flag.String("remote_host", "smtp.gmail.com:587", "Outgoing SMTP server")
remoteUser = flag.String("remote_user", "", "Username for authentication on outgoing SMTP server")
remotePass = flag.String("remote_pass", "", "Password for authentication on outgoing SMTP server")
remoteSender = flag.String("remote_sender", "", "Sender e-mail address on outgoing SMTP server")
versionInfo = flag.Bool("version", false, "Show version information")
)
func ConfigLoad() {
iniflags.Parse()
}

7
go.mod
View File

@@ -1,6 +1,9 @@
module code.bluelife.at/decke/smtp-proxy module code.bluelife.at/decke/smtprelay
require ( require (
github.com/chrj/smtpd v0.1.1 github.com/chrj/smtpd v0.1.2
github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472
) )
go 1.13

11
go.sum
View File

@@ -1,5 +1,12 @@
github.com/chrj/smtpd v0.1.1 h1:Jk9ZimOh4njDpMbDLPmdQX+x9AUYGrfE68EyjuQDPjk= github.com/chrj/smtpd v0.1.2 h1:yWaMOCmnPlcNgJzkak1TBhhkObAfomd+NmZG5epdO88=
github.com/chrj/smtpd v0.1.1/go.mod h1:jt4ydELuZmqhn9hn3YpEPV1dY00aOB+Q1nWXnBDFKeY= github.com/chrj/smtpd v0.1.2/go.mod h1:jt4ydELuZmqhn9hn3YpEPV1dY00aOB+Q1nWXnBDFKeY=
github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb/go.mod h1:FSCIHbrqk7D01Mj8y/jW+NS1uoCerr+ad+IckTHTFf4= github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb/go.mod h1:FSCIHbrqk7D01Mj8y/jW+NS1uoCerr+ad+IckTHTFf4=
github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de h1:fkw+7JkxF3U1GzQoX9h69Wvtvxajo5Rbzy6+YMMzPIg= github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de h1:fkw+7JkxF3U1GzQoX9h69Wvtvxajo5Rbzy6+YMMzPIg=
github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de/go.mod h1:irMhzlTz8+fVFj6CH2AN2i+WI5S6wWFtK3MBCIxIpyI= github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de/go.mod h1:irMhzlTz8+fVFj6CH2AN2i+WI5S6wWFtK3MBCIxIpyI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

230
main.go
View File

@@ -2,30 +2,109 @@ package main
import ( import (
"crypto/tls" "crypto/tls"
"flag" "fmt"
"io"
"log" "log"
"net" "net"
"net/smtp" "net/smtp"
"os"
"regexp"
"strings" "strings"
"time" "time"
"github.com/chrj/smtpd" "github.com/chrj/smtpd"
"github.com/vharitonsky/iniflags"
) )
var ( func connectionChecker(peer smtpd.Peer) error {
hostName = flag.String("hostname", "localhost.localdomain", "Server hostname") var peerIP net.IP
welcomeMsg = flag.String("welcome_msg", "", "Welcome message for SMTP session") if addr, ok := peer.Addr.(*net.TCPAddr); ok {
listen = flag.String("listen", "127.0.0.1:25 [::1]:25", "Address and port to listen for incoming SMTP") peerIP = net.ParseIP(addr.IP.String())
localCert = flag.String("local_cert", "", "SSL certificate for STARTTLS/TLS") } else {
localKey = flag.String("local_key", "", "SSL private key for STARTTLS/TLS") return smtpd.Error{Code: 421, Message: "Denied"}
localForceTLS = flag.Bool("local_forcetls", false, "Force STARTTLS (needs local_cert and local_key)") }
remoteHost = flag.String("remote_host", "smtp.gmail.com:587", "Outgoing SMTP server")
remoteUser = flag.String("remote_user", "", "Username for authentication on outgoing SMTP server")
remotePass = flag.String("remote_pass", "", "Password for authentication on outgoing SMTP server")
)
func handler(peer smtpd.Peer, env smtpd.Envelope) error { nets := strings.Split(*allowedNets, " ")
for i := range nets {
_, allowedNet, _ := net.ParseCIDR(nets[i])
if allowedNet.Contains(peerIP) {
return nil
}
}
return smtpd.Error{Code: 421, Message: "Denied"}
}
func senderChecker(peer smtpd.Peer, addr string) error {
// check sender address from auth file if user is authenticated
if *allowedUsers != "" && peer.Username != "" {
_, email, err := AuthFetch(peer.Username)
if err != nil {
return smtpd.Error{Code: 451, Message: "Bad sender address"}
}
if strings.ToLower(addr) != strings.ToLower(email) {
return smtpd.Error{Code: 451, Message: "Bad sender address"}
}
}
if *allowedSender == "" {
return nil
}
re, err := regexp.Compile(*allowedSender)
if err != nil {
log.Printf("allowed_sender invalid: %v\n", err)
return smtpd.Error{Code: 451, Message: "Bad sender address"}
}
if re.MatchString(addr) {
return nil
}
return smtpd.Error{Code: 451, Message: "Bad sender address"}
}
func recipientChecker(peer smtpd.Peer, addr string) error {
if *allowedRecipients == "" {
return nil
}
re, err := regexp.Compile(*allowedRecipients)
if err != nil {
log.Printf("allowed_recipients invalid: %v\n", err)
return smtpd.Error{Code: 451, Message: "Bad recipient address"}
}
if re.MatchString(addr) {
return nil
}
return smtpd.Error{Code: 451, Message: "Bad recipient address"}
}
func authChecker(peer smtpd.Peer, username string, password string) error {
err := AuthCheckPassword(username, password)
if err != nil {
log.Printf("Auth error: %v\n", err)
return smtpd.Error{Code: 535, Message: "Authentication credentials invalid"}
}
return nil
}
func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error {
if *allowedUsers != "" && peer.Username == "" {
return smtpd.Error{Code: 530, Message: "Authentication Required"}
}
peerIP := ""
if addr, ok := peer.Addr.(*net.TCPAddr); ok {
peerIP = addr.IP.String()
}
log.Printf("new mail from=<%s> to=%s peer=[%s]\n", env.Sender,
env.Recipients, peerIP)
var auth smtp.Auth var auth smtp.Auth
host, _, _ := net.SplitHostPort(*remoteHost) host, _, _ := net.SplitHostPort(*remoteHost)
@@ -34,28 +113,75 @@ func handler(peer smtpd.Peer, env smtpd.Envelope) error {
auth = smtp.PlainAuth("", *remoteUser, *remotePass, host) auth = smtp.PlainAuth("", *remoteUser, *remotePass, host)
} }
return smtp.SendMail( env.AddReceivedLine(peer)
log.Printf("delivering using smarthost %s\n", *remoteHost)
var sender string
if *remoteSender == "" {
sender = env.Sender
} else {
sender = *remoteSender
}
err := SendMail(
*remoteHost, *remoteHost,
auth, auth,
env.Sender, sender,
env.Recipients, env.Recipients,
env.Data, env.Data,
) )
if err != nil {
log.Printf("delivery failed: %v\n", err)
return smtpd.Error{Code: 554, Message: "Forwarding failed"}
}
log.Printf("%s delivery successful\n", env.Recipients)
return nil
} }
func main() { func main() {
iniflags.Parse() ConfigLoad()
if *versionInfo {
fmt.Printf("smtprelay/%s\n", VERSION)
os.Exit(0)
}
if *logFile != "" {
f, err := os.OpenFile(*logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
log.Fatalf("Error opening logfile: %v", err)
}
defer f.Close()
log.SetOutput(io.MultiWriter(os.Stdout, f))
}
listeners := strings.Split(*listen, " ") listeners := strings.Split(*listen, " ")
for i := range(listeners) { for i := range listeners {
listener := listeners[i] listener := listeners[i]
server := &smtpd.Server{ server := &smtpd.Server{
Hostname: *hostName, Hostname: *hostName,
WelcomeMessage: *welcomeMsg, WelcomeMessage: *welcomeMsg,
Handler: handler, ConnectionChecker: connectionChecker,
SenderChecker: senderChecker,
RecipientChecker: recipientChecker,
Handler: mailHandler,
}
if *allowedUsers != "" {
err := AuthLoadFile(*allowedUsers)
if err != nil {
log.Fatalf("Authentication file: %s\n", err)
}
server.Authenticator = authChecker
} }
if strings.Index(listeners[i], "://") == -1 { if strings.Index(listeners[i], "://") == -1 {
@@ -73,13 +199,40 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
server.TLSConfig = &tls.Config { server.TLSConfig = &tls.Config{
Certificates: [] tls.Certificate{cert}, PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS11,
// Ciphersuites as defined in stock Go but without 3DES
// https://golang.org/src/crypto/tls/cipher_suites.go
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // does not provide PFS
tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // does not provide PFS
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
Certificates: []tls.Certificate{cert},
} }
server.ForceTLS = *localForceTLS server.ForceTLS = *localForceTLS
log.Printf("Listen on %s (STARTSSL) ...\n", listener) log.Printf("Listen on %s (STARTSSL) ...\n", listener)
lsnr, err := net.Listen("tcp", listener) lsnr, err := net.Listen("tcp", listener)
if err != nil {
log.Fatal(err)
}
defer lsnr.Close() defer lsnr.Close()
go server.Serve(lsnr) go server.Serve(lsnr)
@@ -96,12 +249,39 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
server.TLSConfig = &tls.Config { server.TLSConfig = &tls.Config{
Certificates: [] tls.Certificate{cert}, PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS11,
// Ciphersuites as defined in stock Go but without 3DES
// https://golang.org/src/crypto/tls/cipher_suites.go
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // does not provide PFS
tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // does not provide PFS
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
Certificates: []tls.Certificate{cert},
} }
log.Printf("Listen on %s (TLS) ...\n", listener) log.Printf("Listen on %s (TLS) ...\n", listener)
lsnr, err := tls.Listen("tcp", listener, server.TLSConfig) lsnr, err := tls.Listen("tcp", listener, server.TLSConfig)
if err != nil {
log.Fatal(err)
}
defer lsnr.Close() defer lsnr.Close()
go server.Serve(lsnr) go server.Serve(lsnr)

453
smtp.go Normal file
View File

@@ -0,0 +1,453 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
// It also implements the following extensions:
// 8BITMIME RFC 1652
// AUTH RFC 2554
// STARTTLS RFC 3207
// Additional extensions may be handled by clients.
//
// The smtp package is frozen and is not accepting new features.
// Some external packages provide more functionality. See:
//
// https://godoc.org/?q=smtp
package main
import (
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/smtp"
"net/textproto"
"strings"
)
// A Client represents a client connection to an SMTP server.
type Client struct {
// Text is the textproto.Conn used by the Client. It is exported to allow for
// clients to add extensions.
Text *textproto.Conn
// keep a reference to the connection so it can be used to create a TLS
// connection later
conn net.Conn
// whether the Client is using TLS
tls bool
serverName string
// map of supported extensions
ext map[string]string
// supported auth mechanisms
auth []string
localName string // the name to use in HELO/EHLO
didHello bool // whether we've said HELO/EHLO
helloError error // the error from the hello
}
// Dial returns a new Client connected to an SMTP server at addr.
// The addr must include a port, as in "mail.example.com:smtp".
func Dial(addr string) (*Client, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
return NewClient(conn, host)
}
// NewClient returns a new Client using an existing connection and host as a
// server name to be used when authenticating.
func NewClient(conn net.Conn, host string) (*Client, error) {
text := textproto.NewConn(conn)
_, _, err := text.ReadResponse(220)
if err != nil {
text.Close()
return nil, err
}
c := &Client{Text: text, conn: conn, serverName: host, localName: *hostName}
_, c.tls = conn.(*tls.Conn)
return c, nil
}
// Close closes the connection.
func (c *Client) Close() error {
return c.Text.Close()
}
// hello runs a hello exchange if needed.
func (c *Client) hello() error {
if !c.didHello {
c.didHello = true
err := c.ehlo()
if err != nil {
c.helloError = c.helo()
}
}
return c.helloError
}
// Hello sends a HELO or EHLO to the server as the given host name.
// Calling this method is only necessary if the client needs control
// over the host name used. The client will introduce itself as "localhost"
// automatically otherwise. If Hello is called, it must be called before
// any of the other methods.
func (c *Client) Hello(localName string) error {
if err := validateLine(localName); err != nil {
return err
}
if c.didHello {
return errors.New("smtp: Hello called after other methods")
}
c.localName = localName
return c.hello()
}
// cmd is a convenience function that sends a command and returns the response
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
id, err := c.Text.Cmd(format, args...)
if err != nil {
return 0, "", err
}
c.Text.StartResponse(id)
defer c.Text.EndResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode)
return code, msg, err
}
// helo sends the HELO greeting to the server. It should be used only when the
// server does not support ehlo.
func (c *Client) helo() error {
c.ext = nil
_, _, err := c.cmd(250, "HELO %s", c.localName)
return err
}
// ehlo sends the EHLO (extended hello) greeting to the server. It
// should be the preferred greeting for servers that support it.
func (c *Client) ehlo() error {
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
if err != nil {
return err
}
ext := make(map[string]string)
extList := strings.Split(msg, "\n")
if len(extList) > 1 {
extList = extList[1:]
for _, line := range extList {
args := strings.SplitN(line, " ", 2)
if len(args) > 1 {
ext[args[0]] = args[1]
} else {
ext[args[0]] = ""
}
}
}
if mechs, ok := ext["AUTH"]; ok {
c.auth = strings.Split(mechs, " ")
}
c.ext = ext
return err
}
// StartTLS sends the STARTTLS command and encrypts all further communication.
// Only servers that advertise the STARTTLS extension support this function.
func (c *Client) StartTLS(config *tls.Config) error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(220, "STARTTLS")
if err != nil {
return err
}
c.conn = tls.Client(c.conn, config)
c.Text = textproto.NewConn(c.conn)
c.tls = true
return c.ehlo()
}
// TLSConnectionState returns the client's TLS connection state.
// The return values are their zero values if StartTLS did
// not succeed.
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
tc, ok := c.conn.(*tls.Conn)
if !ok {
return
}
return tc.ConnectionState(), true
}
// Verify checks the validity of an email address on the server.
// If Verify returns nil, the address is valid. A non-nil return
// does not necessarily indicate an invalid address. Many servers
// will not verify addresses for security reasons.
func (c *Client) Verify(addr string) error {
if err := validateLine(addr); err != nil {
return err
}
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "VRFY %s", addr)
return err
}
// Auth authenticates a client using the provided authentication mechanism.
// A failed authentication closes the connection.
// Only servers that advertise the AUTH extension support this function.
func (c *Client) Auth(a smtp.Auth) error {
if err := c.hello(); err != nil {
return err
}
encoding := base64.StdEncoding
mech, resp, err := a.Start(&smtp.ServerInfo{c.serverName, c.tls, c.auth})
if err != nil {
c.Quit()
return err
}
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64)))
for err == nil {
var msg []byte
switch code {
case 334:
msg, err = encoding.DecodeString(msg64)
case 235:
// the last message isn't base64 because it isn't a challenge
msg = []byte(msg64)
default:
err = &textproto.Error{Code: code, Msg: msg64}
}
if err == nil {
resp, err = a.Next(msg, code == 334)
}
if err != nil {
// abort the AUTH
c.cmd(501, "*")
c.Quit()
break
}
if resp == nil {
break
}
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err = c.cmd(0, string(resp64))
}
return err
}
// Mail issues a MAIL command to the server using the provided email address.
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
// parameter.
// This initiates a mail transaction and is followed by one or more Rcpt calls.
func (c *Client) Mail(from string) error {
if err := validateLine(from); err != nil {
return err
}
if err := c.hello(); err != nil {
return err
}
cmdStr := "MAIL FROM:<%s>"
if c.ext != nil {
if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME"
}
}
_, _, err := c.cmd(250, cmdStr, from)
return err
}
// Rcpt issues a RCPT command to the server using the provided email address.
// A call to Rcpt must be preceded by a call to Mail and may be followed by
// a Data call or another Rcpt call.
func (c *Client) Rcpt(to string) error {
if err := validateLine(to); err != nil {
return err
}
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
return err
}
type dataCloser struct {
c *Client
io.WriteCloser
}
func (d *dataCloser) Close() error {
d.WriteCloser.Close()
_, _, err := d.c.Text.ReadResponse(250)
return err
}
// Data issues a DATA command to the server and returns a writer that
// can be used to write the mail headers and body. The caller should
// close the writer before calling any more methods on c. A call to
// Data must be preceded by one or more calls to Rcpt.
func (c *Client) Data() (io.WriteCloser, error) {
_, _, err := c.cmd(354, "DATA")
if err != nil {
return nil, err
}
return &dataCloser{c, c.Text.DotWriter()}, nil
}
var testHookStartTLS func(*tls.Config) // nil, except for tests
// SendMail connects to the server at addr with TLS when port 465 or
// smtps is specified or unencrypted otherwise and switches to TLS if
// possible, authenticates with the optional mechanism a if possible,
// and then sends an email from address from, to addresses to, with
// message msg.
// The addr must include a port, as in "mail.example.com:smtp".
//
// The addresses in the to parameter are the SMTP RCPT addresses.
//
// The msg parameter should be an RFC 822-style email with headers
// first, a blank line, and then the message body. The lines of msg
// should be CRLF terminated. The msg headers should usually include
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
// messages is accomplished by including an email address in the to
// parameter but not including it in the msg headers.
//
// The SendMail function and the net/smtp package are low-level
// mechanisms and provide no support for DKIM signing, MIME
// attachments (see the mime/multipart package), or other mail
// functionality. Higher-level packages exist outside of the standard
// library.
func SendMail(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
if err := validateLine(from); err != nil {
return err
}
for _, recp := range to {
if err := validateLine(recp); err != nil {
return err
}
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
return err
}
var c *Client
if port == "465" || port == "smtps" {
config := &tls.Config{ServerName: host}
conn, err := tls.Dial("tcp", addr, config)
if err != nil {
return err
}
defer conn.Close()
c, err = NewClient(conn, host)
if err != nil {
return err
}
if err = c.hello(); err != nil {
return err
}
} else {
c, err = Dial(addr)
if err != nil {
return err
}
defer c.Close()
if err = c.hello(); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: c.serverName}
if testHookStartTLS != nil {
testHookStartTLS(config)
}
if err = c.StartTLS(config); err != nil {
return err
}
}
}
if a != nil && c.ext != nil {
if _, ok := c.ext["AUTH"]; !ok {
return errors.New("smtp: server doesn't support AUTH")
}
if err = c.Auth(a); err != nil {
return err
}
}
if err = c.Mail(from); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = w.Write(msg)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}
// Extension reports whether an extension is support by the server.
// The extension name is case-insensitive. If the extension is supported,
// Extension also returns a string that contains any parameters the
// server specifies for the extension.
func (c *Client) Extension(ext string) (bool, string) {
if err := c.hello(); err != nil {
return false, ""
}
if c.ext == nil {
return false, ""
}
ext = strings.ToUpper(ext)
param, ok := c.ext[ext]
return ok, param
}
// Reset sends the RSET command to the server, aborting the current mail
// transaction.
func (c *Client) Reset() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "RSET")
return err
}
// Noop sends the NOOP command to the server. It does nothing but check
// that the connection to the server is okay.
func (c *Client) Noop() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "NOOP")
return err
}
// Quit sends the QUIT command and closes the connection to the server.
func (c *Client) Quit() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(221, "QUIT")
if err != nil {
return err
}
return c.Text.Close()
}
// validateLine checks to see if a line has CR or LF as per RFC 5321
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: A line must not contain CR or LF")
}
return nil
}

View File

@@ -1,4 +1,7 @@
; smtp-proxy configuration ; smtprelay configuration
; Logfile
;logfile = /var/log/smtprelay.log
; Hostname for this SMTP server ; Hostname for this SMTP server
;hostname = "localhost.localdomain" ;hostname = "localhost.localdomain"
@@ -21,6 +24,22 @@
; accepting mails from client. ; accepting mails from client.
;local_forcetls = false ;local_forcetls = false
; Networks that are allowed to send mails to us
;allowed_nets = 127.0.0.1/8 ::1/128
; Regular expression for valid FROM EMail adresses
; Example: ^(.*)@localhost.localdomain$
;allowed_sender =
; Regular expression for valid TO EMail adresses
; Example: ^(.*)@localhost.localdomain$
;allowed_recipients =
; File which contains username and password used for
; authentication before they can send mail.
; File format: username bcrypt-hash email
;allowed_users =
; Relay all mails to this SMTP server ; Relay all mails to this SMTP server
; GMail ; GMail
@@ -35,3 +54,6 @@
; Authentication credentials on outgoing SMTP server ; Authentication credentials on outgoing SMTP server
;remote_user = ;remote_user =
;remote_pass = ;remote_pass =
; Sender e-mail address on outgoing SMTP server
;remote_sender =