+ content = message.get_payload(1)
+ # The content may be missing, but interesting headers still present in the first payload...
+ if not content:
+ content = message.get_payload(0)
+ if 'Action' not in content:
+ print('! Not a valid weird bounce (unable to find content).')
+ return None
+ elif content.get_content_type() != 'text/plain':
+ print('! Not a valid weird bounce (expected text/plain, found %s).' % content.get_content_type())
+ return None
+
+ # Extract the faulty email address
+ if 'Final-Recipient' in content:
+ recipient_match = _recipient_re.search(content['Final-Recipient'])
+ if recipient_match is None:
+ # Be nice, test another regexp
+ recipient_match = _recipient_re2.search(content['Final-Recipient'])
+ if recipient_match is None:
+ print('! Unknown final recipient in weird bounce.')
+ return None
+ email = recipient_match.group(1)
+ elif 'Original-Recipient' in content:
+ recipient = content['Original-Recipient']
+ recipient_match = _recipient_re.search(recipient)
+ if recipient_match is None:
+ # Be nice, test another regexp
+ recipient_match = _recipient_re2.search(recipient)
+ if recipient_match is None:
+ recipient_match = re.match(r'<([^>]+@[^@>]+)>', recipient)
+ if recipient_match is None:
+ print('! Unknown original recipient in weird bounce.')
+ return None
+ email = recipient_match.group(1)
+ else:
+ print('! Missing recipient in weird bounce.')
+ return None
+
+ # Check the action field
+ if content['Action'].lower() != 'failed':
+ print('! Not a failed action (%s).' % content['Action'])
+ return None
+
+ status = content['Status']
+ diag_code = content['Diagnostic-Code']
+
+ # Permanent failure state
+ if status and int(status[:1]) == 5:
+ return email
+
+ # Mail forwarding loops, DNS errors and connection timeouts cause X-Postfix errors
+ if diag_code is not None and diag_code.startswith('X-Postfix'):
+ return email
+
+ failure_hints = [
+ "insufficient system storage",
+ "mailbox full",
+ "requested action aborted: local error in processing",
+ "sender address rejected",
+ "user unknown",
+ ]
+ if status and 'quota' in status.lower():
+ return email
+ if diag_code is not None:
+ ldiag_code = diag_code.lower()
+ if any(hint in ldiag_code for hint in failure_hints):
+ return email
+
+ print('! Not a permanent failure status (%s).' % status)
+ if diag_code is not None:
+ print('! Diagnostic code was: %s' % diag_code)
+ return None
+
+
+def findAddressInPlainBounce(bounce, real_bounce=None):
+ """Finds the faulty email address in a non-RFC-1894 bounced email
+ """
+ # real_bounce is the full email and bounce only the text/plain part, if email have several MIME parts
+ real_bounce = real_bounce or bounce
+ lower_from = real_bounce['From'].lower()
+ if 'mailer-daemon@' not in lower_from and 'postmaster' not in lower_from:
+ print('! Not a valid plain bounce (expected from MAILER-DAEMON or postmaster, found %s).' % bounce['From'])
+ return None
+ if bounce.get_content_type() != 'text/plain':
+ print('! Not a valid plain bounce (expected text/plain, found %s).' % bounce.get_content_type())
+ return None
+ subject = findSubject(real_bounce).lower()
+ known_subjects = [
+ "delivery status notification (failure)",
+ "failure notice",
+ "mail delivery failure",
+ "returned mail: see transcript for details",
+ "undeliverable message",
+ "undelivered mail returned to sender",
+ ]
+ if subject not in known_subjects and not subject.startswith('mail delivery failed'):
+ print('! Not a valid plain bounce (unknown subject: %s).' % subject)
+ return None
+
+ # Read the 15 first lines of content and find some relevant keywords to validate the bounce
+ lines = bounce.get_payload().splitlines()[:15]
+
+ # ALTOSPAM is a service which requires to click on a link when sending an email
+ # Don't consider the "554 5.0.0 Service unavailable" returned by ALTOSPAM as a failure
+ # but put this message in the dsn-temp mailbox so that it can be processed by hand.
+ if any("ALTOSPAM which is used by the person" in line for line in lines):
+ print('! ALTOSPAM has been detected. Moving this message to the dsn-temp mbox')
+ return None
+
+ # Match:
+ # A message that you sent could not be delivered to one or more of its recipients.
+ # I'm afraid I wasn't able to deliver your message to the following addresses.
+ # The following message to <email@example.com> was undeliverable.
+ non_delivery_hints = [
+ "could not be delivered to",
+ "Delivery to the following recipient failed permanently",
+ "I'm sorry to have to inform you that your message could not",
+ "I wasn't able to deliver your message",
+ "try to send your message again at a later time",
+ "User unknown in local recipient table",
+ "> was undeliverable.",
+ "we were unable to deliver your message",
+ ]
+ if not any(any(hint in line for hint in non_delivery_hints) for line in lines):
+ print('! Unknown mailer-daemon message, unable to find an hint for non-delivery in message:')
+ print('\n'.join(lines))
+ return None
+
+ # Match:
+ # This is a permanent error; I've given up. Sorry it didn't work out.
+ # 5.1.0 - Unknown address error 550-'email@example.com... No such user'
+ permanent_error_hints = [
+ "Delivery to the following recipient failed permanently",
+ "failed due to an unavailable mailbox",
+ "following addresses had permanent fatal errors",
+ "I'm sorry to have to inform you that your message could not",
+ "The email account that you tried to reach does not exist",
+ "This is a permanent error",
+ "Unknown address error",
+ "unreachable for too long",
+ "550 Requested action not taken",
+ ]
+ if not any(any(hint in line for hint in permanent_error_hints) for line in lines):
+ print('! Unknown mailer-daemon message, unable to find an hint for permanent error in message:')
+ print('\n'.join(lines))
+ return None
+
+ # Retrieve the first occurence of <email@example.com>
+ for line in lines:
+ match = re.match(r'.*?<([0-9a-zA-Z_.-]+@[0-9a-zA-Z_.-]+)>', line)
+ if match is None:
+ match = re.match(r'^\s*"?([0-9a-zA-Z_.-]+@[0-9a-zA-Z_.-]+)"?\s*$', line)
+ if match is not None:
+ email = match.group(1)
+ if email.endswith('@polytechnique.org'):
+ # First valid mail is something like <info_newsletter@polytechnique.org>, so we missed the real one
+ break
+ return email
+
+ print('! Unknown mailer-daemon message, unable to find email address:')
+ print('\n'.join(lines))
+ return None