Skip to content

Commit

Permalink
Add sendmail subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
emersion committed Nov 10, 2021
1 parent 7ae6a0e commit 7e74bdc
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 37 deletions.
43 changes: 42 additions & 1 deletion cmd/hydroxide/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,6 +169,7 @@ Commands:
imap Run hydroxide as an IMAP server
import-messages <username> <file> Import messages
export-messages [options...] <username> Export messages
sendmail <username> -- <args...> sendmail(1) interface
serve Run all servers
smtp Run hydroxide as an SMTP server
status View hydroxide status
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 <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:
fmt.Println(usage)
if cmd != "help" {
Expand Down
76 changes: 40 additions & 36 deletions smtp/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -381,14 +358,41 @@ 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)
}

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
}
Expand Down

0 comments on commit 7e74bdc

Please sign in to comment.