summaryrefslogtreecommitdiff
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)