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