1"""Netscape Messaging Server bounce formats.
2
3I've seen at least one NMS server version 3.6 (envy.gmp.usyd.edu.au) bounce
4messages of this format.  Bounces come in DSN MIME format, but don't include
5any -Recipient: headers.  Gotta just parse the text :(
6
7NMS 4.1 (dfw-smtpin1.email.verio.net) seems even worse, but we'll try to
8decipher the format here too.
9
10"""
11
12import re
13
14from flufl.bounce.interfaces import (
15    IBounceDetector, NoFailures, NoTemporaryFailures)
16from io import BytesIO
17from public import public
18from zope.interface import implementer
19
20
21pcre = re.compile(
22    b'This Message was undeliverable due to the following reason:',
23    re.IGNORECASE)
24
25acre = re.compile(
26    b'(?P<reply>please reply to)?.*<(?P<addr>[^>]*)>',
27    re.IGNORECASE)
28
29
30def flatten(msg, leaves):
31    # Give us all the leaf (non-multipart) subparts.
32    if msg.is_multipart():
33        for part in msg.get_payload():
34            flatten(part, leaves)
35    else:
36        leaves.append(msg)
37
38
39@public
40@implementer(IBounceDetector)
41class Netscape:
42    """Netscape Messaging Server bounce formats."""
43
44    def process(self, msg):
45        """See `IBounceDetector`."""
46
47        # Sigh.  Some NMS 3.6's show
48        #     multipart/report; report-type=delivery-status
49        # and some show
50        #     multipart/mixed;
51        if not msg.is_multipart():
52            return NoFailures
53        # We're looking for a text/plain subpart occuring before a
54        # message/delivery-status subpart.
55        plainmsg = None
56        leaves = []
57        flatten(msg, leaves)
58        for i, subpart in zip(range(len(leaves)-1), leaves):
59            if subpart.get_content_type() == 'text/plain':
60                plainmsg = subpart
61                break
62        if not plainmsg:
63            return NoFailures
64        # Total guesswork, based on captured examples...
65        body = BytesIO(plainmsg.get_payload(decode=True))
66        addresses = set()
67        for line in body:
68            mo = pcre.search(line)
69            if mo:
70                # We found a bounce section, but I have no idea what the
71                # official format inside here is.  :( We'll just search for
72                # <addr> strings.
73                for line in body:
74                    mo = acre.search(line)
75                    if mo and not mo.group('reply'):
76                        addresses.add(mo.group('addr'))
77        return NoTemporaryFailures, addresses
78