Postfix.py 2.7 KB
Newer Older
Barry Warsaw's avatar
Barry Warsaw committed
1
# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
bwarsaw's avatar
bwarsaw committed
2
#
Barry Warsaw's avatar
Barry Warsaw committed
3
# This file is part of GNU Mailman.
bwarsaw's avatar
bwarsaw committed
4
#
Barry Warsaw's avatar
Barry Warsaw committed
5 6 7 8
# GNU Mailman is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
bwarsaw's avatar
bwarsaw committed
9
#
Barry Warsaw's avatar
Barry Warsaw committed
10 11 12 13 14 15 16
# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# GNU Mailman.  If not, see <http://www.gnu.org/licenses/>.
bwarsaw's avatar
bwarsaw committed
17

18 19 20 21 22 23
"""Parse bounce messages generated by Postfix.

This also matches something called `Keftamail' which looks just like Postfix
bounces with the word Postfix scratched out and the word `Keftamail' written
in in crayon.

twouters's avatar
 
twouters committed
24
It also matches something claiming to be `The BNS Postfix program', and
bwarsaw's avatar
bwarsaw committed
25
`SMTP_Gateway'.  Everybody's gotta be different, huh?
26
"""
bwarsaw's avatar
bwarsaw committed
27 28

import re
bwarsaw's avatar
bwarsaw committed
29
from cStringIO import StringIO
bwarsaw's avatar
bwarsaw committed
30 31 32



bwarsaw's avatar
bwarsaw committed
33 34
def flatten(msg, leaves):
    # give us all the leaf (non-multipart) subparts
bwarsaw's avatar
bwarsaw committed
35
    if msg.is_multipart():
bwarsaw's avatar
bwarsaw committed
36 37 38 39
        for part in msg.get_payload():
            flatten(part, leaves)
    else:
        leaves.append(msg)
bwarsaw's avatar
bwarsaw committed
40 41 42 43



# are these heuristics correct or guaranteed?
twouters's avatar
 
twouters committed
44 45
pcre = re.compile(r'[ \t]*the\s*(bns)?\s*(postfix|keftamail|smtp_gateway)',
                  re.IGNORECASE)
46
rcre = re.compile(r'failure reason:$', re.IGNORECASE)
bwarsaw's avatar
bwarsaw committed
47 48
acre = re.compile(r'<(?P<addr>[^>]*)>:')

bwarsaw's avatar
bwarsaw committed
49
def findaddr(msg):
50
    addrs = []
bwarsaw's avatar
bwarsaw committed
51
    body = StringIO(msg.get_payload())
bwarsaw's avatar
bwarsaw committed
52 53 54 55 56
    # simple state machine
    #     0 == nothing found
    #     1 == salutation found
    state = 0
    while 1:
bwarsaw's avatar
bwarsaw committed
57
        line = body.readline()
bwarsaw's avatar
bwarsaw committed
58
        if not line:
59
            break
bwarsaw's avatar
bwarsaw committed
60
        # preserve leading whitespace
bwarsaw's avatar
bwarsaw committed
61
        line = line.rstrip()
bwarsaw's avatar
bwarsaw committed
62
        # yes use match to match at beginning of string
63
        if state == 0 and (pcre.match(line) or rcre.match(line)):
bwarsaw's avatar
bwarsaw committed
64 65 66 67
            state = 1
        elif state == 1 and line:
            mo = acre.search(line)
            if mo:
68 69
                addrs.append(mo.group('addr'))
            # probably a continuation line
bwarsaw's avatar
bwarsaw committed
70 71 72 73 74
    return addrs



def process(msg):
75
    if msg.get_content_type() not in ('multipart/mixed', 'multipart/report'):
bwarsaw's avatar
bwarsaw committed
76 77 78 79 80 81
        return None
    # We're looking for the plain/text subpart with a Content-Description: of
    # `notification'.
    leaves = []
    flatten(msg, leaves)
    for subpart in leaves:
82
        if subpart.get_content_type() == 'text/plain' and \
bwarsaw's avatar
bwarsaw committed
83 84 85 86
           subpart.get('content-description', '').lower() == 'notification':
            # then...
            return findaddr(subpart)
    return None