From 5535515fcb0f1c4c61c57e47c3a5e8088612830f Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Sun, 12 Mar 2023 15:22:23 +0100 Subject: [PATCH] move sendmail to separate file --- main.go | 222 ----------------------------------------------- sendmail.go | 242 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 222 deletions(-) create mode 100644 sendmail.go diff --git a/main.go b/main.go index 4da26fd..c6f8b6a 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,10 @@ package main import ( - "bufio" "bytes" "context" "crypto/ed25519" "crypto/rsa" - "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" @@ -16,7 +14,6 @@ import ( "io" "log" "net" - "net/mail" "os" "path/filepath" "strings" @@ -42,7 +39,6 @@ import ( "github.com/mjl-/mox/moxvar" "github.com/mjl-/mox/mtasts" "github.com/mjl-/mox/smtp" - "github.com/mjl-/mox/smtpclient" "github.com/mjl-/mox/spf" "github.com/mjl-/mox/store" "github.com/mjl-/mox/tlsrpt" @@ -1990,221 +1986,3 @@ func cmdConfigDescribeSendmail(c *cmd) { err := sconf.Describe(os.Stdout, submitconf) xcheckf(err, "describe config") } - -func cmdSendmail(c *cmd) { - c.params = "[-Fname] [ignoredflags] [-t] [\r\n", submitconf.From) - var haveTo bool - for { - line, err := r.ReadString('\n') - if err != nil && err != io.EOF { - xcheckf(err, "reading message") - } - if line != "" { - if !strings.HasSuffix(line, "\n") { - line += "\n" - } - if !strings.HasSuffix(line, "\r\n") { - line = line[:len(line)-1] + "\r\n" - } - if header && line == "\r\n" { - // Bare \r\n marks end of header. - if !haveTo { - line = fmt.Sprintf("To: <%s>\r\n", recipient) + line - } - header = false - } else if header { - t := strings.SplitN(line, ":", 2) - if len(t) != 2 { - log.Fatalf("invalid message, missing colon in header") - } - k := strings.ToLower(t[0]) - if k == "from" { - // We already added a From header. - if err == io.EOF { - break - } - continue - } else if tflag && k == "to" { - if recipient != "" { - log.Fatalf("only single To header allowed") - } - s := strings.TrimSpace(t[1]) - if !strings.Contains(s, "@") { - if submitconf.DefaultDestination == "" { - log.Fatalf("recipient %q has no @ and no default destination is configured", s) - } - recipient = submitconf.DefaultDestination - } else { - addrs, err := mail.ParseAddressList(s) - xcheckf(err, "parsing To address list") - if len(addrs) != 1 { - log.Fatalf("only single address allowed in To header") - } - recipient = addrs[0].Address - } - } - if k == "to" { - haveTo = true - } - } - sb.WriteString(line) - } - if err == io.EOF { - break - } - } - msg := sb.String() - - if recipient == "" { - log.Fatalf("no recipient") - } - - // Message seems acceptable. We'll try to deliver it from here. If that fails, we - // store the message in the users home directory. - - xcheckf := func(err error, format string, args ...any) { - if err == nil { - return - } - log.Printf("submit failed: %s: %s", fmt.Sprintf(format, args...), err) - homedir, err := os.UserHomeDir() - xcheckf(err, "finding homedir for storing message after failed delivery") - maildir := filepath.Join(homedir, "moxsubmit.failures") - os.Mkdir(maildir, 0700) - f, err := os.CreateTemp(maildir, "newmsg.") - xcheckf(err, "creating temp file for storing message after failed delivery") - defer func() { - if f != nil { - if err := os.Remove(f.Name()); err != nil { - log.Printf("removing temp file after failure storing failed delivery: %v", err) - } - } - }() - _, err = f.Write([]byte(msg)) - xcheckf(err, "writing message to temp file after failed delivery") - name := f.Name() - err = f.Close() - xcheckf(err, "closing message in temp file after failed delivery") - f = nil - log.Printf("saved message in %s", name) - os.Exit(1) - } - - var conn net.Conn - addr := net.JoinHostPort(submitconf.Host, fmt.Sprintf("%d", submitconf.Port)) - d := net.Dialer{Timeout: 30 * time.Second} - if submitconf.TLS { - conn, err = tls.DialWithDialer(&d, "tcp", addr, nil) - } else { - conn, err = d.Dial("tcp", addr) - } - xcheckf(err, "dial submit server") - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) - defer cancel() - tlsMode := smtpclient.TLSStrict - if !submitconf.STARTTLS { - tlsMode = smtpclient.TLSSkip - } - // todo: should have more auth options, scram-sha-256 at least, perhaps cram-md5 for compatibility as well. - authLine := fmt.Sprintf("AUTH PLAIN %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("\u0000%s\u0000%s", submitconf.Username, submitconf.Password)))) - mox.Conf.Static.HostnameDomain.ASCII = submitconf.LocalHostname - client, err := smtpclient.New(ctx, mlog.New("sendmail"), conn, tlsMode, submitconf.Host, authLine) - xcheckf(err, "open smtp session") - - err = client.Deliver(ctx, submitconf.From, recipient, int64(len(msg)), strings.NewReader(msg), true, false) - xcheckf(err, "submit message") - - if err := client.Close(); err != nil { - log.Printf("closing smtp session after message was sent: %v", err) - } -} diff --git a/sendmail.go b/sendmail.go new file mode 100644 index 0000000..be3d6ab --- /dev/null +++ b/sendmail.go @@ -0,0 +1,242 @@ +package main + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "io" + "log" + "net" + "net/mail" + "os" + "path/filepath" + "strings" + "time" + + "github.com/mjl-/sconf" + + "github.com/mjl-/mox/mlog" + "github.com/mjl-/mox/mox-" + "github.com/mjl-/mox/smtp" + "github.com/mjl-/mox/smtpclient" +) + +func cmdSendmail(c *cmd) { + c.params = "[-Fname] [ignoredflags] [-t] [\r\n", submitconf.From) + var haveTo bool + for { + line, err := r.ReadString('\n') + if err != nil && err != io.EOF { + xcheckf(err, "reading message") + } + if line != "" { + if !strings.HasSuffix(line, "\n") { + line += "\n" + } + if !strings.HasSuffix(line, "\r\n") { + line = line[:len(line)-1] + "\r\n" + } + if header && line == "\r\n" { + // Bare \r\n marks end of header. + if !haveTo { + line = fmt.Sprintf("To: <%s>\r\n", recipient) + line + } + header = false + } else if header { + t := strings.SplitN(line, ":", 2) + if len(t) != 2 { + log.Fatalf("invalid message, missing colon in header") + } + k := strings.ToLower(t[0]) + if k == "from" { + // We already added a From header. + if err == io.EOF { + break + } + continue + } else if tflag && k == "to" { + if recipient != "" { + log.Fatalf("only single To header allowed") + } + s := strings.TrimSpace(t[1]) + if !strings.Contains(s, "@") { + if submitconf.DefaultDestination == "" { + log.Fatalf("recipient %q has no @ and no default destination is configured", s) + } + recipient = submitconf.DefaultDestination + } else { + addrs, err := mail.ParseAddressList(s) + xcheckf(err, "parsing To address list") + if len(addrs) != 1 { + log.Fatalf("only single address allowed in To header") + } + recipient = addrs[0].Address + } + } + if k == "to" { + haveTo = true + } + } + sb.WriteString(line) + } + if err == io.EOF { + break + } + } + msg := sb.String() + + if recipient == "" { + log.Fatalf("no recipient") + } + + // Message seems acceptable. We'll try to deliver it from here. If that fails, we + // store the message in the users home directory. + + xcheckf := func(err error, format string, args ...any) { + if err == nil { + return + } + log.Printf("submit failed: %s: %s", fmt.Sprintf(format, args...), err) + homedir, err := os.UserHomeDir() + xcheckf(err, "finding homedir for storing message after failed delivery") + maildir := filepath.Join(homedir, "moxsubmit.failures") + os.Mkdir(maildir, 0700) + f, err := os.CreateTemp(maildir, "newmsg.") + xcheckf(err, "creating temp file for storing message after failed delivery") + defer func() { + if f != nil { + if err := os.Remove(f.Name()); err != nil { + log.Printf("removing temp file after failure storing failed delivery: %v", err) + } + } + }() + _, err = f.Write([]byte(msg)) + xcheckf(err, "writing message to temp file after failed delivery") + name := f.Name() + err = f.Close() + xcheckf(err, "closing message in temp file after failed delivery") + f = nil + log.Printf("saved message in %s", name) + os.Exit(1) + } + + var conn net.Conn + addr := net.JoinHostPort(submitconf.Host, fmt.Sprintf("%d", submitconf.Port)) + d := net.Dialer{Timeout: 30 * time.Second} + if submitconf.TLS { + conn, err = tls.DialWithDialer(&d, "tcp", addr, nil) + } else { + conn, err = d.Dial("tcp", addr) + } + xcheckf(err, "dial submit server") + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + tlsMode := smtpclient.TLSStrict + if !submitconf.STARTTLS { + tlsMode = smtpclient.TLSSkip + } + // todo: should have more auth options, scram-sha-256 at least, perhaps cram-md5 for compatibility as well. + authLine := fmt.Sprintf("AUTH PLAIN %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("\u0000%s\u0000%s", submitconf.Username, submitconf.Password)))) + mox.Conf.Static.HostnameDomain.ASCII = submitconf.LocalHostname + client, err := smtpclient.New(ctx, mlog.New("sendmail"), conn, tlsMode, submitconf.Host, authLine) + xcheckf(err, "open smtp session") + + err = client.Deliver(ctx, submitconf.From, recipient, int64(len(msg)), strings.NewReader(msg), true, false) + xcheckf(err, "submit message") + + if err := client.Close(); err != nil { + log.Printf("closing smtp session after message was sent: %v", err) + } +}