From 7e420c5d5b2bb4c638fe2278ac69a69e4a6c06e6 Mon Sep 17 00:00:00 2001 From: Yves Fischer Date: Wed, 17 Feb 2021 22:58:31 +0100 Subject: smtp-forward --- main.go | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 main.go (limited to 'main.go') 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() +} -- cgit v1.2.1