Projet

Général

Profil

0001-general-remove-bounce-processing-33448.patch

Frédéric Péters, 11 novembre 2019 21:53

Télécharger (67,6 ko)

Voir les différences:

Subject: [PATCH] general: remove bounce processing (#33448)

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