Sending emails with a Reply-To header

We have a client that is part of a larger government organization, which essentially means using the SMTP server with their government email address is not an option. What we'd like to do is configure the mail settings to use a Reply-To header in the message, and otherwise use our company's SMTP server with a relevant email address for the From header.

This would be trivial to do for any given form handler that sends email, but the problem as I see it is there's no central place to change this because Plone/Zope's configuration just does not have a concept for it. (On a lower level, this might be because python's smtplib API has to/from/message parameters, and Reply-To needs to be part of the message). I don't like the idea of monkey patching every place that Plone out-of-the-box sends email. One of the other options I am thinking of is to monkey patch either Products.MailHost or zope.sendmail to always take the From address and inject it in the message as a Reply-To header, and then change the From name to an address that is associated with our SMTP server. This doesn't appeal to me either, because it is probably error prone and also because zope.sendmail looks like it is in active development. Anyone have a better idea?

Monkey patching likely what you want. The mentioned modules are sooooo old...I would not care about any possible side effects.

You can move the construction in a seperate function

from email.mime.multipart import MIMEMultipart
from email.utils import formataddr

def getConstructedMessageHeader(data):
    msg = MIMEMultipart('alternative')
    msg['To'] = data['email_to']
    msg['From'] = formataddr((data['name'], data['email_from']))
    msg['Subject'] = data['subject']
    msg['reply-to'] = data['email_reply_to']
    msg['Return-Path'] = data['return_to']
    

# do somewhere in your codebase:

msg = getConstructedMessageHeader("""your specified data""")

api.portal.send_email(
    recipient="",
    sender=msg['From'],
    subject=msg['Subject'],
    body=msg,
    immediate=False)
1 Like

Send your mails via an MTA you control and insert headers there?

1 Like

Something about this answer feels right. For scale (multiple agencies and reply-to fields) it's the right answer.

But setting up an MTA for one customer? Can it be done that quickly and be a 'set it and forget it' problem that keeps one customer happy? Are there existing use cases with other customers that can leverage the new MTA?

If it's a one-off, I like Jan's answer because it's self-documenting in the customer's code. I would only go MTA if you can gain extra value out of it other than one customer.

Feels like the "customer asks for a nail to be put here, and I built a nailgun factory for them" kind of answer, which is one of my shortcomings, so I know it well.

Just my opinion.

I've been looking into doing a monkey patch instead. There are a lot of different cases I've encountered already, so work in progress (Products.MailHost.MailHost.MailBase) using monkey:patch zcml with preserveOriginal=True:

def send(self, messageText, mto=None, mfrom=None, subject=None, encode=None, immediate=False, charset=None,
     msg_type=None, ):
""" Instead of using the mail-from address in the mail settings, always use an IMS email. However, use
    a Reply-To header that uses the mail setting's "from" address
"""
try:
    from_address = api.portal.get_registry_record('ims.from_email_address')
except api.exc.InvalidParameterError:
    return self._old_send(messageText, mto, mfrom, subject, encode, immediate, charset, msg_type)
else:
    if not from_address:
        return self._old_send(messageText, mto, mfrom, subject, encode, immediate, charset, msg_type)

    if isinstance(messageText, six.string_types):
        messageText = MIMEText(messageText, 'plain', encode)

    if isinstance(messageText, MIMEBase):
        if 'Reply-To' not in messageText:
            messageText['Reply-To'] = mfrom
    mfrom = from_address
    return self._old_send(messageText, mto, mfrom, subject, encode, immediate, charset, msg_type)

messageText can come in as plain text, MIMEText, MIMEMultipart (etc?). If it's plain text I convert it to MIMEText so I can easily add the Reply-To header. It looks like it doesn't matter if it's multipart or one non-multi, the header goes in the same place so I just check for MIMEBase. If it's neither MIMEBase nor string I have no idea what it is so I don't try to do anything other than change the mfrom variable. Finally, some forms like the Contact form have already set a Reply-To header and it does not make sense to change that.

Continued concerns with this approach: writing tests for this looks hard; might be able to adapt what is already in MailHosts's tests. Also concerned about edge cases with messageText data types. The MTA option is mostly beyond me. I'll ask our systems team what they think.

Agree that the MTA may be overkill if you will just be using it for processing a single client's Plone mails.

The option is probably worth exploring if you also want to do DKIM signing, add spf headers and things like that to improve deliverability. In such a case, you could set up qmail with virtualdomains to process your plone-outbound mails.

The qmail setup itself can be a bit of work, but is mostly fire-and-forget once set up the way you want it... I have been doing this a few times using Roberto Puzzanghera's qmail notes.