general: remove bounce processing (#36515)
This commit is contained in:
parent
268d68afa9
commit
abf453b07a
|
@ -1,5 +1,5 @@
|
||||||
[run]
|
[run]
|
||||||
omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
|
omit = wcs/qommon/vendor/*.py
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
|
omit = wcs/qommon/vendor/*.py
|
||||||
|
|
8
README
8
README
|
@ -41,14 +41,6 @@ AUTHORS file for additional credits.
|
||||||
w.c.s. incorporates some other pieces of code, with their own authors and
|
w.c.s. incorporates some other pieces of code, with their own authors and
|
||||||
copyright notices :
|
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+:
|
Some artwork from GTK+:
|
||||||
# http://www.gtk.org/
|
# http://www.gtk.org/
|
||||||
#
|
#
|
||||||
|
|
|
@ -16,10 +16,10 @@ rm -f coverage.xml
|
||||||
rm -f test_results.xml
|
rm -f test_results.xml
|
||||||
cat << _EOF_ > .coveragerc
|
cat << _EOF_ > .coveragerc
|
||||||
[run]
|
[run]
|
||||||
omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
|
omit = wcs/qommon/vendor/*.py
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
|
omit = wcs/qommon/vendor/*.py
|
||||||
_EOF_
|
_EOF_
|
||||||
|
|
||||||
# $PIP_BIN install --upgrade 'pip<8'
|
# $PIP_BIN install --upgrade 'pip<8'
|
||||||
|
|
|
@ -32,7 +32,6 @@ from wcs.qommon.form import UploadedFile
|
||||||
from wcs.qommon.ident.password_accounts import PasswordAccount
|
from wcs.qommon.ident.password_accounts import PasswordAccount
|
||||||
from wcs.qommon.http_request import HTTPRequest
|
from wcs.qommon.http_request import HTTPRequest
|
||||||
from wcs.qommon.template import get_current_theme
|
from wcs.qommon.template import get_current_theme
|
||||||
from wcs.qommon.bounces import Bounce
|
|
||||||
from wcs.admin.settings import UserFieldsFormDef
|
from wcs.admin.settings import UserFieldsFormDef
|
||||||
from wcs.categories import Category
|
from wcs.categories import Category
|
||||||
from wcs.data_sources import NamedDataSource
|
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/alto/../', status=404)
|
||||||
assert app.get('/themes/xxx/../', 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):
|
def test_postgresql_settings(pub):
|
||||||
if not pub.is_using_postgresql():
|
if not pub.is_using_postgresql():
|
||||||
pytest.skip('this requires SQL')
|
pytest.skip('this requires SQL')
|
||||||
|
|
|
@ -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']
|
|
|
@ -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 import Command as CmdMigrate
|
||||||
from wcs.qommon.management.commands.migrate_schemas import Command as CmdMigrateSchemas
|
from wcs.qommon.management.commands.migrate_schemas import Command as CmdMigrateSchemas
|
||||||
from wcs.qommon.management.commands.makemessages import Command as CmdMakeMessages
|
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.rebuild_indexes import rebuild_vhost_indexes
|
||||||
from wcs.ctl.wipe_data import CmdWipeData
|
from wcs.ctl.wipe_data import CmdWipeData
|
||||||
from wcs.ctl.management.commands.runscript import Command as CmdRunScript
|
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):
|
def test_migrate_schemas(two_pubs):
|
||||||
CmdMigrateSchemas().handle()
|
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):
|
def test_wipe_formdata(pub):
|
||||||
form_1 = FormDef()
|
form_1 = FormDef()
|
||||||
form_1.name = 'example'
|
form_1.name = 'example'
|
||||||
|
|
|
@ -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()
|
|
|
@ -554,7 +554,6 @@ class SettingsDirectory(QommonSettingsDirectory):
|
||||||
('users', N_('Users')),
|
('users', N_('Users')),
|
||||||
('roles', N_('Roles')),
|
('roles', N_('Roles')),
|
||||||
('categories', N_('Categories')),
|
('categories', N_('Categories')),
|
||||||
('bounces', N_('Bounces')),
|
|
||||||
('settings', N_('Settings')),
|
('settings', N_('Settings')),
|
||||||
]
|
]
|
||||||
for k, v in admin_sections:
|
for k, v in admin_sections:
|
||||||
|
|
|
@ -29,7 +29,6 @@ from ..qommon.form import *
|
||||||
|
|
||||||
from wcs.formdef import FormDef
|
from wcs.formdef import FormDef
|
||||||
|
|
||||||
import wcs.admin.bounces
|
|
||||||
import wcs.admin.categories
|
import wcs.admin.categories
|
||||||
import wcs.admin.forms
|
import wcs.admin.forms
|
||||||
import wcs.admin.roles
|
import wcs.admin.roles
|
||||||
|
@ -47,7 +46,6 @@ from . import data_management
|
||||||
class RootDirectory(BackofficeRootDirectory):
|
class RootDirectory(BackofficeRootDirectory):
|
||||||
_q_exports = ['', 'pending', 'statistics', ('menu.json', 'menu_json')]
|
_q_exports = ['', 'pending', 'statistics', ('menu.json', 'menu_json')]
|
||||||
|
|
||||||
bounces = wcs.admin.bounces.BouncesDirectory()
|
|
||||||
forms = wcs.admin.forms.FormsDirectory()
|
forms = wcs.admin.forms.FormsDirectory()
|
||||||
roles = wcs.admin.roles.RolesDirectory()
|
roles = wcs.admin.roles.RolesDirectory()
|
||||||
settings = wcs.admin.settings.SettingsDirectory()
|
settings = wcs.admin.settings.SettingsDirectory()
|
||||||
|
@ -69,7 +67,6 @@ class RootDirectory(BackofficeRootDirectory):
|
||||||
('workflows/', N_('Workflows Workshop'), {'sub': True}),
|
('workflows/', N_('Workflows Workshop'), {'sub': True}),
|
||||||
('users/', N_('Users'), {'check_display_function': roles.is_visible}),
|
('users/', N_('Users'), {'check_display_function': roles.is_visible}),
|
||||||
('roles/', N_('Roles'), {'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')),
|
('settings/', N_('Settings')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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 []
|
|
|
@ -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')]
|
|
|
@ -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
|
|
|
@ -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)
|
|
|
@ -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()
|
|
|
@ -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)]
|
|
|
@ -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()
|
|
|
@ -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 []
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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()
|
|
|
@ -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()
|
|
|
@ -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 []
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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.
|
|
|
@ -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()
|
|
|
@ -111,8 +111,6 @@ class EmailsDirectory(Directory):
|
||||||
required = False, value = emails.get('reply_to'))
|
required = False, value = emails.get('reply_to'))
|
||||||
form.add(TextWidget, 'footer', title=_('Email Footer'), cols=70, rows=5,
|
form.add(TextWidget, 'footer', title=_('Email Footer'), cols=70, rows=5,
|
||||||
required=False, value=emails.get('footer'))
|
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',
|
form.add(CheckboxWidget, 'check_domain_with_dns',
|
||||||
title = _('Check DNS for domain name'),
|
title = _('Check DNS for domain name'),
|
||||||
value = emails.get('check_domain_with_dns', True),
|
value = emails.get('check_domain_with_dns', True),
|
||||||
|
@ -140,7 +138,7 @@ class EmailsDirectory(Directory):
|
||||||
return r.getvalue()
|
return r.getvalue()
|
||||||
else:
|
else:
|
||||||
cfg_submit(form, 'emails', [ 'smtp_server', 'smtp_login',
|
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'])
|
'check_domain_with_dns'])
|
||||||
return redirect('.')
|
return redirect('.')
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -302,19 +302,6 @@ def email(subject, mail_body, email_rcpt, replyto=None, bcc=None,
|
||||||
# to that address instead of the real recipients.
|
# to that address instead of the real recipients.
|
||||||
rcpts = [os.environ.get('QOMMON_MAIL_REDIRECTION')]
|
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:
|
if not fire_and_forget:
|
||||||
s = create_smtp_server(emails_cfg, smtp_timeout=smtp_timeout)
|
s = create_smtp_server(emails_cfg, smtp_timeout=smtp_timeout)
|
||||||
try:
|
try:
|
||||||
|
|
Loading…
Reference in New Issue