2
0
forked from drew/smtprelay

1 Commits

Author SHA1 Message Date
Jonathon Reinhart
190c615029 Add SystemD unit file 2021-04-01 23:44:42 -04:00
11 changed files with 74 additions and 169 deletions

View File

@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2.4.0
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -7,14 +7,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.17
uses: actions/setup-go@v2.1.4
- name: Set up Go 1.15
uses: actions/setup-go@v2.1.3
with:
go-version: 1.17
go-version: 1.15
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2.4.0
uses: actions/checkout@v1
- name: Get dependencies
run: |

View File

@@ -13,18 +13,18 @@ jobs:
goos: [freebsd, linux, windows]
goarch: ["386", amd64]
steps:
- uses: actions/checkout@v2.4.0
- uses: actions/checkout@v2
- name: Set APP_VERSION env
run: echo APP_VERSION=$(echo ${GITHUB_REF} | rev | cut -d'/' -f 1 | rev ) >> ${GITHUB_ENV}
- name: Set BUILD_TIME env
run: echo BUILD_TIME=$(date) >> ${GITHUB_ENV}
- uses: wangyoucao577/go-release-action@v1.21
- uses: wangyoucao577/go-release-action@v1.15
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "https://golang.org/dl/go1.17.3.linux-amd64.tar.gz"
goversion: "https://golang.org/dl/go1.15.8.linux-amd64.tar.gz"
extra_files: LICENSE README.md smtprelay.ini
ldflags: -s -w -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}"

View File

@@ -3,12 +3,11 @@ package main
import (
"flag"
"net"
"net/smtp"
"regexp"
"strings"
"net/smtp"
"github.com/sirupsen/logrus"
"github.com/vharitonsky/iniflags"
"github.com/sirupsen/logrus"
)
var (
@@ -22,8 +21,7 @@ var (
logLevel = flag.String("log_level", "info", "Minimum log level to output")
hostName = flag.String("hostname", "localhost.localdomain", "Server hostname")
welcomeMsg = flag.String("welcome_msg", "", "Welcome message for SMTP session")
listenStr = flag.String("listen", "127.0.0.1:25 [::1]:25", "Address and port to listen for incoming SMTP")
listenAddrs = []protoAddr{}
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)")
@@ -34,7 +32,6 @@ var (
allowedRecipStr = flag.String("allowed_recipients", "", "Regular expression for valid TO EMail addresses")
allowedRecipients *regexp.Regexp
allowedUsers = flag.String("allowed_users", "", "Path to file with valid users/passwords")
command = flag.String("command", "", "Path to pipe command")
remoteHost = flag.String("remote_host", "", "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")
@@ -44,9 +41,6 @@ var (
versionInfo = flag.Bool("version", false, "Show version information")
)
func localAuthRequired() bool {
return *allowedUsers != ""
}
func setupAllowedNetworks() {
for _, netstr := range splitstr(*allowedNetsStr, ' ') {
@@ -61,7 +55,7 @@ func setupAllowedNetworks() {
// meaning the address refers to a host and not a network.
if !allowedNet.IP.Equal(baseIP) {
log.WithFields(logrus.Fields{
"given_net": netstr,
"given_net": netstr,
"proper_net": allowedNet,
}).Fatal("Invalid network in allowed_nets (host bits set)")
}
@@ -73,7 +67,7 @@ func setupAllowedNetworks() {
func setupAllowedPatterns() {
var err error
if *allowedSenderStr != "" {
if (*allowedSenderStr != "") {
allowedSender, err = regexp.Compile(*allowedSenderStr)
if err != nil {
log.WithField("allowed_sender", *allowedSenderStr).
@@ -82,7 +76,7 @@ func setupAllowedPatterns() {
}
}
if *allowedRecipStr != "" {
if (*allowedRecipStr != "") {
allowedRecipients, err = regexp.Compile(*allowedRecipStr)
if err != nil {
log.WithField("allowed_recipients", *allowedRecipStr).
@@ -92,6 +86,7 @@ func setupAllowedPatterns() {
}
}
func setupRemoteAuth() {
logger := log.WithField("remote_auth", *remoteAuthStr)
@@ -122,7 +117,7 @@ func setupRemoteAuth() {
host, _, err := net.SplitHostPort(*remoteHost)
if err != nil {
logger.WithField("remote_host", *remoteHost).
Fatal("Invalid remote_host")
Fatal("Invalid remote_host")
}
switch *remoteAuthStr {
@@ -135,50 +130,17 @@ func setupRemoteAuth() {
}
}
type protoAddr struct {
protocol string
address string
}
func splitProto(s string) protoAddr {
idx := strings.Index(s, "://")
if idx == -1 {
return protoAddr{
address: s,
}
}
return protoAddr{
protocol: s[0:idx],
address: s[idx+3:],
}
}
func setupListeners() {
for _, listenAddr := range strings.Split(*listenStr, " ") {
pa := splitProto(listenAddr)
if localAuthRequired() && pa.protocol == "" {
log.WithField("address", pa.address).
Fatal("Local authentication (via allowed_users file) " +
"not allowed with non-TLS listener")
}
listenAddrs = append(listenAddrs, pa)
}
}
func ConfigLoad() {
iniflags.Parse()
// Set up logging as soon as possible
setupLogger()
if *remoteHost == "" && *command == "" {
log.Warn("no remote_host or command set; mail will not be forwarded!")
if (*remoteHost == "") {
log.Warn("remote_host not set; mail will not be forwarded!")
}
setupAllowedNetworks()
setupAllowedPatterns()
setupRemoteAuth()
setupListeners()
}

View File

@@ -1,44 +0,0 @@
package main
import (
"testing"
)
func TestSplitProto(t *testing.T) {
var tests = []struct {
input string
proto string
addr string
}{
{
input: "localhost",
proto: "",
addr: "localhost",
},
{
input: "tls://my.local.domain",
proto: "tls",
addr: "my.local.domain",
},
{
input: "starttls://my.local.domain",
proto: "starttls",
addr: "my.local.domain",
},
}
for i, test := range tests {
testName := test.input
t.Run(testName, func(t *testing.T) {
pa := splitProto(test.input)
if pa.protocol != test.proto {
t.Errorf("Testcase %d: Incorrect proto: expected %v, got %v",
i, test.proto, pa.protocol)
}
if pa.address != test.addr {
t.Errorf("Testcase %d: Incorrect addr: expected %v, got %v",
i, test.addr, pa.address)
}
})
}
}

4
go.mod
View File

@@ -1,8 +1,8 @@
module github.com/decke/smtprelay
require (
github.com/chrj/smtpd v0.3.1
github.com/google/uuid v1.3.0
github.com/chrj/smtpd v0.3.0
github.com/google/uuid v1.2.0
github.com/sirupsen/logrus v1.8.1
github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad

9
go.sum
View File

@@ -1,9 +1,10 @@
github.com/chrj/smtpd v0.3.1 h1:kogHFkbFdKaoH3bgZkqNC9uVtKYOFfM3uV3rroBdooE=
github.com/chrj/smtpd v0.3.1/go.mod h1:JtABvV/LzvLmEIzy0NyDnrfMGOMd8wy5frAokwf6J9Q=
github.com/chrj/smtpd v0.3.0 h1:cw1LSHDOz7N3XbkcZSF/bue9dh7ATKk5ZksfBztV6b0=
github.com/chrj/smtpd v0.3.0/go.mod h1:1hmG9KbrE10JG1SmvG79Krh4F6713oUrw2+gRp1oSYk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb/go.mod h1:FSCIHbrqk7D01Mj8y/jW+NS1uoCerr+ad+IckTHTFf4=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=

View File

@@ -16,7 +16,7 @@ func setupLogger() {
log = logrus.New()
// Handle logfile
if *logFile == "" {
if (*logFile == "") {
log.SetOutput(os.Stderr)
} else {
writer, err := os.OpenFile(*logFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600)

96
main.go
View File

@@ -1,13 +1,11 @@
package main
import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/textproto"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
@@ -83,12 +81,12 @@ func addrAllowed(addr string, allowedAddrs []string) bool {
func senderChecker(peer smtpd.Peer, addr string) error {
// check sender address from auth file if user is authenticated
if localAuthRequired() && peer.Username != "" {
if *allowedUsers != "" && peer.Username != "" {
user, err := AuthFetch(peer.Username)
if err != nil {
// Shouldn't happen: authChecker already validated username+password
log.WithFields(logrus.Fields{
"peer": peer.Addr,
"peer": peer.Addr,
"username": peer.Username,
}).WithError(err).Warn("could not fetch auth user")
return smtpd.Error{Code: 451, Message: "Bad sender address"}
@@ -96,8 +94,8 @@ func senderChecker(peer smtpd.Peer, addr string) error {
if !addrAllowed(addr, user.allowedAddresses) {
log.WithFields(logrus.Fields{
"peer": peer.Addr,
"username": peer.Username,
"peer": peer.Addr,
"username": peer.Username,
"sender_address": addr,
}).Warn("sender address not allowed for authenticated user")
return smtpd.Error{Code: 451, Message: "Bad sender address"}
@@ -116,7 +114,7 @@ func senderChecker(peer smtpd.Peer, addr string) error {
log.WithFields(logrus.Fields{
"sender_address": addr,
"peer": peer.Addr,
"peer": peer.Addr,
}).Warn("sender address not allowed by allowed_sender pattern")
return smtpd.Error{Code: 451, Message: "Bad sender address"}
}
@@ -133,7 +131,7 @@ func recipientChecker(peer smtpd.Peer, addr string) error {
}
log.WithFields(logrus.Fields{
"peer": peer.Addr,
"peer": peer.Addr,
"recipient_address": addr,
}).Warn("recipient address not allowed by allowed_recipients pattern")
return smtpd.Error{Code: 451, Message: "Bad recipient address"}
@@ -143,7 +141,7 @@ func authChecker(peer smtpd.Peer, username string, password string) error {
err := AuthCheckPassword(username, password)
if err != nil {
log.WithFields(logrus.Fields{
"peer": peer.Addr,
"peer": peer.Addr,
"username": username,
}).WithError(err).Warn("auth error")
return smtpd.Error{Code: 535, Message: "Authentication credentials invalid"}
@@ -159,45 +157,21 @@ func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error {
logger := log.WithFields(logrus.Fields{
"from": env.Sender,
"to": env.Recipients,
"to": env.Recipients,
"peer": peerIP,
"host": *remoteHost,
"uuid": generateUUID(),
})
if *remoteHost == "" && *command == "" {
logger.Warning("no remote_host or command set; discarding mail")
return nil
}
env.AddReceivedLine(peer)
if *command != "" {
cmdLogger := logger.WithField("command", *command)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command(*command)
cmd.Stdin = bytes.NewReader(env.Data)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
cmdLogger.WithError(err).Error(stderr.String())
return nil
}
cmdLogger.Info("pipe command successful: " + stdout.String())
}
if *remoteHost == "" {
if (*remoteHost == "") {
logger.Warning("remote_host not set; discarding mail")
return nil
}
logger.Info("delivering mail from peer using smarthost")
env.AddReceivedLine(peer)
var sender string
if *remoteSender == "" {
@@ -223,7 +197,7 @@ func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error {
logger.WithFields(logrus.Fields{
"err_code": err.Code,
"err_msg": err.Msg,
"err_msg": err.Msg,
}).Error("delivery failed")
default:
smtpError = smtpd.Error{Code: 554, Message: "Forwarding failed"}
@@ -272,7 +246,7 @@ func getTLSConfig() *tls.Config {
if *localCert == "" || *localKey == "" {
log.WithFields(logrus.Fields{
"cert_file": *localCert,
"key_file": *localKey,
"key_file": *localKey,
}).Fatal("TLS certificate/key file not defined in config")
}
@@ -302,7 +276,7 @@ func main() {
Debug("starting smtprelay")
// Load allowed users file
if localAuthRequired() {
if *allowedUsers != "" {
err := AuthLoadFile(*allowedUsers)
if err != nil {
log.WithField("file", *allowedUsers).
@@ -314,9 +288,7 @@ func main() {
var servers []*smtpd.Server
// Create a server for each desired listen address
for _, listen := range listenAddrs {
logger := log.WithField("address", listen.address)
for _, listenAddr := range strings.Split(*listen, " ") {
server := &smtpd.Server{
Hostname: *hostName,
WelcomeMessage: *welcomeMsg,
@@ -326,38 +298,44 @@ func main() {
Handler: mailHandler,
}
if localAuthRequired() {
if *allowedUsers != "" {
server.Authenticator = authChecker
}
var lsnr net.Listener
var err error
switch listen.protocol {
case "":
logger.Info("listening on address")
lsnr, err = net.Listen("tcp", listen.address)
if strings.Index(listenAddr, "://") == -1 {
log.WithField("address", listenAddr).
Info("listening on address")
lsnr, err = net.Listen("tcp", listenAddr)
} else if strings.HasPrefix(listenAddr, "starttls://") {
listenAddr = strings.TrimPrefix(listenAddr, "starttls://")
case "starttls":
server.TLSConfig = getTLSConfig()
server.ForceTLS = *localForceTLS
logger.Info("listening on address (STARTTLS)")
lsnr, err = net.Listen("tcp", listen.address)
log.WithField("address", listenAddr).
Info("listening on address (STARTTLS)")
lsnr, err = net.Listen("tcp", listenAddr)
} else if strings.HasPrefix(listenAddr, "tls://") {
listenAddr = strings.TrimPrefix(listenAddr, "tls://")
case "tls":
server.TLSConfig = getTLSConfig()
logger.Info("listening on address (TLS)")
lsnr, err = tls.Listen("tcp", listen.address, server.TLSConfig)
default:
logger.WithField("protocol", listen.protocol).
log.WithField("address", listenAddr).
Info("listening on address (TLS)")
lsnr, err = tls.Listen("tcp", listenAddr, server.TLSConfig)
} else {
log.WithField("address", listenAddr).
Fatal("unknown protocol in listen address")
}
if err != nil {
logger.WithError(err).Fatal("error starting listener")
log.WithFields(logrus.Fields{
"address": listenAddr,
}).WithError(err).Fatal("error starting listener")
}
servers = append(servers, server)

View File

@@ -0,0 +1,11 @@
[Unit]
Description=SMTP Relay
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/opt/smtprelay/bin/smtprelay -config /etc/smtprelay/smtprelay.ini
[Install]
WantedBy=multi-user.target

View File

@@ -78,6 +78,3 @@
; Sender e-mail address on outgoing SMTP server
;remote_sender =
; Pipe messages to external command
;command = /usr/local/bin/script