compose: use a proper header instead of a string map
Prior to this commit, the composer was based on a map[string]string. While this approach was very versatile, it lead to a constant encoding / decoding of addresses and other headers. This commit switches to a different model, where the composer is based on a header. Commands which want to interact with it can simply set some defaults they would like to have. Users can overwrite them however they like. In order to get access to the functions generating / getting the msgid go-message was upgraded.
This commit is contained in:
parent
3ad3a5ede0
commit
20ec2c8eeb
14 changed files with 318 additions and 217 deletions
|
@ -57,18 +57,17 @@ func (Header) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
composer, _ := aerc.SelectedTab().(*widgets.Composer)
|
composer, _ := aerc.SelectedTab().(*widgets.Composer)
|
||||||
|
|
||||||
if !force {
|
if !force {
|
||||||
headers, _, err := composer.PrepareHeader()
|
headers, err := composer.PrepareHeader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if headers.Has(strings.Title(args[optind])) {
|
if headers.Has(args[optind]) {
|
||||||
return fmt.Errorf("Header %s already exists", args[optind])
|
return fmt.Errorf("Header %s already exists", args[optind])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composer.AddEditor(strings.Title(args[optind]),
|
composer.AddEditor(args[optind], strings.Join(args[optind+1:], " "), false)
|
||||||
strings.Join(args[optind+1:], " "), false)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
|
|
||||||
aerc.Logger().Println("Postponing mail")
|
aerc.Logger().Println("Postponing mail")
|
||||||
|
|
||||||
header, _, err := composer.PrepareHeader()
|
header, err := composer.PrepareHeader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "PrepareHeader")
|
return errors.Wrap(err, "PrepareHeader")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/mail"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -17,9 +16,11 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/aerc/lib"
|
"git.sr.ht/~sircmpwn/aerc/lib"
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/lib/format"
|
||||||
"git.sr.ht/~sircmpwn/aerc/models"
|
"git.sr.ht/~sircmpwn/aerc/models"
|
||||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||||
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
"golang.org/x/oauth2"
|
"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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "PrepareHeader")
|
return errors.Wrap(err, "PrepareHeader")
|
||||||
}
|
}
|
||||||
|
rcpts, err := listRecipients(header)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "listRecipients")
|
||||||
|
}
|
||||||
|
|
||||||
if config.From == "" {
|
if config.From == "" {
|
||||||
return errors.New("No 'From' configured for this account")
|
return errors.New("No 'From' configured for this account")
|
||||||
}
|
}
|
||||||
from, err := mail.ParseAddress(config.From)
|
from, err := format.ParseAddress(config.From)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "ParseAddress(config.From)")
|
return errors.Wrap(err, "ParseAddress(config.From)")
|
||||||
}
|
}
|
||||||
|
@ -288,7 +293,12 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
composer.Close()
|
composer.Close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
header, _, _ := composer.PrepareHeader()
|
header, err := composer.PrepareHeader()
|
||||||
|
if err != nil {
|
||||||
|
aerc.PushError(" " + err.Error())
|
||||||
|
w.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
composer.WriteMessage(header, w)
|
composer.WriteMessage(header, w)
|
||||||
w.Close()
|
w.Close()
|
||||||
} else {
|
} else {
|
||||||
|
@ -299,3 +309,17 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
}()
|
}()
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"git.sr.ht/~sircmpwn/aerc/models"
|
"git.sr.ht/~sircmpwn/aerc/models"
|
||||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||||
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/getopt"
|
"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)
|
widget := aerc.SelectedTab().(widgets.ProvidesMessage)
|
||||||
acct := widget.SelectedAccount()
|
acct := widget.SelectedAccount()
|
||||||
if acct == nil {
|
if acct == nil {
|
||||||
|
@ -69,11 +65,19 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
}
|
}
|
||||||
acct.Logger().Println("Forwarding email " + msg.Envelope.MessageId)
|
acct.Logger().Println("Forwarding email " + msg.Envelope.MessageId)
|
||||||
|
|
||||||
|
h := &mail.Header{}
|
||||||
subject := "Fwd: " + msg.Envelope.Subject
|
subject := "Fwd: " + msg.Envelope.Subject
|
||||||
defaults := map[string]string{
|
h.SetSubject(subject)
|
||||||
"To": to,
|
|
||||||
"Subject": 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{
|
original := models.OriginalMail{
|
||||||
From: format.FormatAddresses(msg.Envelope.From),
|
From: format.FormatAddresses(msg.Envelope.From),
|
||||||
Date: msg.Envelope.Date,
|
Date: msg.Envelope.Date,
|
||||||
|
@ -81,15 +85,15 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
addTab := func() (*widgets.Composer, error) {
|
addTab := func() (*widgets.Composer, error) {
|
||||||
composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(),
|
composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
|
||||||
acct.Worker(), template, defaults, original)
|
acct.AccountConfig(), acct.Worker(), template, h, original)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aerc.PushError("Error: " + err.Error())
|
aerc.PushError("Error: " + err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tab := aerc.NewTab(composer, subject)
|
tab := aerc.NewTab(composer, subject)
|
||||||
if to == "" {
|
if !h.Has("to") {
|
||||||
composer.FocusRecipient()
|
composer.FocusRecipient()
|
||||||
} else {
|
} else {
|
||||||
composer.FocusTerminal()
|
composer.FocusTerminal()
|
||||||
|
|
|
@ -53,15 +53,9 @@ func (Recall) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
}
|
}
|
||||||
acct.Logger().Println("Recalling message " + msgInfo.Envelope.MessageId)
|
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(),
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Cannot open a new composer")
|
return errors.Wrap(err, "Cannot open a new composer")
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,13 +145,13 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
subject = msg.Envelope.Subject
|
subject = msg.Envelope.Subject
|
||||||
}
|
}
|
||||||
|
|
||||||
defaults := map[string]string{
|
h := &mail.Header{}
|
||||||
"To": format.FormatAddresses(to),
|
h.SetAddressList("to", to)
|
||||||
"Cc": format.FormatAddresses(cc),
|
h.SetAddressList("cc", cc)
|
||||||
"From": format.AddressForHumans(from),
|
h.SetAddressList("from", []*mail.Address{from})
|
||||||
"Subject": subject,
|
h.SetSubject(subject)
|
||||||
"In-Reply-To": msg.Envelope.MessageId,
|
h.SetMsgIDList("in-reply-to", []string{msg.Envelope.MessageId})
|
||||||
}
|
//TODO: references header
|
||||||
original := models.OriginalMail{
|
original := models.OriginalMail{
|
||||||
From: format.FormatAddresses(msg.Envelope.From),
|
From: format.FormatAddresses(msg.Envelope.From),
|
||||||
Date: msg.Envelope.Date,
|
Date: msg.Envelope.Date,
|
||||||
|
@ -160,7 +160,7 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
|
|
||||||
addTab := func() error {
|
addTab := func() error {
|
||||||
composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
|
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 {
|
if err != nil {
|
||||||
aerc.PushError("Error: " + err.Error())
|
aerc.PushError("Error: " + err.Error())
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"git.sr.ht/~sircmpwn/aerc/lib"
|
"git.sr.ht/~sircmpwn/aerc/lib"
|
||||||
"git.sr.ht/~sircmpwn/aerc/models"
|
"git.sr.ht/~sircmpwn/aerc/models"
|
||||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Unsubscribe helps people unsubscribe from mailing lists by way of the
|
// 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 {
|
func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {
|
||||||
widget := aerc.SelectedTab().(widgets.ProvidesMessage)
|
widget := aerc.SelectedTab().(widgets.ProvidesMessage)
|
||||||
acct := widget.SelectedAccount()
|
acct := widget.SelectedAccount()
|
||||||
defaults := map[string]string{
|
|
||||||
"To": u.Opaque,
|
h := &mail.Header{}
|
||||||
"Subject": u.Query().Get("subject"),
|
h.SetSubject(u.Query().Get("subject"))
|
||||||
|
if to, err := mail.ParseAddressList(u.Opaque); err == nil {
|
||||||
|
h.SetAddressList("to", to)
|
||||||
}
|
}
|
||||||
|
|
||||||
composer, err := widgets.NewComposer(
|
composer, err := widgets.NewComposer(
|
||||||
aerc,
|
aerc,
|
||||||
acct,
|
acct,
|
||||||
|
@ -95,7 +99,7 @@ func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {
|
||||||
acct.AccountConfig(),
|
acct.AccountConfig(),
|
||||||
acct.Worker(),
|
acct.Worker(),
|
||||||
"",
|
"",
|
||||||
defaults,
|
h,
|
||||||
models.OriginalMail{},
|
models.OriginalMail{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -413,8 +413,10 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
||||||
if key == "template-dirs" {
|
if key == "template-dirs" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// we want to fail during startup if the templates are not ok
|
||||||
|
// hence we do a dummy execute here
|
||||||
_, err := templates.ParseTemplateFromFile(
|
_, err := templates.ParseTemplateFromFile(
|
||||||
val, config.Templates.TemplateDirs, templates.TestTemplateData())
|
val, config.Templates.TemplateDirs, templates.DummyData())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -11,7 +11,7 @@ require (
|
||||||
github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e
|
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-imap-sortthread v1.1.1-0.20201009054724-d020d96306b3
|
||||||
github.com/emersion/go-maildir v0.2.0
|
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-pgpmail v0.0.0-20200303213726-db035a3a4139
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||||
github.com/emersion/go-smtp v0.12.1
|
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/net v0.0.0-20200301022130-244492dfa37a // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect
|
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
|
google.golang.org/appengine v1.6.5 // indirect
|
||||||
gopkg.in/ini.v1 v1.44.0 // indirect
|
gopkg.in/ini.v1 v1.44.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||||
|
|
12
go.sum
12
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-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.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.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.13.1-0.20201112194930-f77964fe28bd h1:6CXxdoOzAyQForkd2U/JNceVyNpmg92alCU2R+4dwIY=
|
||||||
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/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 h1:JTUbkRuQFtDrl5KHWR2jrh9SUeSDEEEjUcHJkXdAE2Q=
|
||||||
github.com/emersion/go-pgpmail v0.0.0-20200303213726-db035a3a4139/go.mod h1:+Ovy1VQCUKPdjWkOiWvFoiFaWXkqn1PA793VvfEYWQU=
|
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=
|
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-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 h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
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 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 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
|
||||||
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
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 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
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=
|
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/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.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
golang.org/x/text v0.3.4-0.20201021145329-22f1617af38e h1:0kyKOEC0chG7FKmnf/1uNwvDLc3NtNTRip2rXAN9nwI=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
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=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
|
|
@ -61,6 +61,7 @@ func AddressForHumans(a *mail.Address) string {
|
||||||
|
|
||||||
var atom *regexp.Regexp = regexp.MustCompile("^[a-z0-9!#$%7'*+-/=?^_`{}|~ ]+$")
|
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 {
|
func FormatAddresses(l []*mail.Address) string {
|
||||||
formatted := make([]string, len(l))
|
formatted := make([]string, len(l))
|
||||||
for i, a := range l {
|
for i, a := range l {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/mail"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
|
@ -12,6 +11,8 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/aerc/models"
|
"git.sr.ht/~sircmpwn/aerc/models"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
)
|
)
|
||||||
|
@ -37,47 +38,34 @@ type TemplateData struct {
|
||||||
OriginalMIMEType string
|
OriginalMIMEType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTemplateData() TemplateData {
|
func ParseTemplateData(h *mail.Header, original models.OriginalMail) TemplateData {
|
||||||
defaults := map[string]string{
|
// we ignore errors as this shouldn't fail the sending / replying even if
|
||||||
"To": "John Doe <john@example.com>",
|
// something is wrong with the message we reply to
|
||||||
"Cc": "Josh Doe <josh@example.com>",
|
to, _ := h.AddressList("to")
|
||||||
"From": "Jane Smith <jane@example.com>",
|
cc, _ := h.AddressList("cc")
|
||||||
"Subject": "This is only a test",
|
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 <john@example.com>",
|
|
||||||
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{
|
td := TemplateData{
|
||||||
To: parseAddressList(defaults["To"]),
|
To: to,
|
||||||
Cc: parseAddressList(defaults["Cc"]),
|
Cc: cc,
|
||||||
Bcc: parseAddressList(defaults["Bcc"]),
|
Bcc: bcc,
|
||||||
From: parseAddressList(defaults["From"]),
|
From: from,
|
||||||
Date: time.Now(),
|
Date: time.Now(),
|
||||||
Subject: defaults["Subject"],
|
Subject: subject,
|
||||||
OriginalText: original.Text,
|
OriginalText: original.Text,
|
||||||
OriginalFrom: parseAddressList(original.From),
|
|
||||||
OriginalDate: original.Date,
|
OriginalDate: original.Date,
|
||||||
OriginalMIMEType: original.MIMEType,
|
OriginalMIMEType: original.MIMEType,
|
||||||
}
|
}
|
||||||
return td
|
if original.RFC822Headers != nil {
|
||||||
}
|
origFrom, _ := original.RFC822Headers.AddressList("from")
|
||||||
|
td.OriginalFrom = origFrom
|
||||||
func parseAddressList(list string) []*mail.Address {
|
|
||||||
addrs, err := mail.ParseAddressList(list)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return td
|
||||||
return addrs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrap allows to chain wrapText
|
// 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)
|
"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) {
|
func ParseTemplateFromFile(templateName string, templateDirs []string, data interface{}) (io.Reader, error) {
|
||||||
templateFile, err := findTemplate(templateName, templateDirs)
|
templateFile, err := findTemplate(templateName, templateDirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/google/shlex"
|
"github.com/google/shlex"
|
||||||
"golang.org/x/crypto/openpgp"
|
"golang.org/x/crypto/openpgp"
|
||||||
|
@ -496,27 +497,38 @@ func (aerc *Aerc) Mailto(addr *url.URL) error {
|
||||||
if acct == nil {
|
if acct == nil {
|
||||||
return errors.New("No account selected")
|
return errors.New("No account selected")
|
||||||
}
|
}
|
||||||
defaults := make(map[string]string)
|
|
||||||
defaults["To"] = addr.Opaque
|
var subject string
|
||||||
headerMap := map[string]string{
|
h := &mail.Header{}
|
||||||
"cc": "Cc",
|
h.SetAddressList("to", []*mail.Address{&mail.Address{Address: addr.Opaque}})
|
||||||
"in-reply-to": "In-Reply-To",
|
|
||||||
"subject": "Subject",
|
|
||||||
}
|
|
||||||
for key, vals := range addr.Query() {
|
for key, vals := range addr.Query() {
|
||||||
if header, ok := headerMap[strings.ToLower(key)]; ok {
|
switch strings.ToLower(key) {
|
||||||
defaults[header] = strings.Join(vals, ",")
|
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(),
|
composer, err := NewComposer(aerc, acct, aerc.Config(),
|
||||||
acct.AccountConfig(), acct.Worker(), "", defaults, models.OriginalMail{})
|
acct.AccountConfig(), acct.Worker(), "", h, models.OriginalMail{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
composer.FocusSubject()
|
composer.FocusSubject()
|
||||||
title := "New email"
|
title := "New email"
|
||||||
if subj, ok := defaults["Subject"]; ok {
|
if subject != "" {
|
||||||
title = subj
|
title = subject
|
||||||
composer.FocusTerminal()
|
composer.FocusTerminal()
|
||||||
}
|
}
|
||||||
tab := aerc.NewTab(composer, title)
|
tab := aerc.NewTab(composer, title)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
gomail "net/mail"
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/aerc/completer"
|
"git.sr.ht/~sircmpwn/aerc/completer"
|
||||||
"git.sr.ht/~sircmpwn/aerc/config"
|
"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/templates"
|
||||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||||
"git.sr.ht/~sircmpwn/aerc/models"
|
"git.sr.ht/~sircmpwn/aerc/models"
|
||||||
|
@ -30,7 +31,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Composer struct {
|
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
|
acctConfig *config.AccountConfig
|
||||||
config *config.AercConfig
|
config *config.AercConfig
|
||||||
|
@ -38,13 +41,10 @@ type Composer struct {
|
||||||
aerc *Aerc
|
aerc *Aerc
|
||||||
|
|
||||||
attachments []string
|
attachments []string
|
||||||
date time.Time
|
|
||||||
defaults map[string]string
|
|
||||||
editor *Terminal
|
editor *Terminal
|
||||||
email *os.File
|
email *os.File
|
||||||
grid *ui.Grid
|
grid *ui.Grid
|
||||||
heditors *ui.Grid // from, to, cc display a user can jump to
|
heditors *ui.Grid // from, to, cc display a user can jump to
|
||||||
msgId string
|
|
||||||
review *reviewMessage
|
review *reviewMessage
|
||||||
worker *types.Worker
|
worker *types.Worker
|
||||||
completer *completer.Completer
|
completer *completer.Completer
|
||||||
|
@ -61,22 +61,29 @@ type Composer struct {
|
||||||
|
|
||||||
func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
||||||
acctConfig *config.AccountConfig, worker *types.Worker, template string,
|
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 h == nil {
|
||||||
|
h = new(mail.Header)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
if defaults == nil {
|
|
||||||
defaults = make(map[string]string)
|
|
||||||
}
|
}
|
||||||
if from := defaults["From"]; from == "" {
|
|
||||||
defaults["From"] = acctConfig.From
|
|
||||||
}
|
}
|
||||||
|
|
||||||
templateData := templates.ParseTemplateData(defaults, original)
|
templateData := templates.ParseTemplateData(h, orig)
|
||||||
cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) {
|
cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) {
|
||||||
aerc.PushError(
|
aerc.PushError(
|
||||||
fmt.Sprintf("could not complete header: %v", err))
|
fmt.Sprintf("could not complete header: %v", err))
|
||||||
worker.Logger.Printf("could not complete header: %v", err)
|
worker.Logger.Printf("could not complete header: %v", err)
|
||||||
}, aerc.Logger())
|
}, aerc.Logger())
|
||||||
layout, editors, focusable := buildComposeHeader(aerc, cmpl, defaults)
|
|
||||||
|
|
||||||
email, err := ioutil.TempFile("", "aerc-compose-*.eml")
|
email, err := ioutil.TempFile("", "aerc-compose-*.eml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -89,18 +96,15 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
||||||
acctConfig: acctConfig,
|
acctConfig: acctConfig,
|
||||||
aerc: aerc,
|
aerc: aerc,
|
||||||
config: conf,
|
config: conf,
|
||||||
date: time.Now(),
|
header: h,
|
||||||
defaults: defaults,
|
parent: orig,
|
||||||
editors: editors,
|
|
||||||
email: email,
|
email: email,
|
||||||
layout: layout,
|
|
||||||
msgId: mail.GenerateMessageID(),
|
|
||||||
worker: worker,
|
worker: worker,
|
||||||
// You have to backtab to get to "From", since you usually don't edit it
|
// You have to backtab to get to "From", since you usually don't edit it
|
||||||
focused: 1,
|
focused: 1,
|
||||||
focusable: focusable,
|
|
||||||
completer: cmpl,
|
completer: cmpl,
|
||||||
}
|
}
|
||||||
|
c.buildComposeHeader(aerc, cmpl)
|
||||||
|
|
||||||
if err := c.AddTemplate(template, templateData); err != nil {
|
if err := c.AddTemplate(template, templateData); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -113,56 +117,51 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildComposeHeader(aerc *Aerc, cmpl *completer.Completer,
|
func (c *Composer) 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)
|
|
||||||
|
|
||||||
for _, row := range layout {
|
c.layout = aerc.conf.Compose.HeaderLayout
|
||||||
for _, h := range row {
|
c.editors = make(map[string]*headerEditor)
|
||||||
e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
|
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 {
|
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 {
|
switch h {
|
||||||
case "From":
|
case "from":
|
||||||
// Prepend From to support backtab
|
// Prepend From to support backtab
|
||||||
focusable = append([]ui.MouseableDrawableInteractive{e}, focusable...)
|
c.focusable = append([]ui.MouseableDrawableInteractive{e}, c.focusable...)
|
||||||
default:
|
default:
|
||||||
focusable = append(focusable, e)
|
c.focusable = append(c.focusable, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Cc/Bcc editors to layout if in defaults and not already visible
|
// Add Cc/Bcc editors to layout if present in header and not already visible
|
||||||
for _, h := range []string{"Cc", "Bcc"} {
|
for _, h := range []string{"cc", "bcc"} {
|
||||||
if val, ok := defaults[h]; ok && val != "" {
|
if c.header.Has(h) {
|
||||||
if _, ok := editors[h]; !ok {
|
if _, ok := c.editors[h]; !ok {
|
||||||
e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
|
e := newHeaderEditor(h, c.header, aerc.SelectedAccount().UiConfig())
|
||||||
if aerc.conf.Ui.CompletionPopovers {
|
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
|
||||||
focusable = append(focusable, e)
|
c.focusable = append(c.focusable, e)
|
||||||
layout = append(layout, []string{h})
|
c.layout = append(c.layout, []string{h})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default values for all editors
|
// load current header values into all editors
|
||||||
for key := range editors {
|
for _, e := range c.editors {
|
||||||
if val, ok := defaults[key]; ok {
|
e.loadValue()
|
||||||
editors[key].input.Set(val)
|
|
||||||
delete(defaults, key)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return layout, editors, focusable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Composer) SetSent() {
|
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)
|
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()
|
hf := mr.Header.Fields()
|
||||||
for hf.Next() {
|
for hf.Next() {
|
||||||
var val string
|
c.header.Set(hf.Key(), hf.Value())
|
||||||
var err error
|
|
||||||
if val, err = hf.Text(); err != nil {
|
|
||||||
val = hf.Value()
|
|
||||||
}
|
|
||||||
c.defaults[hf.Key()] = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
part, err := mr.NextPart()
|
part, err := mr.NextPart()
|
||||||
|
@ -293,7 +287,7 @@ func (c *Composer) FocusRecipient() *Composer {
|
||||||
|
|
||||||
// OnHeaderChange registers an OnChange callback for the specified header.
|
// OnHeaderChange registers an OnChange callback for the specified header.
|
||||||
func (c *Composer) OnHeaderChange(header string, fn func(subject string)) {
|
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() {
|
editor.OnChange(func() {
|
||||||
fn(editor.input.String())
|
fn(editor.input.String())
|
||||||
})
|
})
|
||||||
|
@ -378,49 +372,24 @@ func (c *Composer) Worker() *types.Worker {
|
||||||
return c.worker
|
return c.worker
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Composer) PrepareHeader() (*mail.Header, []string, error) {
|
//PrepareHeader finalizes the header, adding the value from the editors
|
||||||
header := &mail.Header{}
|
func (c *Composer) PrepareHeader() (*mail.Header, error) {
|
||||||
for h, val := range c.defaults {
|
for _, editor := range c.editors {
|
||||||
if val == "" {
|
editor.storeValue()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var rcpts []string
|
// control headers not normally set by the user
|
||||||
for h, editor := range c.editors {
|
// repeated calls to PrepareHeader should be a noop
|
||||||
val := editor.input.String()
|
if !c.header.Has("Message-Id") {
|
||||||
if val == "" {
|
err := c.header.GenerateMessageID()
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch h {
|
|
||||||
case "From", "To", "Cc", "Bcc": // Address headers
|
|
||||||
hdrRcpts, err := gomail.ParseAddressList(val)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrapf(err, "ParseAddressList(%s)", val)
|
return nil, err
|
||||||
}
|
|
||||||
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:
|
if !c.header.Has("Date") {
|
||||||
header.SetText(h, val)
|
c.header.SetDate(time.Now())
|
||||||
}
|
}
|
||||||
}
|
return c.header, nil
|
||||||
return header, rcpts, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
|
func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
|
||||||
|
@ -639,20 +608,17 @@ func (c *Composer) FocusEditor(editor *headerEditor) {
|
||||||
|
|
||||||
// AddEditor appends a new header editor to the compose window.
|
// AddEditor appends a new header editor to the compose window.
|
||||||
func (c *Composer) AddEditor(header string, value string, appendHeader bool) {
|
func (c *Composer) AddEditor(header string, value string, appendHeader bool) {
|
||||||
if _, ok := c.editors[header]; ok {
|
var editor *headerEditor
|
||||||
if appendHeader {
|
header = strings.ToLower(header)
|
||||||
header := c.editors[header].input.String()
|
if e, ok := c.editors[header]; ok {
|
||||||
value = strings.TrimSpace(header) + ", " + value
|
e.storeValue() // flush modifications from the user to the header
|
||||||
}
|
editor = e
|
||||||
c.editors[header].input.Set(value)
|
} else {
|
||||||
if value == "" {
|
e := newHeaderEditor(header, c.header,
|
||||||
c.FocusEditor(c.editors[header])
|
c.aerc.SelectedAccount().UiConfig())
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e := newHeaderEditor(header, value, c.aerc.SelectedAccount().UiConfig())
|
|
||||||
if c.config.Ui.CompletionPopovers {
|
if c.config.Ui.CompletionPopovers {
|
||||||
e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay)
|
e.input.TabComplete(c.completer.ForHeader(header),
|
||||||
|
c.config.Ui.CompletionDelay)
|
||||||
}
|
}
|
||||||
c.editors[header] = e
|
c.editors[header] = e
|
||||||
c.layout = append(c.layout, []string{header})
|
c.layout = append(c.layout, []string{header})
|
||||||
|
@ -662,16 +628,31 @@ func (c *Composer) AddEditor(header string, value string, appendHeader bool) {
|
||||||
e,
|
e,
|
||||||
c.focusable[len(c.focusable)-1],
|
c.focusable[len(c.focusable)-1],
|
||||||
)
|
)
|
||||||
c.updateGrid()
|
editor = e
|
||||||
|
}
|
||||||
|
|
||||||
|
if appendHeader {
|
||||||
|
currVal := editor.input.String()
|
||||||
|
if currVal != "" {
|
||||||
|
value = strings.TrimSpace(currVal) + ", " + value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value != "" || appendHeader {
|
||||||
|
c.editors[header].input.Set(value)
|
||||||
|
editor.storeValue()
|
||||||
|
}
|
||||||
if value == "" {
|
if value == "" {
|
||||||
c.FocusEditor(c.editors[header])
|
c.FocusEditor(c.editors[header])
|
||||||
}
|
}
|
||||||
|
c.updateGrid()
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateGrid should be called when the underlying header layout is changed.
|
// updateGrid should be called when the underlying header layout is changed.
|
||||||
func (c *Composer) updateGrid() {
|
func (c *Composer) updateGrid() {
|
||||||
heditors, height := c.layout.grid(
|
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 {
|
if c.grid == nil {
|
||||||
|
@ -707,21 +688,82 @@ func (c *Composer) reloadEmail() error {
|
||||||
|
|
||||||
type headerEditor struct {
|
type headerEditor struct {
|
||||||
name string
|
name string
|
||||||
|
header *mail.Header
|
||||||
focused bool
|
focused bool
|
||||||
input *ui.TextInput
|
input *ui.TextInput
|
||||||
uiConfig config.UIConfig
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHeaderEditor(name string, value string, uiConfig config.UIConfig) *headerEditor {
|
func newHeaderEditor(name string, h *mail.Header,
|
||||||
return &headerEditor{
|
uiConfig config.UIConfig) *headerEditor {
|
||||||
input: ui.NewTextInput(value, uiConfig),
|
he := &headerEditor{
|
||||||
|
input: ui.NewTextInput("", uiConfig),
|
||||||
name: name,
|
name: name,
|
||||||
|
header: h,
|
||||||
uiConfig: uiConfig,
|
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) {
|
func (he *headerEditor) Draw(ctx *ui.Context) {
|
||||||
name := he.name + " "
|
normalized := textproto.CanonicalMIMEHeaderKey(he.name)
|
||||||
|
name := normalized + " "
|
||||||
size := runewidth.StringWidth(name)
|
size := runewidth.StringWidth(name)
|
||||||
defaultStyle := he.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
defaultStyle := he.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||||
headerStyle := he.uiConfig.GetStyle(config.STYLE_HEADER)
|
headerStyle := he.uiConfig.GetStyle(config.STYLE_HEADER)
|
||||||
|
|
Loading…
Reference in a new issue