0001-general-remove-bounce-processing-33448.patch
.coveragerc | ||
---|---|---|
1 | 1 |
[run] |
2 |
omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
|
|
2 |
omit = wcs/qommon/vendor/*.py |
|
3 | 3 | |
4 | 4 |
[report] |
5 |
omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py |
|
5 |
omit = wcs/qommon/vendor/*.py |
README | ||
---|---|---|
41 | 41 |
w.c.s. incorporates some other pieces of code, with their own authors and |
42 | 42 |
copyright notices : |
43 | 43 | |
44 |
Email bounce detection code (wcs/ctl/Bounces/*) from Mailman: |
|
45 |
# http://www.gnu.org/software/mailman/ |
|
46 |
# |
|
47 |
# This program is free software; you can redistribute it and/or |
|
48 |
# modify it under the terms of the GNU General Public License |
|
49 |
# as published by the Free Software Foundation; either version 2 |
|
50 |
# of the License, or (at your option) any later version. |
|
51 | ||
52 | 44 |
Some artwork from GTK+: |
53 | 45 |
# http://www.gtk.org/ |
54 | 46 |
# |
jenkins.sh | ||
---|---|---|
16 | 16 |
rm -f test_results.xml |
17 | 17 |
cat << _EOF_ > .coveragerc |
18 | 18 |
[run] |
19 |
omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
|
|
19 |
omit = wcs/qommon/vendor/*.py |
|
20 | 20 | |
21 | 21 |
[report] |
22 |
omit = wcs/ctl/Bouncers/*.py wcs/qommon/vendor/*.py
|
|
22 |
omit = wcs/qommon/vendor/*.py |
|
23 | 23 |
_EOF_ |
24 | 24 | |
25 | 25 |
# $PIP_BIN install --upgrade 'pip<8' |
tests/test_admin_pages.py | ||
---|---|---|
32 | 32 |
from wcs.qommon.ident.password_accounts import PasswordAccount |
33 | 33 |
from wcs.qommon.http_request import HTTPRequest |
34 | 34 |
from wcs.qommon.template import get_current_theme |
35 |
from wcs.qommon.bounces import Bounce |
|
36 | 35 |
from wcs.admin.settings import UserFieldsFormDef |
37 | 36 |
from wcs.categories import Category |
38 | 37 |
from wcs.data_sources import NamedDataSource |
... | ... | |
5260 | 5259 |
assert app.get('/themes/alto/../', status=404) |
5261 | 5260 |
assert app.get('/themes/xxx/../', status=404) |
5262 | 5261 | |
5263 |
def test_bounces(pub): |
|
5264 |
create_superuser(pub) |
|
5265 |
app = login(get_app(pub)) |
|
5266 |
resp = app.get('/backoffice/') |
|
5267 |
assert not 'bounces' in resp.body |
|
5268 | ||
5269 |
resp = app.get('/backoffice/settings/emails/options') |
|
5270 |
resp.form['from'] = 'noreply@localhost' |
|
5271 |
resp.form['bounce_handler'].checked = True |
|
5272 |
resp = resp.form.submit('submit') |
|
5273 |
pub.reload_cfg() |
|
5274 |
assert pub.cfg['emails']['bounce_handler'] |
|
5275 | ||
5276 |
bounce = Bounce() |
|
5277 |
bounce.arrival_time = time.time() |
|
5278 |
bounce.bounce_message = 'foobar' |
|
5279 |
bounce.addrs = ['foo@localhost'] |
|
5280 |
bounce.email_type = 'change-password-request' |
|
5281 |
bounce.original_rcpts = ['foo@localhost', 'bar@localhost'] |
|
5282 |
msg = MIMEText('hello world') |
|
5283 |
msg['Subject'] = 'hello world' |
|
5284 |
bounce.original_message = msg.as_string() |
|
5285 |
bounce.store() |
|
5286 | ||
5287 |
resp = app.get('/backoffice/') |
|
5288 |
assert 'bounces' in resp.body |
|
5289 |
resp = resp.click('Bounces') |
|
5290 |
resp = resp.click(href='%s/' % bounce.id, index=0) |
|
5291 |
assert 'hello world' in resp.body |
|
5292 | ||
5293 |
resp = app.get('/backoffice/bounces/') |
|
5294 |
resp = resp.click(href='%s/delete' % bounce.id, index=0) |
|
5295 |
resp = resp.form.submit('cancel').follow() |
|
5296 |
assert Bounce.count() == 1 |
|
5297 |
resp = resp.click(href='%s/delete' % bounce.id, index=0) |
|
5298 |
resp = resp.form.submit('delete') |
|
5299 |
assert Bounce.count() == 0 |
|
5300 | ||
5301 | 5262 |
def test_postgresql_settings(pub): |
5302 | 5263 |
if not pub.is_using_postgresql(): |
5303 | 5264 |
pytest.skip('this requires SQL') |
tests/test_bounce_processing.py | ||
---|---|---|
1 |
import email |
|
2 | ||
3 |
from wcs.ctl.process_bounce import CmdProcessBounce |
|
4 | ||
5 |
def test_normal_email(): |
|
6 |
msg = email.message_from_string('test') |
|
7 |
msg['From'] = 'bar@example.net' |
|
8 |
msg['To'] = 'foo@example.net' |
|
9 |
addrs = CmdProcessBounce.get_bounce_addrs(msg) |
|
10 |
assert addrs is None |
|
11 | ||
12 |
def test_bounce_email(): |
|
13 |
msg = email.message_from_string('test') |
|
14 |
msg['From'] = 'bar@example.net' |
|
15 |
msg['To'] = 'foo@example.net' |
|
16 | ||
17 |
# this is how exim adds failed recipients in its outgoing bounce emails |
|
18 |
msg['x-failed-recipients'] = 'baz@example.net' |
|
19 | ||
20 |
addrs = CmdProcessBounce.get_bounce_addrs(msg) |
|
21 |
assert addrs == ['baz@example.net'] |
tests/test_ctl.py | ||
---|---|---|
16 | 16 |
from wcs.qommon.management.commands.migrate import Command as CmdMigrate |
17 | 17 |
from wcs.qommon.management.commands.migrate_schemas import Command as CmdMigrateSchemas |
18 | 18 |
from wcs.qommon.management.commands.makemessages import Command as CmdMakeMessages |
19 |
from wcs.ctl.process_bounce import CmdProcessBounce |
|
20 | 19 |
from wcs.ctl.rebuild_indexes import rebuild_vhost_indexes |
21 | 20 |
from wcs.ctl.wipe_data import CmdWipeData |
22 | 21 |
from wcs.ctl.management.commands.runscript import Command as CmdRunScript |
... | ... | |
67 | 66 |
def test_migrate_schemas(two_pubs): |
68 | 67 |
CmdMigrateSchemas().handle() |
69 | 68 | |
70 |
def test_get_bounce_addrs(): |
|
71 |
msg = MIMEText('Hello world') |
|
72 |
assert CmdProcessBounce.get_bounce_addrs(msg) is None |
|
73 | ||
74 |
msg = MIMEMultipart(_subtype='mixed') |
|
75 |
msg.attach(MIMEText('Hello world')) |
|
76 |
msg.attach(MIMEText('<p>Hello world</p>', _subtype='html')) |
|
77 |
assert CmdProcessBounce.get_bounce_addrs(msg) is None |
|
78 | ||
79 |
msg = MIMEText('Hello world') |
|
80 |
msg['x-failed-recipients'] = 'foobar@localhost' |
|
81 |
assert CmdProcessBounce.get_bounce_addrs(msg) == ['foobar@localhost'] |
|
82 | ||
83 |
msg = MIMEText('''failed addresses follow: |
|
84 |
foobar@localhost |
|
85 |
message text follows:''') |
|
86 |
assert CmdProcessBounce.get_bounce_addrs(msg) == ['foobar@localhost'] |
|
87 | ||
88 | 69 |
def test_wipe_formdata(pub): |
89 | 70 |
form_1 = FormDef() |
90 | 71 |
form_1.name = 'example' |
wcs/admin/bounces.py | ||
---|---|---|
1 |
# w.c.s. - web application for online forms |
|
2 |
# Copyright (C) 2005-2010 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or modify |
|
5 |
# it under the terms of the GNU General Public License as published by |
|
6 |
# the Free Software Foundation; either version 2 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import time |
|
18 |
import email.Parser |
|
19 | ||
20 |
from django.utils.encoding import force_text |
|
21 | ||
22 |
from quixote import get_response, redirect |
|
23 |
from quixote.directory import Directory |
|
24 |
from quixote.html import htmltext, TemplateIO |
|
25 | ||
26 |
from ..qommon import _ |
|
27 |
from ..qommon import errors |
|
28 |
from ..qommon import misc |
|
29 |
from ..qommon.bounces import Bounce |
|
30 |
from ..qommon.backoffice.menu import html_top |
|
31 |
from ..qommon.admin.menu import command_icon |
|
32 | ||
33 |
from ..qommon.form import * |
|
34 |
from ..qommon.misc import get_cfg |
|
35 | ||
36 |
def get_email_type_label(type): |
|
37 |
from .settings import EmailsDirectory |
|
38 |
return EmailsDirectory.emails_dict.get(type, {}).get('description') |
|
39 | ||
40 |
class BouncePage(Directory): |
|
41 |
_q_exports = ['', 'delete'] |
|
42 | ||
43 |
def __init__(self, component): |
|
44 |
self.bounce = Bounce.get(component) |
|
45 |
get_response().breadcrumb.append((component + '/', _('bounce'))) |
|
46 | ||
47 | ||
48 |
def _q_index(self): |
|
49 |
html_top('bounces', title = _('Bounce')) |
|
50 |
r = TemplateIO(html=True) |
|
51 | ||
52 |
r += htmltext('<div class="form">') |
|
53 | ||
54 |
if self.bounce.email_type: |
|
55 |
r += htmltext('<div class="title">%s</div>') % _('Email Type') |
|
56 |
r += htmltext('<div class="StringWidget content">') |
|
57 |
r += _(get_email_type_label(self.bounce.email_type)) |
|
58 |
r += htmltext('</div>') |
|
59 | ||
60 |
r += htmltext('<div class="title">%s</div>') % _('Arrival Time') |
|
61 |
r += htmltext('<div class="StringWidget content">') |
|
62 |
r += misc.localstrftime(time.localtime(self.bounce.arrival_time)) |
|
63 |
r += htmltext('</div>') |
|
64 | ||
65 |
if self.bounce.addrs: |
|
66 |
r += htmltext('<div class="title">%s</div>') % _('Failed Addresses') |
|
67 |
r += htmltext('<div class="StringWidget content">') |
|
68 |
r += ', '.join(self.bounce.addrs) |
|
69 |
r += htmltext('</div>') |
|
70 | ||
71 |
if self.bounce.original_rcpts: |
|
72 |
r += htmltext('<div class="title">%s</div>') % _('Original Recipients') |
|
73 |
r += htmltext('<div class="StringWidget content">') |
|
74 |
r += ', '.join(self.bounce.original_rcpts) |
|
75 |
r += htmltext('</div>') |
|
76 | ||
77 |
r += htmltext('<div class="title">%s</div>') % _('Bounce Message') |
|
78 |
r += htmltext('<div class="StringWidget content">') |
|
79 |
r += htmltext('<pre style="max-height: 20em;">') |
|
80 |
r += self.bounce.bounce_message |
|
81 |
r += htmltext('</pre>') |
|
82 |
r += htmltext('</div>') |
|
83 | ||
84 |
if self.bounce.original_message: |
|
85 |
parser = email.Parser.Parser() |
|
86 | ||
87 |
msg = parser.parsestr(self.bounce.original_message) |
|
88 |
if msg.is_multipart(): |
|
89 |
for m in msg.get_payload(): |
|
90 |
if m.get_content_type() == 'text/plain': |
|
91 |
break |
|
92 |
else: |
|
93 |
m = None |
|
94 |
elif msg.get_content_type() == 'text/plain': |
|
95 |
m = msg |
|
96 |
else: |
|
97 |
m = None |
|
98 | ||
99 |
r += htmltext('<div class="title">%s</div>') % _('Original Message') |
|
100 |
r += htmltext('<div class="StringWidget content">') |
|
101 |
r += _('Subject: ') |
|
102 |
subject, charset = email.Header.decode_header(msg['Subject'])[0] |
|
103 |
if charset: |
|
104 |
encoding = get_publisher().site_charset |
|
105 |
r += force_text(subject, charset).encode(encoding) |
|
106 |
else: |
|
107 |
r += subject |
|
108 |
r += htmltext('</div>') |
|
109 | ||
110 |
if m: |
|
111 |
r += htmltext('<div class="StringWidget content">') |
|
112 |
r += htmltext('<pre>') |
|
113 |
r += m.get_payload() |
|
114 |
r += htmltext('</pre>') |
|
115 |
r += htmltext('</div>') |
|
116 | ||
117 |
r += htmltext('</div>') # form |
|
118 |
return r.getvalue() |
|
119 | ||
120 | ||
121 |
def delete(self): |
|
122 |
form = Form(enctype='multipart/form-data') |
|
123 |
form.widgets.append(HtmlWidget('<p>%s</p>' % _( |
|
124 |
'You are about to irrevocably delete this bounce.'))) |
|
125 |
form.add_submit('delete', _('Delete')) |
|
126 |
form.add_submit('cancel', _('Cancel')) |
|
127 |
if form.get_widget('cancel').parse(): |
|
128 |
return redirect('..') |
|
129 |
if not form.is_submitted() or form.has_errors(): |
|
130 |
get_response().breadcrumb.append(('delete', _('Delete'))) |
|
131 |
html_top('bounces', title = _('Delete Bounce')) |
|
132 |
r = TemplateIO(html=True) |
|
133 |
r += htmltext('<h2>%s</h2>') % _('Deleting Bounce') |
|
134 |
r += form.render() |
|
135 |
return r.getvalue() |
|
136 |
else: |
|
137 |
self.bounce.remove_self() |
|
138 |
return redirect('..') |
|
139 | ||
140 | ||
141 | ||
142 |
class BouncesDirectory(Directory): |
|
143 |
_q_exports = [''] |
|
144 | ||
145 |
def _q_traverse(self, path): |
|
146 |
get_response().breadcrumb.append( ('bounces/', _('Bounces')) ) |
|
147 |
return Directory._q_traverse(self, path) |
|
148 | ||
149 |
def is_visible(self, *args): |
|
150 |
return bool(get_cfg('emails', {}).get('bounce_handler') is True) |
|
151 | ||
152 |
def _q_index(self): |
|
153 |
html_top('bounces', title = _('Bounces')) |
|
154 | ||
155 |
bounces = Bounce.select(ignore_errors=True) |
|
156 |
bounces.sort(lambda x,y: cmp(x.arrival_time, y.arrival_time)) |
|
157 | ||
158 |
r = TemplateIO(html=True) |
|
159 |
r += htmltext('<ul class="biglist">') |
|
160 |
for bounce in bounces: |
|
161 |
r += htmltext('<li>') |
|
162 |
r += htmltext('<strong class="label">') |
|
163 |
r += misc.localstrftime(time.localtime(bounce.arrival_time)) |
|
164 |
if bounce.email_type: |
|
165 |
r += ' - ' |
|
166 |
r += _(get_email_type_label(bounce.email_type)) |
|
167 |
r += htmltext('</strong>') |
|
168 |
r += htmltext('<p class="details">') |
|
169 |
if bounce.addrs: |
|
170 |
r += ', '.join(bounce.addrs) |
|
171 |
r += htmltext('</p>') |
|
172 | ||
173 |
r += htmltext('<p class="commands">') |
|
174 |
r += command_icon('%s/' % bounce.id, 'view') |
|
175 |
r += command_icon('%s/delete' % bounce.id, 'remove', popup = True) |
|
176 |
r += htmltext('</p></li>') |
|
177 |
r += htmltext('</ul>') |
|
178 |
return r.getvalue() |
|
179 | ||
180 |
def _q_lookup(self, component): |
|
181 |
try: |
|
182 |
return BouncePage(component) |
|
183 |
except KeyError: |
|
184 |
raise errors.TraversalError() |
wcs/admin/settings.py | ||
---|---|---|
554 | 554 |
('users', N_('Users')), |
555 | 555 |
('roles', N_('Roles')), |
556 | 556 |
('categories', N_('Categories')), |
557 |
('bounces', N_('Bounces')), |
|
558 | 557 |
('settings', N_('Settings')), |
559 | 558 |
] |
560 | 559 |
for k, v in admin_sections: |
wcs/backoffice/root.py | ||
---|---|---|
29 | 29 | |
30 | 30 |
from wcs.formdef import FormDef |
31 | 31 | |
32 |
import wcs.admin.bounces |
|
33 | 32 |
import wcs.admin.categories |
34 | 33 |
import wcs.admin.forms |
35 | 34 |
import wcs.admin.roles |
... | ... | |
47 | 46 |
class RootDirectory(BackofficeRootDirectory): |
48 | 47 |
_q_exports = ['', 'pending', 'statistics', ('menu.json', 'menu_json')] |
49 | 48 | |
50 |
bounces = wcs.admin.bounces.BouncesDirectory() |
|
51 | 49 |
forms = wcs.admin.forms.FormsDirectory() |
52 | 50 |
roles = wcs.admin.roles.RolesDirectory() |
53 | 51 |
settings = wcs.admin.settings.SettingsDirectory() |
... | ... | |
69 | 67 |
('workflows/', N_('Workflows Workshop'), {'sub': True}), |
70 | 68 |
('users/', N_('Users'), {'check_display_function': roles.is_visible}), |
71 | 69 |
('roles/', N_('Roles'), {'check_display_function': roles.is_visible}), |
72 |
('bounces/', N_('Bounces'), {'check_display_function': bounces.is_visible}), |
|
73 | 70 |
('settings/', N_('Settings')), |
74 | 71 |
] |
75 | 72 |
wcs/ctl/Bouncers/BouncerAPI.py | ||
---|---|---|
1 |
# Copyright (C) 1998-2006 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, |
|
16 |
# USA. |
|
17 | ||
18 |
"""Contains all the common functionality for msg bounce scanning API. |
|
19 | ||
20 |
This module can also be used as the basis for a bounce detection testing |
|
21 |
framework. When run as a script, it expects two arguments, the listname and |
|
22 |
the filename containing the bounce message. |
|
23 | ||
24 |
""" |
|
25 | ||
26 |
import sys |
|
27 | ||
28 | ||
29 |
# If a bounce detector returns Stop, that means to just discard the message. |
|
30 |
# An example is warning messages for temporary delivery problems. These |
|
31 |
# shouldn't trigger a bounce notification, but we also don't want to send them |
|
32 |
# on to the list administrator. |
|
33 |
class _Stop: |
|
34 |
pass |
|
35 |
Stop = _Stop() |
|
36 | ||
37 | ||
38 |
BOUNCE_PIPELINE = [ |
|
39 |
'DSN', |
|
40 |
'Qmail', |
|
41 |
'Postfix', |
|
42 |
'Yahoo', |
|
43 |
'Caiwireless', |
|
44 |
'Exchange', |
|
45 |
'Exim', |
|
46 |
'Netscape', |
|
47 |
'Compuserve', |
|
48 |
'Microsoft', |
|
49 |
'GroupWise', |
|
50 |
'SMTP32', |
|
51 |
'SimpleMatch', |
|
52 |
'SimpleWarning', |
|
53 |
'Yale', |
|
54 |
'LLNL', |
|
55 |
] |
|
56 | ||
57 | ||
58 | ||
59 |
# msg must be a mimetools.Message |
|
60 |
def ScanMessages(mlist, msg): |
|
61 |
for module in BOUNCE_PIPELINE: |
|
62 |
modname = 'Mailman.Bouncers.' + module |
|
63 |
__import__(modname) |
|
64 |
addrs = sys.modules[modname].process(msg) |
|
65 |
if addrs: |
|
66 |
# Return addrs even if it is Stop. BounceRunner needs this info. |
|
67 |
return addrs |
|
68 |
return [] |
wcs/ctl/Bouncers/Caiwireless.py | ||
---|---|---|
1 |
# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""Parse mystery style generated by MTA at caiwireless.net.""" |
|
18 | ||
19 |
import re |
|
20 |
import email |
|
21 |
from cStringIO import StringIO |
|
22 | ||
23 |
tcre = re.compile(r'the following recipients did not receive this message:', |
|
24 |
re.IGNORECASE) |
|
25 |
acre = re.compile(r'<(?P<addr>[^>]*)>') |
|
26 | ||
27 | ||
28 | ||
29 |
def process(msg): |
|
30 |
if msg.get_content_type() <> 'multipart/mixed': |
|
31 |
return None |
|
32 |
# simple state machine |
|
33 |
# 0 == nothing seen |
|
34 |
# 1 == tag line seen |
|
35 |
state = 0 |
|
36 |
# This format thinks it's a MIME, but it really isn't |
|
37 |
for line in email.Iterators.body_line_iterator(msg): |
|
38 |
line = line.strip() |
|
39 |
if state == 0 and tcre.match(line): |
|
40 |
state = 1 |
|
41 |
elif state == 1 and line: |
|
42 |
mo = acre.match(line) |
|
43 |
if not mo: |
|
44 |
return None |
|
45 |
return [mo.group('addr')] |
wcs/ctl/Bouncers/Compuserve.py | ||
---|---|---|
1 |
# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""Compuserve has its own weird format for bounces.""" |
|
18 | ||
19 |
import re |
|
20 |
import email |
|
21 | ||
22 |
dcre = re.compile(r'your message could not be delivered', re.IGNORECASE) |
|
23 |
acre = re.compile(r'Invalid receiver address: (?P<addr>.*)') |
|
24 | ||
25 | ||
26 | ||
27 |
def process(msg): |
|
28 |
# simple state machine |
|
29 |
# 0 = nothing seen yet |
|
30 |
# 1 = intro line seen |
|
31 |
state = 0 |
|
32 |
addrs = [] |
|
33 |
for line in email.Iterators.body_line_iterator(msg): |
|
34 |
if state == 0: |
|
35 |
mo = dcre.search(line) |
|
36 |
if mo: |
|
37 |
state = 1 |
|
38 |
elif state == 1: |
|
39 |
mo = dcre.search(line) |
|
40 |
if mo: |
|
41 |
break |
|
42 |
mo = acre.search(line) |
|
43 |
if mo: |
|
44 |
addrs.append(mo.group('addr')) |
|
45 |
return addrs |
wcs/ctl/Bouncers/DSN.py | ||
---|---|---|
1 |
# Copyright (C) 1998-2006 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, |
|
16 |
# USA. |
|
17 | ||
18 |
"""Parse RFC 3464 (i.e. DSN) bounce formats. |
|
19 | ||
20 |
RFC 3464 obsoletes 1894 which was the old DSN standard. This module has not |
|
21 |
been audited for differences between the two. |
|
22 |
""" |
|
23 | ||
24 |
from email.Iterators import typed_subpart_iterator |
|
25 |
from email.Utils import parseaddr |
|
26 |
from cStringIO import StringIO |
|
27 | ||
28 |
from BouncerAPI import Stop |
|
29 | ||
30 |
try: |
|
31 |
True, False |
|
32 |
except NameError: |
|
33 |
True = 1 |
|
34 |
False = 0 |
|
35 | ||
36 | ||
37 | ||
38 |
def check(msg): |
|
39 |
# Iterate over each message/delivery-status subpart |
|
40 |
addrs = [] |
|
41 |
for part in typed_subpart_iterator(msg, 'message', 'delivery-status'): |
|
42 |
if not part.is_multipart(): |
|
43 |
# Huh? |
|
44 |
continue |
|
45 |
# Each message/delivery-status contains a list of Message objects |
|
46 |
# which are the header blocks. Iterate over those too. |
|
47 |
for msgblock in part.get_payload(): |
|
48 |
# We try to dig out the Original-Recipient (which is optional) and |
|
49 |
# Final-Recipient (which is mandatory, but may not exactly match |
|
50 |
# an address on our list). Some MTA's also use X-Actual-Recipient |
|
51 |
# as a synonym for Original-Recipient, but some apparently use |
|
52 |
# that for other purposes :( |
|
53 |
# |
|
54 |
# Also grok out Action so we can do something with that too. |
|
55 |
action = msgblock.get('action', '').lower() |
|
56 |
# Some MTAs have been observed that put comments on the action. |
|
57 |
if action.startswith('delayed'): |
|
58 |
return Stop |
|
59 |
if not action.startswith('fail'): |
|
60 |
# Some non-permanent failure, so ignore this block |
|
61 |
continue |
|
62 |
params = [] |
|
63 |
foundp = False |
|
64 |
for header in ('original-recipient', 'final-recipient'): |
|
65 |
for k, v in msgblock.get_params([], header): |
|
66 |
if k.lower() == 'rfc822': |
|
67 |
foundp = True |
|
68 |
else: |
|
69 |
params.append(k) |
|
70 |
if foundp: |
|
71 |
# Note that params should already be unquoted. |
|
72 |
addrs.extend(params) |
|
73 |
break |
|
74 |
else: |
|
75 |
# MAS: This is a kludge, but SMTP-GATEWAY01.intra.home.dk |
|
76 |
# has a final-recipient with an angle-addr and no |
|
77 |
# address-type parameter at all. Non-compliant, but ... |
|
78 |
for param in params: |
|
79 |
if param.startswith('<') and param.endswith('>'): |
|
80 |
addrs.append(param[1:-1]) |
|
81 |
# Uniquify |
|
82 |
rtnaddrs = {} |
|
83 |
for a in addrs: |
|
84 |
if a is not None: |
|
85 |
realname, a = parseaddr(a) |
|
86 |
rtnaddrs[a] = True |
|
87 |
return rtnaddrs.keys() |
|
88 | ||
89 | ||
90 | ||
91 |
def process(msg): |
|
92 |
# A DSN has been seen wrapped with a "legal disclaimer" by an outgoing MTA |
|
93 |
# in a multipart/mixed outer part. |
|
94 |
if msg.is_multipart() and msg.get_content_subtype() == 'mixed': |
|
95 |
msg = msg.get_payload()[0] |
|
96 |
# The report-type parameter should be "delivery-status", but it seems that |
|
97 |
# some DSN generating MTAs don't include this on the Content-Type: header, |
|
98 |
# so let's relax the test a bit. |
|
99 |
if not msg.is_multipart() or msg.get_content_subtype() <> 'report': |
|
100 |
return None |
|
101 |
return check(msg) |
wcs/ctl/Bouncers/Exchange.py | ||
---|---|---|
1 |
# Copyright (C) 2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""Recognizes (some) Microsoft Exchange formats.""" |
|
18 | ||
19 |
import re |
|
20 |
import email.Iterators |
|
21 | ||
22 |
scre = re.compile('did not reach the following recipient') |
|
23 |
ecre = re.compile('MSEXCH:') |
|
24 |
a1cre = re.compile('SMTP=(?P<addr>[^;]+); on ') |
|
25 |
a2cre = re.compile('(?P<addr>[^ ]+) on ') |
|
26 | ||
27 | ||
28 | ||
29 |
def process(msg): |
|
30 |
addrs = {} |
|
31 |
it = email.Iterators.body_line_iterator(msg) |
|
32 |
# Find the start line |
|
33 |
for line in it: |
|
34 |
if scre.search(line): |
|
35 |
break |
|
36 |
else: |
|
37 |
return [] |
|
38 |
# Search each line until we hit the end line |
|
39 |
for line in it: |
|
40 |
if ecre.search(line): |
|
41 |
break |
|
42 |
mo = a1cre.search(line) |
|
43 |
if not mo: |
|
44 |
mo = a2cre.search(line) |
|
45 |
if mo: |
|
46 |
addrs[mo.group('addr')] = 1 |
|
47 |
return addrs.keys() |
wcs/ctl/Bouncers/Exim.py | ||
---|---|---|
1 |
# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""Parse bounce messages generated by Exim. |
|
18 | ||
19 |
Exim adds an X-Failed-Recipients: header to bounce messages containing |
|
20 |
an `addresslist' of failed addresses. |
|
21 | ||
22 |
""" |
|
23 | ||
24 |
from email.Utils import getaddresses |
|
25 | ||
26 | ||
27 | ||
28 |
def process(msg): |
|
29 |
all = msg.get_all('x-failed-recipients', []) |
|
30 |
return [a for n, a in getaddresses(all)] |
wcs/ctl/Bouncers/GroupWise.py | ||
---|---|---|
1 |
# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""This appears to be the format for Novell GroupWise and NTMail |
|
18 | ||
19 |
X-Mailer: Novell GroupWise Internet Agent 5.5.3.1 |
|
20 |
X-Mailer: NTMail v4.30.0012 |
|
21 |
X-Mailer: Internet Mail Service (5.5.2653.19) |
|
22 |
""" |
|
23 | ||
24 |
import re |
|
25 |
from email.Message import Message |
|
26 |
from cStringIO import StringIO |
|
27 | ||
28 |
acre = re.compile(r'<(?P<addr>[^>]*)>') |
|
29 | ||
30 | ||
31 | ||
32 |
def find_textplain(msg): |
|
33 |
if msg.get_content_type() == 'text/plain': |
|
34 |
return msg |
|
35 |
if msg.is_multipart: |
|
36 |
for part in msg.get_payload(): |
|
37 |
if not isinstance(part, Message): |
|
38 |
continue |
|
39 |
ret = find_textplain(part) |
|
40 |
if ret: |
|
41 |
return ret |
|
42 |
return None |
|
43 | ||
44 | ||
45 | ||
46 |
def process(msg): |
|
47 |
if msg.get_content_type() <> 'multipart/mixed' or not msg['x-mailer']: |
|
48 |
return None |
|
49 |
addrs = {} |
|
50 |
# find the first text/plain part in the message |
|
51 |
textplain = find_textplain(msg) |
|
52 |
if not textplain: |
|
53 |
return None |
|
54 |
body = StringIO(textplain.get_payload()) |
|
55 |
while 1: |
|
56 |
line = body.readline() |
|
57 |
if not line: |
|
58 |
break |
|
59 |
mo = acre.search(line) |
|
60 |
if mo: |
|
61 |
addrs[mo.group('addr')] = 1 |
|
62 |
elif '@' in line: |
|
63 |
i = line.find(' ') |
|
64 |
if i == 0: |
|
65 |
continue |
|
66 |
if i < 0: |
|
67 |
addrs[line] = 1 |
|
68 |
else: |
|
69 |
addrs[line[:i]] = 1 |
|
70 |
return addrs.keys() |
wcs/ctl/Bouncers/LLNL.py | ||
---|---|---|
1 |
# Copyright (C) 2001,2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""LLNL's custom Sendmail bounce message.""" |
|
18 | ||
19 |
import re |
|
20 |
import email |
|
21 | ||
22 |
acre = re.compile(r',\s*(?P<addr>\S+@[^,]+),', re.IGNORECASE) |
|
23 | ||
24 | ||
25 | ||
26 |
def process(msg): |
|
27 |
for line in email.Iterators.body_line_iterator(msg): |
|
28 |
mo = acre.search(line) |
|
29 |
if mo: |
|
30 |
return [mo.group('addr')] |
|
31 |
return [] |
wcs/ctl/Bouncers/Microsoft.py | ||
---|---|---|
1 |
# Copyright (C) 1998-2003 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""Microsoft's `SMTPSVC' nears I kin tell.""" |
|
18 | ||
19 |
import re |
|
20 |
from cStringIO import StringIO |
|
21 |
from types import ListType |
|
22 | ||
23 |
scre = re.compile(r'transcript of session follows', re.IGNORECASE) |
|
24 | ||
25 | ||
26 | ||
27 |
def process(msg): |
|
28 |
if msg.get_content_type() <> 'multipart/mixed': |
|
29 |
return None |
|
30 |
# Find the first subpart, which has no MIME type |
|
31 |
try: |
|
32 |
subpart = msg.get_payload(0) |
|
33 |
except IndexError: |
|
34 |
# The message *looked* like a multipart but wasn't |
|
35 |
return None |
|
36 |
data = subpart.get_payload() |
|
37 |
if isinstance(data, ListType): |
|
38 |
# The message is a multi-multipart, so not a matching bounce |
|
39 |
return None |
|
40 |
body = StringIO(data) |
|
41 |
state = 0 |
|
42 |
addrs = [] |
|
43 |
while 1: |
|
44 |
line = body.readline() |
|
45 |
if not line: |
|
46 |
break |
|
47 |
if state == 0: |
|
48 |
if scre.search(line): |
|
49 |
state = 1 |
|
50 |
if state == 1: |
|
51 |
if '@' in line: |
|
52 |
addrs.append(line) |
|
53 |
return addrs |
wcs/ctl/Bouncers/Netscape.py | ||
---|---|---|
1 |
# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""Netscape Messaging Server bounce formats. |
|
18 | ||
19 |
I've seen at least one NMS server version 3.6 (envy.gmp.usyd.edu.au) bounce |
|
20 |
messages of this format. Bounces come in DSN MIME format, but don't include |
|
21 |
any -Recipient: headers. Gotta just parse the text :( |
|
22 | ||
23 |
NMS 4.1 (dfw-smtpin1.email.verio.net) seems even worse, but we'll try to |
|
24 |
decipher the format here too. |
|
25 | ||
26 |
""" |
|
27 | ||
28 |
import re |
|
29 |
from cStringIO import StringIO |
|
30 | ||
31 |
pcre = re.compile( |
|
32 |
r'This Message was undeliverable due to the following reason:', |
|
33 |
re.IGNORECASE) |
|
34 | ||
35 |
acre = re.compile( |
|
36 |
r'(?P<reply>please reply to)?.*<(?P<addr>[^>]*)>', |
|
37 |
re.IGNORECASE) |
|
38 | ||
39 | ||
40 | ||
41 |
def flatten(msg, leaves): |
|
42 |
# give us all the leaf (non-multipart) subparts |
|
43 |
if msg.is_multipart(): |
|
44 |
for part in msg.get_payload(): |
|
45 |
flatten(part, leaves) |
|
46 |
else: |
|
47 |
leaves.append(msg) |
|
48 | ||
49 | ||
50 | ||
51 |
def process(msg): |
|
52 |
# Sigh. Some show NMS 3.6's show |
|
53 |
# multipart/report; report-type=delivery-status |
|
54 |
# and some show |
|
55 |
# multipart/mixed; |
|
56 |
if not msg.is_multipart(): |
|
57 |
return None |
|
58 |
# We're looking for a text/plain subpart occuring before a |
|
59 |
# message/delivery-status subpart. |
|
60 |
plainmsg = None |
|
61 |
leaves = [] |
|
62 |
flatten(msg, leaves) |
|
63 |
for i, subpart in zip(range(len(leaves)-1), leaves): |
|
64 |
if subpart.get_content_type() == 'text/plain': |
|
65 |
plainmsg = subpart |
|
66 |
break |
|
67 |
if not plainmsg: |
|
68 |
return None |
|
69 |
# Total guesswork, based on captured examples... |
|
70 |
body = StringIO(plainmsg.get_payload()) |
|
71 |
addrs = [] |
|
72 |
while 1: |
|
73 |
line = body.readline() |
|
74 |
if not line: |
|
75 |
break |
|
76 |
mo = pcre.search(line) |
|
77 |
if mo: |
|
78 |
# We found a bounce section, but I have no idea what the official |
|
79 |
# format inside here is. :( We'll just search for <addr> |
|
80 |
# strings. |
|
81 |
while 1: |
|
82 |
line = body.readline() |
|
83 |
if not line: |
|
84 |
break |
|
85 |
mo = acre.search(line) |
|
86 |
if mo and not mo.group('reply'): |
|
87 |
addrs.append(mo.group('addr')) |
|
88 |
return addrs |
wcs/ctl/Bouncers/Postfix.py | ||
---|---|---|
1 |
# Copyright (C) 1998-2003 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""Parse bounce messages generated by Postfix. |
|
18 | ||
19 |
This also matches something called `Keftamail' which looks just like Postfix |
|
20 |
bounces with the word Postfix scratched out and the word `Keftamail' written |
|
21 |
in in crayon. |
|
22 | ||
23 |
It also matches something claiming to be `The BNS Postfix program', and |
|
24 |
`SMTP_Gateway'. Everybody's gotta be different, huh? |
|
25 |
""" |
|
26 | ||
27 |
import re |
|
28 |
from cStringIO import StringIO |
|
29 | ||
30 | ||
31 | ||
32 |
def flatten(msg, leaves): |
|
33 |
# give us all the leaf (non-multipart) subparts |
|
34 |
if msg.is_multipart(): |
|
35 |
for part in msg.get_payload(): |
|
36 |
flatten(part, leaves) |
|
37 |
else: |
|
38 |
leaves.append(msg) |
|
39 | ||
40 | ||
41 | ||
42 |
# are these heuristics correct or guaranteed? |
|
43 |
pcre = re.compile(r'[ \t]*the\s*(bns)?\s*(postfix|keftamail|smtp_gateway)', |
|
44 |
re.IGNORECASE) |
|
45 |
rcre = re.compile(r'failure reason:$', re.IGNORECASE) |
|
46 |
acre = re.compile(r'<(?P<addr>[^>]*)>:') |
|
47 | ||
48 |
def findaddr(msg): |
|
49 |
addrs = [] |
|
50 |
body = StringIO(msg.get_payload()) |
|
51 |
# simple state machine |
|
52 |
# 0 == nothing found |
|
53 |
# 1 == salutation found |
|
54 |
state = 0 |
|
55 |
while 1: |
|
56 |
line = body.readline() |
|
57 |
if not line: |
|
58 |
break |
|
59 |
# preserve leading whitespace |
|
60 |
line = line.rstrip() |
|
61 |
# yes use match to match at beginning of string |
|
62 |
if state == 0 and (pcre.match(line) or rcre.match(line)): |
|
63 |
state = 1 |
|
64 |
elif state == 1 and line: |
|
65 |
mo = acre.search(line) |
|
66 |
if mo: |
|
67 |
addrs.append(mo.group('addr')) |
|
68 |
# probably a continuation line |
|
69 |
return addrs |
|
70 | ||
71 | ||
72 | ||
73 |
def process(msg): |
|
74 |
if msg.get_content_type() not in ('multipart/mixed', 'multipart/report'): |
|
75 |
return None |
|
76 |
# We're looking for the plain/text subpart with a Content-Description: of |
|
77 |
# `notification'. |
|
78 |
leaves = [] |
|
79 |
flatten(msg, leaves) |
|
80 |
for subpart in leaves: |
|
81 |
if subpart.get_content_type() == 'text/plain' and \ |
|
82 |
subpart.get('content-description', '').lower() == 'notification': |
|
83 |
# then... |
|
84 |
return findaddr(subpart) |
|
85 |
return None |
wcs/ctl/Bouncers/Qmail.py | ||
---|---|---|
1 |
# Copyright (C) 1998-2006 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, |
|
16 |
# USA. |
|
17 | ||
18 |
"""Parse bounce messages generated by qmail. |
|
19 | ||
20 |
Qmail actually has a standard, called QSBMF (qmail-send bounce message |
|
21 |
format), as described in |
|
22 | ||
23 |
http://cr.yp.to/proto/qsbmf.txt |
|
24 | ||
25 |
This module should be conformant. |
|
26 | ||
27 |
""" |
|
28 | ||
29 |
import re |
|
30 |
import email.Iterators |
|
31 | ||
32 |
# Other (non-standard?) intros have been observed in the wild. |
|
33 |
introtags = [ |
|
34 |
'Hi. This is the', |
|
35 |
"We're sorry. There's a problem", |
|
36 |
'Check your send e-mail address.' |
|
37 |
] |
|
38 |
acre = re.compile(r'<(?P<addr>[^>]*)>:') |
|
39 | ||
40 | ||
41 | ||
42 |
def process(msg): |
|
43 |
addrs = [] |
|
44 |
# simple state machine |
|
45 |
# 0 = nothing seen yet |
|
46 |
# 1 = intro paragraph seen |
|
47 |
# 2 = recip paragraphs seen |
|
48 |
state = 0 |
|
49 |
for line in email.Iterators.body_line_iterator(msg): |
|
50 |
line = line.strip() |
|
51 |
if state == 0: |
|
52 |
for introtag in introtags: |
|
53 |
if line.startswith(introtag): |
|
54 |
state = 1 |
|
55 |
break |
|
56 |
elif state == 1 and not line: |
|
57 |
# Looking for the end of the intro paragraph |
|
58 |
state = 2 |
|
59 |
elif state == 2: |
|
60 |
if line.startswith('-'): |
|
61 |
# We're looking at the break paragraph, so we're done |
|
62 |
break |
|
63 |
# At this point we know we must be looking at a recipient |
|
64 |
# paragraph |
|
65 |
mo = acre.match(line) |
|
66 |
if mo: |
|
67 |
addrs.append(mo.group('addr')) |
|
68 |
# Otherwise, it must be a continuation line, so just ignore it |
|
69 |
# Not looking at anything in particular |
|
70 |
return addrs |
wcs/ctl/Bouncers/SMTP32.py | ||
---|---|---|
1 |
# Copyright (C) 1998-2006 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, |
|
16 |
# USA. |
|
17 | ||
18 |
"""Something which claims |
|
19 |
X-Mailer: <SMTP32 vXXXXXX> |
|
20 | ||
21 |
What the heck is this thing? Here's a recent host: |
|
22 | ||
23 |
% telnet 207.51.255.218 smtp |
|
24 |
Trying 207.51.255.218... |
|
25 |
Connected to 207.51.255.218. |
|
26 |
Escape character is '^]'. |
|
27 |
220 X1 NT-ESMTP Server 208.24.118.205 (IMail 6.00 45595-15) |
|
28 | ||
29 |
""" |
|
30 | ||
31 |
import re |
|
32 |
import email |
|
33 | ||
34 |
ecre = re.compile('original message follows', re.IGNORECASE) |
|
35 |
acre = re.compile(r''' |
|
36 |
( # several different prefixes |
|
37 |
user\ mailbox[^:]*: # have been spotted in the |
|
38 |
|delivery\ failed[^:]*: # wild... |
|
39 |
|unknown\ user[^:]*: |
|
40 |
|undeliverable\ +to |
|
41 |
) |
|
42 |
\s* # space separator |
|
43 |
(?P<addr>.*) # and finally, the address |
|
44 |
''', re.IGNORECASE | re.VERBOSE) |
|
45 | ||
46 | ||
47 | ||
48 |
def process(msg): |
|
49 |
mailer = msg.get('x-mailer', '') |
|
50 |
if not mailer.startswith('<SMTP32 v'): |
|
51 |
return |
|
52 |
addrs = {} |
|
53 |
for line in email.Iterators.body_line_iterator(msg): |
|
54 |
if ecre.search(line): |
|
55 |
break |
|
56 |
mo = acre.search(line) |
|
57 |
if mo: |
|
58 |
addrs[mo.group('addr')] = 1 |
|
59 |
return addrs.keys() |
wcs/ctl/Bouncers/SimpleMatch.py | ||
---|---|---|
1 |
# Copyright (C) 1998-2006 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, |
|
16 |
# USA. |
|
17 | ||
18 |
"""Recognizes simple heuristically delimited bounces.""" |
|
19 | ||
20 |
import re |
|
21 |
import email.Iterators |
|
22 | ||
23 | ||
24 | ||
25 |
def _c(pattern): |
|
26 |
return re.compile(pattern, re.IGNORECASE) |
|
27 | ||
28 |
# This is a list of tuples of the form |
|
29 |
# |
|
30 |
# (start cre, end cre, address cre) |
|
31 |
# |
|
32 |
# where `cre' means compiled regular expression, start is the line just before |
|
33 |
# the bouncing address block, end is the line just after the bouncing address |
|
34 |
# block, and address cre is the regexp that will recognize the addresses. It |
|
35 |
# must have a group called `addr' which will contain exactly and only the |
|
36 |
# address that bounced. |
|
37 |
PATTERNS = [ |
|
38 |
# sdm.de |
|
39 |
(_c('here is your list of failed recipients'), |
|
40 |
_c('here is your returned mail'), |
|
41 |
_c(r'<(?P<addr>[^>]*)>')), |
|
42 |
# sz-sb.de, corridor.com, nfg.nl |
|
43 |
(_c('the following addresses had'), |
|
44 |
_c('transcript of session follows'), |
|
45 |
_c(r'<(?P<fulladdr>[^>]*)>|\(expanded from: <?(?P<addr>[^>)]*)>?\)')), |
|
46 |
# robanal.demon.co.uk |
|
47 |
(_c('this message was created automatically by mail delivery software'), |
|
48 |
_c('original message follows'), |
|
49 |
_c('rcpt to:\s*<(?P<addr>[^>]*)>')), |
|
50 |
# s1.com (InterScan E-Mail VirusWall NT ???) |
|
51 |
(_c('message from interscan e-mail viruswall nt'), |
|
52 |
_c('end of message'), |
|
53 |
_c('rcpt to:\s*<(?P<addr>[^>]*)>')), |
|
54 |
# Smail |
|
55 |
(_c('failed addresses follow:'), |
|
56 |
_c('message text follows:'), |
|
57 |
_c(r'\s*(?P<addr>\S+@\S+)')), |
|
58 |
# newmail.ru |
|
59 |
(_c('This is the machine generated message from mail service.'), |
|
60 |
_c('--- Below the next line is a copy of the message.'), |
|
61 |
_c('<(?P<addr>[^>]*)>')), |
|
62 |
# turbosport.com runs something called `MDaemon 3.5.2' ??? |
|
63 |
(_c('The following addresses did NOT receive a copy of your message:'), |
|
64 |
_c('--- Session Transcript ---'), |
|
65 |
_c('[>]\s*(?P<addr>.*)$')), |
|
66 |
# usa.net |
|
67 |
(_c('Intended recipient:\s*(?P<addr>.*)$'), |
|
68 |
_c('--------RETURNED MAIL FOLLOWS--------'), |
|
69 |
_c('Intended recipient:\s*(?P<addr>.*)$')), |
|
70 |
# hotpop.com |
|
71 |
(_c('Undeliverable Address:\s*(?P<addr>.*)$'), |
|
72 |
_c('Original message attached'), |
|
73 |
_c('Undeliverable Address:\s*(?P<addr>.*)$')), |
|
74 |
# Another demon.co.uk format |
|
75 |
(_c('This message was created automatically by mail delivery'), |
|
76 |
_c('^---- START OF RETURNED MESSAGE ----'), |
|
77 |
_c("addressed to '(?P<addr>[^']*)'")), |
|
78 |
# Prodigy.net full mailbox |
|
79 |
(_c("User's mailbox is full:"), |
|
80 |
_c('Unable to deliver mail.'), |
|
81 |
_c("User's mailbox is full:\s*<(?P<addr>[^>]*)>")), |
|
82 |
# Microsoft SMTPSVC |
|
83 |
(_c('The email below could not be delivered to the following user:'), |
|
84 |
_c('Old message:'), |
|
85 |
_c('<(?P<addr>[^>]*)>')), |
|
86 |
# Yahoo on behalf of other domains like sbcglobal.net |
|
87 |
(_c('Unable to deliver message to the following address\(es\)\.'), |
|
88 |
_c('--- Original message follows\.'), |
|
89 |
_c('<(?P<addr>[^>]*)>:')), |
|
90 |
# kundenserver.de |
|
91 |
(_c('A message that you sent could not be delivered'), |
|
92 |
_c('^---'), |
|
93 |
_c('<(?P<addr>[^>]*)>')), |
|
94 |
# another kundenserver.de |
|
95 |
(_c('A message that you sent could not be delivered'), |
|
96 |
_c('^---'), |
|
97 |
_c('^(?P<addr>[^\s@]+@[^\s@:]+):')), |
|
98 |
# thehartford.com |
|
99 |
(_c('Delivery to the following recipients failed'), |
|
100 |
_c("Bogus - there actually isn't anything"), |
|
101 |
_c('^\s*(?P<addr>[^\s@]+@[^\s@]+)\s*$')), |
|
102 |
# and another thehartfod.com/hartfordlife.com |
|
103 |
(_c('^Your message\s*$'), |
|
104 |
_c('^because:'), |
|
105 |
_c('^\s*(?P<addr>[^\s@]+@[^\s@]+)\s*$')), |
|
106 |
# kviv.be (NTMail) |
|
107 |
(_c('^Unable to deliver message to'), |
|
108 |
_c(r'\*+\s+End of message\s+\*+'), |
|
109 |
_c('<(?P<addr>[^>]*)>')), |
|
110 |
# earthlink.net supported domains |
|
111 |
(_c('^Sorry, unable to deliver your message to'), |
|
112 |
_c('^A copy of the original message'), |
|
113 |
_c('\s*(?P<addr>[^\s@]+@[^\s@]+)\s+')), |
|
114 |
# ademe.fr |
|
115 |
(_c('^A message could not be delivered to:'), |
|
116 |
_c('^Subject:'), |
|
117 |
_c('^\s*(?P<addr>[^\s@]+@[^\s@]+)\s*$')), |
|
118 |
# andrew.ac.jp |
|
119 |
(_c('^Invalid final delivery userid:'), |
|
120 |
_c('^Original message follows.'), |
|
121 |
_c('\s*(?P<addr>[^\s@]+@[^\s@]+)\s*$')), |
|
122 |
# E500_SMTP_Mail_Service@lerctr.org |
|
123 |
(_c('------ Failed Recipients ------'), |
|
124 |
_c('-------- Returned Mail --------'), |
|
125 |
_c('<(?P<addr>[^>]*)>')), |
|
126 |
# cynergycom.net |
|
127 |
(_c('A message that you sent could not be delivered'), |
|
128 |
_c('^---'), |
|
129 |
_c('(?P<addr>[^\s@]+@[^\s@)]+)')), |
|
130 |
# Next one goes here... |
|
131 |
] |
|
132 | ||
133 | ||
134 | ||
135 |
def process(msg, patterns=None): |
|
136 |
if patterns is None: |
|
137 |
patterns = PATTERNS |
|
138 |
# simple state machine |
|
139 |
# 0 = nothing seen yet |
|
140 |
# 1 = intro seen |
|
141 |
addrs = {} |
|
142 |
# MAS: This is a mess. The outer loop used to be over the message |
|
143 |
# so we only looped through the message once. Looping through the |
|
144 |
# message for each set of patterns is obviously way more work, but |
|
145 |
# if we don't do it, problems arise because scre from the wrong |
|
146 |
# pattern set matches first and then acre doesn't match. The |
|
147 |
# alternative is to split things into separate modules, but then |
|
148 |
# we process the message multiple times anyway. |
|
149 |
for scre, ecre, acre in patterns: |
|
150 |
state = 0 |
|
151 |
for line in email.Iterators.body_line_iterator(msg): |
|
152 |
if state == 0: |
|
153 |
if scre.search(line): |
|
154 |
state = 1 |
|
155 |
if state == 1: |
|
156 |
mo = acre.search(line) |
|
157 |
if mo: |
|
158 |
addr = mo.group('addr') |
|
159 |
if addr: |
|
160 |
addrs[mo.group('addr')] = 1 |
|
161 |
elif ecre.search(line): |
|
162 |
break |
|
163 |
if addrs: |
|
164 |
break |
|
165 |
return addrs.keys() |
wcs/ctl/Bouncers/SimpleWarning.py | ||
---|---|---|
1 |
# Copyright (C) 2001-2006 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, |
|
16 |
# USA. |
|
17 | ||
18 |
"""Recognizes simple heuristically delimited warnings.""" |
|
19 | ||
20 |
from BouncerAPI import Stop |
|
21 |
from SimpleMatch import _c |
|
22 |
from SimpleMatch import process as _process |
|
23 | ||
24 | ||
25 | ||
26 |
# This is a list of tuples of the form |
|
27 |
# |
|
28 |
# (start cre, end cre, address cre) |
|
29 |
# |
|
30 |
# where `cre' means compiled regular expression, start is the line just before |
|
31 |
# the bouncing address block, end is the line just after the bouncing address |
|
32 |
# block, and address cre is the regexp that will recognize the addresses. It |
|
33 |
# must have a group called `addr' which will contain exactly and only the |
|
34 |
# address that bounced. |
|
35 |
patterns = [ |
|
36 |
# pop3.pta.lia.net |
|
37 |
(_c('The address to which the message has not yet been delivered is'), |
|
38 |
_c('No action is required on your part'), |
|
39 |
_c(r'\s*(?P<addr>\S+@\S+)\s*')), |
|
40 |
# Next one goes here... |
|
41 |
] |
|
42 | ||
43 | ||
44 | ||
45 |
def process(msg): |
|
46 |
if _process(msg, patterns): |
|
47 |
# It's a recognized warning so stop now |
|
48 |
return Stop |
|
49 |
else: |
|
50 |
return [] |
wcs/ctl/Bouncers/Yahoo.py | ||
---|---|---|
1 |
# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""Yahoo! has its own weird format for bounces.""" |
|
18 | ||
19 |
import re |
|
20 |
import email |
|
21 |
from email.Utils import parseaddr |
|
22 | ||
23 |
tcre = re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE) |
|
24 |
acre = re.compile(r'<(?P<addr>[^>]*)>:') |
|
25 |
ecre = re.compile(r'--- Original message follows') |
|
26 | ||
27 | ||
28 | ||
29 |
def process(msg): |
|
30 |
# Yahoo! bounces seem to have a known subject value and something called |
|
31 |
# an x-uidl: header, the value of which seems unimportant. |
|
32 |
sender = parseaddr(msg.get('from', '').lower())[1] or '' |
|
33 |
if not sender.startswith('mailer-daemon@yahoo'): |
|
34 |
return None |
|
35 |
addrs = [] |
|
36 |
# simple state machine |
|
37 |
# 0 == nothing seen |
|
38 |
# 1 == tag line seen |
|
39 |
state = 0 |
|
40 |
for line in email.Iterators.body_line_iterator(msg): |
|
41 |
line = line.strip() |
|
42 |
if state == 0 and tcre.match(line): |
|
43 |
state = 1 |
|
44 |
elif state == 1: |
|
45 |
mo = acre.match(line) |
|
46 |
if mo: |
|
47 |
addrs.append(mo.group('addr')) |
|
48 |
continue |
|
49 |
mo = ecre.match(line) |
|
50 |
if mo: |
|
51 |
# we're at the end of the error response |
|
52 |
break |
|
53 |
return addrs |
wcs/ctl/Bouncers/Yale.py | ||
---|---|---|
1 |
# Copyright (C) 2000,2001,2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | ||
17 |
"""Yale's mail server is pretty dumb. |
|
18 | ||
19 |
Its reports include the end user's name, but not the full domain. I think we |
|
20 |
can usually guess it right anyway. This is completely based on examination of |
|
21 |
the corpse, and is subject to failure whenever Yale even slightly changes |
|
22 |
their MTA. :( |
|
23 | ||
24 |
""" |
|
25 | ||
26 |
import re |
|
27 |
from cStringIO import StringIO |
|
28 |
from email.Utils import getaddresses |
|
29 | ||
30 |
scre = re.compile(r'Message not delivered to the following', re.IGNORECASE) |
|
31 |
ecre = re.compile(r'Error Detail', re.IGNORECASE) |
|
32 |
acre = re.compile(r'\s+(?P<addr>\S+)\s+') |
|
33 | ||
34 | ||
35 | ||
36 |
def process(msg): |
|
37 |
if msg.is_multipart(): |
|
38 |
return None |
|
39 |
try: |
|
40 |
whofrom = getaddresses([msg.get('from', '')])[0][1] |
|
41 |
if not whofrom: |
|
42 |
return None |
|
43 |
username, domain = whofrom.split('@', 1) |
|
44 |
except (IndexError, ValueError): |
|
45 |
return None |
|
46 |
if username.lower() <> 'mailer-daemon': |
|
47 |
return None |
|
48 |
parts = domain.split('.') |
|
49 |
parts.reverse() |
|
50 |
for part1, part2 in zip(parts, ('edu', 'yale')): |
|
51 |
if part1 <> part2: |
|
52 |
return None |
|
53 |
# Okay, we've established that the bounce came from the mailer-daemon at |
|
54 |
# yale.edu. Let's look for a name, and then guess the relevant domains. |
|
55 |
names = {} |
|
56 |
body = StringIO(msg.get_payload()) |
|
57 |
state = 0 |
|
58 |
# simple state machine |
|
59 |
# 0 == init |
|
60 |
# 1 == intro found |
|
61 |
while 1: |
|
62 |
line = body.readline() |
|
63 |
if not line: |
|
64 |
break |
|
65 |
if state == 0 and scre.search(line): |
|
66 |
state = 1 |
|
67 |
elif state == 1 and ecre.search(line): |
|
68 |
break |
|
69 |
elif state == 1: |
|
70 |
mo = acre.search(line) |
|
71 |
if mo: |
|
72 |
names[mo.group('addr')] = 1 |
|
73 |
# Now we have a bunch of names, these are either @yale.edu or |
|
74 |
# @cs.yale.edu. Add them both. |
|
75 |
addrs = [] |
|
76 |
for name in names.keys(): |
|
77 |
addrs.append(name + '@yale.edu') |
|
78 |
addrs.append(name + '@cs.yale.edu') |
|
79 |
return addrs |
wcs/ctl/Bouncers/__init__.py | ||
---|---|---|
1 |
# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. |
|
2 |
# |
|
3 |
# This program is free software; you can redistribute it and/or |
|
4 |
# modify it under the terms of the GNU General Public License |
|
5 |
# as published by the Free Software Foundation; either version 2 |
|
6 |
# of the License, or (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU General Public License |
|
14 |
# along with this program; if not, write to the Free Software |
|
15 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
wcs/ctl/process_bounce.py | ||
---|---|---|
1 |
# w.c.s. - web application for online forms |
|
2 |
# Copyright (C) 2005-2010 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or modify |
|
5 |
# it under the terms of the GNU General Public License as published by |
|
6 |
# the Free Software Foundation; either version 2 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import sys |
|
18 |
import time |
|
19 |
import os |
|
20 |
import email.Parser |
|
21 | ||
22 |
from Bouncers import BouncerAPI |
|
23 | ||
24 |
from ..qommon.ctl import Command |
|
25 | ||
26 |
COMMA_SPACE = ', ' |
|
27 | ||
28 |
class CmdProcessBounce(Command): |
|
29 |
name = 'process_bounce' |
|
30 | ||
31 |
def execute(self, base_options, sub_options, args): |
|
32 |
from ..qommon.tokens import Token |
|
33 |
from ..qommon.bounces import Bounce |
|
34 | ||
35 |
from .. import publisher |
|
36 | ||
37 |
try: |
|
38 |
publisher.WcsPublisher.configure(self.config) |
|
39 |
pub = publisher.WcsPublisher.create_publisher( |
|
40 |
register_tld_names=False) |
|
41 |
except: |
|
42 |
# not much we can do if we don't have a publisher object :/ |
|
43 |
return |
|
44 | ||
45 |
try: |
|
46 |
parser = email.Parser.Parser() |
|
47 |
msg = parser.parse(sys.stdin) |
|
48 |
addrs = self.get_bounce_addrs(msg) |
|
49 |
if addrs is None: |
|
50 |
# not a bounce |
|
51 |
return |
|
52 | ||
53 |
try: |
|
54 |
to = msg['To'] |
|
55 |
local_part, server_part = to.split('@') |
|
56 |
token_id = local_part.split('+')[1] |
|
57 |
except (IndexError, KeyError): |
|
58 |
return |
|
59 | ||
60 |
pub.app_dir = os.path.join(pub.app_dir, server_part) |
|
61 |
if not os.path.exists(pub.app_dir): |
|
62 |
return |
|
63 | ||
64 |
try: |
|
65 |
token = Token.get(token_id) |
|
66 |
except KeyError: |
|
67 |
return |
|
68 | ||
69 |
if token.type != 'email-bounce': |
|
70 |
return |
|
71 | ||
72 |
token.remove_self() |
|
73 | ||
74 |
bounce = Bounce() |
|
75 |
bounce.arrival_time = time.time() |
|
76 |
bounce.bounce_message = msg.as_string() |
|
77 |
bounce.addrs = addrs |
|
78 |
bounce.original_message = token.email_message |
|
79 |
bounce.original_rcpts = token.email_rcpts |
|
80 |
bounce.email_type = token.email_type |
|
81 |
bounce.store() |
|
82 |
except: |
|
83 |
pub.notify_of_exception(sys.exc_info(), context='[BOUNCE]') |
|
84 |
sys.exit(1) |
|
85 | ||
86 |
@classmethod |
|
87 |
def get_bounce_addrs(cls, msg): |
|
88 |
bouncers_dir = os.path.join(os.path.dirname(__file__), 'Bouncers') |
|
89 |
sys.path.append(bouncers_dir) |
|
90 |
for modname in BouncerAPI.BOUNCE_PIPELINE: |
|
91 |
__import__(modname) |
|
92 |
addrs = sys.modules[modname].process(msg) |
|
93 |
if addrs is BouncerAPI.Stop: |
|
94 |
return None # Stop means to ignore message |
|
95 |
if addrs: |
|
96 |
return addrs |
|
97 |
return None # didn't find any match |
|
98 | ||
99 |
CmdProcessBounce.register() |
wcs/qommon/admin/emails.py | ||
---|---|---|
111 | 111 |
required = False, value = emails.get('reply_to')) |
112 | 112 |
form.add(TextWidget, 'footer', title=_('Email Footer'), cols=70, rows=5, |
113 | 113 |
required=False, value=emails.get('footer')) |
114 |
form.add(CheckboxWidget, 'bounce_handler', title = _('Handle Bounces'), |
|
115 |
value = emails.get('bounce_handler')) |
|
116 | 114 |
form.add(CheckboxWidget, 'check_domain_with_dns', |
117 | 115 |
title = _('Check DNS for domain name'), |
118 | 116 |
value = emails.get('check_domain_with_dns', True), |
... | ... | |
140 | 138 |
return r.getvalue() |
141 | 139 |
else: |
142 | 140 |
cfg_submit(form, 'emails', [ 'smtp_server', 'smtp_login', |
143 |
'smtp_password', 'from', 'reply_to', 'footer', 'bounce_handler',
|
|
141 |
'smtp_password', 'from', 'reply_to', 'footer', |
|
144 | 142 |
'check_domain_with_dns']) |
145 | 143 |
return redirect('.') |
146 | 144 |
wcs/qommon/bounces.py | ||
---|---|---|
1 |
# w.c.s. - web application for online forms |
|
2 |
# Copyright (C) 2005-2010 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or modify |
|
5 |
# it under the terms of the GNU General Public License as published by |
|
6 |
# the Free Software Foundation; either version 2 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from .storage import StorableObject |
|
18 | ||
19 |
class Bounce(StorableObject): |
|
20 |
_names = 'bounces' |
|
21 | ||
22 |
arrival_time = None |
|
23 |
bounce_message = None |
|
24 |
addrs = None |
|
25 |
original_message = None |
|
26 |
original_rcpts = None |
|
27 |
email_type = None |
|
28 |
wcs/qommon/emails.py | ||
---|---|---|
302 | 302 |
# to that address instead of the real recipients. |
303 | 303 |
rcpts = [os.environ.get('QOMMON_MAIL_REDIRECTION')] |
304 | 304 | |
305 |
if emails_cfg.get('bounce_handler'): |
|
306 |
if get_request(): |
|
307 |
server_name = get_request().get_server().split(':')[0] |
|
308 |
else: |
|
309 |
server_name = email_from.split('@')[1] |
|
310 |
token = tokens.Token(7 * 86400) |
|
311 |
token.type = 'email-bounce' |
|
312 |
token.email_rcpts = [str(x) for x in rcpts] |
|
313 |
token.email_message = msg.as_string() |
|
314 |
token.email_type = str(email_type) |
|
315 |
token.store() |
|
316 |
email_from = '%s-bounces+%s@%s' % (get_publisher().APP_NAME, token.id, server_name) |
|
317 | ||
318 | 305 |
if not fire_and_forget: |
319 | 306 |
s = create_smtp_server(emails_cfg, smtp_timeout=smtp_timeout) |
320 | 307 |
try: |
321 |
- |