about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--commands/compose/header.go7
-rw-r--r--commands/compose/postpone.go2
-rw-r--r--commands/compose/send.go32
-rw-r--r--commands/msg/forward.go26
-rw-r--r--commands/msg/recall.go10
-rw-r--r--commands/msg/reply.go20
-rw-r--r--commands/msg/unsubscribe.go12
-rw-r--r--config/config.go4
-rw-r--r--go.mod3
-rw-r--r--go.sum12
-rw-r--r--lib/format/format.go1
-rw-r--r--lib/templates/template.go82
-rw-r--r--widgets/aerc.go36
-rw-r--r--widgets/compose.go288
14 files changed, 318 insertions, 217 deletions
diff --git a/commands/compose/header.go b/commands/compose/header.go
index 5188a8a..dd0adee 100644
--- a/commands/compose/header.go
+++ b/commands/compose/header.go
@@ -57,18 +57,17 @@ func (Header) Execute(aerc *widgets.Aerc, args []string) error {
 	composer, _ := aerc.SelectedTab().(*widgets.Composer)
 
 	if !force {
-		headers, _, err := composer.PrepareHeader()
+		headers, err := composer.PrepareHeader()
 		if err != nil {
 			return err
 		}
 
-		if headers.Has(strings.Title(args[optind])) {
+		if headers.Has(args[optind]) {
 			return fmt.Errorf("Header %s already exists", args[optind])
 		}
 	}
 
-	composer.AddEditor(strings.Title(args[optind]),
-		strings.Join(args[optind+1:], " "), false)
+	composer.AddEditor(args[optind], strings.Join(args[optind+1:], " "), false)
 
 	return nil
 }
diff --git a/commands/compose/postpone.go b/commands/compose/postpone.go
index 60c9df1..365b683 100644
--- a/commands/compose/postpone.go
+++ b/commands/compose/postpone.go
@@ -40,7 +40,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
 
 	aerc.Logger().Println("Postponing mail")
 
-	header, _, err := composer.PrepareHeader()
+	header, err := composer.PrepareHeader()
 	if err != nil {
 		return errors.Wrap(err, "PrepareHeader")
 	}
diff --git a/commands/compose/send.go b/commands/compose/send.go
index abbcb54..70446da 100644
--- a/commands/compose/send.go
+++ b/commands/compose/send.go
@@ -4,7 +4,6 @@ import (
 	"crypto/tls"
 	"fmt"
 	"io"
-	"net/mail"
 	"net/url"
 	"os/exec"
 	"strings"
@@ -17,9 +16,11 @@ import (
 	"github.com/pkg/errors"
 
 	"git.sr.ht/~sircmpwn/aerc/lib"
+	"git.sr.ht/~sircmpwn/aerc/lib/format"
 	"git.sr.ht/~sircmpwn/aerc/models"
 	"git.sr.ht/~sircmpwn/aerc/widgets"
 	"git.sr.ht/~sircmpwn/aerc/worker/types"
+	"github.com/emersion/go-message/mail"
 	"golang.org/x/oauth2"
 )
 
@@ -71,15 +72,19 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
 		}
 	}
 
-	header, rcpts, err := composer.PrepareHeader()
+	header, err := composer.PrepareHeader()
 	if err != nil {
 		return errors.Wrap(err, "PrepareHeader")
 	}
+	rcpts, err := listRecipients(header)
+	if err != nil {
+		return errors.Wrap(err, "listRecipients")
+	}
 
 	if config.From == "" {
 		return errors.New("No 'From' configured for this account")
 	}
-	from, err := mail.ParseAddress(config.From)
+	from, err := format.ParseAddress(config.From)
 	if err != nil {
 		return errors.Wrap(err, "ParseAddress(config.From)")
 	}
@@ -288,7 +293,12 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
 					composer.Close()
 				}
 			})
-			header, _, _ := composer.PrepareHeader()
+			header, err := composer.PrepareHeader()
+			if err != nil {
+				aerc.PushError(" " + err.Error())
+				w.Close()
+				return
+			}
 			composer.WriteMessage(header, w)
 			w.Close()
 		} else {
@@ -299,3 +309,17 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
 	}()
 	return nil
 }
+
+func listRecipients(h *mail.Header) ([]string, error) {
+	var rcpts []string
+	for _, key := range []string{"to", "cc", "bcc"} {
+		list, err := h.AddressList(key)
+		if err != nil {
+			return nil, err
+		}
+		for _, addr := range list {
+			rcpts = append(rcpts, addr.Address)
+		}
+	}
+	return rcpts, nil
+}
diff --git a/commands/msg/forward.go b/commands/msg/forward.go
index b17482f..475d680 100644
--- a/commands/msg/forward.go
+++ b/commands/msg/forward.go
@@ -15,6 +15,7 @@ import (
 	"git.sr.ht/~sircmpwn/aerc/models"
 	"git.sr.ht/~sircmpwn/aerc/widgets"
 	"git.sr.ht/~sircmpwn/aerc/worker/types"
+	"github.com/emersion/go-message/mail"
 
 	"git.sr.ht/~sircmpwn/getopt"
 )
@@ -49,11 +50,6 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
 		}
 	}
 
-	to := ""
-	if len(args) != 1 {
-		to = strings.Join(args[optind:], ", ")
-	}
-
 	widget := aerc.SelectedTab().(widgets.ProvidesMessage)
 	acct := widget.SelectedAccount()
 	if acct == nil {
@@ -69,11 +65,19 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
 	}
 	acct.Logger().Println("Forwarding email " + msg.Envelope.MessageId)
 
+	h := &mail.Header{}
 	subject := "Fwd: " + msg.Envelope.Subject
-	defaults := map[string]string{
-		"To":      to,
-		"Subject": subject,
+	h.SetSubject(subject)
+
+	if len(args) != 1 {
+		to := strings.Join(args[optind:], ", ")
+		tolist, err := mail.ParseAddressList(to)
+		if err != nil {
+			return fmt.Errorf("invalid to address(es): %v", err)
+		}
+		h.SetAddressList("to", tolist)
 	}
+
 	original := models.OriginalMail{
 		From:          format.FormatAddresses(msg.Envelope.From),
 		Date:          msg.Envelope.Date,
@@ -81,15 +85,15 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
 	}
 
 	addTab := func() (*widgets.Composer, error) {
-		composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(),
-			acct.Worker(), template, defaults, original)
+		composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
+			acct.AccountConfig(), acct.Worker(), template, h, original)
 		if err != nil {
 			aerc.PushError("Error: " + err.Error())
 			return nil, err
 		}
 
 		tab := aerc.NewTab(composer, subject)
-		if to == "" {
+		if !h.Has("to") {
 			composer.FocusRecipient()
 		} else {
 			composer.FocusTerminal()
diff --git a/commands/msg/recall.go b/commands/msg/recall.go
index 5212041..b6c7f65 100644
--- a/commands/msg/recall.go
+++ b/commands/msg/recall.go
@@ -53,15 +53,9 @@ func (Recall) Execute(aerc *widgets.Aerc, args []string) error {
 	}
 	acct.Logger().Println("Recalling message " + msgInfo.Envelope.MessageId)
 
-	// copy the headers to the defaults map for addition to the composition
-	defaults := make(map[string]string)
-	headerFields := msgInfo.RFC822Headers.Fields()
-	for headerFields.Next() {
-		defaults[headerFields.Key()] = headerFields.Value()
-	}
-
 	composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
-		acct.AccountConfig(), acct.Worker(), "", defaults, models.OriginalMail{})
+		acct.AccountConfig(), acct.Worker(), "", msgInfo.RFC822Headers,
+		models.OriginalMail{})
 	if err != nil {
 		return errors.Wrap(err, "Cannot open a new composer")
 	}
diff --git a/commands/msg/reply.go b/commands/msg/reply.go
index 0298ac2..863c7d2 100644
--- a/commands/msg/reply.go
+++ b/commands/msg/reply.go
@@ -145,22 +145,22 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
 		subject = msg.Envelope.Subject
 	}
 
-	defaults := map[string]string{
-		"To":          format.FormatAddresses(to),
-		"Cc":          format.FormatAddresses(cc),
-		"From":        format.AddressForHumans(from),
-		"Subject":     subject,
-		"In-Reply-To": msg.Envelope.MessageId,
-	}
+	h := &mail.Header{}
+	h.SetAddressList("to", to)
+	h.SetAddressList("cc", cc)
+	h.SetAddressList("from", []*mail.Address{from})
+	h.SetSubject(subject)
+	h.SetMsgIDList("in-reply-to", []string{msg.Envelope.MessageId})
+	//TODO: references header
 	original := models.OriginalMail{
-		From: format.FormatAddresses(msg.Envelope.From),
-		Date: msg.Envelope.Date,
+		From:          format.FormatAddresses(msg.Envelope.From),
+		Date:          msg.Envelope.Date,
 		RFC822Headers: msg.RFC822Headers,
 	}
 
 	addTab := func() error {
 		composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
-			acct.AccountConfig(), acct.Worker(), template, defaults, original)
+			acct.AccountConfig(), acct.Worker(), template, h, original)
 		if err != nil {
 			aerc.PushError("Error: " + err.Error())
 			return err
diff --git a/commands/msg/unsubscribe.go b/commands/msg/unsubscribe.go
index dec90d5..205a255 100644
--- a/commands/msg/unsubscribe.go
+++ b/commands/msg/unsubscribe.go
@@ -9,6 +9,7 @@ import (
 	"git.sr.ht/~sircmpwn/aerc/lib"
 	"git.sr.ht/~sircmpwn/aerc/models"
 	"git.sr.ht/~sircmpwn/aerc/widgets"
+	"github.com/emersion/go-message/mail"
 )
 
 // Unsubscribe helps people unsubscribe from mailing lists by way of the
@@ -84,10 +85,13 @@ func parseUnsubscribeMethods(header string) (methods []*url.URL) {
 func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {
 	widget := aerc.SelectedTab().(widgets.ProvidesMessage)
 	acct := widget.SelectedAccount()
-	defaults := map[string]string{
-		"To":      u.Opaque,
-		"Subject": u.Query().Get("subject"),
+
+	h := &mail.Header{}
+	h.SetSubject(u.Query().Get("subject"))
+	if to, err := mail.ParseAddressList(u.Opaque); err == nil {
+		h.SetAddressList("to", to)
 	}
+
 	composer, err := widgets.NewComposer(
 		aerc,
 		acct,
@@ -95,7 +99,7 @@ func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {
 		acct.AccountConfig(),
 		acct.Worker(),
 		"",
-		defaults,
+		h,
 		models.OriginalMail{},
 	)
 	if err != nil {
diff --git a/config/config.go b/config/config.go
index 87d183a..51982d2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -413,8 +413,10 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
 			if key == "template-dirs" {
 				continue
 			}
+			// we want to fail during startup if the templates are not ok
+			// hence we do a dummy execute here
 			_, err := templates.ParseTemplateFromFile(
-				val, config.Templates.TemplateDirs, templates.TestTemplateData())
+				val, config.Templates.TemplateDirs, templates.DummyData())
 			if err != nil {
 				return err
 			}
diff --git a/go.mod b/go.mod
index 380b7a1..2a5be54 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e
 	github.com/emersion/go-imap-sortthread v1.1.1-0.20201009054724-d020d96306b3
 	github.com/emersion/go-maildir v0.2.0
-	github.com/emersion/go-message v0.12.1-0.20200824204225-9094bd0b8bc0
+	github.com/emersion/go-message v0.13.1-0.20201112194930-f77964fe28bd
 	github.com/emersion/go-pgpmail v0.0.0-20200303213726-db035a3a4139
 	github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
 	github.com/emersion/go-smtp v0.12.1
@@ -39,7 +39,6 @@ require (
 	golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
 	golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect
-	golang.org/x/text v0.3.3 // indirect
 	google.golang.org/appengine v1.6.5 // indirect
 	gopkg.in/ini.v1 v1.44.0 // indirect
 	gopkg.in/yaml.v2 v2.2.8 // indirect
diff --git a/go.sum b/go.sum
index f4f418f..cefdaac 100644
--- a/go.sum
+++ b/go.sum
@@ -26,8 +26,8 @@ github.com/emersion/go-maildir v0.2.0 h1:fC4+UVGl8GcQGbFF7AWab2JMf4VbKz+bMNv07xx
 github.com/emersion/go-maildir v0.2.0/go.mod h1:I2j27lND/SRLgxROe50Vam81MSaqPFvJ0OHNnDZ7n84=
 github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
 github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
-github.com/emersion/go-message v0.12.1-0.20200824204225-9094bd0b8bc0 h1:G2VV/Wp2opDvR0ecue3UY/IX1/8OlTmMKKi+ENe1nG0=
-github.com/emersion/go-message v0.12.1-0.20200824204225-9094bd0b8bc0/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
+github.com/emersion/go-message v0.13.1-0.20201112194930-f77964fe28bd h1:6CXxdoOzAyQForkd2U/JNceVyNpmg92alCU2R+4dwIY=
+github.com/emersion/go-message v0.13.1-0.20201112194930-f77964fe28bd/go.mod h1:SXSs/8KamlsyxjpHL1Q3yf5Jrv7QG5icuvPK1SMcnzw=
 github.com/emersion/go-pgpmail v0.0.0-20200303213726-db035a3a4139 h1:JTUbkRuQFtDrl5KHWR2jrh9SUeSDEEEjUcHJkXdAE2Q=
 github.com/emersion/go-pgpmail v0.0.0-20200303213726-db035a3a4139/go.mod h1:+Ovy1VQCUKPdjWkOiWvFoiFaWXkqn1PA793VvfEYWQU=
 github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
@@ -40,6 +40,8 @@ github.com/emersion/go-smtp v0.12.1 h1:1R8BDqrR2HhlGwgFYcOi+BVTvK1bMjAB65QcVpJ5s
 github.com/emersion/go-smtp v0.12.1/go.mod h1:SD9V/xa4ndMw77lR3Mf7htkp8RBNYuPh9UeuBs9tpUQ=
 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/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
@@ -67,6 +69,8 @@ github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
 github.com/martinlindhe/base36 v0.0.0-20190418230009-7c6542dfbb41/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
 github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
 github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
+github.com/martinlindhe/base36 v1.1.0 h1:cIwvvwYse/0+1CkUPYH5ZvVIYG3JrILmQEIbLuar02Y=
+github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
@@ -114,8 +118,8 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4-0.20201021145329-22f1617af38e h1:0kyKOEC0chG7FKmnf/1uNwvDLc3NtNTRip2rXAN9nwI=
+golang.org/x/text v0.3.4-0.20201021145329-22f1617af38e/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/lib/format/format.go b/lib/format/format.go
index e19ca31..2ba4d64 100644
--- a/lib/format/format.go
+++ b/lib/format/format.go
@@ -61,6 +61,7 @@ func AddressForHumans(a *mail.Address) string {
 
 var atom *regexp.Regexp = regexp.MustCompile("^[a-z0-9!#$%7'*+-/=?^_`{}|~ ]+$")
 
+// FormatAddresses formats a list of addresses into a human readable string
 func FormatAddresses(l []*mail.Address) string {
 	formatted := make([]string, len(l))
 	for i, a := range l {
diff --git a/lib/templates/template.go b/lib/templates/template.go
index f979ba2..197f159 100644
--- a/lib/templates/template.go
+++ b/lib/templates/template.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"net/mail"
 	"os"
 	"os/exec"
 	"path"
@@ -12,6 +11,8 @@ import (
 	"text/template"
 	"time"
 
+	"github.com/emersion/go-message/mail"
+
 	"git.sr.ht/~sircmpwn/aerc/models"
 	"github.com/mitchellh/go-homedir"
 )
@@ -37,47 +38,34 @@ type TemplateData struct {
 	OriginalMIMEType string
 }
 
-func TestTemplateData() TemplateData {
-	defaults := map[string]string{
-		"To":      "John Doe <john@example.com>",
-		"Cc":      "Josh Doe <josh@example.com>",
-		"From":    "Jane Smith <jane@example.com>",
-		"Subject": "This is only a test",
-	}
-
-	original := models.OriginalMail{
-		Date:     time.Now(),
-		From:     "John Doe <john@example.com>",
-		Text:     "This is only a test text",
-		MIMEType: "text/plain",
+func ParseTemplateData(h *mail.Header, original models.OriginalMail) TemplateData {
+	// we ignore errors as this shouldn't fail the sending / replying even if
+	// something is wrong with the message we reply to
+	to, _ := h.AddressList("to")
+	cc, _ := h.AddressList("cc")
+	bcc, _ := h.AddressList("bcc")
+	from, _ := h.AddressList("from")
+	subject, err := h.Text("subject")
+	if err != nil {
+		subject = h.Get("subject")
 	}
 
-	return ParseTemplateData(defaults, original)
-}
-
-func ParseTemplateData(defaults map[string]string, original models.OriginalMail) TemplateData {
 	td := TemplateData{
-		To:               parseAddressList(defaults["To"]),
-		Cc:               parseAddressList(defaults["Cc"]),
-		Bcc:              parseAddressList(defaults["Bcc"]),
-		From:             parseAddressList(defaults["From"]),
+		To:               to,
+		Cc:               cc,
+		Bcc:              bcc,
+		From:             from,
 		Date:             time.Now(),
-		Subject:          defaults["Subject"],
+		Subject:          subject,
 		OriginalText:     original.Text,
-		OriginalFrom:     parseAddressList(original.From),
 		OriginalDate:     original.Date,
 		OriginalMIMEType: original.MIMEType,
 	}
-	return td
-}
-
-func parseAddressList(list string) []*mail.Address {
-	addrs, err := mail.ParseAddressList(list)
-	if err != nil {
-		return nil
+	if original.RFC822Headers != nil {
+		origFrom, _ := original.RFC822Headers.AddressList("from")
+		td.OriginalFrom = origFrom
 	}
-
-	return addrs
+	return td
 }
 
 // wrap allows to chain wrapText
@@ -194,6 +182,34 @@ func findTemplate(templateName string, templateDirs []string) (string, error) {
 		"Can't find template %q in any of %v ", templateName, templateDirs)
 }
 
+//DummyData provides dummy data to test template validity
+func DummyData() interface{} {
+	from := &mail.Address{
+		Name:    "John Doe",
+		Address: "john@example.com",
+	}
+	to := &mail.Address{
+		Name:    "Alice Doe",
+		Address: "alice@example.com",
+	}
+	h := &mail.Header{}
+	h.SetAddressList("from", []*mail.Address{from})
+	h.SetAddressList("to", []*mail.Address{to})
+
+	oh := &mail.Header{}
+	oh.SetAddressList("from", []*mail.Address{to})
+	oh.SetAddressList("to", []*mail.Address{from})
+
+	original := models.OriginalMail{
+		Date:          time.Now(),
+		From:          from.String(),
+		Text:          "This is only a test text",
+		MIMEType:      "text/plain",
+		RFC822Headers: oh,
+	}
+	return ParseTemplateData(h, original)
+}
+
 func ParseTemplateFromFile(templateName string, templateDirs []string, data interface{}) (io.Reader, error) {
 	templateFile, err := findTemplate(templateName, templateDirs)
 	if err != nil {
diff --git a/widgets/aerc.go b/widgets/aerc.go
index acdd8b4..b4b4e28 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/emersion/go-message/mail"
 	"github.com/gdamore/tcell"
 	"github.com/google/shlex"
 	"golang.org/x/crypto/openpgp"
@@ -496,27 +497,38 @@ func (aerc *Aerc) Mailto(addr *url.URL) error {
 	if acct == nil {
 		return errors.New("No account selected")
 	}
-	defaults := make(map[string]string)
-	defaults["To"] = addr.Opaque
-	headerMap := map[string]string{
-		"cc":          "Cc",
-		"in-reply-to": "In-Reply-To",
-		"subject":     "Subject",
-	}
+
+	var subject string
+	h := &mail.Header{}
+	h.SetAddressList("to", []*mail.Address{&mail.Address{Address: addr.Opaque}})
 	for key, vals := range addr.Query() {
-		if header, ok := headerMap[strings.ToLower(key)]; ok {
-			defaults[header] = strings.Join(vals, ",")
+		switch strings.ToLower(key) {
+		case "cc":
+			list, err := mail.ParseAddressList(strings.Join(vals, ","))
+			if err != nil {
+				break
+			}
+			h.SetAddressList("Cc", list)
+		case "in-reply-to":
+			h.SetMsgIDList("In-Reply-To", vals)
+		case "subject":
+			subject = strings.Join(vals, ",")
+			h.SetText("Subject", subject)
+		default:
+			// any other header gets ignored on purpose to avoid control headers
+			// being injected
 		}
 	}
+
 	composer, err := NewComposer(aerc, acct, aerc.Config(),
-		acct.AccountConfig(), acct.Worker(), "", defaults, models.OriginalMail{})
+		acct.AccountConfig(), acct.Worker(), "", h, models.OriginalMail{})
 	if err != nil {
 		return nil
 	}
 	composer.FocusSubject()
 	title := "New email"
-	if subj, ok := defaults["Subject"]; ok {
-		title = subj
+	if subject != "" {
+		title = subject
 		composer.FocusTerminal()
 	}
 	tab := aerc.NewTab(composer, title)
diff --git a/widgets/compose.go b/widgets/compose.go
index 522146a..73ebcb3 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -8,7 +8,7 @@ import (
 	"io/ioutil"
 	"mime"
 	"net/http"
-	gomail "net/mail"
+	"net/textproto"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -23,6 +23,7 @@ import (
 
 	"git.sr.ht/~sircmpwn/aerc/completer"
 	"git.sr.ht/~sircmpwn/aerc/config"
+	"git.sr.ht/~sircmpwn/aerc/lib/format"
 	"git.sr.ht/~sircmpwn/aerc/lib/templates"
 	"git.sr.ht/~sircmpwn/aerc/lib/ui"
 	"git.sr.ht/~sircmpwn/aerc/models"
@@ -30,7 +31,9 @@ import (
 )
 
 type Composer struct {
-	editors map[string]*headerEditor
+	editors map[string]*headerEditor // indexes in lower case (from / cc / bcc)
+	header  *mail.Header
+	parent  models.OriginalMail // parent of current message, only set if reply
 
 	acctConfig *config.AccountConfig
 	config     *config.AercConfig
@@ -38,13 +41,10 @@ type Composer struct {
 	aerc       *Aerc
 
 	attachments []string
-	date        time.Time
-	defaults    map[string]string
 	editor      *Terminal
 	email       *os.File
 	grid        *ui.Grid
 	heditors    *ui.Grid // from, to, cc display a user can jump to
-	msgId       string
 	review      *reviewMessage
 	worker      *types.Worker
 	completer   *completer.Completer
@@ -61,22 +61,29 @@ type Composer struct {
 
 func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
 	acctConfig *config.AccountConfig, worker *types.Worker, template string,
-	defaults map[string]string, original models.OriginalMail) (*Composer, error) {
+	h *mail.Header, orig models.OriginalMail) (*Composer, error) {
 
-	if defaults == nil {
-		defaults = make(map[string]string)
+	if h == nil {
+		h = new(mail.Header)
 	}
-	if from := defaults["From"]; from == "" {
-		defaults["From"] = acctConfig.From
+	if fl, err := h.AddressList("from"); err != nil || fl == nil {
+		fl, err = mail.ParseAddressList(acctConfig.From)
+		// realistically this blows up way before us during the config loading
+		if err != nil {
+			return nil, err
+		}
+		if fl != nil {
+			h.SetAddressList("from", fl)
+
+		}
 	}
 
-	templateData := templates.ParseTemplateData(defaults, original)
+	templateData := templates.ParseTemplateData(h, orig)
 	cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) {
 		aerc.PushError(
 			fmt.Sprintf("could not complete header: %v", err))
 		worker.Logger.Printf("could not complete header: %v", err)
 	}, aerc.Logger())
-	layout, editors, focusable := buildComposeHeader(aerc, cmpl, defaults)
 
 	email, err := ioutil.TempFile("", "aerc-compose-*.eml")
 	if err != nil {
@@ -89,18 +96,15 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
 		acctConfig: acctConfig,
 		aerc:       aerc,
 		config:     conf,
-		date:       time.Now(),
-		defaults:   defaults,
-		editors:    editors,
+		header:     h,
+		parent:     orig,
 		email:      email,
-		layout:     layout,
-		msgId:      mail.GenerateMessageID(),
 		worker:     worker,
 		// You have to backtab to get to "From", since you usually don't edit it
 		focused:   1,
-		focusable: focusable,
 		completer: cmpl,
 	}
+	c.buildComposeHeader(aerc, cmpl)
 
 	if err := c.AddTemplate(template, templateData); err != nil {
 		return nil, err
@@ -113,56 +117,51 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
 	return c, nil
 }
 
-func buildComposeHeader(aerc *Aerc, cmpl *completer.Completer,
-	defaults map[string]string) (
-	newLayout HeaderLayout,
-	editors map[string]*headerEditor,
-	focusable []ui.MouseableDrawableInteractive,
-) {
-	layout := aerc.conf.Compose.HeaderLayout
-	editors = make(map[string]*headerEditor)
-	focusable = make([]ui.MouseableDrawableInteractive, 0)
+func (c *Composer) buildComposeHeader(aerc *Aerc, cmpl *completer.Completer) {
+
+	c.layout = aerc.conf.Compose.HeaderLayout
+	c.editors = make(map[string]*headerEditor)
+	c.focusable = make([]ui.MouseableDrawableInteractive, 0)
 
-	for _, row := range layout {
-		for _, h := range row {
-			e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
+	for i, row := range c.layout {
+		for j, h := range row {
+			h = strings.ToLower(h)
+			c.layout[i][j] = h // normalize to lowercase
+			e := newHeaderEditor(h, c.header, aerc.SelectedAccount().UiConfig())
 			if aerc.conf.Ui.CompletionPopovers {
-				e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
+				e.input.TabComplete(cmpl.ForHeader(h),
+					aerc.SelectedAccount().UiConfig().CompletionDelay)
 			}
-			editors[h] = e
+			c.editors[h] = e
 			switch h {
-			case "From":
+			case "from":
 				// Prepend From to support backtab
-				focusable = append([]ui.MouseableDrawableInteractive{e}, focusable...)
+				c.focusable = append([]ui.MouseableDrawableInteractive{e}, c.focusable...)
 			default:
-				focusable = append(focusable, e)
+				c.focusable = append(c.focusable, e)
 			}
 		}
 	}
 
-	// Add Cc/Bcc editors to layout if in defaults and not already visible
-	for _, h := range []string{"Cc", "Bcc"} {
-		if val, ok := defaults[h]; ok && val != "" {
-			if _, ok := editors[h]; !ok {
-				e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
+	// Add Cc/Bcc editors to layout if present in header and not already visible
+	for _, h := range []string{"cc", "bcc"} {
+		if c.header.Has(h) {
+			if _, ok := c.editors[h]; !ok {
+				e := newHeaderEditor(h, c.header, aerc.SelectedAccount().UiConfig())
 				if aerc.conf.Ui.CompletionPopovers {
 					e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
 				}
-				editors[h] = e
-				focusable = append(focusable, e)
-				layout = append(layout, []string{h})
+				c.editors[h] = e
+				c.focusable = append(c.focusable, e)
+				c.layout = append(c.layout, []string{h})
 			}
 		}
 	}
 
-	// Set default values for all editors
-	for key := range editors {
-		if val, ok := defaults[key]; ok {
-			editors[key].input.Set(val)
-			delete(defaults, key)
-		}
+	// load current header values into all editors
+	for _, e := range c.editors {
+		e.loadValue()
 	}
-	return layout, editors, focusable
 }
 
 func (c *Composer) SetSent() {
@@ -205,15 +204,10 @@ func (c *Composer) AddTemplate(template string, data interface{}) error {
 		return fmt.Errorf("Template loading failed: %v", err)
 	}
 
-	// add the headers contained in the template to the default headers
+	// copy the headers contained in the template to the compose headers
 	hf := mr.Header.Fields()
 	for hf.Next() {
-		var val string
-		var err error
-		if val, err = hf.Text(); err != nil {
-			val = hf.Value()
-		}
-		c.defaults[hf.Key()] = val
+		c.header.Set(hf.Key(), hf.Value())
 	}
 
 	part, err := mr.NextPart()
@@ -293,7 +287,7 @@ func (c *Composer) FocusRecipient() *Composer {
 
 // OnHeaderChange registers an OnChange callback for the specified header.
 func (c *Composer) OnHeaderChange(header string, fn func(subject string)) {
-	if editor, ok := c.editors[header]; ok {
+	if editor, ok := c.editors[strings.ToLower(header)]; ok {
 		editor.OnChange(func() {
 			fn(editor.input.String())
 		})
@@ -378,49 +372,24 @@ func (c *Composer) Worker() *types.Worker {
 	return c.worker
 }
 
-func (c *Composer) PrepareHeader() (*mail.Header, []string, error) {
-	header := &mail.Header{}
-	for h, val := range c.defaults {
-		if val == "" {
-			continue
-		}
-		header.SetText(h, val)
-	}
-	header.SetText("Message-Id", c.msgId)
-	header.SetDate(c.date)
-
-	headerKeys := make([]string, 0, len(c.editors))
-	for key := range c.editors {
-		headerKeys = append(headerKeys, key)
+//PrepareHeader finalizes the header, adding the value from the editors
+func (c *Composer) PrepareHeader() (*mail.Header, error) {
+	for _, editor := range c.editors {
+		editor.storeValue()
 	}
 
-	var rcpts []string
-	for h, editor := range c.editors {
-		val := editor.input.String()
-		if val == "" {
-			continue
-		}
-		switch h {
-		case "From", "To", "Cc", "Bcc": // Address headers
-			hdrRcpts, err := gomail.ParseAddressList(val)
-			if err != nil {
-				return nil, nil, errors.Wrapf(err, "ParseAddressList(%s)", val)
-			}
-			edRcpts := make([]*mail.Address, len(hdrRcpts))
-			for i, addr := range hdrRcpts {
-				edRcpts[i] = (*mail.Address)(addr)
-			}
-			header.SetAddressList(h, edRcpts)
-			if h != "From" {
-				for _, addr := range edRcpts {
-					rcpts = append(rcpts, addr.Address)
-				}
-			}
-		default:
-			header.SetText(h, val)
+	// control headers not normally set by the user
+	// repeated calls to PrepareHeader should be a noop
+	if !c.header.Has("Message-Id") {
+		err := c.header.GenerateMessageID()
+		if err != nil {
+			return nil, err
 		}
 	}
-	return header, rcpts, nil
+	if !c.header.Has("Date") {
+		c.header.SetDate(time.Now())
+	}
+	return c.header, nil
 }
 
 func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
@@ -639,39 +608,51 @@ func (c *Composer) FocusEditor(editor *headerEditor) {
 
 // AddEditor appends a new header editor to the compose window.
 func (c *Composer) AddEditor(header string, value string, appendHeader bool) {
-	if _, ok := c.editors[header]; ok {
-		if appendHeader {
-			header := c.editors[header].input.String()
-			value = strings.TrimSpace(header) + ", " + value
+	var editor *headerEditor
+	header = strings.ToLower(header)
+	if e, ok := c.editors[header]; ok {
+		e.storeValue() // flush modifications from the user to the header
+		editor = e
+	} else {
+		e := newHeaderEditor(header, c.header,
+			c.aerc.SelectedAccount().UiConfig())
+		if c.config.Ui.CompletionPopovers {
+			e.input.TabComplete(c.completer.ForHeader(header),
+				c.config.Ui.CompletionDelay)
 		}
-		c.editors[header].input.Set(value)
-		if value == "" {
-			c.FocusEditor(c.editors[header])
+		c.editors[header] = e
+		c.layout = append(c.layout, []string{header})
+		// Insert focus of new editor before terminal editor
+		c.focusable = append(
+			c.focusable[:len(c.focusable)-1],
+			e,
+			c.focusable[len(c.focusable)-1],
+		)
+		editor = e
+	}
+
+	if appendHeader {
+		currVal := editor.input.String()
+		if currVal != "" {
+			value = strings.TrimSpace(currVal) + ", " + value
 		}
-		return
 	}
-	e := newHeaderEditor(header, value, c.aerc.SelectedAccount().UiConfig())
-	if c.config.Ui.CompletionPopovers {
-		e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay)
-	}
-	c.editors[header] = e
-	c.layout = append(c.layout, []string{header})
-	// Insert focus of new editor before terminal editor
-	c.focusable = append(
-		c.focusable[:len(c.focusable)-1],
-		e,
-		c.focusable[len(c.focusable)-1],
-	)
-	c.updateGrid()
+	if value != "" || appendHeader {
+		c.editors[header].input.Set(value)
+		editor.storeValue()
+	}
 	if value == "" {
 		c.FocusEditor(c.editors[header])
 	}
+	c.updateGrid()
 }
 
 // updateGrid should be called when the underlying header layout is changed.
 func (c *Composer) updateGrid() {
 	heditors, height := c.layout.grid(
-		func(h string) ui.Drawable { return c.editors[h] },
+		func(h string) ui.Drawable {
+			return c.editors[h]
+		},
 	)
 
 	if c.grid == nil {
@@ -707,21 +688,82 @@ func (c *Composer) reloadEmail() error {
 
 type headerEditor struct {
 	name     string
+	header   *mail.Header
 	focused  bool
 	input    *ui.TextInput
 	uiConfig config.UIConfig
 }
 
-func newHeaderEditor(name string, value string, uiConfig config.UIConfig) *headerEditor {
-	return &headerEditor{
-		input:    ui.NewTextInput(value, uiConfig),
+func newHeaderEditor(name string, h *mail.Header,
+	uiConfig config.UIConfig) *headerEditor {
+	he := &headerEditor{
+		input:    ui.NewTextInput("", uiConfig),
 		name:     name,
+		header:   h,
 		uiConfig: uiConfig,
 	}
+	he.loadValue()
+	return he
+}
+
+//extractHumanHeaderValue extracts the human readable string for key from the
+//header. If a parsing error occurs the raw value is returned
+func extractHumanHeaderValue(key string, h *mail.Header) string {
+	var val string
+	var err error
+	switch strings.ToLower(key) {
+	case "to", "from", "cc", "bcc":
+		var list []*mail.Address
+		list, err = h.AddressList(key)
+		val = format.FormatAddresses(list)
+	default:
+		val, err = h.Text(key)
+	}
+	if err != nil {
+		// if we can't parse it, show it raw
+		val = h.Get(key)
+	}
+	return val
+}
+
+//loadValue loads the value of he.name form the underlying header
+//the value is decoded and meant for human consumption.
+//decoding issues are ignored and return their raw values
+func (he *headerEditor) loadValue() {
+	he.input.Set(extractHumanHeaderValue(he.name, he.header))
+	he.input.Invalidate()
+}
+
+//storeValue writes the current state back to the underlying header.
+//errors are ignored
+func (he *headerEditor) storeValue() {
+	val := he.input.String()
+	switch strings.ToLower(he.name) {
+	case "to", "from", "cc", "bcc":
+		list, err := mail.ParseAddressList(val)
+		if err == nil {
+			he.header.SetAddressList(he.name, list)
+		} else {
+			// garbage, but it'll blow up upon sending and the user can
+			// fix the issue
+			he.header.SetText(he.name, val)
+		}
+		val = format.FormatAddresses(list)
+	default:
+		he.header.SetText(he.name, val)
+	}
+}
+
+//setValue overwrites the current value of the header editor and flushes it
+//to the underlying header
+func (he *headerEditor) setValue(val string) {
+	he.input.Set(val)
+	he.storeValue()
 }
 
 func (he *headerEditor) Draw(ctx *ui.Context) {
-	name := he.name + " "
+	normalized := textproto.CanonicalMIMEHeaderKey(he.name)
+	name := normalized + " "
 	size := runewidth.StringWidth(name)
 	defaultStyle := he.uiConfig.GetStyle(config.STYLE_DEFAULT)
 	headerStyle := he.uiConfig.GetStyle(config.STYLE_HEADER)