general: remove bounce processing (#36515)

This commit is contained in:
Frédéric Péters 2019-11-11 21:52:07 +01:00
parent 268d68afa9
commit abf453b07a
31 changed files with 5 additions and 1576 deletions

View File

@ -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

8
README
View File

@ -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/
#

View File

@ -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'

View File

@ -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')

View File

@ -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']

View File

@ -16,7 +16,6 @@ from wcs.qommon.management.commands.collectstatic import Command as CmdCollectSt
from wcs.qommon.management.commands.migrate import Command as CmdMigrate
from wcs.qommon.management.commands.migrate_schemas import Command as CmdMigrateSchemas
from wcs.qommon.management.commands.makemessages import Command as CmdMakeMessages
from wcs.ctl.process_bounce import CmdProcessBounce
from wcs.ctl.rebuild_indexes import rebuild_vhost_indexes
from wcs.ctl.wipe_data import CmdWipeData
from wcs.ctl.management.commands.runscript import Command as CmdRunScript
@ -67,24 +66,6 @@ def test_migrate(two_pubs):
def test_migrate_schemas(two_pubs):
CmdMigrateSchemas().handle()
def test_get_bounce_addrs():
msg = MIMEText('Hello world')
assert CmdProcessBounce.get_bounce_addrs(msg) is None
msg = MIMEMultipart(_subtype='mixed')
msg.attach(MIMEText('Hello world'))
msg.attach(MIMEText('<p>Hello world</p>', _subtype='html'))
assert CmdProcessBounce.get_bounce_addrs(msg) is None
msg = MIMEText('Hello world')
msg['x-failed-recipients'] = 'foobar@localhost'
assert CmdProcessBounce.get_bounce_addrs(msg) == ['foobar@localhost']
msg = MIMEText('''failed addresses follow:
foobar@localhost
message text follows:''')
assert CmdProcessBounce.get_bounce_addrs(msg) == ['foobar@localhost']
def test_wipe_formdata(pub):
form_1 = FormDef()
form_1.name = 'example'

View File

@ -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()

View File

@ -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:

View File

@ -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')),
]

View File

@ -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 []

View File

@ -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')]

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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)]

View File

@ -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()

View File

@ -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 []

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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 []

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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()

View File

@ -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('.')

View File

@ -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

View File

@ -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: