summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYves Fischer <yvesf+git@xapek.org>2021-02-17 22:58:31 +0100
committerYves Fischer <yvesf+git@xapek.org>2021-02-18 23:01:44 +0100
commit7e420c5d5b2bb4c638fe2278ac69a69e4a6c06e6 (patch)
tree5d30d89d7dce89f75c8a1395696fd085cac395a2
downloadsmtp-forward-7e420c5d5b2bb4c638fe2278ac69a69e4a6c06e6.tar.gz
smtp-forward-7e420c5d5b2bb4c638fe2278ac69a69e4a6c06e6.zip
smtp-forward
-rw-r--r--default.nix8
-rw-r--r--go.mod9
-rw-r--r--go.sum8
-rw-r--r--main.go142
-rw-r--r--module.nix78
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";
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..37e4c60
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..72332f7
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..37ebedc
--- /dev/null
+++ b/main.go
@@ -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;
+ };
+ };
+}