diff options
Diffstat (limited to 'widgets')
-rw-r--r-- | widgets/aerc.go | 36 | ||||
-rw-r--r-- | widgets/compose.go | 288 |
2 files changed, 189 insertions, 135 deletions
diff --git a/widgets/aerc.go b/widgets/aerc.go index acdd8b4a..b4b4e28f 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 522146ab..73ebcb3e 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) |