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