1"""This appears to be the format for Novell GroupWise and NTMail
2
3X-Mailer: Novell GroupWise Internet Agent 5.5.3.1
4X-Mailer: NTMail v4.30.0012
5X-Mailer: Internet Mail Service (5.5.2653.19)
6"""
7
8import re
9
10from email.message import Message
11from flufl.bounce.interfaces import (
12    IBounceDetector, NoFailures, NoTemporaryFailures)
13from io import BytesIO
14from public import public
15from zope.interface import implementer
16
17
18acre = re.compile(b'<(?P<addr>[^>]*)>')
19
20
21def find_textplain(msg):
22    if msg.get_content_type() == 'text/plain':
23        return msg
24    if msg.is_multipart:
25        for part in msg.get_payload():
26            if not isinstance(part, Message):
27                continue
28            ret = find_textplain(part)
29            if ret:
30                return ret
31    return None
32
33
34@public
35@implementer(IBounceDetector)
36class GroupWise:
37    """Parse Novell GroupWise and NTMail bounces."""
38
39    def process(self, msg):
40        """See `IBounceDetector`."""
41        if msg.get_content_type() != 'multipart/mixed' or not msg['x-mailer']:
42            return NoFailures
43        if msg['x-mailer'][:3].lower() not in ('nov', 'ntm', 'int'):
44            return NoFailures
45        addresses = set()
46        # Find the first text/plain part in the message.
47        text_plain = find_textplain(msg)
48        if text_plain is None:
49            return NoFailures
50        body = BytesIO(text_plain.get_payload(decode=True))
51        for line in body:
52            mo = acre.search(line)
53            if mo:
54                addresses.add(mo.group('addr'))
55            elif b'@' in line:
56                i = line.find(b' ')
57                if i == 0:
58                    continue
59                if i < 0:
60                    addresses.add(line)
61                else:
62                    addresses.add(line[:i])
63        return NoTemporaryFailures, set(addresses)
64