draft for a SMTP Bounce Proxy
authorPierre Habouzit (MadCoder <pierre.habouzit@m4x.org>
Fri, 17 Dec 2004 16:20:42 +0000 (16:20 +0000)
committerFlorent Bruneau <florent.bruneau@polytechnique.org>
Thu, 26 Jun 2008 21:26:44 +0000 (23:26 +0200)
quite advanced skeleton for an SMTP Bounce processing Proxy
search for < raise NotImplementedError > to find places that remains to be implemented.

git-archimport-id: opensource@polytechnique.org--2005/platal--mainline--0.9--patch-106

bin/smtp_bounce_proxy.py [new file with mode: 0755]

diff --git a/bin/smtp_bounce_proxy.py b/bin/smtp_bounce_proxy.py
new file mode 100755 (executable)
index 0000000..01f48b8
--- /dev/null
@@ -0,0 +1,156 @@
+#! /usr/bin/python
+
+import asyncore
+import email
+import email.Message
+from email.Iterators import typed_subpart_iterator, _structure
+import re
+
+from smtpd import PureProxy
+
+IGNORE    = 0
+NOTICE    = 1
+TEMPORARY = 2
+PERMANENT = 3
+
+FROM_PORT = 20024
+TO_PORT   = 20025
+
+def msg_of_str(data): return email.message_from_string(data, _class=BounceMessage)
+
+class BounceMessage(email.Message.Message):
+    def body(self):
+        """this method returns the part that is commonely designed as the 'body'
+
+        for the multipart mails, we go into the first part that have non multipart childs, and then :
+               we return its first text/plain part if it exsists
+          else we return the first text/* part if it exists
+          else we return None else
+
+        for non multipart mails, we just return the current payload
+        """
+        if self.is_multipart():
+            _body = self
+            while _body.get_payload(0).is_multipart():
+                _body = _body.get_payload(0)
+                
+            buffer = None
+            for part in typed_subpart_iterator(_body):
+                if part.get_content_subtype() == 'plain':
+                    return part.get_payload(decode=True)
+                if buffer is None:
+                    buffer = part
+            return buffer.get_payload(decode=True)
+        return self.get_payload(decode=True)
+
+    def _qmail_attached_mail(self):
+        """qmail is a dumb MTA that put the mail that has bounced RAW into the bounce message,
+        instead of making a traditionnal message/rfc822 attachement like any other MTA
+
+        it seems to be designed like this :
+
+        =============================================
+        [...QMAIL crap...]
+        --- Below this line is a copy of the message.
+
+        Return-Path: <...>
+        [rest of the embeded mail]
+        =============================================
+
+        so we just cut the qmail crap, and build a new message from the rest.
+        
+        may DJB burn into coder's hell
+        """
+        msg = self.get_payload(decode=True)
+        pos = msg.find("\n--- Below this line is a copy of the message.")
+        if pos is -1:
+            return None
+        pos = msg.find("Return-Path:", pos)
+        return msg_of_str(msg[pos:])
+        
+    def attached_mail(self):
+        """returns the attached mail that bounced, if it exists.
+        we try this :
+        
+        is the mail multipart ?
+        Yes :
+            (1) return the first message/rfc822 part.
+            (2) return the first text/rfc822-headers part (AOHell)
+            (3) return None (may be a vacation + some disclaimer in attachment)
+        No:
+            try to return the qmail-style embeded mail (but may be a vacation)
+        """
+        if self.is_multipart():
+            for part in typed_subpart_iterator(self, 'message', 'rfc822'):
+                return part
+            for part in typed_subpart_iterator(self, 'text', 'rfc822-headers'):
+                return part
+            return None
+        return self._qmail_attached_mail()
+
+    def error_level(self):
+        """determine the level of an error:
+        NOTICE    == vacation, or any informative message we want to forward as is
+        TEMPORARY == temporary failure, fixable (e.g.  over quota)
+        PERMANENT == permanent failure, broken for good (e.g. account do not exists)
+        """
+        raise NotImplementedError
+
+    def to_bounce(self, alias, dest):
+        """this function returns a new Message, the one we really want to send.
+
+        alias holds one valide plat/al alias of the user
+        
+
+        Case 0: the error is IGNORE : return None
+        Case 1: the error is NOTICE : we just return self
+
+        Case 2: we have to do our little treatments for the broken mail,
+                and then we create an informative message for the original SENDER to :
+                - explain to him what happened (the detailed error)
+                - try to guess if the user may or may not have had the mail (by another leg)
+                - if no other leg, give an information to the SENDER on how he can give to us a real good leg
+                and attach any sensible information about the original mail (@see attached_mail)
+        """
+        if   self.error_level() is IGNORE:
+            return None
+        elif self.error_level() is NOTICE:
+            return self
+        elif self.error_level() in [ TEMPORARY , PERMANENT ] :
+            raise NotImplementedError
+        else:
+            raise
+
+class BounceProxy(PureProxy):
+    def __init__(self, localaddr, remoteaddr):
+        PureProxy.__init__(self, localaddr, remoteaddr)
+        self._rcpt_re = re.compile(r'^([^_]*)__(.*)__([^_+=]*)\+(.*)=([^_+=]*)@bounces.m4x.org$')
+
+
+    def process_rcpt(self, rcpttos):
+        for to in rcpttos:
+            m = self._rcpt_re.match(to)
+            if m is None: continue
+            return ( m.group(1), m.group(2)+'@'+m.group(3), m.group(4)+'@'+m.group(5) )
+        return None
+
+
+    def process_message(self, peer, mailfrom, rcpttos, data):
+        try:
+            alias, sender, dest = self.process_rcpt(rcpttos)
+        except:
+            # SPAM or broken msg, we just drop it
+            # if we want to return an error uncomment this line :
+            #return { int_code: "some error message" }
+            return { }
+        
+        bounce = msg_of_str(data).to_bounce(alias, dest)
+        if bounce is None:
+            return { }
+        else:
+            return self._deliver("MAILER-DAEMON@bounces.m4x.org", sender, bounce)
+
+
+Proxy = BounceProxy(('127.0.0.1', FROM_PORT), ('127.0.0.1',TO_PORT))
+asyncore.loop()
+