From 29de3297a157c0ac109121152fe2b5737fc81d95 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Tue, 14 May 2019 14:05:29 -0400 Subject: [PATCH] Implement sending emails /o/ --- commands/compose/send-message.go | 122 +++++++++++++++++++++++++++++-- go.mod | 2 + go.sum | 2 + widgets/compose.go | 64 +++++++++++----- 4 files changed, 163 insertions(+), 27 deletions(-) diff --git a/commands/compose/send-message.go b/commands/compose/send-message.go index b9fc9d2..b101e12 100644 --- a/commands/compose/send-message.go +++ b/commands/compose/send-message.go @@ -1,8 +1,15 @@ package compose import ( + "crypto/tls" "errors" - "os" + "fmt" + "net/mail" + "net/url" + "strings" + + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" "git.sr.ht/~sircmpwn/aerc2/widgets" ) @@ -16,14 +23,115 @@ func SendMessage(aerc *widgets.Aerc, args []string) error { return errors.New("Usage: send-message") } composer, _ := aerc.SelectedTab().(*widgets.Composer) - //config := composer.Config() - f, err := os.Create("/tmp/test.eml") - if err != nil { - panic(err) + config := composer.Config() + + if config.Outgoing == "" { + return errors.New( + "No outgoing mail transport configured for this account") } - _, err = composer.Message(f) + + uri, err := url.Parse(config.Outgoing) if err != nil { - panic(err) + return err } + var ( + scheme string + auth string = "plain" + ) + parts := strings.Split(uri.Scheme, "+") + if len(parts) == 1 { + scheme = parts[0] + } else if len(parts) == 2 { + scheme = parts[0] + auth = parts[1] + } else { + return fmt.Errorf("Unknown transfer protocol %s", uri.Scheme) + } + + header, rcpts, err := composer.Header() + if err != nil { + return err + } + + if config.From == "" { + return errors.New("No 'From' configured for this account") + } + from, err := mail.ParseAddress(config.From) + if err != nil { + return err + } + + var ( + saslClient sasl.Client + conn *smtp.Client + ) + switch auth { + case "": + fallthrough + case "none": + saslClient = nil + case "plain": + password, _ := uri.User.Password() + saslClient = sasl.NewPlainClient("", uri.User.Username(), password) + default: + return fmt.Errorf("Unsupported auth mechanism %s", auth) + } + + tlsConfig := &tls.Config{ + // TODO: ask user first + InsecureSkipVerify: true, + } + switch scheme { + case "smtp": + host := uri.Host + if !strings.ContainsRune(host, ':') { + host = host + ":587" // Default to submission port + } + conn, err = smtp.Dial(host) + if err != nil { + return err + } + defer conn.Close() + if sup, _ := conn.Extension("STARTTLS"); sup { + // TODO: let user configure tls? + if err = conn.StartTLS(tlsConfig); err != nil { + return err + } + } + case "smtps": + host := uri.Host + if !strings.ContainsRune(host, ':') { + host = host + ":465" // Default to smtps port + } + conn, err = smtp.DialTLS(host, tlsConfig) + if err != nil { + return err + } + defer conn.Close() + } + + // TODO: sendmail + if saslClient != nil { + if err = conn.Auth(saslClient); err != nil { + return err + } + } + // TODO: the user could conceivably want to use a different From and sender + if err = conn.Mail(from.Address); err != nil { + return err + } + for _, rcpt := range rcpts { + if err = conn.Rcpt(rcpt); err != nil { + return err + } + } + wc, err := conn.Data() + if err != nil { + return err + } + defer wc.Close() + composer.WriteMessage(header, wc) + composer.Close() + aerc.RemoveTab(composer) return nil } diff --git a/go.mod b/go.mod index 3d61fa4..a543daf 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/emersion/go-imap v1.0.0-beta.4 github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b github.com/emersion/go-message v0.10.0 + github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 + github.com/emersion/go-smtp v0.11.0 github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 // indirect github.com/gdamore/tcell v1.0.0 github.com/go-ini/ini v1.42.0 diff --git a/go.sum b/go.sum index 0134760..c1e0ba0 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/emersion/go-message v0.10.0 h1:V8hwhZPNIuAIGNLcMZiCzzavUIiODG3COYLsQM github.com/emersion/go-message v0.10.0/go.mod h1:7d2eJfhjiJSnlaKcUPq7sEC7ekWELG6F5Lw2BxOGj6Y= github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 h1:rDJPbyliyym8ZL/Wt71kdolp6yaD4fLIQz638E6JEt0= github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= +github.com/emersion/go-smtp v0.11.0 h1:lM9M2JSxSKEb1dfvB4stkIaIkNJxd5na5Mok8FJDle8= +github.com/emersion/go-smtp v0.11.0/go.mod h1:CfUbM5NgspbOMHFEgCdoK2PVrKt48HAPtL8hnahwfYg= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI= diff --git a/widgets/compose.go b/widgets/compose.go index 318bfc4..38c33fc 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -107,6 +107,19 @@ func (c *Composer) OnInvalidate(fn func(d ui.Drawable)) { }) } +func (c *Composer) Close() { + if c.email != nil { + path := c.email.Name() + c.email.Close() + os.Remove(path) + c.email = nil + } + if c.editor != nil { + c.editor.Destroy() + c.editor = nil + } +} + func (c *Composer) Event(event tcell.Event) bool { return c.focusable[c.focused].Event(event) } @@ -119,29 +132,19 @@ func (c *Composer) Config() *config.AccountConfig { return c.config } -// Writes the email to the given writer, and returns a list of recipients -func (c *Composer) Message(writeto io.Writer) ([]string, error) { +func (c *Composer) Header() (*mail.Header, []string, error) { // Extract headers from the email, if present c.email.Seek(0, os.SEEK_SET) var ( rcpts []string header mail.Header - body io.Reader ) reader, err := mail.CreateReader(c.email) if err == nil { header = reader.Header - // TODO: Do we want to let users write a full blown multipart email - // into the editor? If so this needs to change - part, err := reader.NextPart() - if err != nil { - return nil, err - } - body = part.Body defer reader.Close() } else { c.email.Seek(0, os.SEEK_SET) - body = c.email } // Update headers // TODO: Custom header fields @@ -161,11 +164,11 @@ func (c *Composer) Message(writeto io.Writer) ([]string, error) { // your types aren't compatible enough with each other to_rcpts, err := gomail.ParseAddressList(to) if err != nil { - return nil, err + return nil, nil, err } ed_rcpts, err := header.AddressList("To") if err != nil { - return nil, err + return nil, nil, err } for _, addr := range to_rcpts { ed_rcpts = append(ed_rcpts, (*mail.Address)(addr)) @@ -176,14 +179,34 @@ func (c *Composer) Message(writeto io.Writer) ([]string, error) { } } // TODO: Add cc, bcc to rcpts - // TODO: attachments - writer, err := mail.CreateSingleInlineWriter(writeto, header) - if err != nil { - return nil, err + return &header, rcpts, nil +} + +func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error { + c.email.Seek(0, os.SEEK_SET) + var body io.Reader + reader, err := mail.CreateReader(c.email) + if err == nil { + // TODO: Do we want to let users write a full blown multipart email + // into the editor? If so this needs to change + part, err := reader.NextPart() + if err != nil { + return err + } + body = part.Body + defer reader.Close() + } else { + c.email.Seek(0, os.SEEK_SET) + body = c.email } - defer writer.Close() - io.Copy(writer, body) - return rcpts, nil + // TODO: attachments + w, err := mail.CreateSingleInlineWriter(writer, *header) + if err != nil { + return err + } + defer w.Close() + _, err = io.Copy(w, body) + return err } func (c *Composer) termClosed(err error) { @@ -191,6 +214,7 @@ func (c *Composer) termClosed(err error) { c.grid.RemoveChild(c.editor) c.grid.AddChild(newReviewMessage(c)).At(1, 0) c.editor.Destroy() + c.editor = nil } func (c *Composer) PrevField() {