about 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() {
pan class="w"> (list 'no-method name)) (define (no-method? x) (if (pair? x) (eq? (car x) 'no-method) #f)) (define (method? x) (not (no-method? x))) ;; INSTANTIATE and INSTANTIATE-PARENT: Create an instance of a class ; The difference is that only INSTANTIATE initializes the new object (define (instantiate class . arguments) (let ((new-instance (apply (class 'instantiate) arguments))) (ask new-instance 'initialize new-instance) new-instance)) (define (instantiate-parent class . arguments) (apply (class 'instantiate) arguments)) ;; GET-METHOD: Send a message to several objects and return the first ;; method found (for multiple inheritance) (define (get-method give-up-name message . objects) (if (null? objects) (no-method give-up-name) (let ((method ((car objects) message))) (if (method? method) method (apply get-method (cons give-up-name (cons message (cdr objects)) )))))) ;; USUAL: Invoke a parent's method ;; Note: The 'send-usual-to-parent method is put in automatically by ;; define-class. (define-macro (usual . args) `(ask dispatch 'send-usual-to-parent . ,args)) ;; DEFINE-CLASS: Create a new class. ; DEFINE-CLASS is a special form. When you type (define-class body...) ; it's as if you typed (make-definitions (quote body...)). In other ; words, the argument to DEFINE-CLASS isn't evaluated. This makes sense ; because the argument isn't Scheme syntax, but rather is the special ; object-oriented programming language we're defining. ; Make-definitions transforms the OOP notation into a standard Scheme ; expression, then uses EVAL to evaluate the result. (You'll see EVAL ; again in chapter 4 with the metacircular evaluator.) ; When you define a class named THING, for example, two global Scheme ; variables are created. The variable THING has as its value the ; procedure that represents the class. This procedure is invoked by ; INSTANTIATE to create instances of the class. A second variable, ; THING-DEFINITION, has as its value the text of the Scheme expression ; that defines THING. This text is used only by SHOW-CLASS, the ; procedure that lets you examine the result of the OOP-to-Scheme ; translation process. (define-macro (define-class . body) (make-definitions body)) (define (make-definitions form) (let ((definition (translate form))) (eval `(define ,(maknam (class-name form) '-definition) ',definition)) (eval definition) (list 'quote (class-name form)))) (define (show-class name) (eval (maknam name '-definition)) ) ; TRANSLATE does all the work of DEFINE-CLASS. ; The backquote operator (`) works just like regular quote (') except ; that expressions proceeded by a comma are evaluated. Also, expressions ; proceeded by ",@" evaluate to lists; the lists are inserted into the ; text without the outermost level of parentheses. (define (translate form) (cond ((null? form) (error "Define-class: empty body")) ((not (null? (obj-filter form (lambda (x) (not (pair? x)))))) (error "Each argument to define-class must be a list")) ((not (null? (extra-clauses form))) (error "Unrecognized clause in define-class:" (extra-clauses form))) (else `(define ,(class-name form) (let ,(class-var-bindings form) (lambda (class-message) (cond ,@(class-variable-methods form) ((eq? class-message 'instantiate) (lambda ,(instantiation-vars form) (let ((self '()) ,@(parent-let-list form) ,@(instance-vars-let-list form)) (define (dispatch message) (cond ,(init-clause form) ,(usual-clause form) ,@(method-clauses form) ,@(local-variable-methods form) ,(else-clause form) )) dispatch ))) (else (error "Bad message to class" class-message)) ))))))) (define *legal-clauses* '(instance-vars class-vars method default-method parent initialize)) (define (extra-clauses form) (obj-filter (cdr form) (lambda (x) (null? (member (car x) *legal-clauses*))))) (define class-name caar) (define (class-var-bindings form) (let ((classvar-clause (find-a-clause 'class-vars form))) (if (null? classvar-clause) '() (cdr classvar-clause) ))) (define instantiation-vars cdar) (define (parent-let-list form) (let ((parent-clause (find-a-clause 'parent form))) (if (null? parent-clause) '() (map (lambda (parent-and-args) (list (maknam 'my- (car parent-and-args)) (cons 'instantiate-parent parent-and-args))) (cdr parent-clause))))) (define (instance-vars-let-list form) (let ((instance-vars-clause (find-a-clause 'instance-vars form))) (if (null? instance-vars-clause) '() (cdr instance-vars-clause)))) (define (init-clause form) (define (parent-initialization form) (let ((parent-clause (find-a-clause 'parent form))) (if (null? parent-clause) '() (map (lambda (parent-and-args) `(ask ,(maknam 'my- (car parent-and-args)) 'initialize self) ) (cdr parent-clause) )))) (define (my-initialization form) (let ((init-clause (find-a-clause 'initialize form))) (if (null? init-clause) '() (cdr init-clause)))) (define (init-body form) (append (parent-initialization form) (my-initialization form) )) `((eq? message 'initialize) (lambda (value-for-self) (set! self value-for-self) ,@(init-body form) ))) (define (variable-list var-type form) (let ((clause (find-a-clause var-type form))) (if (null? clause) '() (map car (cdr clause)) ))) (define (class-variable-methods form) (cons `((eq? class-message 'class-name) (lambda () ',(class-name form))) (map (lambda (variable) `((eq? class-message ',variable) (lambda () ,variable))) (variable-list 'class-vars form)))) (define (local-variable-methods form) (cons `((eq? message 'class-name) (lambda () ',(class-name form))) (map (lambda (variable) `((eq? message ',variable) (lambda () ,variable))) (append (cdr (car form)) (variable-list 'instance-vars form) (variable-list 'class-vars form))))) (define (method-clauses form) (map (lambda (method-defn) (let ((this-message (car (cadr method-defn))) (args (cdr (cadr method-defn))) (body (cddr method-defn))) `((eq? message ',this-message) (lambda ,args ,@body)))) (obj-filter (cdr form) (lambda (x) (eq? (car x) 'method))) )) (define (parent-list form) (let ((parent-clause (find-a-clause 'parent form))) (if (null? parent-clause) '() (map (lambda (class) (maknam 'my- class)) (map car (cdr parent-clause)))))) (define (usual-clause form) (let ((parent-clause (find-a-clause 'parent form))) (if (null? parent-clause) `((eq? message 'send-usual-to-parent) (error "Can't use USUAL without a parent." ',(class-name form))) `((eq? message 'send-usual-to-parent) (lambda (message . args) (let ((method (get-method ',(class-name form) message ,@(parent-list form)))) (if (method? method) (apply method args) (error "No USUAL method" message ',(class-name form)) ))))))) (define (else-clause form) (let ((parent-clause (find-a-clause 'parent form)) (default-method (find-a-clause 'default-method form))) (cond ((and (null? parent-clause) (null? default-method)) `(else (no-method ',(class-name form)))) ((null? parent-clause) `(else (lambda args ,@(cdr default-method)))) ((null? default-method) `(else (get-method ',(class-name form) message ,@(parent-list form))) ) (else `(else (let ((method (get-method ',(class-name form) message ,@(parent-list form)))) (if (method? method) method (lambda args ,@(cdr default-method)) ))))))) (define (find-a-clause clause-name form) (let ((clauses (obj-filter (cdr form) (lambda (x) (eq? (car x) clause-name))))) (cond ((null? clauses) '()) ((null? (cdr clauses)) (car clauses)) (else (error "Error in define-class: too many " clause-name "clauses.")) ))) (define (obj-filter l pred) (cond ((null? l) '()) ((pred (car l)) (cons (car l) (obj-filter (cdr l) pred))) (else (obj-filter (cdr l) pred)))) (provide "obj")