From 70c326a52552777c31a4c9206dd2446aa7164f67 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 11 Nov 2019 21:52:07 +0100
Subject: [PATCH] general: remove bounce processing (#33448)

---
 .coveragerc                       |   4 +-
 README                            |   8 --
 jenkins.sh                        |   4 +-
 tests/test_admin_pages.py         |  39 -------
 tests/test_bounce_processing.py   |  21 ----
 tests/test_ctl.py                 |  19 ---
 wcs/admin/bounces.py              | 184 ------------------------------
 wcs/admin/settings.py             |   1 -
 wcs/backoffice/root.py            |   3 -
 wcs/ctl/Bouncers/BouncerAPI.py    |  68 -----------
 wcs/ctl/Bouncers/Caiwireless.py   |  45 --------
 wcs/ctl/Bouncers/Compuserve.py    |  45 --------
 wcs/ctl/Bouncers/DSN.py           | 101 ----------------
 wcs/ctl/Bouncers/Exchange.py      |  47 --------
 wcs/ctl/Bouncers/Exim.py          |  30 -----
 wcs/ctl/Bouncers/GroupWise.py     |  70 ------------
 wcs/ctl/Bouncers/LLNL.py          |  31 -----
 wcs/ctl/Bouncers/Microsoft.py     |  53 ---------
 wcs/ctl/Bouncers/Netscape.py      |  88 --------------
 wcs/ctl/Bouncers/Postfix.py       |  85 --------------
 wcs/ctl/Bouncers/Qmail.py         |  70 ------------
 wcs/ctl/Bouncers/SMTP32.py        |  59 ----------
 wcs/ctl/Bouncers/SimpleMatch.py   | 165 ---------------------------
 wcs/ctl/Bouncers/SimpleWarning.py |  50 --------
 wcs/ctl/Bouncers/Yahoo.py         |  53 ---------
 wcs/ctl/Bouncers/Yale.py          |  79 -------------
 wcs/ctl/Bouncers/__init__.py      |  15 ---
 wcs/ctl/process_bounce.py         |  99 ----------------
 wcs/qommon/admin/emails.py        |   4 +-
 wcs/qommon/bounces.py             |  28 -----
 wcs/qommon/emails.py              |  13 ---
 31 files changed, 5 insertions(+), 1576 deletions(-)
 delete mode 100644 tests/test_bounce_processing.py
 delete mode 100644 wcs/admin/bounces.py
 delete mode 100644 wcs/ctl/Bouncers/BouncerAPI.py
 delete mode 100644 wcs/ctl/Bouncers/Caiwireless.py
 delete mode 100644 wcs/ctl/Bouncers/Compuserve.py
 delete mode 100644 wcs/ctl/Bouncers/DSN.py
 delete mode 100644 wcs/ctl/Bouncers/Exchange.py
 delete mode 100644 wcs/ctl/Bouncers/Exim.py
 delete mode 100644 wcs/ctl/Bouncers/GroupWise.py
 delete mode 100644 wcs/ctl/Bouncers/LLNL.py
 delete mode 100644 wcs/ctl/Bouncers/Microsoft.py
 delete mode 100644 wcs/ctl/Bouncers/Netscape.py
 delete mode 100644 wcs/ctl/Bouncers/Postfix.py
 delete mode 100644 wcs/ctl/Bouncers/Qmail.py
 delete mode 100644 wcs/ctl/Bouncers/SMTP32.py
 delete mode 100644 wcs/ctl/Bouncers/SimpleMatch.py
 delete mode 100644 wcs/ctl/Bouncers/SimpleWarning.py
 delete mode 100644 wcs/ctl/Bouncers/Yahoo.py
 delete mode 100644 wcs/ctl/Bouncers/Yale.py
 delete mode 100644 wcs/ctl/Bouncers/__init__.py
 delete mode 100644 wcs/ctl/process_bounce.py
 delete mode 100644 wcs/qommon/bounces.py

diff --git a/.coveragerc b/.coveragerc
index 26a251fa1..b0b8fe681 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,5 +1,5 @@
 [run]
-omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
+omit = wcs/qommon/vendor/*.py
 
 [report]
-omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
+omit = wcs/qommon/vendor/*.py
diff --git a/README b/README
index ef79e624d..944e4ee29 100644
--- a/README
+++ b/README
@@ -41,14 +41,6 @@ AUTHORS file for additional credits.
 w.c.s. incorporates some other pieces of code, with their own authors and
 copyright notices :
 
-Email bounce detection code (wcs/ctl/Bounces/*) from Mailman:
- # http://www.gnu.org/software/mailman/
- #
- # 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.
-
 Some artwork from GTK+:
  # http://www.gtk.org/
  #
diff --git a/jenkins.sh b/jenkins.sh
index 85a726777..5e14afc5b 100755
--- a/jenkins.sh
+++ b/jenkins.sh
@@ -16,10 +16,10 @@ rm -f coverage.xml
 rm -f test_results.xml
 cat << _EOF_ > .coveragerc
 [run]
-omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
+omit = wcs/qommon/vendor/*.py
 
 [report]
-omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
+omit = wcs/qommon/vendor/*.py
 _EOF_
 
 # $PIP_BIN install --upgrade 'pip<8'
diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py
index 4e7c859cc..de86c3947 100644
--- a/tests/test_admin_pages.py
+++ b/tests/test_admin_pages.py
@@ -32,7 +32,6 @@ from wcs.qommon.form import UploadedFile
 from wcs.qommon.ident.password_accounts import PasswordAccount
 from wcs.qommon.http_request import HTTPRequest
 from wcs.qommon.template import get_current_theme
-from wcs.qommon.bounces import Bounce
 from wcs.admin.settings import UserFieldsFormDef
 from wcs.categories import Category
 from wcs.data_sources import NamedDataSource
@@ -5260,44 +5259,6 @@ def test_settings_theme_download_upload(pub):
     assert app.get('/themes/alto/../', status=404)
     assert app.get('/themes/xxx/../', status=404)
 
-def test_bounces(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/')
-    assert not 'bounces' in resp.body
-
-    resp = app.get('/backoffice/settings/emails/options')
-    resp.form['from'] = 'noreply@localhost'
-    resp.form['bounce_handler'].checked = True
-    resp = resp.form.submit('submit')
-    pub.reload_cfg()
-    assert pub.cfg['emails']['bounce_handler']
-
-    bounce = Bounce()
-    bounce.arrival_time = time.time()
-    bounce.bounce_message = 'foobar'
-    bounce.addrs = ['foo@localhost']
-    bounce.email_type = 'change-password-request'
-    bounce.original_rcpts = ['foo@localhost', 'bar@localhost']
-    msg = MIMEText('hello world')
-    msg['Subject'] = 'hello world'
-    bounce.original_message = msg.as_string()
-    bounce.store()
-
-    resp = app.get('/backoffice/')
-    assert 'bounces' in resp.body
-    resp = resp.click('Bounces')
-    resp = resp.click(href='%s/' % bounce.id, index=0)
-    assert 'hello world' in resp.body
-
-    resp = app.get('/backoffice/bounces/')
-    resp = resp.click(href='%s/delete' % bounce.id, index=0)
-    resp = resp.form.submit('cancel').follow()
-    assert Bounce.count() == 1
-    resp = resp.click(href='%s/delete' % bounce.id, index=0)
-    resp = resp.form.submit('delete')
-    assert Bounce.count() == 0
-
 def test_postgresql_settings(pub):
     if not pub.is_using_postgresql():
         pytest.skip('this requires SQL')
diff --git a/tests/test_bounce_processing.py b/tests/test_bounce_processing.py
deleted file mode 100644
index 35a680daf..000000000
--- a/tests/test_bounce_processing.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import email
-
-from wcs.ctl.process_bounce import CmdProcessBounce
-
-def test_normal_email():
-    msg = email.message_from_string('test')
-    msg['From'] = 'bar@example.net'
-    msg['To'] = 'foo@example.net'
-    addrs = CmdProcessBounce.get_bounce_addrs(msg)
-    assert addrs is None
-
-def test_bounce_email():
-    msg = email.message_from_string('test')
-    msg['From'] = 'bar@example.net'
-    msg['To'] = 'foo@example.net'
-
-    # this is how exim adds failed recipients in its outgoing bounce emails
-    msg['x-failed-recipients'] = 'baz@example.net'
-
-    addrs = CmdProcessBounce.get_bounce_addrs(msg)
-    assert addrs == ['baz@example.net']
diff --git a/tests/test_ctl.py b/tests/test_ctl.py
index a14418566..5f6721f2f 100644
--- a/tests/test_ctl.py
+++ b/tests/test_ctl.py
@@ -16,7 +16,6 @@ from wcs.qommon.management.commands.collectstatic import Command as CmdCollectSt
 from wcs.qommon.management.commands.migrate import Command as CmdMigrate
 from wcs.qommon.management.commands.migrate_schemas import Command as CmdMigrateSchemas
 from wcs.qommon.management.commands.makemessages import Command as CmdMakeMessages
-from wcs.ctl.process_bounce import CmdProcessBounce
 from wcs.ctl.rebuild_indexes import rebuild_vhost_indexes
 from wcs.ctl.wipe_data import CmdWipeData
 from wcs.ctl.management.commands.runscript import Command as CmdRunScript
@@ -67,24 +66,6 @@ def test_migrate(two_pubs):
 def test_migrate_schemas(two_pubs):
     CmdMigrateSchemas().handle()
 
-def test_get_bounce_addrs():
-    msg = MIMEText('Hello world')
-    assert CmdProcessBounce.get_bounce_addrs(msg) is None
-
-    msg = MIMEMultipart(_subtype='mixed')
-    msg.attach(MIMEText('Hello world'))
-    msg.attach(MIMEText('<p>Hello world</p>', _subtype='html'))
-    assert CmdProcessBounce.get_bounce_addrs(msg) is None
-
-    msg = MIMEText('Hello world')
-    msg['x-failed-recipients'] = 'foobar@localhost'
-    assert CmdProcessBounce.get_bounce_addrs(msg) == ['foobar@localhost']
-
-    msg = MIMEText('''failed addresses follow:
-                      foobar@localhost
-                      message text follows:''')
-    assert CmdProcessBounce.get_bounce_addrs(msg) == ['foobar@localhost']
-
 def test_wipe_formdata(pub):
     form_1 = FormDef()
     form_1.name = 'example'
diff --git a/wcs/admin/bounces.py b/wcs/admin/bounces.py
deleted file mode 100644
index 78b434c5a..000000000
--- a/wcs/admin/bounces.py
+++ /dev/null
@@ -1,184 +0,0 @@
-# w.c.s. - web application for online forms
-# Copyright (C) 2005-2010  Entr'ouvert
-#
-# 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, see <http://www.gnu.org/licenses/>.
-
-import time
-import email.Parser
-
-from django.utils.encoding import force_text
-
-from quixote import get_response, redirect
-from quixote.directory import Directory
-from quixote.html import htmltext, TemplateIO
-
-from ..qommon import _
-from ..qommon import errors
-from ..qommon import misc
-from ..qommon.bounces import Bounce
-from ..qommon.backoffice.menu import html_top
-from ..qommon.admin.menu import command_icon
-
-from ..qommon.form import *
-from ..qommon.misc import get_cfg
-
-def get_email_type_label(type):
-    from .settings import EmailsDirectory
-    return EmailsDirectory.emails_dict.get(type, {}).get('description')
-
-class BouncePage(Directory):
-    _q_exports = ['', 'delete']
-
-    def __init__(self, component):
-        self.bounce = Bounce.get(component)
-        get_response().breadcrumb.append((component + '/', _('bounce')))
-
-
-    def _q_index(self):
-        html_top('bounces', title = _('Bounce'))
-        r = TemplateIO(html=True)
-
-        r += htmltext('<div class="form">')
-
-        if self.bounce.email_type:
-            r += htmltext('<div class="title">%s</div>') % _('Email Type')
-            r += htmltext('<div class="StringWidget content">')
-            r += _(get_email_type_label(self.bounce.email_type))
-            r += htmltext('</div>')
-
-        r += htmltext('<div class="title">%s</div>') % _('Arrival Time')
-        r += htmltext('<div class="StringWidget content">')
-        r += misc.localstrftime(time.localtime(self.bounce.arrival_time))
-        r += htmltext('</div>')
-
-        if self.bounce.addrs:
-            r += htmltext('<div class="title">%s</div>') % _('Failed Addresses')
-            r += htmltext('<div class="StringWidget content">')
-            r += ', '.join(self.bounce.addrs)
-            r += htmltext('</div>')
-
-        if self.bounce.original_rcpts:
-            r += htmltext('<div class="title">%s</div>') % _('Original Recipients')
-            r += htmltext('<div class="StringWidget content">')
-            r += ', '.join(self.bounce.original_rcpts)
-            r += htmltext('</div>')
-
-        r += htmltext('<div class="title">%s</div>') % _('Bounce Message')
-        r += htmltext('<div class="StringWidget content">')
-        r += htmltext('<pre style="max-height: 20em;">')
-        r += self.bounce.bounce_message
-        r += htmltext('</pre>')
-        r += htmltext('</div>')
-
-        if self.bounce.original_message:
-            parser = email.Parser.Parser()
-
-            msg = parser.parsestr(self.bounce.original_message)
-            if msg.is_multipart():
-                for m in msg.get_payload():
-                    if m.get_content_type() == 'text/plain':
-                        break
-                else:
-                    m = None
-            elif msg.get_content_type() == 'text/plain':
-                m = msg
-            else:
-                m = None
-
-            r += htmltext('<div class="title">%s</div>') % _('Original Message')
-            r += htmltext('<div class="StringWidget content">')
-            r += _('Subject: ')
-            subject, charset = email.Header.decode_header(msg['Subject'])[0]
-            if charset:
-                encoding = get_publisher().site_charset
-                r += force_text(subject, charset).encode(encoding)
-            else:
-                r += subject
-            r += htmltext('</div>')
-
-            if m:
-                r += htmltext('<div class="StringWidget content">')
-                r += htmltext('<pre>')
-                r += m.get_payload()
-                r += htmltext('</pre>')
-                r += htmltext('</div>')
-
-        r += htmltext('</div>') # form
-        return r.getvalue()
-
-
-    def delete(self):
-        form = Form(enctype='multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to irrevocably delete this bounce.')))
-        form.add_submit('delete', _('Delete'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_widget('cancel').parse():
-            return redirect('..')
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('delete', _('Delete')))
-            html_top('bounces', title = _('Delete Bounce'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Deleting Bounce')
-            r += form.render()
-            return r.getvalue()
-        else:
-            self.bounce.remove_self()
-            return redirect('..')
-
-
-
-class BouncesDirectory(Directory):
-    _q_exports = ['']
-
-    def _q_traverse(self, path):
-        get_response().breadcrumb.append( ('bounces/', _('Bounces')) )
-        return Directory._q_traverse(self, path)
-
-    def is_visible(self, *args):
-        return bool(get_cfg('emails', {}).get('bounce_handler') is True)
-
-    def _q_index(self):
-        html_top('bounces', title = _('Bounces'))
-
-        bounces = Bounce.select(ignore_errors=True)
-        bounces.sort(lambda x,y: cmp(x.arrival_time, y.arrival_time))
-
-        r = TemplateIO(html=True)
-        r += htmltext('<ul class="biglist">')
-        for bounce in bounces:
-            r += htmltext('<li>')
-            r += htmltext('<strong class="label">')
-            r += misc.localstrftime(time.localtime(bounce.arrival_time))
-            if bounce.email_type:
-                r += ' - '
-                r += _(get_email_type_label(bounce.email_type))
-            r += htmltext('</strong>')
-            r += htmltext('<p class="details">')
-            if bounce.addrs:
-                r += ', '.join(bounce.addrs)
-            r += htmltext('</p>')
-
-            r += htmltext('<p class="commands">')
-            r += command_icon('%s/' % bounce.id, 'view')
-            r += command_icon('%s/delete' % bounce.id, 'remove', popup = True)
-            r += htmltext('</p></li>')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        try:
-            return BouncePage(component)
-        except KeyError:
-            raise errors.TraversalError()
diff --git a/wcs/admin/settings.py b/wcs/admin/settings.py
index 6230b4ab1..66fec0c04 100644
--- a/wcs/admin/settings.py
+++ b/wcs/admin/settings.py
@@ -554,7 +554,6 @@ class SettingsDirectory(QommonSettingsDirectory):
                 ('users', N_('Users')),
                 ('roles', N_('Roles')),
                 ('categories', N_('Categories')),
-                ('bounces', N_('Bounces')),
                 ('settings', N_('Settings')),
         ]
         for k, v in admin_sections:
diff --git a/wcs/backoffice/root.py b/wcs/backoffice/root.py
index 2163738db..8ed1192b0 100644
--- a/wcs/backoffice/root.py
+++ b/wcs/backoffice/root.py
@@ -29,7 +29,6 @@ from ..qommon.form import *
 
 from wcs.formdef import FormDef
 
-import wcs.admin.bounces
 import wcs.admin.categories
 import wcs.admin.forms
 import wcs.admin.roles
@@ -47,7 +46,6 @@ from . import data_management
 class RootDirectory(BackofficeRootDirectory):
     _q_exports = ['', 'pending', 'statistics', ('menu.json', 'menu_json')]
 
-    bounces = wcs.admin.bounces.BouncesDirectory()
     forms = wcs.admin.forms.FormsDirectory()
     roles = wcs.admin.roles.RolesDirectory()
     settings = wcs.admin.settings.SettingsDirectory()
@@ -69,7 +67,6 @@ class RootDirectory(BackofficeRootDirectory):
         ('workflows/', N_('Workflows Workshop'), {'sub': True}),
         ('users/', N_('Users'), {'check_display_function': roles.is_visible}),
         ('roles/', N_('Roles'), {'check_display_function': roles.is_visible}),
-        ('bounces/', N_('Bounces'), {'check_display_function': bounces.is_visible}),
         ('settings/', N_('Settings')),
     ]
 
diff --git a/wcs/ctl/Bouncers/BouncerAPI.py b/wcs/ctl/Bouncers/BouncerAPI.py
deleted file mode 100644
index fc51597ec..000000000
--- a/wcs/ctl/Bouncers/BouncerAPI.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Contains all the common functionality for msg bounce scanning API.
-
-This module can also be used as the basis for a bounce detection testing
-framework.  When run as a script, it expects two arguments, the listname and
-the filename containing the bounce message.
-
-"""
-
-import sys
-
-
-# If a bounce detector returns Stop, that means to just discard the message.
-# An example is warning messages for temporary delivery problems.  These
-# shouldn't trigger a bounce notification, but we also don't want to send them
-# on to the list administrator.
-class _Stop:
-    pass
-Stop = _Stop()
-
-
-BOUNCE_PIPELINE = [
-    'DSN',
-    'Qmail',
-    'Postfix',
-    'Yahoo',
-    'Caiwireless',
-    'Exchange',
-    'Exim',
-    'Netscape',
-    'Compuserve',
-    'Microsoft',
-    'GroupWise',
-    'SMTP32',
-    'SimpleMatch',
-    'SimpleWarning',
-    'Yale',
-    'LLNL',
-    ]
-
-
-
-# msg must be a mimetools.Message
-def ScanMessages(mlist, msg):
-    for module in BOUNCE_PIPELINE:
-        modname = 'Mailman.Bouncers.' + module
-        __import__(modname)
-        addrs = sys.modules[modname].process(msg)
-        if addrs:
-            # Return addrs even if it is Stop. BounceRunner needs this info.
-            return addrs
-    return []
diff --git a/wcs/ctl/Bouncers/Caiwireless.py b/wcs/ctl/Bouncers/Caiwireless.py
deleted file mode 100644
index b565f29be..000000000
--- a/wcs/ctl/Bouncers/Caiwireless.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Parse mystery style generated by MTA at caiwireless.net."""
-
-import re
-import email
-from cStringIO import StringIO
-
-tcre = re.compile(r'the following recipients did not receive this message:',
-                  re.IGNORECASE)
-acre = re.compile(r'<(?P<addr>[^>]*)>')
-
-
-
-def process(msg):
-    if msg.get_content_type() <> 'multipart/mixed':
-        return None
-    # simple state machine
-    #     0 == nothing seen
-    #     1 == tag line seen
-    state = 0
-    # This format thinks it's a MIME, but it really isn't
-    for line in email.Iterators.body_line_iterator(msg):
-        line = line.strip()
-        if state == 0 and tcre.match(line):
-            state = 1
-        elif state == 1 and line:
-            mo = acre.match(line)
-            if not mo:
-                return None
-            return [mo.group('addr')]
diff --git a/wcs/ctl/Bouncers/Compuserve.py b/wcs/ctl/Bouncers/Compuserve.py
deleted file mode 100644
index 89085d2c3..000000000
--- a/wcs/ctl/Bouncers/Compuserve.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Compuserve has its own weird format for bounces."""
-
-import re
-import email
-
-dcre = re.compile(r'your message could not be delivered', re.IGNORECASE)
-acre = re.compile(r'Invalid receiver address: (?P<addr>.*)')
-
-
-
-def process(msg):
-    # simple state machine
-    #    0 = nothing seen yet
-    #    1 = intro line seen
-    state = 0
-    addrs = []
-    for line in email.Iterators.body_line_iterator(msg):
-        if state == 0:
-            mo = dcre.search(line)
-            if mo:
-                state = 1
-        elif state == 1:
-            mo = dcre.search(line)
-            if mo:
-                break
-            mo = acre.search(line)
-            if mo:
-                addrs.append(mo.group('addr'))
-    return addrs
diff --git a/wcs/ctl/Bouncers/DSN.py b/wcs/ctl/Bouncers/DSN.py
deleted file mode 100644
index 3e8e4fd99..000000000
--- a/wcs/ctl/Bouncers/DSN.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Parse RFC 3464 (i.e. DSN) bounce formats.
-
-RFC 3464 obsoletes 1894 which was the old DSN standard.  This module has not
-been audited for differences between the two.
-"""
-
-from email.Iterators import typed_subpart_iterator
-from email.Utils import parseaddr
-from cStringIO import StringIO
-
-from BouncerAPI import Stop
-
-try:
-    True, False
-except NameError:
-    True = 1
-    False = 0
-
-
-
-def check(msg):
-    # Iterate over each message/delivery-status subpart
-    addrs = []
-    for part in typed_subpart_iterator(msg, 'message', 'delivery-status'):
-        if not part.is_multipart():
-            # Huh?
-            continue
-        # Each message/delivery-status contains a list of Message objects
-        # which are the header blocks.  Iterate over those too.
-        for msgblock in part.get_payload():
-            # We try to dig out the Original-Recipient (which is optional) and
-            # Final-Recipient (which is mandatory, but may not exactly match
-            # an address on our list).  Some MTA's also use X-Actual-Recipient
-            # as a synonym for Original-Recipient, but some apparently use
-            # that for other purposes :(
-            #
-            # Also grok out Action so we can do something with that too.
-            action = msgblock.get('action', '').lower()
-            # Some MTAs have been observed that put comments on the action.
-            if action.startswith('delayed'):
-                return Stop
-            if not action.startswith('fail'):
-                # Some non-permanent failure, so ignore this block
-                continue
-            params = []
-            foundp = False
-            for header in ('original-recipient', 'final-recipient'):
-                for k, v in msgblock.get_params([], header):
-                    if k.lower() == 'rfc822':
-                        foundp = True
-                    else:
-                        params.append(k)
-                if foundp:
-                    # Note that params should already be unquoted.
-                    addrs.extend(params)
-                    break
-                else:
-                    # MAS: This is a kludge, but SMTP-GATEWAY01.intra.home.dk
-                    # has a final-recipient with an angle-addr and no
-                    # address-type parameter at all. Non-compliant, but ...
-                    for param in params:
-                        if param.startswith('<') and param.endswith('>'):
-                            addrs.append(param[1:-1])
-    # Uniquify
-    rtnaddrs = {}
-    for a in addrs:
-        if a is not None:
-            realname, a = parseaddr(a)
-            rtnaddrs[a] = True
-    return rtnaddrs.keys()
-
-
-
-def process(msg):
-    # A DSN has been seen wrapped with a "legal disclaimer" by an outgoing MTA
-    # in a multipart/mixed outer part.
-    if msg.is_multipart() and msg.get_content_subtype() == 'mixed':
-        msg = msg.get_payload()[0]
-    # The report-type parameter should be "delivery-status", but it seems that
-    # some DSN generating MTAs don't include this on the Content-Type: header,
-    # so let's relax the test a bit.
-    if not msg.is_multipart() or msg.get_content_subtype() <> 'report':
-        return None
-    return check(msg)
diff --git a/wcs/ctl/Bouncers/Exchange.py b/wcs/ctl/Bouncers/Exchange.py
deleted file mode 100644
index 2f6eeda09..000000000
--- a/wcs/ctl/Bouncers/Exchange.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright (C) 2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Recognizes (some) Microsoft Exchange formats."""
-
-import re
-import email.Iterators
-
-scre = re.compile('did not reach the following recipient')
-ecre = re.compile('MSEXCH:')
-a1cre = re.compile('SMTP=(?P<addr>[^;]+); on ')
-a2cre = re.compile('(?P<addr>[^ ]+) on ')
-
-
-
-def process(msg):
-    addrs = {}
-    it = email.Iterators.body_line_iterator(msg)
-    # Find the start line
-    for line in it:
-        if scre.search(line):
-            break
-    else:
-        return []
-    # Search each line until we hit the end line
-    for line in it:
-        if ecre.search(line):
-            break
-        mo = a1cre.search(line)
-        if not mo:
-            mo = a2cre.search(line)
-        if mo:
-            addrs[mo.group('addr')] = 1
-    return addrs.keys()
diff --git a/wcs/ctl/Bouncers/Exim.py b/wcs/ctl/Bouncers/Exim.py
deleted file mode 100644
index ce677385c..000000000
--- a/wcs/ctl/Bouncers/Exim.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Parse bounce messages generated by Exim.
-
-Exim adds an X-Failed-Recipients: header to bounce messages containing
-an `addresslist' of failed addresses.
-
-"""
-
-from email.Utils import getaddresses
-
-
-
-def process(msg):
-    all = msg.get_all('x-failed-recipients', [])
-    return [a for n, a in getaddresses(all)]
diff --git a/wcs/ctl/Bouncers/GroupWise.py b/wcs/ctl/Bouncers/GroupWise.py
deleted file mode 100644
index eb4075f1d..000000000
--- a/wcs/ctl/Bouncers/GroupWise.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""This appears to be the format for Novell GroupWise and NTMail
-
-X-Mailer: Novell GroupWise Internet Agent 5.5.3.1
-X-Mailer: NTMail v4.30.0012
-X-Mailer: Internet Mail Service (5.5.2653.19)
-"""
-
-import re
-from email.Message import Message
-from cStringIO import StringIO
-
-acre = re.compile(r'<(?P<addr>[^>]*)>')
-
-
-
-def find_textplain(msg):
-    if msg.get_content_type() == 'text/plain':
-        return msg
-    if msg.is_multipart:
-        for part in msg.get_payload():
-            if not isinstance(part, Message):
-                continue
-            ret = find_textplain(part)
-            if ret:
-                return ret
-    return None
-
-
-
-def process(msg):
-    if msg.get_content_type() <> 'multipart/mixed' or not msg['x-mailer']:
-        return None
-    addrs = {}
-    # find the first text/plain part in the message
-    textplain = find_textplain(msg)
-    if not textplain:
-        return None
-    body = StringIO(textplain.get_payload())
-    while 1:
-        line = body.readline()
-        if not line:
-            break
-        mo = acre.search(line)
-        if mo:
-            addrs[mo.group('addr')] = 1
-        elif '@' in line:
-            i = line.find(' ')
-            if i == 0:
-                continue
-            if i < 0:
-                addrs[line] = 1
-            else:
-                addrs[line[:i]] = 1
-    return addrs.keys()
diff --git a/wcs/ctl/Bouncers/LLNL.py b/wcs/ctl/Bouncers/LLNL.py
deleted file mode 100644
index 97c56e222..000000000
--- a/wcs/ctl/Bouncers/LLNL.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""LLNL's custom Sendmail bounce message."""
-
-import re
-import email
-
-acre = re.compile(r',\s*(?P<addr>\S+@[^,]+),', re.IGNORECASE)
-
-
-
-def process(msg):
-    for line in email.Iterators.body_line_iterator(msg):
-        mo = acre.search(line)
-        if mo:
-            return [mo.group('addr')]
-    return []
diff --git a/wcs/ctl/Bouncers/Microsoft.py b/wcs/ctl/Bouncers/Microsoft.py
deleted file mode 100644
index f5adcf5ce..000000000
--- a/wcs/ctl/Bouncers/Microsoft.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# Copyright (C) 1998-2003 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Microsoft's `SMTPSVC' nears I kin tell."""
-
-import re
-from cStringIO import StringIO
-from types import ListType
-
-scre = re.compile(r'transcript of session follows', re.IGNORECASE)
-
-
-
-def process(msg):
-    if msg.get_content_type() <> 'multipart/mixed':
-        return None
-    # Find the first subpart, which has no MIME type
-    try:
-        subpart = msg.get_payload(0)
-    except IndexError:
-        # The message *looked* like a multipart but wasn't
-        return None
-    data = subpart.get_payload()
-    if isinstance(data, ListType):
-        # The message is a multi-multipart, so not a matching bounce
-        return None
-    body = StringIO(data)
-    state = 0
-    addrs = []
-    while 1:
-        line = body.readline()
-        if not line:
-            break
-        if state == 0:
-            if scre.search(line):
-                state = 1
-        if state == 1:
-            if '@' in line:
-                addrs.append(line)
-    return addrs
diff --git a/wcs/ctl/Bouncers/Netscape.py b/wcs/ctl/Bouncers/Netscape.py
deleted file mode 100644
index b7f29e81b..000000000
--- a/wcs/ctl/Bouncers/Netscape.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Netscape Messaging Server bounce formats.
-
-I've seen at least one NMS server version 3.6 (envy.gmp.usyd.edu.au) bounce
-messages of this format.  Bounces come in DSN MIME format, but don't include
-any -Recipient: headers.  Gotta just parse the text :(
-
-NMS 4.1 (dfw-smtpin1.email.verio.net) seems even worse, but we'll try to
-decipher the format here too.
-
-"""
-
-import re
-from cStringIO import StringIO
-
-pcre = re.compile(
-    r'This Message was undeliverable due to the following reason:',
-    re.IGNORECASE)
-
-acre = re.compile(
-    r'(?P<reply>please reply to)?.*<(?P<addr>[^>]*)>',
-    re.IGNORECASE)
-
-
-
-def flatten(msg, leaves):
-    # give us all the leaf (non-multipart) subparts
-    if msg.is_multipart():
-        for part in msg.get_payload():
-            flatten(part, leaves)
-    else:
-        leaves.append(msg)
-
-
-
-def process(msg):
-    # Sigh.  Some show NMS 3.6's show
-    #     multipart/report; report-type=delivery-status
-    # and some show
-    #     multipart/mixed;
-    if not msg.is_multipart():
-        return None
-    # We're looking for a text/plain subpart occuring before a
-    # message/delivery-status subpart.
-    plainmsg = None
-    leaves = []
-    flatten(msg, leaves)
-    for i, subpart in zip(range(len(leaves)-1), leaves):
-        if subpart.get_content_type() == 'text/plain':
-            plainmsg = subpart
-            break
-    if not plainmsg:
-        return None
-    # Total guesswork, based on captured examples...
-    body = StringIO(plainmsg.get_payload())
-    addrs = []
-    while 1:
-        line = body.readline()
-        if not line:
-            break
-        mo = pcre.search(line)
-        if mo:
-            # We found a bounce section, but I have no idea what the official
-            # format inside here is.  :(  We'll just search for <addr>
-            # strings.
-            while 1:
-                line = body.readline()
-                if not line:
-                    break
-                mo = acre.search(line)
-                if mo and not mo.group('reply'):
-                    addrs.append(mo.group('addr'))
-    return addrs
diff --git a/wcs/ctl/Bouncers/Postfix.py b/wcs/ctl/Bouncers/Postfix.py
deleted file mode 100644
index 1d5e638f3..000000000
--- a/wcs/ctl/Bouncers/Postfix.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# Copyright (C) 1998-2003 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Parse bounce messages generated by Postfix.
-
-This also matches something called `Keftamail' which looks just like Postfix
-bounces with the word Postfix scratched out and the word `Keftamail' written
-in in crayon.
-
-It also matches something claiming to be `The BNS Postfix program', and
-`SMTP_Gateway'.  Everybody's gotta be different, huh?
-"""
-
-import re
-from cStringIO import StringIO
-
-
-
-def flatten(msg, leaves):
-    # give us all the leaf (non-multipart) subparts
-    if msg.is_multipart():
-        for part in msg.get_payload():
-            flatten(part, leaves)
-    else:
-        leaves.append(msg)
-
-
-
-# are these heuristics correct or guaranteed?
-pcre = re.compile(r'[ \t]*the\s*(bns)?\s*(postfix|keftamail|smtp_gateway)',
-                  re.IGNORECASE)
-rcre = re.compile(r'failure reason:$', re.IGNORECASE)
-acre = re.compile(r'<(?P<addr>[^>]*)>:')
-
-def findaddr(msg):
-    addrs = []
-    body = StringIO(msg.get_payload())
-    # simple state machine
-    #     0 == nothing found
-    #     1 == salutation found
-    state = 0
-    while 1:
-        line = body.readline()
-        if not line:
-            break
-        # preserve leading whitespace
-        line = line.rstrip()
-        # yes use match to match at beginning of string
-        if state == 0 and (pcre.match(line) or rcre.match(line)):
-            state = 1
-        elif state == 1 and line:
-            mo = acre.search(line)
-            if mo:
-                addrs.append(mo.group('addr'))
-            # probably a continuation line
-    return addrs
-
-
-
-def process(msg):
-    if msg.get_content_type() not in ('multipart/mixed', 'multipart/report'):
-        return None
-    # We're looking for the plain/text subpart with a Content-Description: of
-    # `notification'.
-    leaves = []
-    flatten(msg, leaves)
-    for subpart in leaves:
-        if subpart.get_content_type() == 'text/plain' and \
-           subpart.get('content-description', '').lower() == 'notification':
-            # then...
-            return findaddr(subpart)
-    return None
diff --git a/wcs/ctl/Bouncers/Qmail.py b/wcs/ctl/Bouncers/Qmail.py
deleted file mode 100644
index def4abfa0..000000000
--- a/wcs/ctl/Bouncers/Qmail.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Parse bounce messages generated by qmail.
-
-Qmail actually has a standard, called QSBMF (qmail-send bounce message
-format), as described in
-
-    http://cr.yp.to/proto/qsbmf.txt
-
-This module should be conformant.
-
-"""
-
-import re
-import email.Iterators
-
-# Other (non-standard?) intros have been observed in the wild.
-introtags = [
-    'Hi. This is the',
-    "We're sorry. There's a problem",
-    'Check your send e-mail address.'
-    ]
-acre = re.compile(r'<(?P<addr>[^>]*)>:')
-
-
-
-def process(msg):
-    addrs = []
-    # simple state machine
-    #    0 = nothing seen yet
-    #    1 = intro paragraph seen
-    #    2 = recip paragraphs seen
-    state = 0
-    for line in email.Iterators.body_line_iterator(msg):
-        line = line.strip()
-        if state == 0:
-            for introtag in introtags:
-                if line.startswith(introtag):
-                    state = 1
-                    break
-        elif state == 1 and not line:
-            # Looking for the end of the intro paragraph
-            state = 2
-        elif state == 2:
-            if line.startswith('-'):
-                # We're looking at the break paragraph, so we're done
-                break
-            # At this point we know we must be looking at a recipient
-            # paragraph
-            mo = acre.match(line)
-            if mo:
-                addrs.append(mo.group('addr'))
-            # Otherwise, it must be a continuation line, so just ignore it
-        # Not looking at anything in particular
-    return addrs
diff --git a/wcs/ctl/Bouncers/SMTP32.py b/wcs/ctl/Bouncers/SMTP32.py
deleted file mode 100644
index 3ad7ce44c..000000000
--- a/wcs/ctl/Bouncers/SMTP32.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Something which claims
-X-Mailer: <SMTP32 vXXXXXX>
-
-What the heck is this thing?  Here's a recent host:
-
-% telnet 207.51.255.218 smtp
-Trying 207.51.255.218...
-Connected to 207.51.255.218.
-Escape character is '^]'.
-220 X1 NT-ESMTP Server 208.24.118.205 (IMail 6.00 45595-15)
-
-"""
-
-import re
-import email
-
-ecre = re.compile('original message follows', re.IGNORECASE)
-acre = re.compile(r'''
-    (                                             # several different prefixes
-    user\ mailbox[^:]*:                           # have been spotted in the
-    |delivery\ failed[^:]*:                       # wild...
-    |unknown\ user[^:]*:
-    |undeliverable\ +to
-    )
-    \s*                                           # space separator
-    (?P<addr>.*)                                  # and finally, the address
-    ''', re.IGNORECASE | re.VERBOSE)
-
-
-
-def process(msg):
-    mailer = msg.get('x-mailer', '')
-    if not mailer.startswith('<SMTP32 v'):
-        return
-    addrs = {}
-    for line in email.Iterators.body_line_iterator(msg):
-        if ecre.search(line):
-            break
-        mo = acre.search(line)
-        if mo:
-            addrs[mo.group('addr')] = 1
-    return addrs.keys()
diff --git a/wcs/ctl/Bouncers/SimpleMatch.py b/wcs/ctl/Bouncers/SimpleMatch.py
deleted file mode 100644
index 12eeffdca..000000000
--- a/wcs/ctl/Bouncers/SimpleMatch.py
+++ /dev/null
@@ -1,165 +0,0 @@
-# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Recognizes simple heuristically delimited bounces."""
-
-import re
-import email.Iterators
-
-
-
-def _c(pattern):
-    return re.compile(pattern, re.IGNORECASE)
-
-# This is a list of tuples of the form
-#
-#     (start cre, end cre, address cre)
-#
-# where `cre' means compiled regular expression, start is the line just before
-# the bouncing address block, end is the line just after the bouncing address
-# block, and address cre is the regexp that will recognize the addresses.  It
-# must have a group called `addr' which will contain exactly and only the
-# address that bounced.
-PATTERNS = [
-    # sdm.de
-    (_c('here is your list of failed recipients'),
-     _c('here is your returned mail'),
-     _c(r'<(?P<addr>[^>]*)>')),
-    # sz-sb.de, corridor.com, nfg.nl
-    (_c('the following addresses had'),
-     _c('transcript of session follows'),
-     _c(r'<(?P<fulladdr>[^>]*)>|\(expanded from: <?(?P<addr>[^>)]*)>?\)')),
-    # robanal.demon.co.uk
-    (_c('this message was created automatically by mail delivery software'),
-     _c('original message follows'),
-     _c('rcpt to:\s*<(?P<addr>[^>]*)>')),
-    # s1.com (InterScan E-Mail VirusWall NT ???)
-    (_c('message from interscan e-mail viruswall nt'),
-     _c('end of message'),
-     _c('rcpt to:\s*<(?P<addr>[^>]*)>')),
-    # Smail
-    (_c('failed addresses follow:'),
-     _c('message text follows:'),
-     _c(r'\s*(?P<addr>\S+@\S+)')),
-    # newmail.ru
-    (_c('This is the machine generated message from mail service.'),
-     _c('--- Below the next line is a copy of the message.'),
-     _c('<(?P<addr>[^>]*)>')),
-    # turbosport.com runs something called `MDaemon 3.5.2' ???
-    (_c('The following addresses did NOT receive a copy of your message:'),
-     _c('--- Session Transcript ---'),
-     _c('[>]\s*(?P<addr>.*)$')),
-    # usa.net
-    (_c('Intended recipient:\s*(?P<addr>.*)$'),
-     _c('--------RETURNED MAIL FOLLOWS--------'),
-     _c('Intended recipient:\s*(?P<addr>.*)$')),
-    # hotpop.com
-    (_c('Undeliverable Address:\s*(?P<addr>.*)$'),
-     _c('Original message attached'),
-     _c('Undeliverable Address:\s*(?P<addr>.*)$')),
-    # Another demon.co.uk format
-    (_c('This message was created automatically by mail delivery'),
-     _c('^---- START OF RETURNED MESSAGE ----'),
-     _c("addressed to '(?P<addr>[^']*)'")),
-    # Prodigy.net full mailbox
-    (_c("User's mailbox is full:"),
-     _c('Unable to deliver mail.'),
-     _c("User's mailbox is full:\s*<(?P<addr>[^>]*)>")),
-    # Microsoft SMTPSVC
-    (_c('The email below could not be delivered to the following user:'),
-     _c('Old message:'),
-     _c('<(?P<addr>[^>]*)>')),
-    # Yahoo on behalf of other domains like sbcglobal.net
-    (_c('Unable to deliver message to the following address\(es\)\.'),
-     _c('--- Original message follows\.'),
-     _c('<(?P<addr>[^>]*)>:')),
-    # kundenserver.de
-    (_c('A message that you sent could not be delivered'),
-     _c('^---'),
-     _c('<(?P<addr>[^>]*)>')),
-    # another kundenserver.de
-    (_c('A message that you sent could not be delivered'),
-     _c('^---'),
-     _c('^(?P<addr>[^\s@]+@[^\s@:]+):')),
-    # thehartford.com
-    (_c('Delivery to the following recipients failed'),
-     _c("Bogus - there actually isn't anything"),
-     _c('^\s*(?P<addr>[^\s@]+@[^\s@]+)\s*$')),
-    # and another thehartfod.com/hartfordlife.com
-    (_c('^Your message\s*$'),
-     _c('^because:'),
-     _c('^\s*(?P<addr>[^\s@]+@[^\s@]+)\s*$')),
-    # kviv.be (NTMail)
-    (_c('^Unable to deliver message to'),
-     _c(r'\*+\s+End of message\s+\*+'),
-     _c('<(?P<addr>[^>]*)>')),
-    # earthlink.net supported domains
-    (_c('^Sorry, unable to deliver your message to'),
-     _c('^A copy of the original message'),
-     _c('\s*(?P<addr>[^\s@]+@[^\s@]+)\s+')),
-    # ademe.fr
-    (_c('^A message could not be delivered to:'),
-     _c('^Subject:'),
-     _c('^\s*(?P<addr>[^\s@]+@[^\s@]+)\s*$')),
-    # andrew.ac.jp
-    (_c('^Invalid final delivery userid:'),
-     _c('^Original message follows.'),
-     _c('\s*(?P<addr>[^\s@]+@[^\s@]+)\s*$')),
-    # E500_SMTP_Mail_Service@lerctr.org
-    (_c('------ Failed Recipients ------'),
-     _c('-------- Returned Mail --------'),
-     _c('<(?P<addr>[^>]*)>')),
-    # cynergycom.net
-    (_c('A message that you sent could not be delivered'),
-     _c('^---'),
-     _c('(?P<addr>[^\s@]+@[^\s@)]+)')),
-    # Next one goes here...
-    ]
-
-
-
-def process(msg, patterns=None):
-    if patterns is None:
-        patterns = PATTERNS
-    # simple state machine
-    #     0 = nothing seen yet
-    #     1 = intro seen
-    addrs = {}
-    # MAS: This is a mess. The outer loop used to be over the message
-    # so we only looped through the message once.  Looping through the
-    # message for each set of patterns is obviously way more work, but
-    # if we don't do it, problems arise because scre from the wrong
-    # pattern set matches first and then acre doesn't match.  The
-    # alternative is to split things into separate modules, but then
-    # we process the message multiple times anyway.
-    for scre, ecre, acre in patterns:
-        state = 0
-        for line in email.Iterators.body_line_iterator(msg):
-            if state == 0:
-                if scre.search(line):
-                    state = 1
-            if state == 1:
-                mo = acre.search(line)
-                if mo:
-                    addr = mo.group('addr')
-                    if addr:
-                        addrs[mo.group('addr')] = 1
-                elif ecre.search(line):
-                    break
-        if addrs:
-            break
-    return addrs.keys()
diff --git a/wcs/ctl/Bouncers/SimpleWarning.py b/wcs/ctl/Bouncers/SimpleWarning.py
deleted file mode 100644
index 0053766bc..000000000
--- a/wcs/ctl/Bouncers/SimpleWarning.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright (C) 2001-2006 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Recognizes simple heuristically delimited warnings."""
-
-from BouncerAPI import Stop
-from SimpleMatch import _c
-from SimpleMatch import process as _process
-
-
-
-# This is a list of tuples of the form
-#
-#     (start cre, end cre, address cre)
-#
-# where `cre' means compiled regular expression, start is the line just before
-# the bouncing address block, end is the line just after the bouncing address
-# block, and address cre is the regexp that will recognize the addresses.  It
-# must have a group called `addr' which will contain exactly and only the
-# address that bounced.
-patterns = [
-    # pop3.pta.lia.net
-    (_c('The address to which the message has not yet been delivered is'),
-     _c('No action is required on your part'),
-     _c(r'\s*(?P<addr>\S+@\S+)\s*')),
-    # Next one goes here...
-    ]
-
-
-
-def process(msg):
-    if _process(msg, patterns):
-        # It's a recognized warning so stop now
-        return Stop
-    else:
-        return []
diff --git a/wcs/ctl/Bouncers/Yahoo.py b/wcs/ctl/Bouncers/Yahoo.py
deleted file mode 100644
index b3edf4fa7..000000000
--- a/wcs/ctl/Bouncers/Yahoo.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Yahoo! has its own weird format for bounces."""
-
-import re
-import email
-from email.Utils import parseaddr
-
-tcre = re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE)
-acre = re.compile(r'<(?P<addr>[^>]*)>:')
-ecre = re.compile(r'--- Original message follows')
-
-
-
-def process(msg):
-    # Yahoo! bounces seem to have a known subject value and something called
-    # an x-uidl: header, the value of which seems unimportant.
-    sender = parseaddr(msg.get('from', '').lower())[1] or ''
-    if not sender.startswith('mailer-daemon@yahoo'):
-        return None
-    addrs = []
-    # simple state machine
-    #     0 == nothing seen
-    #     1 == tag line seen
-    state = 0
-    for line in email.Iterators.body_line_iterator(msg):
-        line = line.strip()
-        if state == 0 and tcre.match(line):
-            state = 1
-        elif state == 1:
-            mo = acre.match(line)
-            if mo:
-                addrs.append(mo.group('addr'))
-                continue
-            mo = ecre.match(line)
-            if mo:
-                # we're at the end of the error response
-                break
-    return addrs
diff --git a/wcs/ctl/Bouncers/Yale.py b/wcs/ctl/Bouncers/Yale.py
deleted file mode 100644
index 6b4aa3ee1..000000000
--- a/wcs/ctl/Bouncers/Yale.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# Copyright (C) 2000,2001,2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-"""Yale's mail server is pretty dumb.
-
-Its reports include the end user's name, but not the full domain.  I think we
-can usually guess it right anyway.  This is completely based on examination of
-the corpse, and is subject to failure whenever Yale even slightly changes
-their MTA. :(
-
-"""
-
-import re
-from cStringIO import StringIO
-from email.Utils import getaddresses
-
-scre = re.compile(r'Message not delivered to the following', re.IGNORECASE)
-ecre = re.compile(r'Error Detail', re.IGNORECASE)
-acre = re.compile(r'\s+(?P<addr>\S+)\s+')
-
-
-
-def process(msg):
-    if msg.is_multipart():
-        return None
-    try:
-        whofrom = getaddresses([msg.get('from', '')])[0][1]
-        if not whofrom:
-            return None
-        username, domain = whofrom.split('@', 1)
-    except (IndexError, ValueError):
-        return None
-    if username.lower() <> 'mailer-daemon':
-        return None
-    parts = domain.split('.')
-    parts.reverse()
-    for part1, part2 in zip(parts, ('edu', 'yale')):
-        if part1 <> part2:
-            return None
-    # Okay, we've established that the bounce came from the mailer-daemon at
-    # yale.edu.  Let's look for a name, and then guess the relevant domains.
-    names = {}
-    body = StringIO(msg.get_payload())
-    state = 0
-    # simple state machine
-    #     0 == init
-    #     1 == intro found
-    while 1:
-        line = body.readline()
-        if not line:
-            break
-        if state == 0 and scre.search(line):
-            state = 1
-        elif state == 1 and ecre.search(line):
-            break
-        elif state == 1:
-            mo = acre.search(line)
-            if mo:
-                names[mo.group('addr')] = 1
-    # Now we have a bunch of names, these are either @yale.edu or
-    # @cs.yale.edu.  Add them both.
-    addrs = []
-    for name in names.keys():
-        addrs.append(name + '@yale.edu')
-        addrs.append(name + '@cs.yale.edu')
-    return addrs
diff --git a/wcs/ctl/Bouncers/__init__.py b/wcs/ctl/Bouncers/__init__.py
deleted file mode 100644
index f569e43f4..000000000
--- a/wcs/ctl/Bouncers/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
-#
-# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/wcs/ctl/process_bounce.py b/wcs/ctl/process_bounce.py
deleted file mode 100644
index 32ab89dc8..000000000
--- a/wcs/ctl/process_bounce.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# w.c.s. - web application for online forms
-# Copyright (C) 2005-2010  Entr'ouvert
-#
-# 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, see <http://www.gnu.org/licenses/>.
-
-import sys
-import time
-import os
-import email.Parser
-
-from Bouncers import BouncerAPI
-
-from ..qommon.ctl import Command
-
-COMMA_SPACE = ', '
-
-class CmdProcessBounce(Command):
-    name = 'process_bounce'
-
-    def execute(self, base_options, sub_options, args):
-        from ..qommon.tokens import Token
-        from ..qommon.bounces import Bounce
-
-        from .. import publisher
-
-        try:
-            publisher.WcsPublisher.configure(self.config)
-            pub = publisher.WcsPublisher.create_publisher(
-                    register_tld_names=False)
-        except:
-            # not much we can do if we don't have a publisher object :/
-            return
-
-        try:
-            parser = email.Parser.Parser()
-            msg = parser.parse(sys.stdin)
-            addrs = self.get_bounce_addrs(msg)
-            if addrs is None:
-                # not a bounce
-                return
-
-            try:
-                to = msg['To']
-                local_part, server_part = to.split('@')
-                token_id = local_part.split('+')[1]
-            except (IndexError, KeyError):
-                return
-
-            pub.app_dir = os.path.join(pub.app_dir, server_part)
-            if not os.path.exists(pub.app_dir):
-                return
-
-            try:
-                token = Token.get(token_id)
-            except KeyError:
-                return
-
-            if token.type != 'email-bounce':
-                return
-
-            token.remove_self()
-
-            bounce = Bounce()
-            bounce.arrival_time = time.time()
-            bounce.bounce_message = msg.as_string()
-            bounce.addrs = addrs
-            bounce.original_message = token.email_message
-            bounce.original_rcpts = token.email_rcpts
-            bounce.email_type = token.email_type
-            bounce.store()
-        except:
-            pub.notify_of_exception(sys.exc_info(), context='[BOUNCE]')
-            sys.exit(1)
-
-    @classmethod
-    def get_bounce_addrs(cls, msg):
-        bouncers_dir = os.path.join(os.path.dirname(__file__), 'Bouncers')
-        sys.path.append(bouncers_dir)
-        for modname in BouncerAPI.BOUNCE_PIPELINE:
-            __import__(modname)
-            addrs = sys.modules[modname].process(msg)
-            if addrs is BouncerAPI.Stop:
-                return None # Stop means to ignore message
-            if addrs:
-                return addrs
-        return None # didn't find any match
-
-CmdProcessBounce.register()
diff --git a/wcs/qommon/admin/emails.py b/wcs/qommon/admin/emails.py
index 879d25e8c..b6aa636e2 100644
--- a/wcs/qommon/admin/emails.py
+++ b/wcs/qommon/admin/emails.py
@@ -111,8 +111,6 @@ class EmailsDirectory(Directory):
                 required = False, value = emails.get('reply_to'))
         form.add(TextWidget, 'footer', title=_('Email Footer'), cols=70, rows=5,
                  required=False, value=emails.get('footer'))
-        form.add(CheckboxWidget, 'bounce_handler', title = _('Handle Bounces'),
-                value = emails.get('bounce_handler'))
         form.add(CheckboxWidget, 'check_domain_with_dns',
                 title = _('Check DNS for domain name'),
                 value = emails.get('check_domain_with_dns', True),
@@ -140,7 +138,7 @@ class EmailsDirectory(Directory):
             return r.getvalue()
         else:
             cfg_submit(form, 'emails', [ 'smtp_server', 'smtp_login',
-                'smtp_password', 'from', 'reply_to', 'footer', 'bounce_handler',
+                'smtp_password', 'from', 'reply_to', 'footer',
                 'check_domain_with_dns'])
             return redirect('.')
 
diff --git a/wcs/qommon/bounces.py b/wcs/qommon/bounces.py
deleted file mode 100644
index 120c25802..000000000
--- a/wcs/qommon/bounces.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# w.c.s. - web application for online forms
-# Copyright (C) 2005-2010  Entr'ouvert
-#
-# 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, see <http://www.gnu.org/licenses/>.
-
-from .storage import StorableObject
-
-class Bounce(StorableObject):
-    _names = 'bounces'
-
-    arrival_time = None
-    bounce_message = None
-    addrs = None
-    original_message = None
-    original_rcpts = None
-    email_type = None
-
diff --git a/wcs/qommon/emails.py b/wcs/qommon/emails.py
index 3e592732d..1555e3779 100644
--- a/wcs/qommon/emails.py
+++ b/wcs/qommon/emails.py
@@ -302,19 +302,6 @@ def email(subject, mail_body, email_rcpt, replyto=None, bcc=None,
         # to that address instead of the real recipients.
         rcpts = [os.environ.get('QOMMON_MAIL_REDIRECTION')]
 
-    if emails_cfg.get('bounce_handler'):
-        if get_request():
-            server_name = get_request().get_server().split(':')[0]
-        else:
-            server_name = email_from.split('@')[1]
-        token = tokens.Token(7 * 86400)
-        token.type = 'email-bounce'
-        token.email_rcpts = [str(x) for x in rcpts]
-        token.email_message = msg.as_string()
-        token.email_type = str(email_type)
-        token.store()
-        email_from = '%s-bounces+%s@%s' % (get_publisher().APP_NAME, token.id, server_name)
-
     if not fire_and_forget:
         s = create_smtp_server(emails_cfg, smtp_timeout=smtp_timeout)
         try:
-- 
2.24.0

