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