some progress on the smtp bounce proxy
authorPierre Habouzit (MadCoder <pierre.habouzit@m4x.org>
Mon, 20 Dec 2004 22:31:37 +0000 (22:31 +0000)
committerFlorent Bruneau <florent.bruneau@polytechnique.org>
Thu, 26 Jun 2008 21:26:46 +0000 (23:26 +0200)
we don't want to make distinction between permanent or temporary
errors, because we should add a "unknown" level, and that will be too
complicated for really to little gain.

so we have basicaly :
 - ignore (drop the mails)
 - notice (forward the "bounce" as is - default)
 - error  (do the broken leg + forge a new bounce)

the errors levels are regexp-driven (read from a mysql db)

the only thing left is the treatment of the ERROR level (others are
fine, and the web interface to edit the regexps too)

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

bin/smtp_bounce_proxy.py
htdocs/admin/emails_bounces_re.php [new file with mode: 0644]
templates/admin/emails_bounces_re.tpl [new file with mode: 0644]
upgrade/0.9.3/85_bounces_proxy.sql [new file with mode: 0644]

index 01f48b8..28ae641 100755 (executable)
@@ -3,21 +3,59 @@
 import asyncore
 import email
 import email.Message
-from email.Iterators import typed_subpart_iterator, _structure
-import re
+import os, re, sys
 
+from email.Iterators import typed_subpart_iterator, _structure
 from smtpd import PureProxy
 
+import ConfigParser
+import MySQLdb
+
 IGNORE    = 0
 NOTICE    = 1
-TEMPORARY = 2
-PERMANENT = 3
+ERROR     = 2
 
 FROM_PORT = 20024
-TO_PORT   = 20025
+TO_HOST   = 'olympe.madism.org'
+TO_PORT   = 25
+
+
+################################################################################
+#
+# Functions
+#
+#-------------------------------------------------------------------------------
+
+config = ConfigParser.ConfigParser()
+config.read(os.path.dirname(__file__)+'/../configs/platal.conf')
+
+def get_config(sec,val,default=None):
+    try:
+        return config.get(sec, val)[1:-1]
+    except ConfigParser.NoOptionError, e:
+        if default is None:
+            print e
+            sys.exit(1)
+        else:
+            return default
+
+def connectDB():
+    db = MySQLdb.connect(
+            db     = 'x4dat',
+            user   = get_config('Core', 'dbuser'),
+            passwd = get_config('Core', 'dbpwd'),
+            unix_socket='/var/run/mysqld/mysqld.sock')
+    db.ping()
+    return db.cursor()
 
 def msg_of_str(data): return email.message_from_string(data, _class=BounceMessage)
 
+################################################################################
+#
+# Classes
+#
+#-------------------------------------------------------------------------------
+
 class BounceMessage(email.Message.Message):
     def body(self):
         """this method returns the part that is commonely designed as the 'body'
@@ -90,9 +128,32 @@ class BounceMessage(email.Message.Message):
 
     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)
+        IGNORE == drop the mail
+        NOTICE == vacation, or any informative message we want to forward as is
+        ERROR  == errors, that we want to handle
+        """
+
+        body = self.body()
+        if not body:
+            return (IGNORE, '')
+        
+        mysql.execute ( "SELECT lvl,re,text FROM emails_bounces_re ORDER BY pos" )
+        nb = int(mysql.rowcount)
+        for x in range(0,nb):
+            row = mysql.fetchone()
+            rxp = re.compile(str(row[1]))
+            if rxp.match(body):
+                return (int(row[0]), str(row[2]))
+       
+        return (NOTICE, '')
+
+    def forge_error(self, txt):
+        """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)
         """
         raise NotImplementedError
 
@@ -104,22 +165,15 @@ class BounceMessage(email.Message.Message):
 
         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)
+        Case 2: we have a REAL error: use forge_error
         """
-        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
+        lvl, txt = self.error_level()
+
+        if   lvl is IGNORE: return None
+        elif lvl is NOTICE: return self
+        elif lvl is ERROR : return self.forge_error(txt)
+        else:               raise
+
 
 class BounceProxy(PureProxy):
     def __init__(self, localaddr, remoteaddr):
@@ -138,19 +192,22 @@ class BounceProxy(PureProxy):
     def process_message(self, peer, mailfrom, rcpttos, data):
         try:
             alias, sender, dest = self.process_rcpt(rcpttos)
+            bounce = msg_of_str(data).to_bounce(alias, dest)
+            if bounce is not None:
+                self._deliver("MAILER-DAEMON@bounces.m4x.org", [sender], bounce.as_string())
         except:
+            pass
             # 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)
+        return None
+
 
+################################################################################
+#
+# Main
+#
+#-------------------------------------------------------------------------------
 
-Proxy = BounceProxy(('127.0.0.1', FROM_PORT), ('127.0.0.1',TO_PORT))
+mysql = connectDB()
+Proxy = BounceProxy(('127.0.0.1', FROM_PORT), (TO_HOST, TO_PORT))
 asyncore.loop()
 
diff --git a/htdocs/admin/emails_bounces_re.php b/htdocs/admin/emails_bounces_re.php
new file mode 100644 (file)
index 0000000..d5537d0
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/***************************************************************************
+ *  Copyright (C) 2003-2004 Polytechnique.org                              *
+ *  http://opensource.polytechnique.org/                                   *
+ *                                                                         *
+ *  This program is free software; you can redistribute it and/or modify   *
+ *  it under the terms of the GNU General Public License as published by   *
+ *  the Free Software Foundation; either version 2 of the License, or      *
+ *  (at your option) any later version.                                    *
+ *                                                                         *
+ *  This program is distributed in the hope that it will be useful,        *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
+ *  GNU General Public License for more details.                           *
+ *                                                                         *
+ *  You should have received a copy of the GNU General Public License      *
+ *  along with this program; if not, write to the Free Software            *
+ *  Foundation, Inc.,                                                      *
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
+ ***************************************************************************/
+
+require_once('xorg.inc.php');
+new_admin_page('admin/emails_bounces_re.tpl');
+
+if (Post::has('submit')) {
+    foreach (Env::getMixed('lvl') as $id=>$val) {
+        $globals->db->query("REPLACE INTO  emails_bounces_re (id,pos,lvl,re,text)
+                                   VALUES  ($id, '{$_POST['pos'][$id]}', '{$_POST['lvl'][$id]}',
+                                                 '{$_POST['re'][$id]}', '{$_POST['text'][$id]}')");
+    }
+}
+
+$page->mysql_assign("SELECT * FROM emails_bounces_re ORDER BY pos", 'bre');
+
+$page->run();
+?>
diff --git a/templates/admin/emails_bounces_re.tpl b/templates/admin/emails_bounces_re.tpl
new file mode 100644 (file)
index 0000000..bf09bee
--- /dev/null
@@ -0,0 +1,97 @@
+{***************************************************************************
+ *  Copyright (C) 2003-2004 Polytechnique.org                              *
+ *  http://opensource.polytechnique.org/                                   *
+ *                                                                         *
+ *  This program is free software; you can redistribute it and/or modify   *
+ *  it under the terms of the GNU General Public License as published by   *
+ *  the Free Software Foundation; either version 2 of the License, or      *
+ *  (at your option) any later version.                                    *
+ *                                                                         *
+ *  This program is distributed in the hope that it will be useful,        *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
+ *  GNU General Public License for more details.                           *
+ *                                                                         *
+ *  You should have received a copy of the GNU General Public License      *
+ *  along with this program; if not, write to the Free Software            *
+ *  Foundation, Inc.,                                                      *
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
+ ***************************************************************************}
+
+
+<h1>Regexps pour les détections de bounces</h1>
+
+<p>
+Rappel sur les niveaux :
+</p>
+<ul>
+  <li>0: IGNORE == ignorer le bounce</li>
+  <li>1: NOTICE == forwarder le bounce (typiquement vacation)</li>
+  <li>2: ERREUR == erreur</li>
+</ul>
+
+{dynamic}
+
+<form action="{$smarty.server.PHP_SELF}" method="post">
+  <table class="bicol" cellpadding='0' cellspacing='0'>
+    <tr>
+      <th>Position/Niveau</th>
+      <th>Regexp/Raison</th>
+    </tr>
+    {if $smarty.get.new}
+    <tr class="impair">
+      <td>
+        <input type='text' name='pos[NULL]' value='' size='4' maxlength='4' />
+      </td>
+      <td>
+        <input type="text" size="82" name='re[NULL]'   value="{$re.re}" />
+      </td>
+    </tr>
+    <tr class="impair">
+      <td style="white-space: nowrap">
+        <input type='radio' name='lvl[NULL]' value='0' {if $re.lvl eq 0}checked="checked"{/if} />
+        <input type='radio' name='lvl[NULL]' value='1' {if $re.lvl eq 1}checked="checked"{/if} />
+        <input type='radio' name='lvl[NULL]' value='2' {if $re.lvl eq 2}checked="checked"{/if} />
+      </td>
+      <td>
+        <input type="text" size="32" name='text[NULL]' value="{$re.text}" />
+      </td>
+    </tr>
+    {else}
+    <tr class="impair">
+      <td colspan="2" class="right action">
+        <a href="?new=1">nouveau</a>
+      </td>
+    </tr>
+    {/if}
+    {foreach from=$bre item=re}
+    <tr class="{cycle values="pair,pair,impair,impair"}">
+      <td>
+        <input type='text' name='pos[{$re.id}]' value='{$re.pos}' size='4' maxlength='4' />
+      </td>
+      <td>
+        <input type="text" size="82" name='re[{$re.id}]'   value="{$re.re}" />
+      </td>
+    </tr>
+    <tr class="{cycle values="pair,pair,impair,impair"}">
+      <td style="white-space: nowrap">
+        <input type='radio' name='lvl[{$re.id}]' value='0' {if $re.lvl eq 0}checked="checked"{/if} />
+        <input type='radio' name='lvl[{$re.id}]' value='1' {if $re.lvl eq 1}checked="checked"{/if} />
+        <input type='radio' name='lvl[{$re.id}]' value='2' {if $re.lvl eq 2}checked="checked"{/if} />
+      </td>
+      <td>
+        <input type="text" size="32" name='text[{$re.id}]' value="{$re.text}" /><br />
+      </td>
+    </tr>
+    {/foreach}
+    <tr class="{cycle values="pair,impair"}">
+      <td colspan="2" class="center">
+        <input type="submit" value="valider" name="submit" />
+      </td>
+    </tr>
+  </table>
+</form>
+
+{/dynamic}
+
+{* vim:set et sw=2 sts=2 sws=2: *}
diff --git a/upgrade/0.9.3/85_bounces_proxy.sql b/upgrade/0.9.3/85_bounces_proxy.sql
new file mode 100644 (file)
index 0000000..97eba0b
--- /dev/null
@@ -0,0 +1,12 @@
+create table emails_bounces_re (
+    id   int not null auto_increment,
+    pos  smallint unsigned not null default 0,
+    lvl  tinyint unsigned not null default 0,
+    re   text not null,
+    text varchar(255) not null,
+    primary key (id),
+    index (lvl),
+    index (pos)
+);
+
+insert into admin_a values(1,'Regexps Bounces', 'admin/emails_bounces_re.php', 30);