Re: Some parsing/generation issues of email in Python 3

Hans-Peter Jansen <[email protected]> Sun, 12 Jun 2016 16:22:28 +0200
Newsgroups gmane.comp.python.mime.devel
Message-ID <7800289.qb3L3Yz5NG@xrated>
This is a multi-part message in MIME format.

--nextPart1804458.c4oWNNuu3I
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset="iso-8859-1"

[This mail is intentionally hand wrapped..]

On Freitag, 10. Juni 2016 17:54:34 Hans-Peter Jansen wrote:
> On Donnerstag, 9. Juni 2016 18:37:54 Hans-Peter Jansen wrote:
> >=20
> > Let's see, how this goes.
>=20
> Hmm, compat32 and Python3 start to get in my way in no funny ways.

Okay, got it working now. It turned out to be a problem in the logging =
module.=20
Unfortunately, it's not reproducible outside postfix mail filter setup.=

=09
=09http://www.postfix.org/FILTER_README.html

I will try to explain this in short words, but you will not believe me,=
 as I=20
have a hard time to believe this myself..

When a python3 process is used in a simple mail filter setup, as descri=
bed in=20
the FILTER_README.html document, it is executed in a stripped down envi=
ronment.=20
As relevant parts, just LANG=3DC and PATH=3D/bin;/usr/bin is set.

The filter reads the mail from stdin, and calls sendmail for passing it=
 on,=20
again, with the mail on stdin..

Now, it takes a mail with some "higher" encoding (I'm using a utf-8 enc=
oded=20
subject containing german umlauts), and an attempt to log the subject l=
ine=20
(which has to be a log file for obvious reasons). Now take a save seat,=
 this=20
attempt of unicode logging results in a manipulation of the execution f=
rame,=20
the execution precedes a few instructions "below". Yes, I'm not kidding=
, no=20
escaped surrogates output, no error message, just no logging of the off=
ending=20
line, and this "esoteric" behavior. Be assured, that if I would still h=
ave=20
enough hair on my head, I would have teared it off completely by now.

This is reproducible for Python 3.4.4 on openSUSE 13.2/x86_64 here.=20

For the brave, who want to reproduce/investigate this issue, I'm attach=
ing=20
everything necessary. All others should stop reading now. Thank you.

Still with me, here we go: [$: root prompt]

A working postfix setup is implied. Stop all usual processing (fetchmai=
l, ...)

$ useradd --gid mail mfilter
$ cat >> /etc/postfix/master.cf << EOF
mfilter   unix  -       n       n       -       1       pipe
  flags=3DRq user=3Dmfilter argv=3D/path/to/mail_filter_test.py -f ${se=
nder} -- ${recipient}
EOF

$ systemctl restart postfix
$ sendmail -f [email protected] [email protected] < umlaut-subject-2.=
mail
$ less +F /tmp/mail_filter_test.log

Defective output:
2016-06-12 15:36:28,540 [mail_filter_test] DEBUG: parse message
2016-06-12 15:36:28,543 [mail_filter_test] DEBUG: call ['/usr/sbin/send=
mail', '-G', '-i', '-f', '[email protected]', '--', '[email protected]=
s']

Output with SILLY_BEHAVIOR =3D 0:
2016-06-12 15:37:50,887 [mail_filter_test] DEBUG: parse message
2016-06-12 15:37:50,889 [mail_filter_test] DEBUG: subject: Wie deaktivi=
ere oder l=F6sche ich meine SprachBox IP der Telekom?
2016-06-12 15:37:50,890 [mail_filter_test] DEBUG: call ['/usr/sbin/send=
mail', '-G', '-i', '-f', '[email protected]', '--', '[email protected]=
s']

Note, that the subject line is missing. In my real filter, it left the=20=

current execution frame, and execution continued one or two level up th=
e
stack.

This reminds me at my assembler times (680x0 power, long ago!), where I=
 used
such tricks like modifying the program counter in "very special arrange=
ments".

I don't think, this is an adequate outcome of the attached code, do you=
?

I'm directing this here, while I know, this is quite off-topic in the r=
esult.
OTOH, Stephen and David discussed the LANG=3DC issues with Victor, and =
this=20
is one example, where this is very relevant to Python3 fitness to act a=
s=20
such a filter. Apart from fixing the logging, I'm encoding all paths an=
d file=20
names with a configurable encoding before calling the OS. Butt ugly, bu=
t=20
feasible.

I hope, that at least one of you is able to reproduce this, before we d=
ecide,
how to precede. I hope, I don't offend anybody here with that approach.=


Please speak up, if I should go away and search for another tree to bar=
k at.

Thanks,
Pete
--nextPart1804458.c4oWNNuu3I
Content-Disposition: attachment; filename="mail_filter_test.py"
Content-Transfer-Encoding: 7Bit
Content-Type: text/x-python; charset="UTF-8"; name="mail_filter_test.py"

#!/usr/bin/env python3

import sys
import getopt
import logging
import subprocess

import email
import email.policy
import email.header
import email.generator

logfile = '/tmp/mail_filter_test.log'
logformat = '%(asctime)s [%(name)s] %(levelname)5s: %(message)s'
encoding = 'utf-8'

# setup logging
log = logging.getLogger('mail_filter_test')

EX_TEMPFAIL = 75        # queue and retry
SILLY_BEHAVIOR = 1

if SILLY_BEHAVIOR:
    logging.basicConfig(
        level = logging.DEBUG,
        format = logformat,
        filename = logfile,
    )
else:
    log.setLevel(logging.DEBUG)
    filelog = logging.FileHandler(logfile, encoding = encoding)
    filelog.setLevel(logging.DEBUG)
    filelog.setFormatter(logging.Formatter(logformat))
    log.addHandler(filelog)


def decode_header(value):
    if value is not None:
        value = str(email.header.make_header(email.header.decode_header(value)))
    return value


def mail_filter(sender, recipients):
    log.debug('parse message')
    msg = email.message_from_binary_file(sys.stdin.buffer, policy = email.policy.compat32)
    if not sender:
        sender = msg.get('from')
    if not recipients:
        recipients = msg.get_all('to')
    subject = decode_header(msg.get('subject'))
    log.debug('subject: %s', subject)
    sendmail = ['/usr/sbin/sendmail', '-G', '-i', '-f', sender, '--'] + recipients
    log.debug('call %s', sendmail)
    p = subprocess.Popen(sendmail, stdin = subprocess.PIPE)
    email.generator.BytesGenerator(p.stdin).flatten(msg)
    p.stdin.close()
    return p.wait()


if __name__ == '__main__':
    try:
        optlist, recipients = getopt.getopt(sys.argv[1:], 'f:')
    except getopt.error as msg:
        print(msg, file = sys.stderr, flush = True)
        sys.exit(EX_TEMPFAIL)

    sender = None
    for opt, par in optlist:
        if opt in ('-f', '--from'):
            sender = par

    sys.exit(mail_filter(sender, recipients))


--nextPart1804458.c4oWNNuu3I
Return-Path: <[email protected]>
From: Hans-Peter Jansen <[email protected]>
To: Hans-Peter Jansen <[email protected]>
Subject: Wie deaktiviere oder =?UTF-8?B?bMO2c2NoZQ==?= ich meine SprachBox IP
 der Telekom?
Date: Sat, 11 Jun 2016 13:23:09 +0200
Content-Transfer-Encoding: 7Bit
Content-Type: text/plain; charset="us-ascii"

https://www.telekom.de/hilfe/festnetz-internet-tv/telefonieren-einstellungen/sprachbox/sprachbox-ip/sprachbox-ip-deaktivieren-oder-loeschen?samChecked=true

--nextPart1804458.c4oWNNuu3I
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline