Well, the previous attempt to solve this problem was a bust, for the following reasons:

  • The template only applied to new mails, not replies
  • The round trip thing where messages had to get filed on the server, then pulled down again by mbsync was slow and annoying

After chatting with folks on the #aerc IRC channel, I've learned that my mail provider (Mailbox.org) is doing me a great disservice by not implementing anything server-side for automatically saving mails sent via SMTP into an IMAP Sent folder. Mailbox.org's official stance suggests that this is intentional:

Saving a Mail to Sent must be done by the mailclient. This is not triggered by the server. Please check your settings in your mailclient.

Having this done server-side means that all sent mails via SMTP would be saved, regardless of the client used to save them. SMTP clients (understandably...) have no concept of IMAP or saving things in IMAP folders.

It was mentioned in the #aerc channel that aerc can use an arbitrary command to send mails, and indeed the manpage for aerc-sendmail specifies that aerc will pass the message content over stdin, and pass a list of recipients to the command as arguments. So this second attempt creates a wrapper for msmtp (or whatever you want to use for a SMTP client), copies the sent mail to your maildir's sent folder, and then forwards it to your SMTP client:

#!/usr/bin/env python3
import argparse
import mailbox
import os
import subprocess
import sys


# read stdin early so pdb can be used later
message = sys.stdin.read()
sys.stdin = open("/dev/tty")

ap = argparse.ArgumentParser(description=('Save sent mail before forwarding '
                                          'to smtp client.'))
ap.add_argument('--smtp_exe', '-e', action='store', default='/usr/bin/msmtp',
                help=('SMTP client to run for sending message '
                      '[defaut: %(default)s]'))
ap.add_argument('--maildir', '-m', action='store', default='~/.mail',
                help=('Maildir to copy sent mail into '
                      '[defaut: %(default)s]'))
ap.add_argument('--sent-dir', '-s', action='store', default='sent',
                help=('Directory in mailbox to copy sent mail to '
                      '[defaut: %(default)s]'))
ap.add_argument('recipients', nargs='+')
args = ap.parse_args()

maildir = os.path.expanduser(args.maildir)

sent_path = os.path.join(maildir, args.sent_dir)
if not os.path.exists(sent_path):
    raise RuntimeError(f"ERROR: path does not exist: {sent_path}")

mbox = mailbox.Maildir(sent_path)
mbox.lock()
try:
    msg = mailbox.MaildirMessage(message)
    msg.set_subdir('cur')
    msg.set_flags('S')
    mbox.add(msg)
except Exception as e:
    print(f"Error saving mail: {e}")
finally:
    mbox.flush()
    mbox.unlock()

# send mail using smtp client
cmd = [args.smtp_exe] + args.recipients
p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)

out, err = p.communicate(input=message.encode())
ret = p.returncode
if ret:
    print(err.decode())
else:
    print(out)

Update ~/.config/aerc/accounts.conf to use this command for outgoing:

outgoing          = ~/bin/sendmail -m ~/Mail

I use the following notmuch tagging to +sent to mails, while keeping a +inbox tag to mails I sent to myself:

+sent -- from:<my email here>
-inbox -- tag:inbox and from:<my email here> and not to:<my email here>

The filter I set up in my mail provider's config has been dropped.

Now when I send mails in aerc, they are piped through the wrapper above, which saves a copy of the mail in my maildir's sent subdirectory, and forwards it on to the SMTP server using an external SMTP client (msmtp in my case). The sent mail is then copied to the IMAP Sent folder when mbsync runs, so a copy of the sent mail is then stored in IMAP, which in turn is accessible by any IMAP clients I use.

I hope there's not a part 3 to this saga.