From 7e74bdc0a8a4f49ecd6da2fadb788c30b7fb887a Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 10 Nov 2021 14:29:54 +0100 Subject: [PATCH] Add sendmail subcommand --- cmd/hydroxide/main.go | 43 +++++++++++++++++++++++- smtp/smtp.go | 76 +++++++++++++++++++++++-------------------- 2 files changed, 82 insertions(+), 37 deletions(-) diff --git a/cmd/hydroxide/main.go b/cmd/hydroxide/main.go index bbd89c5..70a3c6e 100644 --- a/cmd/hydroxide/main.go +++ b/cmd/hydroxide/main.go @@ -45,7 +45,8 @@ func newClient() *protonmail.Client { func askPass(prompt string) ([]byte, error) { f := os.Stdin if !isatty.IsTerminal(f.Fd()) { - // TODO: this assumes Unix + // This can happen if stdin is used for piping data + // TODO: the following assumes Unix var err error if f, err = os.Open("/dev/tty"); err != nil { return nil, err @@ -168,6 +169,7 @@ Commands: imap Run hydroxide as an IMAP server import-messages Import messages export-messages [options...] Export messages + sendmail -- sendmail(1) interface serve Run all servers smtp Run hydroxide as an SMTP server status View hydroxide status @@ -223,6 +225,7 @@ func main() { exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError) importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError) exportMessagesCmd := flag.NewFlagSet("export-messages", flag.ExitOnError) + sendmailCmd := flag.NewFlagSet("sendmail", flag.ExitOnError) flag.Usage = func() { fmt.Println(usage) @@ -506,6 +509,44 @@ func main() { }() } log.Fatal(<-done) + case "sendmail": + username := flag.Arg(1) + if username == "" || flag.Arg(2) != "--" { + log.Fatal("usage: hydroxide sendmail -- ") + } + + // TODO: other sendmail flags + var dotEOF bool + sendmailCmd.BoolVar(&dotEOF, "i", false, "don't treat a line with only a . character as the end of input") + sendmailCmd.Parse(flag.Args()[3:]) + rcpt := sendmailCmd.Args() + + var bridgePassword string + if pass, err := askPass("Bridge password"); err != nil { + log.Fatal(err) + } else { + bridgePassword = string(pass) + } + + c, privateKeys, err := auth.NewManager(newClient).Auth(username, bridgePassword) + if err != nil { + log.Fatal(err) + } + + u, err := c.GetCurrentUser() + if err != nil { + log.Fatal(err) + } + + addrs, err := c.ListAddresses() + if err != nil { + log.Fatal(err) + } + + err = smtpbackend.SendMail(c, u, privateKeys, addrs, rcpt, os.Stdin) + if err != nil { + log.Fatal(err) + } default: fmt.Println(usage) if cmd != "help" { diff --git a/smtp/smtp.go b/smtp/smtp.go index 5240d26..69caddd 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -37,37 +37,14 @@ func formatHeader(h mail.Header) string { return b.String() } -type session struct { - c *protonmail.Client - u *protonmail.User - privateKeys openpgp.EntityList - addrs []*protonmail.Address - allReceivers []string -} - -func (s *session) Mail(from string, options smtp.MailOptions) error { - return nil -} - -func (s *session) Rcpt(to string) error { - if to == "" { - return nil - } - - // Seems like github.com/emersion/go-smtp/conn.go:487 removes marks on message - // "to" is added into allReceivers blindly - s.allReceivers = append(s.allReceivers, to) - return nil -} - -func (s *session) bccFromRest(ignoreMails []*mail.Address) []*mail.Address { +func bccFromRest(rcpt []string, ignoreMails []*mail.Address) []*mail.Address { ignore := make(map[string]struct{}) for _, mail := range ignoreMails { ignore[mail.Address] = struct{}{} } - final := make([]*mail.Address, 0, len(s.allReceivers)) - for _, addr := range s.allReceivers { + final := make([]*mail.Address, 0, len(rcpt)) + for _, addr := range rcpt { if _, exists := ignore[addr]; exists { continue } @@ -78,7 +55,7 @@ func (s *session) bccFromRest(ignoreMails []*mail.Address) []*mail.Address { return final } -func (s *session) Data(r io.Reader) error { +func SendMail(c *protonmail.Client, u *protonmail.User, privateKeys openpgp.EntityList, addrs []*protonmail.Address, rcpt []string, r io.Reader) error { // Parse the incoming MIME message header mr, err := mail.CreateReader(r) if err != nil { @@ -92,7 +69,7 @@ func (s *session) Data(r io.Reader) error { bccList, _ := mr.Header.AddressList("Bcc") if len(bccList) == 0 { - bccList = s.bccFromRest(append(toList, ccList...)) + bccList = bccFromRest(rcpt, append(toList, ccList...)) } if len(fromList) != 1 { @@ -105,7 +82,7 @@ func (s *session) Data(r io.Reader) error { rawFrom := fromList[0] fromAddrStr := rawFrom.Address var fromAddr *protonmail.Address - for _, addr := range s.addrs { + for _, addr := range addrs { if strings.EqualFold(addr.Email, fromAddrStr) { fromAddr = addr break @@ -125,7 +102,7 @@ func (s *session) Data(r io.Reader) error { } var privateKey *openpgp.Entity - for _, e := range s.privateKeys { + for _, e := range privateKeys { if e.PrimaryKey.KeyId == encryptedPrivateKey.PrimaryKey.KeyId { privateKey = e break @@ -175,7 +152,7 @@ func (s *session) Data(r io.Reader) error { ExternalID: inReplyTo, AddressID: fromAddr.ID, } - total, msgs, err := s.c.ListMessages(&filter) + total, msgs, err := c.ListMessages(&filter) if err != nil { return err } @@ -184,7 +161,7 @@ func (s *session) Data(r io.Reader) error { } } - msg, err = s.c.CreateDraftMessage(msg, parentID) + msg, err = c.CreateDraftMessage(msg, parentID) if err != nil { return fmt.Errorf("cannot create draft message: %v", err) } @@ -262,7 +239,7 @@ func (s *session) Data(r io.Reader) error { pw.CloseWithError(cleartext.Close()) }() - att, err = s.c.CreateAttachment(att, pr) + att, err = c.CreateAttachment(att, pr) if err != nil { return fmt.Errorf("cannot upload attachment: %v", err) } @@ -290,7 +267,7 @@ func (s *session) Data(r io.Reader) error { return err } - msg, err = s.c.UpdateDraftMessage(msg) + msg, err = c.UpdateDraftMessage(msg) if err != nil { return fmt.Errorf("cannot update draft message: %v", err) } @@ -305,7 +282,7 @@ func (s *session) Data(r io.Reader) error { var plaintextRecipients []string encryptedRecipients := make(map[string]*openpgp.Entity) for _, rcpt := range recipients { - resp, err := s.c.GetPublicKeys(rcpt.Address) + resp, err := c.GetPublicKeys(rcpt.Address) if err != nil { return fmt.Errorf("cannot get public key for address %q: %v", rcpt.Address, err) } @@ -381,7 +358,7 @@ func (s *session) Data(r io.Reader) error { outgoing.Packages = append(outgoing.Packages, encryptedSet) } - _, _, err = s.c.SendMessage(outgoing) + _, _, err = c.SendMessage(outgoing) if err != nil { return fmt.Errorf("cannot send message: %v", err) } @@ -389,6 +366,33 @@ func (s *session) Data(r io.Reader) error { return nil } +type session struct { + c *protonmail.Client + u *protonmail.User + privateKeys openpgp.EntityList + addrs []*protonmail.Address + allReceivers []string +} + +func (s *session) Mail(from string, options smtp.MailOptions) error { + return nil +} + +func (s *session) Rcpt(to string) error { + if to == "" { + return nil + } + + // Seems like github.com/emersion/go-smtp/conn.go:487 removes marks on message + // "to" is added into allReceivers blindly + s.allReceivers = append(s.allReceivers, to) + return nil +} + +func (s *session) Data(r io.Reader) error { + return SendMail(s.c, s.u, s.privateKeys, s.addrs, s.allReceivers, r) +} + func (s *session) Reset() { s.allReceivers = nil }