1
0
mirror of https://github.com/chylex/SMTP-Relay.git synced 2024-10-16 23:42:47 +02:00
SMTP-Relay/config.go
2022-08-15 13:35:12 +00:00

276 lines
7.8 KiB
Go

package main
import (
"bufio"
"flag"
"fmt"
"io"
"net"
"os"
"regexp"
"strings"
"time"
"github.com/peterbourgon/ff/v3"
"github.com/sirupsen/logrus"
)
var (
appVersion = "unknown"
buildTime = "unknown"
)
var (
flagset = flag.NewFlagSet("smtprelay", flag.ContinueOnError)
// config flags
logFile = flagset.String("logfile", "", "Path to logfile")
logFormat = flagset.String("log_format", "default", "Log output format")
logLevel = flagset.String("log_level", "info", "Minimum log level to output")
hostName = flagset.String("hostname", "localhost.localdomain", "Server hostname")
welcomeMsg = flagset.String("welcome_msg", "", "Welcome message for SMTP session")
listenStr = flagset.String("listen", "127.0.0.1:25 [::1]:25", "Address and port to listen for incoming SMTP")
localCert = flagset.String("local_cert", "", "SSL certificate for STARTTLS/TLS")
localKey = flagset.String("local_key", "", "SSL private key for STARTTLS/TLS")
localForceTLS = flagset.Bool("local_forcetls", false, "Force STARTTLS (needs local_cert and local_key)")
readTimeoutStr = flagset.String("read_timeout", "60s", "Socket timeout for read operations")
writeTimeoutStr = flagset.String("write_timeout", "60s", "Socket timeout for write operations")
dataTimeoutStr = flagset.String("data_timeout", "5m", "Socket timeout for DATA command")
maxConnections = flagset.Int("max_connections", 100, "Max concurrent connections, use -1 to disable")
maxMessageSize = flagset.Int("max_message_size", 10240000, "Max message size in bytes")
maxRecipients = flagset.Int("max_recipients", 100, "Max RCPT TO calls for each envelope")
allowedNetsStr = flagset.String("allowed_nets", "127.0.0.0/8 ::1/128", "Networks allowed to send mails")
allowedSenderStr = flagset.String("allowed_sender", "", "Regular expression for valid FROM EMail addresses")
allowedRecipStr = flagset.String("allowed_recipients", "", "Regular expression for valid TO EMail addresses")
allowedUsers = flagset.String("allowed_users", "", "Path to file with valid users/passwords")
command = flagset.String("command", "", "Path to pipe command")
remotesStr = flagset.String("remotes", "", "Outgoing SMTP servers")
// additional flags
_ = flagset.String("config", "", "Path to config file (ini format)")
versionInfo = flagset.Bool("version", false, "Show version information")
// internal
listenAddrs = []protoAddr{}
readTimeout time.Duration
writeTimeout time.Duration
dataTimeout time.Duration
allowedNets = []*net.IPNet{}
allowedSender *regexp.Regexp
allowedRecipients *regexp.Regexp
remotes = []*Remote{}
)
func localAuthRequired() bool {
return *allowedUsers != ""
}
func setupAllowedNetworks() {
for _, netstr := range splitstr(*allowedNetsStr, ' ') {
baseIP, allowedNet, err := net.ParseCIDR(netstr)
if err != nil {
log.WithField("netstr", netstr).
WithError(err).
Fatal("Invalid CIDR notation in allowed_nets")
}
// Reject any network specification where any host bits are set,
// meaning the address refers to a host and not a network.
if !allowedNet.IP.Equal(baseIP) {
log.WithFields(logrus.Fields{
"given_net": netstr,
"proper_net": allowedNet,
}).Fatal("Invalid network in allowed_nets (host bits set)")
}
allowedNets = append(allowedNets, allowedNet)
}
}
func setupAllowedPatterns() {
var err error
if *allowedSenderStr != "" {
allowedSender, err = regexp.Compile(*allowedSenderStr)
if err != nil {
log.WithField("allowed_sender", *allowedSenderStr).
WithError(err).
Fatal("allowed_sender pattern invalid")
}
}
if *allowedRecipStr != "" {
allowedRecipients, err = regexp.Compile(*allowedRecipStr)
if err != nil {
log.WithField("allowed_recipients", *allowedRecipStr).
WithError(err).
Fatal("allowed_recipients pattern invalid")
}
}
}
func setupRemotes() {
logger := log.WithField("remotes", *remotesStr)
if *remotesStr != "" {
for _, remoteURL := range strings.Split(*remotesStr, " ") {
r, err := ParseRemote(remoteURL)
if err != nil {
logger.Fatal(fmt.Sprintf("error parsing url: '%s': %v", remoteURL, err))
}
remotes = append(remotes, r)
}
}
}
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 setupTimeouts() {
var err error
readTimeout, err = time.ParseDuration(*readTimeoutStr)
if err != nil {
log.WithField("read_timeout", *readTimeoutStr).
WithError(err).
Fatal("read_timeout duration string invalid")
}
if readTimeout.Seconds() < 1 {
log.WithField("read_timeout", *readTimeoutStr).
Fatal("read_timeout less than one second")
}
writeTimeout, err = time.ParseDuration(*writeTimeoutStr)
if err != nil {
log.WithField("write_timeout", *writeTimeoutStr).
WithError(err).
Fatal("write_timeout duration string invalid")
}
if writeTimeout.Seconds() < 1 {
log.WithField("write_timeout", *writeTimeoutStr).
Fatal("write_timeout less than one second")
}
dataTimeout, err = time.ParseDuration(*dataTimeoutStr)
if err != nil {
log.WithField("data_timeout", *dataTimeoutStr).
WithError(err).
Fatal("data_timeout duration string invalid")
}
if dataTimeout.Seconds() < 1 {
log.WithField("data_timeout", *dataTimeoutStr).
Fatal("data_timeout less than one second")
}
}
func ConfigLoad() {
// use .env file if it exists
if _, err := os.Stat(".env"); err == nil {
if err := ff.Parse(flagset, os.Args[1:],
ff.WithEnvVarPrefix("smtprelay"),
ff.WithConfigFile(".env"),
ff.WithConfigFileParser(ff.EnvParser),
); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
} else {
// use env variables and smtprelay.ini file
if err := ff.Parse(flagset, os.Args[1:],
ff.WithEnvVarPrefix("smtprelay"),
ff.WithConfigFileFlag("config"),
ff.WithConfigFileParser(IniParser),
); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
// Set up logging as soon as possible
setupLogger()
if *versionInfo {
fmt.Printf("smtprelay/%s (%s)\n", appVersion, buildTime)
os.Exit(0)
}
if *remotesStr == "" && *command == "" {
log.Warn("no remotes or command set; mail will not be forwarded!")
}
setupAllowedNetworks()
setupAllowedPatterns()
setupRemotes()
setupListeners()
setupTimeouts()
}
// IniParser is a parser for config files in classic key/value style format. Each
// line is tokenized as a single key/value pair. The first "=" delimited
// token in the line is interpreted as the flag name, and all remaining tokens
// are interpreted as the value. Any leading hyphens on the flag name are
// ignored.
func IniParser(r io.Reader, set func(name, value string) error) error {
s := bufio.NewScanner(r)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" {
continue // skip empties
}
if line[0] == '#' || line[0] == ';' {
continue // skip comments
}
var (
name string
value string
index = strings.IndexRune(line, '=')
)
if index < 0 {
name, value = line, "true" // boolean option
} else {
name, value = strings.TrimSpace(line[:index]), strings.Trim(strings.TrimSpace(line[index+1:]), "\"")
}
if i := strings.Index(value, " #"); i >= 0 {
value = strings.TrimSpace(value[:i])
}
if err := set(name, value); err != nil {
return err
}
}
return nil
}