Add sendmail subcommand

This commit is contained in:
Simon Ser 2021-11-10 14:29:54 +01:00
parent 7ae6a0ea07
commit 7e74bdc0a8
2 changed files with 82 additions and 37 deletions

View File

@ -45,7 +45,8 @@ func newClient() *protonmail.Client {
func askPass(prompt string) ([]byte, error) { func askPass(prompt string) ([]byte, error) {
f := os.Stdin f := os.Stdin
if !isatty.IsTerminal(f.Fd()) { 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 var err error
if f, err = os.Open("/dev/tty"); err != nil { if f, err = os.Open("/dev/tty"); err != nil {
return nil, err return nil, err
@ -168,6 +169,7 @@ Commands:
imap Run hydroxide as an IMAP server imap Run hydroxide as an IMAP server
import-messages <username> <file> Import messages import-messages <username> <file> Import messages
export-messages [options...] <username> Export messages export-messages [options...] <username> Export messages
sendmail <username> -- <args...> sendmail(1) interface
serve Run all servers serve Run all servers
smtp Run hydroxide as an SMTP server smtp Run hydroxide as an SMTP server
status View hydroxide status status View hydroxide status
@ -223,6 +225,7 @@ func main() {
exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError) exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError)
importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError) importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError)
exportMessagesCmd := flag.NewFlagSet("export-messages", flag.ExitOnError) exportMessagesCmd := flag.NewFlagSet("export-messages", flag.ExitOnError)
sendmailCmd := flag.NewFlagSet("sendmail", flag.ExitOnError)
flag.Usage = func() { flag.Usage = func() {
fmt.Println(usage) fmt.Println(usage)
@ -506,6 +509,44 @@ func main() {
}() }()
} }
log.Fatal(<-done) log.Fatal(<-done)
case "sendmail":
username := flag.Arg(1)
if username == "" || flag.Arg(2) != "--" {
log.Fatal("usage: hydroxide sendmail <username> -- <args...>")
}
// 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: default:
fmt.Println(usage) fmt.Println(usage)
if cmd != "help" { if cmd != "help" {

View File

@ -37,37 +37,14 @@ func formatHeader(h mail.Header) string {
return b.String() return b.String()
} }
type session struct { func bccFromRest(rcpt []string, ignoreMails []*mail.Address) []*mail.Address {
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 {
ignore := make(map[string]struct{}) ignore := make(map[string]struct{})
for _, mail := range ignoreMails { for _, mail := range ignoreMails {
ignore[mail.Address] = struct{}{} ignore[mail.Address] = struct{}{}
} }
final := make([]*mail.Address, 0, len(s.allReceivers)) final := make([]*mail.Address, 0, len(rcpt))
for _, addr := range s.allReceivers { for _, addr := range rcpt {
if _, exists := ignore[addr]; exists { if _, exists := ignore[addr]; exists {
continue continue
} }
@ -78,7 +55,7 @@ func (s *session) bccFromRest(ignoreMails []*mail.Address) []*mail.Address {
return final 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 // Parse the incoming MIME message header
mr, err := mail.CreateReader(r) mr, err := mail.CreateReader(r)
if err != nil { if err != nil {
@ -92,7 +69,7 @@ func (s *session) Data(r io.Reader) error {
bccList, _ := mr.Header.AddressList("Bcc") bccList, _ := mr.Header.AddressList("Bcc")
if len(bccList) == 0 { if len(bccList) == 0 {
bccList = s.bccFromRest(append(toList, ccList...)) bccList = bccFromRest(rcpt, append(toList, ccList...))
} }
if len(fromList) != 1 { if len(fromList) != 1 {
@ -105,7 +82,7 @@ func (s *session) Data(r io.Reader) error {
rawFrom := fromList[0] rawFrom := fromList[0]
fromAddrStr := rawFrom.Address fromAddrStr := rawFrom.Address
var fromAddr *protonmail.Address var fromAddr *protonmail.Address
for _, addr := range s.addrs { for _, addr := range addrs {
if strings.EqualFold(addr.Email, fromAddrStr) { if strings.EqualFold(addr.Email, fromAddrStr) {
fromAddr = addr fromAddr = addr
break break
@ -125,7 +102,7 @@ func (s *session) Data(r io.Reader) error {
} }
var privateKey *openpgp.Entity var privateKey *openpgp.Entity
for _, e := range s.privateKeys { for _, e := range privateKeys {
if e.PrimaryKey.KeyId == encryptedPrivateKey.PrimaryKey.KeyId { if e.PrimaryKey.KeyId == encryptedPrivateKey.PrimaryKey.KeyId {
privateKey = e privateKey = e
break break
@ -175,7 +152,7 @@ func (s *session) Data(r io.Reader) error {
ExternalID: inReplyTo, ExternalID: inReplyTo,
AddressID: fromAddr.ID, AddressID: fromAddr.ID,
} }
total, msgs, err := s.c.ListMessages(&filter) total, msgs, err := c.ListMessages(&filter)
if err != nil { if err != nil {
return err 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 { if err != nil {
return fmt.Errorf("cannot create draft message: %v", err) 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()) pw.CloseWithError(cleartext.Close())
}() }()
att, err = s.c.CreateAttachment(att, pr) att, err = c.CreateAttachment(att, pr)
if err != nil { if err != nil {
return fmt.Errorf("cannot upload attachment: %v", err) return fmt.Errorf("cannot upload attachment: %v", err)
} }
@ -290,7 +267,7 @@ func (s *session) Data(r io.Reader) error {
return err return err
} }
msg, err = s.c.UpdateDraftMessage(msg) msg, err = c.UpdateDraftMessage(msg)
if err != nil { if err != nil {
return fmt.Errorf("cannot update draft message: %v", err) return fmt.Errorf("cannot update draft message: %v", err)
} }
@ -305,7 +282,7 @@ func (s *session) Data(r io.Reader) error {
var plaintextRecipients []string var plaintextRecipients []string
encryptedRecipients := make(map[string]*openpgp.Entity) encryptedRecipients := make(map[string]*openpgp.Entity)
for _, rcpt := range recipients { for _, rcpt := range recipients {
resp, err := s.c.GetPublicKeys(rcpt.Address) resp, err := c.GetPublicKeys(rcpt.Address)
if err != nil { if err != nil {
return fmt.Errorf("cannot get public key for address %q: %v", rcpt.Address, err) 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) outgoing.Packages = append(outgoing.Packages, encryptedSet)
} }
_, _, err = s.c.SendMessage(outgoing) _, _, err = c.SendMessage(outgoing)
if err != nil { if err != nil {
return fmt.Errorf("cannot send message: %v", err) return fmt.Errorf("cannot send message: %v", err)
} }
@ -389,6 +366,33 @@ func (s *session) Data(r io.Reader) error {
return nil 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() { func (s *session) Reset() {
s.allReceivers = nil s.allReceivers = nil
} }