summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorDrew DeVault <sir@cmpwn.com>2019-05-14 14:05:29 -0400
committerDrew DeVault <sir@cmpwn.com>2019-05-14 14:07:27 -0400
commit29de3297a157c0ac109121152fe2b5737fc81d95 (patch)
tree875fed0384d689392a7454c7a71fa2017d28d15c
parent6c36e04c1f7f7e222c71c5c8e7e7337744fe9c34 (diff)
downloadaerc-29de3297a157c0ac109121152fe2b5737fc81d95.tar.gz
Implement sending emails /o/
-rw-r--r--commands/compose/send-message.go120
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--widgets/compose.go60
4 files changed, 160 insertions, 24 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")
+	config := composer.Config()
+
+	if config.Outgoing == "" {
+		return errors.New(
+			"No outgoing mail transport configured for this account")
+	}
+
+	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
+		}
 	}
-	_, err = composer.Message(f)
+	wc, err := conn.Data()
 	if err != nil {
-		panic(err)
+		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
+	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
+	}
 	// TODO: attachments
-	writer, err := mail.CreateSingleInlineWriter(writeto, header)
+	w, err := mail.CreateSingleInlineWriter(writer, *header)
 	if err != nil {
-		return nil, err
+		return err
 	}
-	defer writer.Close()
-	io.Copy(writer, body)
-	return rcpts, nil
+	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() {