diff --git a/CHANGELOG.md b/CHANGELOG.md index 107f8df..6554f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support for attaching files with `mailto:`-links - Filter commands now have the `AERC_MIME_TYPE` and `AERC_FILENAME` variables defined in their environment. +- Warn before sending emails that may need an attachment with + `no-attachment-warning` in `aerc.conf`. ### Changed diff --git a/commands/compose/send.go b/commands/compose/send.go index 3786721..ccfe788 100644 --- a/commands/compose/send.go +++ b/commands/compose/send.go @@ -100,6 +100,40 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error { rcpts: rcpts, } + warn, err := composer.ShouldWarnAttachment() + if err != nil || warn { + msg := "You may have forgotten an attachment." + if err != nil { + logging.Warnf("failed to check for a forgotten attachment: %v", err) + msg = "Failed to check for a forgotten attachment." + } + + prompt := widgets.NewPrompt(aerc.Config(), + msg+" Abort send? [Y/n] ", + func(text string) { + if text == "n" || text == "N" { + send(aerc, composer, ctx, header, tabName) + } + }, func(cmd string) ([]string, string) { + if cmd == "" { + return []string{"y", "n"}, "" + } + + return nil, "" + }, + ) + + aerc.PushPrompt(prompt) + } else { + send(aerc, composer, ctx, header, tabName) + } + + return nil +} + +func send(aerc *widgets.Aerc, composer *widgets.Composer, ctx sendCtx, + header *mail.Header, tabName string, +) { // we don't want to block the UI thread while we are sending // so we do everything in a goroutine and hide the composer from the user aerc.RemoveTab(composer) @@ -109,6 +143,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error { mode.NoQuit() var copyBuf bytes.Buffer // for the Sent folder content if CopyTo is set + config := composer.Config() failCh := make(chan error) // writer @@ -116,6 +151,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error { defer logging.PanicHandler() var sender io.WriteCloser + var err error switch ctx.scheme { case "smtp": fallthrough @@ -151,7 +187,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error { // leave no-quit mode defer mode.NoQuitDone() - err = <-failCh + err := <-failCh if err != nil { aerc.PushError(strings.ReplaceAll(err.Error(), "\n", " ")) aerc.NewTab(composer, tabName) @@ -176,7 +212,6 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error { composer.SetSent() composer.Close() }() - return nil } func listRecipients(h *mail.Header) ([]*mail.Address, error) { diff --git a/config/aerc.conf b/config/aerc.conf index a980c70..384a0db 100644 --- a/config/aerc.conf +++ b/config/aerc.conf @@ -297,6 +297,18 @@ address-book-cmd= # Default: true reply-to-self=true +# +# Warn before sending an email that matches the specified regexp but does not +# have any attachments. Leave empty to disable this feature. +# +# Uses Go's regexp syntax, documented at https://golang.org/s/re2syntax. The +# "(?im)" flags are set by default (case-insensitive and multi-line). +# +# Example: +# no-attachment-warning=^[^>]*attach(ed|ment) +# +no-attachment-warning= + [filters] # # Filters allow you to pipe an email body through a shell command to render diff --git a/config/config.go b/config/config.go index e40d964..0de1780 100644 --- a/config/config.go +++ b/config/config.go @@ -203,10 +203,11 @@ type BindingConfigContext struct { } type ComposeConfig struct { - Editor string `ini:"editor"` - HeaderLayout [][]string `ini:"-"` - AddressBookCmd string `ini:"address-book-cmd"` - ReplyToSelf bool `ini:"reply-to-self"` + Editor string `ini:"editor"` + HeaderLayout [][]string `ini:"-"` + AddressBookCmd string `ini:"address-book-cmd"` + ReplyToSelf bool `ini:"reply-to-self"` + NoAttachmentWarning *regexp.Regexp `ini:"-"` } type FilterConfig struct { @@ -523,6 +524,18 @@ func (config *AercConfig) LoadConfig(file *ini.File) error { if key == "header-layout" { config.Compose.HeaderLayout = parseLayout(val) } + + if key == "no-attachment-warning" && len(val) > 0 { + re, err := regexp.Compile("(?im)" + val) + if err != nil { + return fmt.Errorf( + "Invalid no-attachment-warning '%s': %w", + val, err, + ) + } + + config.Compose.NoAttachmentWarning = re + } } } diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index 2c5f809..21472e6 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -493,6 +493,20 @@ These options are configured in the *[compose]* section of aerc.conf. Default: true +*no-attachment-warning* + Specifies a regular expression against which an email's body should be + tested before sending an email with no attachment. If the regexp + matches, aerc will warn you before sending the message. Leave empty to + disable this feature. + + Uses Go's regexp syntax, documented at https://golang.org/s/re2syntax. + The "(?im)" flags are set by default (case-insensitive and multi-line). + + Example: + no-attachment-warning=^[^>]\*attach(ed|ment) + + Default: none + ## FILTERS Filters allow you to pipe an email body through a shell command to render diff --git a/widgets/aerc.go b/widgets/aerc.go index 35c32fe..81d0747 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -598,6 +598,10 @@ func (aerc *Aerc) BeginExCommand(cmd string) { aerc.focus(exline) } +func (aerc *Aerc) PushPrompt(prompt *ExLine) { + aerc.prompts.Push(prompt) +} + func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) { p := NewPrompt(aerc.conf, prompt, func(text string) { if text != "" { diff --git a/widgets/compose.go b/widgets/compose.go index dc0c21a..6e34365 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -776,6 +776,26 @@ func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error { } } +func (c *Composer) ShouldWarnAttachment() (bool, error) { + regex := c.config.Compose.NoAttachmentWarning + + if regex == nil || len(c.attachments) > 0 { + return false, nil + } + + err := c.reloadEmail() + if err != nil { + return false, errors.Wrap(err, "reloadEmail") + } + + body, err := io.ReadAll(c.email) + if err != nil { + return false, errors.Wrap(err, "io.ReadAll") + } + + return regex.Match(body), nil +} + func writeMsgImpl(c *Composer, header *mail.Header, writer io.Writer) error { if len(c.attachments) == 0 && len(c.textParts) == 0 { // no attachments