From abf453b07a4d926c9f2dc95eaf6c96d13ed3613b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Mon, 11 Nov 2019 21:52:07 +0100 Subject: [PATCH] general: remove bounce processing (#36515) --- .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('

Hello world

', _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 . - -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('
') - - if self.bounce.email_type: - r += htmltext('
%s
') % _('Email Type') - r += htmltext('
') - r += _(get_email_type_label(self.bounce.email_type)) - r += htmltext('
') - - r += htmltext('
%s
') % _('Arrival Time') - r += htmltext('
') - r += misc.localstrftime(time.localtime(self.bounce.arrival_time)) - r += htmltext('
') - - if self.bounce.addrs: - r += htmltext('
%s
') % _('Failed Addresses') - r += htmltext('
') - r += ', '.join(self.bounce.addrs) - r += htmltext('
') - - if self.bounce.original_rcpts: - r += htmltext('
%s
') % _('Original Recipients') - r += htmltext('
') - r += ', '.join(self.bounce.original_rcpts) - r += htmltext('
') - - r += htmltext('
%s
') % _('Bounce Message') - r += htmltext('
') - r += htmltext('
')
-        r += self.bounce.bounce_message
-        r += htmltext('
') - r += htmltext('
') - - 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('
%s
') % _('Original Message') - r += htmltext('
') - 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('
') - - if m: - r += htmltext('
') - r += htmltext('
')
-                r += m.get_payload()
-                r += htmltext('
') - r += htmltext('
') - - r += htmltext('
') # form - return r.getvalue() - - - def delete(self): - form = Form(enctype='multipart/form-data') - form.widgets.append(HtmlWidget('

%s

' % _( - '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('

%s

') % _('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('') - 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[^>]*)>') - - - -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.*)') - - - -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[^;]+); on ') -a2cre = re.compile('(?P[^ ]+) 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[^>]*)>') - - - -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\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'(?Pplease reply to)?.*<(?P[^>]*)>', - 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 - # 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[^>]*)>:') - -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[^>]*)>:') - - - -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: - -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.*) # and finally, the address - ''', re.IGNORECASE | re.VERBOSE) - - - -def process(msg): - mailer = msg.get('x-mailer', '') - if not mailer.startswith('[^>]*)>')), - # sz-sb.de, corridor.com, nfg.nl - (_c('the following addresses had'), - _c('transcript of session follows'), - _c(r'<(?P[^>]*)>|\(expanded from: [^>)]*)>?\)')), - # robanal.demon.co.uk - (_c('this message was created automatically by mail delivery software'), - _c('original message follows'), - _c('rcpt to:\s*<(?P[^>]*)>')), - # s1.com (InterScan E-Mail VirusWall NT ???) - (_c('message from interscan e-mail viruswall nt'), - _c('end of message'), - _c('rcpt to:\s*<(?P[^>]*)>')), - # Smail - (_c('failed addresses follow:'), - _c('message text follows:'), - _c(r'\s*(?P\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[^>]*)>')), - # 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.*)$')), - # usa.net - (_c('Intended recipient:\s*(?P.*)$'), - _c('--------RETURNED MAIL FOLLOWS--------'), - _c('Intended recipient:\s*(?P.*)$')), - # hotpop.com - (_c('Undeliverable Address:\s*(?P.*)$'), - _c('Original message attached'), - _c('Undeliverable Address:\s*(?P.*)$')), - # Another demon.co.uk format - (_c('This message was created automatically by mail delivery'), - _c('^---- START OF RETURNED MESSAGE ----'), - _c("addressed to '(?P[^']*)'")), - # Prodigy.net full mailbox - (_c("User's mailbox is full:"), - _c('Unable to deliver mail.'), - _c("User's mailbox is full:\s*<(?P[^>]*)>")), - # Microsoft SMTPSVC - (_c('The email below could not be delivered to the following user:'), - _c('Old message:'), - _c('<(?P[^>]*)>')), - # 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[^>]*)>:')), - # kundenserver.de - (_c('A message that you sent could not be delivered'), - _c('^---'), - _c('<(?P[^>]*)>')), - # another kundenserver.de - (_c('A message that you sent could not be delivered'), - _c('^---'), - _c('^(?P[^\s@]+@[^\s@:]+):')), - # thehartford.com - (_c('Delivery to the following recipients failed'), - _c("Bogus - there actually isn't anything"), - _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')), - # and another thehartfod.com/hartfordlife.com - (_c('^Your message\s*$'), - _c('^because:'), - _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')), - # kviv.be (NTMail) - (_c('^Unable to deliver message to'), - _c(r'\*+\s+End of message\s+\*+'), - _c('<(?P[^>]*)>')), - # earthlink.net supported domains - (_c('^Sorry, unable to deliver your message to'), - _c('^A copy of the original message'), - _c('\s*(?P[^\s@]+@[^\s@]+)\s+')), - # ademe.fr - (_c('^A message could not be delivered to:'), - _c('^Subject:'), - _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')), - # andrew.ac.jp - (_c('^Invalid final delivery userid:'), - _c('^Original message follows.'), - _c('\s*(?P[^\s@]+@[^\s@]+)\s*$')), - # E500_SMTP_Mail_Service@lerctr.org - (_c('------ Failed Recipients ------'), - _c('-------- Returned Mail --------'), - _c('<(?P[^>]*)>')), - # cynergycom.net - (_c('A message that you sent could not be delivered'), - _c('^---'), - _c('(?P[^\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\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[^>]*)>:') -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\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 . - -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 . - -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: