diff options
-rw-r--r-- | default.nix | 8 | ||||
-rw-r--r-- | go.mod | 9 | ||||
-rw-r--r-- | go.sum | 8 | ||||
-rw-r--r-- | main.go | 142 | ||||
-rw-r--r-- | module.nix | 78 |
5 files changed, 245 insertions, 0 deletions
diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..f26065c --- /dev/null +++ b/default.nix @@ -0,0 +1,8 @@ +{ pkgs ? import <nixpkgs> {} }: +pkgs.buildGoModule rec { + pname = "smtp-forward"; + version = "0.0.1"; + + src = ./.; + vendorSha256 = "1vcrlc8i5xibgalgha34g75yx24sdr5bpjqa8p9qcmbwiywrfirw"; +} @@ -0,0 +1,9 @@ +module smtp-forward + +go 1.15 + +require ( + github.com/mhale/smtpd v0.0.0-20210209185612-36ee4150ae3b + github.com/pkg/errors v0.9.1 + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect +) @@ -0,0 +1,8 @@ +github.com/mhale/smtpd v0.0.0-20210209185612-36ee4150ae3b h1:z4hiO3RG2qmFYXauNt7k4zsVU9Z8nHxXy00SsNTlcso= +github.com/mhale/smtpd v0.0.0-20210209185612-36ee4150ae3b/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= @@ -0,0 +1,142 @@ +package main + +import ( + "bytes" + "context" + "crypto/tls" + "flag" + "io/ioutil" + "log" + "net" + "net/mail" + "net/smtp" + "net/textproto" + "strings" + + "github.com/mhale/smtpd" + "github.com/pkg/errors" +) + +var flagListen = flag.String(`l`, `:25`, `Address to listen on`) +var flagHostname = flag.String(`h`, `HOSTNAME-NOT-SET`, `Server flagHostname`) +var flagMap = flag.String(`m`, ``, `-m prefix-matcher1:target@targethost,prefix-matcher2:target@targethost`) +var flagCertFile = flag.String(`c`, ``, ``) +var flagKeyFile = flag.String(`k`, ``, ``) + +func logWrapper(handler smtpd.Handler) smtpd.Handler { + return func(remoteAddr net.Addr, from string, to []string, data []byte) (err error) { + log.Printf(`received email remoteAddr=%v from=%v`, remoteAddr, from) + err = handler(remoteAddr, from, to, data) + if err != nil { + log.Printf(`failed to forward: %v`, err.Error()) + return err + } + log.Printf(`forwarded email remoteAddr=%v from=%v`, remoteAddr, from) + return nil + } +} + +func transformEmail(data []byte) (headers mail.Header, msgData []byte, err error) { + msg, err := mail.ReadMessage(bytes.NewReader(data)) + if err != nil { + return nil, nil, errors.Wrap(err, `failed to parse email`) + } + headers = make(mail.Header) + + msgData, err = ioutil.ReadAll(msg.Body) + if err != nil { + return nil, nil, errors.Wrap(err, `failed to parse email body`) + } + + for headerKey, headerVal := range msg.Header { + headers[headerKey] = headerVal + } + + return headers, msgData, nil +} + +func forward(targetEmail string, data []byte) error { + toAddress := strings.SplitN(targetEmail, `@`, 2) + if len(toAddress) != 2 { + return errors.New(`invalid targetEmail address: ` + targetEmail) + } + mxes, err := net.DefaultResolver.LookupMX(context.Background(), toAddress[1]) + if err != nil || len(mxes) == 0 { + return errors.Wrap(err, `failed targetEmail resolve mx`) + } + + headers, msgData, err := transformEmail(data) + if err != nil { + return errors.Wrap(err, `failed targetEmail transform email`) + } + headers[textproto.CanonicalMIMEHeaderKey(`To`)] = []string{targetEmail} + headers[textproto.CanonicalMIMEHeaderKey(`From`)] = []string{`forwarder@localnet.cc`} + headers[textproto.CanonicalMIMEHeaderKey(`Subject`)] = []string{`Forwarded: ` + headers.Get(`subject`)} + + var builder bytes.Buffer + for headerName, headerValues := range headers { + for _, value := range headerValues { + builder.WriteString(textproto.CanonicalMIMEHeaderKey(headerName)) + builder.WriteString(`: `) + builder.WriteString(value) + builder.WriteString("\r\n") + } + } + builder.WriteString("\r\n") + builder.Write(msgData) + + err = smtp.SendMail(mxes[0].Host+":25", nil, `forwarder@localnet.cc`, []string{targetEmail}, builder.Bytes()) + if err != nil { + return errors.Wrap(err, `failed targetEmail send mail via smtp`) + } + log.Printf("forwarded targetEmail=%v", targetEmail) + + return nil +} + +func makeHandleEmail(mapping map[string]string) smtpd.Handler { + return func(remoteAddr net.Addr, from string, to []string, data []byte) error { + for prefix, targetEmail := range mapping { + for _, to := range to { + if strings.HasPrefix(to, prefix) { + err := forward(targetEmail, data) + if err != nil { + log.Print(`forwarded failed `, to, ` to `, targetEmail, err.Error()) + } + } + } + } + return nil + } +} + +func main() { + flag.Parse() + var mapping = make(map[string]string) + for _, m := range strings.Split(*flagMap, `,`) { + if m == `` { + continue + } + m := strings.SplitN(m, `:`, 2) + if len(m) != 2 { + panic(`invalid flag -m: ` + *flagMap) + } + mapping[m[0]] = m[1] + } + + var tlsConfig *tls.Config + if *flagCertFile != `` && *flagKeyFile != `` { + cert, err := tls.LoadX509KeyPair(*flagCertFile, *flagKeyFile) + if err != nil { + panic(err) + } + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } + server := &smtpd.Server{ + TLSConfig: tlsConfig, + Addr: *flagListen, + Hostname: *flagHostname, + Handler: logWrapper(makeHandleEmail(mapping)), + } + _ = server.ListenAndServe() +} diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..85e033d --- /dev/null +++ b/module.nix @@ -0,0 +1,78 @@ +{ config, pkgs, lib, runCommand, ... }: +let + package = pkgs.callPackage ./default.nix {}; + cfg = config.services.smtp-forward; +in { + options.services.smtp-forward = { + enable = lib.mkEnableOption "the smtp-forward service"; + mapping = lib.mkOption { + type = lib.types.str; + default = "prefix1:addres@host,prefix2:address@host"; + description = "-m maps prefixes to email addresses"; + }; + listen = lib.mkOption { + type = lib.types.str; + default = ":25"; + description = "Adress to listen on"; + }; + hostname = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = "-h sets the server hostname"; + }; + key = lib.mkOption { + type = lib.types.str; + default = "setme"; + description = "-k sets the TLS key"; + }; + cert = lib.mkOption { + type = lib.types.str; + default = "setme"; + description = "-c sets the TLS key"; + }; + }; + config = lib.mkIf config.services.smtp-forward.enable { + systemd.services.smtp-forward = { + description = "Run smtp-forward"; + path = [ package ]; + wantedBy = [ "default.target" ]; + script = '' + ${package}/bin/smtp-forward -l ${cfg.listen} -m ${cfg.mapping} -h ${cfg.hostname} -k ${cfg.key} -c ${cfg.cert} + ''; + serviceConfig = { + User = "smtp-forward"; + # Security + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + ProtectUsers = true; + ProtectKernelLogs = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + PrivateMounts = true; + SystemCallArchitectures = "native"; + ProtectClock = true; + SystemCallFilter= [ "~@mount" "~@reboot" "~@swap" "~@module" "~@debug" "~@cpu-emulation" "SystemCallFilter=~@obsolete" ]; + }; + }; + + users.users.smtp-forward = { + description = "smtp-forward user"; + group = "nogroup"; + extraGroups = [ "keys" ]; + uid = config.ids.uids.firebird; + }; + }; +} |