python content_filter for Postfix (rewriting the subject)

A python script which can be used as a Postfix content_filter to headers email going through it … (example shows changing the Subject).

To use, you’d need to add a content_filter argument to one of the services defined in master.cf as described here

#!/usr/bin/python
from email import Parser
import smtplib
import sys
import logging
from subprocess import Popen, PIPE
logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)s %(message)s',
                    filename='/tmp/content-filter.log',
                    filemode='a')

# Get the CLI arguments.
try:
    cli_from = sys.argv[2].lower()
    cli_to = sys.argv[4:]
    logging.debug("To / From : %r" % sys.argv)
except:
    logging.error("Invalid to / from : %r" % sys.argv)
    sys.exit(69) # postfix will bounce the mail. retrying bad args won't work

logging.debug("From : %s, to : %r" % (cli_from, cli_to))
# Get the email content from STDIN.
content = ''.join(sys.stdin.readlines())
p = Parser.Parser()
parsed = p.parsestr(content, True)
#logging.debug("email source : %s" % parsed.as_string())

old_subject = parsed.get('Subject');

# remove the old header, and add a new one.
del parsed['subject']
parsed['Subject'] = "New Subject -- " + old_subject

# convert it back to a big string.
content = str(parsed)
# and let's try reinjecting it into Postfix.
command = ["/usr/sbin/sendmail", "-G", "-i", "-f", cli_from, cli_to]
stdout = ''
stderr = ''
retval = 0
try :
    process = Popen(command, stdin=PIPE)
    (stdout, stderr) = process.communicate(content);
    retval = process.wait()
    if retval == 0:
        logging.debug("Mail resent via sendmail, stdout: %s, stderr: %s" % (stdout, stderr))
        sys.exit(0)    
    else:
        raise Exception("retval not zero - %s" % retval)
except Exception, e:
    print "Error re-injecting via /usr/sbin/sendmail."
    logging.error("Error resending mail %s -- stdout:%s, stderr:%s, retval: %s" % (e, stdout, stderr, retval))
    sys.exit(75) # tempfail, we hope.

8 Replies to “python content_filter for Postfix (rewriting the subject)”

  1. getting this error trying this as a “cat FILE | ./filt.py -f — <from"

    ERROR Error resending mail execv() arg 2 must contain only strings — stdout:, stderr:, retval: 0

    Ideas?

  2. Hi,

    What i need write to master.cf and main.cf ? How to run this script ?

  3. Im having issues getting this to work as expected…. this is my config at the moment, not sure if its right but i tried to make it from what i found around on the net…

    master.cf
    10025 inet n – n – – smtpd
    -o receive_override_options=no_header_body_checks
    -o smtpd_recipient_restrictions=${my_switcher_restrictions}
    policy unix – n n – 0 spawn user=nobody argv=/etc/postfix/rewrite_filter

    main.cf
    my_switcher_restrictions = check_policy_service unix:private/policy

    header_checks
    /^X-Relay-Domain: relay.server.com / FILTER smtp:127.0.0.1:10025

    rewrite_filter
    #!/usr/bin/python
    from email import Parser

    import smtplib
    import sys
    import logging
    import re

    logging.basicConfig(level=logging.DEBUG,
    format=’%(asctime)s %(levelname)s %(message)s’,
    filename=’/tmp/content-filter.log’,
    filemode=’a’)

    # Get the CLI arguments.
    try:
    cli_from = sys.argv[2].lower()
    cli_to = sys.argv[4:]
    logging.debug(“To / From : %r” % sys.argv)
    except:
    logging.error(“Invalid to / from : %r” % sys.argv)
    sys.exit(69) # postfix will bounce the mail. retrying bad args won’t work

    #logging.debug(“From : %s, to : %r” % (cli_from, cli_to))
    # Get the email content from STDIN.
    content = ”.join(sys.stdin.readlines())
    p = Parser.Parser()
    parsed = p.parsestr(content, True)
    logging.debug(“email source : %s” % parsed.as_string())

    old_received = parsed.get(‘Received’);
    search_string = re.compile(ur’for ‘, re.MULTILINE)

    new_to = re.search(search_string, old_received)

    # remove the old header, and add a new one.
    del parsed[‘To’]
    del parsed[‘X-Relay-Domain’]
    parsed[‘To’] = new_to

    # convert it back to a big string.
    content = str(parsed)
    # and let’s try reinjecting it into Postfix.
    command = [“/usr/sbin/sendmail”, “-G”, “-i”, “-f”, cli_from, cli_to]
    stdout = ”
    stderr = ”
    retval = 0
    try :
    process = Popen(command, stdin=PIPE)
    (stdout, stderr) = process.communicate(content);
    retval = process.wait()
    if retval == 0:
    logging.debug(“Mail resent via sendmail, stdout: %s, stderr: %s” % (stdout, stderr))
    sys.exit(0)
    else:
    raise Exception(“retval not zero – %s” % retval)
    except Exception, e:
    print “Error re-injecting via /usr/sbin/sendmail.”
    logging.error(“Error resending mail %s — stdout:%s, stderr:%s, retval: %s” % (e, stdout, stderr, retval))
    sys.exit(75) # tempfail, we hope.

    the X-Relay-Domain is set on specific emails, and in these cases im trying to pull the “for” email from the Received header (needed as the to field is wrong because of other manipulation im doing)

    there are a few issues, the arguments are not set by default, so im not sure how to set them correctly, if i bypass the arguments, the error log contains this and i cant change the values in the header i want to change, note im trying to do this as part of the header_checks, not sure if that will even work…:

    DEBUG email source :
    request=smtpd_access_policy
    protocol_state=RCPT
    protocol_name=ESMTP
    client_address=127.0.0.1
    client_name=localhost
    client_port=53555
    reverse_client_name=localhost
    helo_name=mx1.server.com
    sender=sender@server.com
    recipient=recipient@gmail.com
    recipient_count=0
    queue_id=
    instance=7fbc.570609ea.237fd.0
    size=784
    etrn_domain=
    stress=
    sasl_method=
    sasl_username=
    sasl_sender=
    ccert_subject=
    ccert_issuer=
    ccert_fingerprint=
    ccert_pubkey_fingerprint=
    encryption_protocol=TLSv1.2
    encryption_cipher=AECDH-AES256-SHA
    encryption_keysize=256

    Any help would be much appreciated 🙂

  4. Thanks for that code, that was helpful. I inspired myself from it to create the following boilerplate code: https://gist.github.com/Xowap/e998dbdf9cf8022b7a7b99503599a16b

    All you have to do (besides configuring Postfix) is to fill the “apply_filter()” function which takes as arguments the from email, list of “to” emails and the content of the email as a “email.message.Message” instance. The function is supposed to return filtered output in the same order.

  5. not sure if you forgot this or its something with the python version your using. but i had to add this to the top of my python file:
    from subprocess import Popen, PIPE

Leave a Reply

Your email address will not be published. Required fields are marked *