compose: warn before sending without attachment
Prevent the embarrassing forgotten attachment scenario by warning the user before sending a message that may need an attachment but does not have one. Whether a message needs an attachment is determined by testing a configurable regex against the message body. Signed-off-by: Jason Cox <dev@jasoncarloscox.com> Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
parent
8ffcd3e5ad
commit
7647dfb8b4
7 changed files with 106 additions and 6 deletions
|
@ -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
|
- Support for attaching files with `mailto:`-links
|
||||||
- Filter commands now have the `AERC_MIME_TYPE` and `AERC_FILENAME` variables
|
- Filter commands now have the `AERC_MIME_TYPE` and `AERC_FILENAME` variables
|
||||||
defined in their environment.
|
defined in their environment.
|
||||||
|
- Warn before sending emails that may need an attachment with
|
||||||
|
`no-attachment-warning` in `aerc.conf`.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -100,6 +100,40 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
rcpts: rcpts,
|
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
|
// 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
|
// so we do everything in a goroutine and hide the composer from the user
|
||||||
aerc.RemoveTab(composer)
|
aerc.RemoveTab(composer)
|
||||||
|
@ -109,6 +143,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
mode.NoQuit()
|
mode.NoQuit()
|
||||||
|
|
||||||
var copyBuf bytes.Buffer // for the Sent folder content if CopyTo is set
|
var copyBuf bytes.Buffer // for the Sent folder content if CopyTo is set
|
||||||
|
config := composer.Config()
|
||||||
|
|
||||||
failCh := make(chan error)
|
failCh := make(chan error)
|
||||||
// writer
|
// writer
|
||||||
|
@ -116,6 +151,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
defer logging.PanicHandler()
|
defer logging.PanicHandler()
|
||||||
|
|
||||||
var sender io.WriteCloser
|
var sender io.WriteCloser
|
||||||
|
var err error
|
||||||
switch ctx.scheme {
|
switch ctx.scheme {
|
||||||
case "smtp":
|
case "smtp":
|
||||||
fallthrough
|
fallthrough
|
||||||
|
@ -151,7 +187,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
// leave no-quit mode
|
// leave no-quit mode
|
||||||
defer mode.NoQuitDone()
|
defer mode.NoQuitDone()
|
||||||
|
|
||||||
err = <-failCh
|
err := <-failCh
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aerc.PushError(strings.ReplaceAll(err.Error(), "\n", " "))
|
aerc.PushError(strings.ReplaceAll(err.Error(), "\n", " "))
|
||||||
aerc.NewTab(composer, tabName)
|
aerc.NewTab(composer, tabName)
|
||||||
|
@ -176,7 +212,6 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
composer.SetSent()
|
composer.SetSent()
|
||||||
composer.Close()
|
composer.Close()
|
||||||
}()
|
}()
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func listRecipients(h *mail.Header) ([]*mail.Address, error) {
|
func listRecipients(h *mail.Header) ([]*mail.Address, error) {
|
||||||
|
|
|
@ -297,6 +297,18 @@ address-book-cmd=
|
||||||
# Default: true
|
# Default: true
|
||||||
reply-to-self=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]
|
||||||
#
|
#
|
||||||
# Filters allow you to pipe an email body through a shell command to render
|
# Filters allow you to pipe an email body through a shell command to render
|
||||||
|
|
|
@ -203,10 +203,11 @@ type BindingConfigContext struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComposeConfig struct {
|
type ComposeConfig struct {
|
||||||
Editor string `ini:"editor"`
|
Editor string `ini:"editor"`
|
||||||
HeaderLayout [][]string `ini:"-"`
|
HeaderLayout [][]string `ini:"-"`
|
||||||
AddressBookCmd string `ini:"address-book-cmd"`
|
AddressBookCmd string `ini:"address-book-cmd"`
|
||||||
ReplyToSelf bool `ini:"reply-to-self"`
|
ReplyToSelf bool `ini:"reply-to-self"`
|
||||||
|
NoAttachmentWarning *regexp.Regexp `ini:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilterConfig struct {
|
type FilterConfig struct {
|
||||||
|
@ -523,6 +524,18 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
||||||
if key == "header-layout" {
|
if key == "header-layout" {
|
||||||
config.Compose.HeaderLayout = parseLayout(val)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -493,6 +493,20 @@ These options are configured in the *[compose]* section of aerc.conf.
|
||||||
|
|
||||||
Default: true
|
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
|
||||||
|
|
||||||
Filters allow you to pipe an email body through a shell command to render
|
Filters allow you to pipe an email body through a shell command to render
|
||||||
|
|
|
@ -598,6 +598,10 @@ func (aerc *Aerc) BeginExCommand(cmd string) {
|
||||||
aerc.focus(exline)
|
aerc.focus(exline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (aerc *Aerc) PushPrompt(prompt *ExLine) {
|
||||||
|
aerc.prompts.Push(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
|
func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
|
||||||
p := NewPrompt(aerc.conf, prompt, func(text string) {
|
p := NewPrompt(aerc.conf, prompt, func(text string) {
|
||||||
if text != "" {
|
if text != "" {
|
||||||
|
|
|
@ -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 {
|
func writeMsgImpl(c *Composer, header *mail.Header, writer io.Writer) error {
|
||||||
if len(c.attachments) == 0 && len(c.textParts) == 0 {
|
if len(c.attachments) == 0 && len(c.textParts) == 0 {
|
||||||
// no attachments
|
// no attachments
|
||||||
|
|
Loading…
Reference in a new issue