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 ", - "Cc": "Josh Doe ", - "From": "Jane Smith ", - "Subject": "This is only a test", +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") } - original := models.OriginalMail{ - Date: time.Now(), - From: "John Doe ", - Text: "This is only a test text", - MIMEType: "text/plain", - } - - 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) { - for _, row := range layout { - for _, h := range row { - e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig()) + c.layout = aerc.conf.Compose.HeaderLayout + c.editors = make(map[string]*headerEditor) + c.focusable = make([]ui.MouseableDrawableInteractive, 0) + + 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] = 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 + } + } + if value != "" || appendHeader { c.editors[header].input.Set(value) - if value == "" { - c.FocusEditor(c.editors[header]) - } - return + editor.storeValue() } - 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 == "" { 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)