flufl.bounce-2.3/ 0000775 0001750 0001750 00000000000 12374417757 014243 5 ustar barry barry 0000000 0000000 flufl.bounce-2.3/MANIFEST.in 0000664 0001750 0001750 00000000171 12374415113 015760 0 ustar barry barry 0000000 0000000 include *.py COPYING.LESSER MANIFEST.in
exclude *.egg
global-include *.rst *.txt *.ini
prune build
prune dist
prune .tox
flufl.bounce-2.3/flufl.bounce.egg-info/ 0000775 0001750 0001750 00000000000 12374417757 020317 5 ustar barry barry 0000000 0000000 flufl.bounce-2.3/flufl.bounce.egg-info/SOURCES.txt 0000664 0001750 0001750 00000012423 12374417757 022205 0 ustar barry barry 0000000 0000000 COPYING.LESSER
MANIFEST.in
README.rst
setup.cfg
setup.py
setup_helpers.py
template.py
tox.ini
flufl/__init__.py
flufl.bounce.egg-info/PKG-INFO
flufl.bounce.egg-info/SOURCES.txt
flufl.bounce.egg-info/dependency_links.txt
flufl.bounce.egg-info/namespace_packages.txt
flufl.bounce.egg-info/requires.txt
flufl.bounce.egg-info/top_level.txt
flufl/bounce/NEWS.rst
flufl/bounce/README.rst
flufl/bounce/__init__.py
flufl/bounce/_scan.py
flufl/bounce/conf.py
flufl/bounce/interfaces.py
flufl/bounce/_detectors/__init__.py
flufl/bounce/_detectors/aol.py
flufl/bounce/_detectors/caiwireless.py
flufl/bounce/_detectors/dsn.py
flufl/bounce/_detectors/exchange.py
flufl/bounce/_detectors/exim.py
flufl/bounce/_detectors/groupwise.py
flufl/bounce/_detectors/llnl.py
flufl/bounce/_detectors/microsoft.py
flufl/bounce/_detectors/netscape.py
flufl/bounce/_detectors/postfix.py
flufl/bounce/_detectors/qmail.py
flufl/bounce/_detectors/simplematch.py
flufl/bounce/_detectors/simplewarning.py
flufl/bounce/_detectors/sina.py
flufl/bounce/_detectors/smtp32.py
flufl/bounce/_detectors/yahoo.py
flufl/bounce/_detectors/yale.py
flufl/bounce/_detectors/tests/__init__.py
flufl/bounce/_detectors/tests/detectors.py
flufl/bounce/docs/__init__.py
flufl/bounce/docs/using.rst
flufl/bounce/tests/__init__.py
flufl/bounce/tests/helpers.py
flufl/bounce/tests/test_detectors.py
flufl/bounce/tests/test_documentation.py
flufl/bounce/tests/data/__init__.py
flufl/bounce/tests/data/aol_01.txt
flufl/bounce/tests/data/bounce_01.txt
flufl/bounce/tests/data/bounce_02.txt
flufl/bounce/tests/data/bounce_03.txt
flufl/bounce/tests/data/dsn_01.txt
flufl/bounce/tests/data/dsn_02.txt
flufl/bounce/tests/data/dsn_03.txt
flufl/bounce/tests/data/dsn_04.txt
flufl/bounce/tests/data/dsn_05.txt
flufl/bounce/tests/data/dsn_06.txt
flufl/bounce/tests/data/dsn_07.txt
flufl/bounce/tests/data/dsn_08.txt
flufl/bounce/tests/data/dsn_09.txt
flufl/bounce/tests/data/dsn_10.txt
flufl/bounce/tests/data/dsn_11.txt
flufl/bounce/tests/data/dsn_12.txt
flufl/bounce/tests/data/dsn_13.txt
flufl/bounce/tests/data/dsn_14.txt
flufl/bounce/tests/data/dsn_15.txt
flufl/bounce/tests/data/dsn_16.txt
flufl/bounce/tests/data/dsn_17.txt
flufl/bounce/tests/data/dumbass_01.txt
flufl/bounce/tests/data/exim_01.txt
flufl/bounce/tests/data/groupwise_01.txt
flufl/bounce/tests/data/groupwise_02.txt
flufl/bounce/tests/data/hotpop_01.txt
flufl/bounce/tests/data/llnl_01.txt
flufl/bounce/tests/data/microsoft_01.txt
flufl/bounce/tests/data/microsoft_02.txt
flufl/bounce/tests/data/microsoft_03.txt
flufl/bounce/tests/data/netscape_01.txt
flufl/bounce/tests/data/newmailru_01.txt
flufl/bounce/tests/data/postfix_01.txt
flufl/bounce/tests/data/postfix_02.txt
flufl/bounce/tests/data/postfix_03.txt
flufl/bounce/tests/data/postfix_04.txt
flufl/bounce/tests/data/postfix_05.txt
flufl/bounce/tests/data/qmail_01.txt
flufl/bounce/tests/data/qmail_02.txt
flufl/bounce/tests/data/qmail_03.txt
flufl/bounce/tests/data/qmail_04.txt
flufl/bounce/tests/data/qmail_05.txt
flufl/bounce/tests/data/qmail_06.txt
flufl/bounce/tests/data/qmail_07.txt
flufl/bounce/tests/data/qmail_08.txt
flufl/bounce/tests/data/sendmail_01.txt
flufl/bounce/tests/data/simple_01.txt
flufl/bounce/tests/data/simple_02.txt
flufl/bounce/tests/data/simple_03.txt
flufl/bounce/tests/data/simple_04.txt
flufl/bounce/tests/data/simple_05.txt
flufl/bounce/tests/data/simple_06.txt
flufl/bounce/tests/data/simple_07.txt
flufl/bounce/tests/data/simple_08.txt
flufl/bounce/tests/data/simple_09.txt
flufl/bounce/tests/data/simple_10.txt
flufl/bounce/tests/data/simple_11.txt
flufl/bounce/tests/data/simple_12.txt
flufl/bounce/tests/data/simple_13.txt
flufl/bounce/tests/data/simple_14.txt
flufl/bounce/tests/data/simple_15.txt
flufl/bounce/tests/data/simple_16.txt
flufl/bounce/tests/data/simple_17.txt
flufl/bounce/tests/data/simple_18.txt
flufl/bounce/tests/data/simple_19.txt
flufl/bounce/tests/data/simple_20.txt
flufl/bounce/tests/data/simple_21.txt
flufl/bounce/tests/data/simple_22.txt
flufl/bounce/tests/data/simple_23.txt
flufl/bounce/tests/data/simple_24.txt
flufl/bounce/tests/data/simple_25.txt
flufl/bounce/tests/data/simple_26.txt
flufl/bounce/tests/data/simple_27.txt
flufl/bounce/tests/data/simple_28.txt
flufl/bounce/tests/data/simple_29.txt
flufl/bounce/tests/data/simple_30.txt
flufl/bounce/tests/data/simple_31.txt
flufl/bounce/tests/data/simple_32.txt
flufl/bounce/tests/data/simple_33.txt
flufl/bounce/tests/data/simple_34.txt
flufl/bounce/tests/data/simple_35.txt
flufl/bounce/tests/data/simple_36.txt
flufl/bounce/tests/data/simple_37.txt
flufl/bounce/tests/data/simple_38.txt
flufl/bounce/tests/data/simple_39.txt
flufl/bounce/tests/data/simple_40.txt
flufl/bounce/tests/data/sina_01.txt
flufl/bounce/tests/data/smtp32_01.txt
flufl/bounce/tests/data/smtp32_02.txt
flufl/bounce/tests/data/smtp32_03.txt
flufl/bounce/tests/data/smtp32_04.txt
flufl/bounce/tests/data/smtp32_05.txt
flufl/bounce/tests/data/smtp32_06.txt
flufl/bounce/tests/data/smtp32_07.txt
flufl/bounce/tests/data/yahoo_01.txt
flufl/bounce/tests/data/yahoo_02.txt
flufl/bounce/tests/data/yahoo_03.txt
flufl/bounce/tests/data/yahoo_04.txt
flufl/bounce/tests/data/yahoo_05.txt
flufl/bounce/tests/data/yahoo_06.txt
flufl/bounce/tests/data/yahoo_07.txt
flufl/bounce/tests/data/yahoo_08.txt
flufl/bounce/tests/data/yahoo_09.txt
flufl/bounce/tests/data/yahoo_10.txt
flufl/bounce/tests/data/yahoo_11.txt
flufl/bounce/tests/data/yale_01.txt flufl.bounce-2.3/flufl.bounce.egg-info/requires.txt 0000664 0001750 0001750 00000000026 12374417757 022715 0 ustar barry barry 0000000 0000000 zope.interface
enum34
flufl.bounce-2.3/flufl.bounce.egg-info/top_level.txt 0000664 0001750 0001750 00000000006 12374417757 023045 0 ustar barry barry 0000000 0000000 flufl
flufl.bounce-2.3/flufl.bounce.egg-info/dependency_links.txt 0000664 0001750 0001750 00000000001 12374417757 024365 0 ustar barry barry 0000000 0000000
flufl.bounce-2.3/flufl.bounce.egg-info/namespace_packages.txt 0000664 0001750 0001750 00000000006 12374417757 024646 0 ustar barry barry 0000000 0000000 flufl
flufl.bounce-2.3/flufl.bounce.egg-info/PKG-INFO 0000664 0001750 0001750 00000015322 12374417757 021417 0 ustar barry barry 0000000 0000000 Metadata-Version: 1.1
Name: flufl.bounce
Version: 2.3
Summary: Email bounce detectors.
Home-page: http://launchpad.net/flufl.bounce
Author: Barry Warsaw
Author-email: barry@python.org
License: LGPLv3
Download-URL: https://launchpad.net/flufl.bounce/+download
Description: ============
flufl.bounce
============
Email bounce detectors.
The ``flufl.bounce`` library provides a set of heuristics and an API for
detecting the original bouncing email addresses from a bounce message. Many
formats found in the wild are supported, as are VERP_ and RFC 3464 (DSN_).
.. _VERP: http://en.wikipedia.org/wiki/Variable_envelope_return_path
.. _DSN: http://www.faqs.org/rfcs/rfc3464.html
License
=======
Copyright (C) 2004-2014 by Barry A. Warsaw
This file is part of flufl.bounce.
flufl.bounce is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, version 3 of the License.
flufl.bounce 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 Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with flufl.bounce. If not, see .
Author
======
Barry Warsaw
with additional help from Mark Sapiro
=====================
NEWS for flufl.bounce
=====================
2.3 (2014-08-18)
================
* Added recognition for a kundenserver.de warning to simplewarning.py.
(LP: #1263247)
* Stop using the deprecated `distribute` package in favor of the now-merged
`setuptools` package.
* Stop using the deprecated `flufl.enum` package in favor of the enum34
package (for Python 2) or built-in enum package (for Python 3).
2.2.1 (2013-06-21)
==================
* Prune some artifacts unintentionally leaked into the release tarball.
2.2 (2013-06-20)
================
* Added recognition for a bogus Dovecot over-quota rejection sent as an MDN
rather than a DSN. (LP: #693134)
* Tweaked a simplematch regexp that didn't always work. (LP: #1079254)
* Added recognition for bounces from mail.ru. Thanks to Andrey
Rahmatullin. (LP: #1079249)
* Fixed UnicodeDecodeError in qmail.py with non-ascii message. Thanks
to Theo Spears. (LP: #1074592)
* Added recognition for another Yahoo bounce format. Thanks to Mark
Sapiro. (LP: #1157961)
* Fix documentation bug. (LP: #1026403)
* Document the zope.interface requirement. (LP: #1021383)
2.1.1 (2012-04-19)
==================
* Add classifiers to setup.py and make the long description more compatible
with the Cheeseshop.
* Other changes to make the Cheeseshop page look nicer. (LP: #680136)
* setup_helper.py version 2.1.
2.1 (2012-01-19)
================
* Fix TypeError thrown when None is returned by Caiwireless. Given by Paul
Egan. (LP: #917720)
2.0 (2012-01-04)
================
* Port to Python 3 without the use of `2to3`. Switch to class decorator
syntax for declaring that a class implements an interface. The functional
form doesn't work for Python 3.
* All returned addresses are bytes objects in Python 3 and 8-bit strings in
Python 2 (no change there).
* Add an additional in-the-wild example of a qmail bounce. Given by Mark
Sapiro.
* Export `all_failures` in the package's namespace.
* Fix `python setup.py test` so that it runs all the tests exactly once.
There seems to be no portable way to support that and unittest discovery
(i.e. `python -m unittest discover`) and since the latter requires
virtualenv, just disable it for now. (LP: #911399)
* Add full copy of LGPLv3 to source tarball. (LP: #871961)
1.0.2 (2011-10-10)
==================
* Fixed MANIFEST.in to exclude the .egg.
1.0.1 (2011-10-07)
==================
* Fixed licenses. All code is LGPLv3.
1.0 (2011-08-22)
================
* Initial release.
0.91 (2011-07-15)
=================
* Provide a nicer interface for detector modules. Instead of using the magic
empty tuple returns, provide three convenience constants in the interfaces
module: NoFailures, NoTemporaryFailures, and NoPermanentFailures.
* Add logging support. Applications can initialize the `flufl.bounce`
logger. The test suite does its own logging.basicConfig(), which can be
influenced by the environment variable $FLUFL_LOGGING. See
flufl/bounce/tests/helpers.py for details.
0.90 (2011-07-02)
=================
* Initial refactoring from Mailman 3.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
Classifier: Operating System :: POSIX
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
flufl.bounce-2.3/setup.py 0000664 0001750 0001750 00000004730 12374411560 015743 0 ustar barry barry 0000000 0000000 # Copyright (C) 2004-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce.
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
import sys
from setup_helpers import (
description, find_doctests, get_version, long_description, require_python)
from setuptools import setup, find_packages
require_python(0x20600f0)
__version__ = get_version('flufl/bounce/__init__.py')
# Don't try to fix the tests messages.
doctests = [doctest for doctest in find_doctests()
if 'tests/data' not in doctest]
install_requires = ['zope.interface']
if sys.version_info < (3, 4):
install_requires.append('enum34')
setup(
name='flufl.bounce',
version=__version__,
namespace_packages=['flufl'],
packages=find_packages(),
include_package_data=True,
maintainer='Barry Warsaw',
maintainer_email='barry@python.org',
description=description('README.rst'),
long_description=long_description('README.rst', 'flufl/bounce/NEWS.rst'),
license='LGPLv3',
url='http://launchpad.net/flufl.bounce',
download_url='https://launchpad.net/flufl.bounce/+download',
install_requires=install_requires,
test_suite='flufl.bounce.tests',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: '
'GNU Lesser General Public License v3 or later (LGPLv3+)',
'Operating System :: POSIX',
'Operating System :: Microsoft :: Windows',
'Operating System :: MacOS :: MacOS X',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Topic :: Software Development :: Libraries',
'Topic :: Communications :: Email',
'Topic :: Software Development :: Libraries :: Python Modules',
]
)
flufl.bounce-2.3/setup.cfg 0000664 0001750 0001750 00000000223 12374417757 016061 0 ustar barry barry 0000000 0000000 [build_sphinx]
source_dir = flufl/bounce
[upload_docs]
upload_dir = build/sphinx/html
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
flufl.bounce-2.3/setup_helpers.py 0000664 0001750 0001750 00000012052 12374411560 017461 0 ustar barry barry 0000000 0000000 # Copyright (C) 2009-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""setup.py helper functions."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'description',
'find_doctests',
'get_version',
'long_description',
'require_python',
]
import os
import re
import sys
DEFAULT_VERSION_RE = re.compile(r'(?P\d+\.\d+(?:\.\d+)?)')
EMPTYSTRING = ''
__version__ = '2.1'
def require_python(minimum):
"""Require at least a minimum Python version.
The version number is expressed in terms of `sys.hexversion`. E.g. to
require a minimum of Python 2.6, use::
>>> require_python(0x206000f0)
:param minimum: Minimum Python version supported.
:type minimum: integer
"""
if sys.hexversion < minimum:
hversion = hex(minimum)[2:]
if len(hversion) % 2 != 0:
hversion = '0' + hversion
split = list(hversion)
parts = []
while split:
parts.append(int(''.join((split.pop(0), split.pop(0))), 16))
major, minor, micro, release = parts
if release == 0xf0:
print('Python {0}.{1}.{2} or better is required'.format(
major, minor, micro))
else:
print('Python {0}.{1}.{2} ({3}) or better is required'.format(
major, minor, micro, hex(release)[2:]))
sys.exit(1)
def get_version(filename, pattern=None):
"""Extract the __version__ from a file without importing it.
While you could get the __version__ by importing the module, the very act
of importing can cause unintended consequences. For example, Distribute's
automatic 2to3 support will break. Instead, this searches the file for a
line that starts with __version__, and extract the version number by
regular expression matching.
By default, two or three dot-separated digits are recognized, but by
passing a pattern parameter, you can recognize just about anything. Use
the `version` group name to specify the match group.
:param filename: The name of the file to search.
:type filename: string
:param pattern: Optional alternative regular expression pattern to use.
:type pattern: string
:return: The version that was extracted.
:rtype: string
"""
if pattern is None:
cre = DEFAULT_VERSION_RE
else:
cre = re.compile(pattern)
with open(filename) as fp:
for line in fp:
if line.startswith('__version__'):
mo = cre.search(line)
assert mo, 'No valid __version__ string found'
return mo.group('version')
raise AssertionError('No __version__ assignment found')
def find_doctests(start='.', extension='.rst'):
"""Find separate-file doctests in the package.
This is useful for Distribute's automatic 2to3 conversion support. The
`setup()` keyword argument `convert_2to3_doctests` requires file names,
which may be difficult to track automatically as you add new doctests.
:param start: Directory to start searching in (default is cwd)
:type start: string
:param extension: Doctest file extension (default is .txt)
:type extension: string
:return: The doctest files found.
:rtype: list
"""
doctests = []
for dirpath, dirnames, filenames in os.walk(start):
doctests.extend(os.path.join(dirpath, filename)
for filename in filenames
if filename.endswith(extension))
return doctests
def long_description(*filenames):
"""Provide a long description."""
res = ['']
for filename in filenames:
with open(filename) as fp:
for line in fp:
res.append(' ' + line)
res.append('')
res.append('\n')
return EMPTYSTRING.join(res)
def description(filename):
"""Provide a short description."""
# This ends up in the Summary header for PKG-INFO and it should be a
# one-liner. It will get rendered on the package page just below the
# package version header but above the long_description, which ironically
# gets stuff into the Description header. It should not include reST, so
# pick out the first single line after the double header.
with open(filename) as fp:
for lineno, line in enumerate(fp):
if lineno < 3:
continue
line = line.strip()
if len(line) > 0:
return line
flufl.bounce-2.3/flufl/ 0000775 0001750 0001750 00000000000 12374417757 015353 5 ustar barry barry 0000000 0000000 flufl.bounce-2.3/flufl/bounce/ 0000775 0001750 0001750 00000000000 12374417757 016626 5 ustar barry barry 0000000 0000000 flufl.bounce-2.3/flufl/bounce/docs/ 0000775 0001750 0001750 00000000000 12374417757 017556 5 ustar barry barry 0000000 0000000 flufl.bounce-2.3/flufl/bounce/docs/using.rst 0000664 0001750 0001750 00000005137 12374411560 021425 0 ustar barry barry 0000000 0000000 ==============================
Using the flufl.bounce library
==============================
The ``flufl.bounce`` library provides a set of heuristic detectors for
discerning the original bouncing email address from a bounce message. It
contains detectors for a wide variety of formats found in the wild over the
last 15 years, as well as standard formats such as VERP_ and RFC 3464 (DSN_).
It also provides an API for extension with your own detector formats.
Basic usage
===========
In the most basic form of use, you can just pass an email message to the
top-level function, and get back a set of email addresses detected as
bouncing.
In Python 3, you should parse the message in binary (i.e. bytes) mode using
say `email.message_from_bytes()`. You will get back a set of byte addresses.
In Python 2, you should use `email.message_from_string()` to parse the
message, and you will get back 8-bit strings.
Here for example, is a simple DSN-like bounce message. `parse()` is the
appropriate email parsing function described above.
>>> msg = parse(b"""\
... From: Mail Delivery Subsystem
... To: list-bounces@example.com
... Subject: Delivery Report
... MIME-Version: 1.0
... Content-Type: multipart/report; report-type=delivery-status;
... boundary=AAA
...
... --AAA
... Content-Type: message/delivery-status
...
... Original-Recipient: rfc822;anne@example.com
... Action: failed
...
... Original-Recipient: rfc822;bart@example.com
... Action: delayed
...
... --AAA--
... """)
..
>>> def print_emails(recipients):
... if recipients is None:
... print('None')
... return
... if len(recipients) == 0:
... print('No addresses')
... for email in sorted(recipients):
... # Remove the Py3 extraneous b'' prefixes.
... if bytes is not str:
... email = repr(email)[2:-1]
... print(email)
You can scan the bounce message object to get a set of all the email addresses
that have permanent failures.
>>> from flufl.bounce import scan_message
>>> recipients = scan_message(msg)
>>> print_emails(recipients)
anne@example.com
You can also get the set of all temporarily and permanent failures.
>>> from flufl.bounce import all_failures
>>> temporary, permanent = all_failures(msg)
>>> print_emails(temporary)
bart@example.com
>>> print_emails(permanent)
anne@example.com
.. _VERP: http://en.wikipedia.org/wiki/Variable_envelope_return_path
.. _DSN: http://www.faqs.org/rfcs/rfc3464.html
flufl.bounce-2.3/flufl/bounce/docs/__init__.py 0000664 0001750 0001750 00000000000 12374411560 021637 0 ustar barry barry 0000000 0000000 flufl.bounce-2.3/flufl/bounce/interfaces.py 0000664 0001750 0001750 00000004212 12374411560 021304 0 ustar barry barry 0000000 0000000 # Copyright (C) 2011-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""Interfaces."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'IBounceDetector',
'NoFailures',
'NoPermanentFailures',
'NoTemporaryFailures',
]
from zope.interface import Interface
# Constants for improved readability in detector classes. Use these like so:
#
# - to signal that no temporary or permanent failures were found:
# `return NoFailures`
# - to signal that no temporary failures, but some permanent failures were
# found:
# `return NoTemporaryFailures, my_permanent_failures`
# - to signal that some temporary failures, but no permanent failures were
# found:
# `return my_temporary_failures, NoPermanentFailures`
NoTemporaryFailures = NoPermanentFailures = ()
NoFailures = (NoTemporaryFailures, NoPermanentFailures)
class IBounceDetector(Interface):
"""Detect a bounce in an email message."""
def process(self, msg):
"""Scan an email message looking for bounce addresses.
:param msg: An email message.
:type msg: `Message`
:return: A 2-tuple of the detected temporary and permanent bouncing
addresses. Both elements of the tuple are sets of string
email addresses. Not all detectors can tell the difference
between temporary and permanent failures, in which case, the
addresses will be considered to be permanently bouncing.
:rtype: (set of strings, set of string)
"""
flufl.bounce-2.3/flufl/bounce/NEWS.rst 0000664 0001750 0001750 00000006144 12374415617 020132 0 ustar barry barry 0000000 0000000 =====================
NEWS for flufl.bounce
=====================
2.3 (2014-08-18)
================
* Added recognition for a kundenserver.de warning to simplewarning.py.
(LP: #1263247)
* Stop using the deprecated `distribute` package in favor of the now-merged
`setuptools` package.
* Stop using the deprecated `flufl.enum` package in favor of the enum34
package (for Python 2) or built-in enum package (for Python 3).
2.2.1 (2013-06-21)
==================
* Prune some artifacts unintentionally leaked into the release tarball.
2.2 (2013-06-20)
================
* Added recognition for a bogus Dovecot over-quota rejection sent as an MDN
rather than a DSN. (LP: #693134)
* Tweaked a simplematch regexp that didn't always work. (LP: #1079254)
* Added recognition for bounces from mail.ru. Thanks to Andrey
Rahmatullin. (LP: #1079249)
* Fixed UnicodeDecodeError in qmail.py with non-ascii message. Thanks
to Theo Spears. (LP: #1074592)
* Added recognition for another Yahoo bounce format. Thanks to Mark
Sapiro. (LP: #1157961)
* Fix documentation bug. (LP: #1026403)
* Document the zope.interface requirement. (LP: #1021383)
2.1.1 (2012-04-19)
==================
* Add classifiers to setup.py and make the long description more compatible
with the Cheeseshop.
* Other changes to make the Cheeseshop page look nicer. (LP: #680136)
* setup_helper.py version 2.1.
2.1 (2012-01-19)
================
* Fix TypeError thrown when None is returned by Caiwireless. Given by Paul
Egan. (LP: #917720)
2.0 (2012-01-04)
================
* Port to Python 3 without the use of `2to3`. Switch to class decorator
syntax for declaring that a class implements an interface. The functional
form doesn't work for Python 3.
* All returned addresses are bytes objects in Python 3 and 8-bit strings in
Python 2 (no change there).
* Add an additional in-the-wild example of a qmail bounce. Given by Mark
Sapiro.
* Export `all_failures` in the package's namespace.
* Fix `python setup.py test` so that it runs all the tests exactly once.
There seems to be no portable way to support that and unittest discovery
(i.e. `python -m unittest discover`) and since the latter requires
virtualenv, just disable it for now. (LP: #911399)
* Add full copy of LGPLv3 to source tarball. (LP: #871961)
1.0.2 (2011-10-10)
==================
* Fixed MANIFEST.in to exclude the .egg.
1.0.1 (2011-10-07)
==================
* Fixed licenses. All code is LGPLv3.
1.0 (2011-08-22)
================
* Initial release.
0.91 (2011-07-15)
=================
* Provide a nicer interface for detector modules. Instead of using the magic
empty tuple returns, provide three convenience constants in the interfaces
module: NoFailures, NoTemporaryFailures, and NoPermanentFailures.
* Add logging support. Applications can initialize the `flufl.bounce`
logger. The test suite does its own logging.basicConfig(), which can be
influenced by the environment variable $FLUFL_LOGGING. See
flufl/bounce/tests/helpers.py for details.
0.90 (2011-07-02)
=================
* Initial refactoring from Mailman 3.
flufl.bounce-2.3/flufl/bounce/__init__.py 0000664 0001750 0001750 00000001654 12374415610 020727 0 ustar barry barry 0000000 0000000 # Copyright (C) 2011-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""Package init."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'__version__',
'all_failures',
'scan_message',
]
__version__ = '2.3'
from ._scan import all_failures, scan_message
flufl.bounce-2.3/flufl/bounce/_detectors/ 0000775 0001750 0001750 00000000000 12374417757 020761 5 ustar barry barry 0000000 0000000 flufl.bounce-2.3/flufl/bounce/_detectors/exim.py 0000664 0001750 0001750 00000003244 12374411560 022262 0 ustar barry barry 0000000 0000000 # Copyright (C) 1998-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""Parse bounce messages generated by Exim.
Exim adds an X-Failed-Recipients: header to bounce messages containing
an `addresslist' of failed addresses.
"""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'Exim',
]
from email.utils import getaddresses
from zope.interface import implementer
from flufl.bounce.interfaces import IBounceDetector, NoTemporaryFailures
@implementer(IBounceDetector)
class Exim:
"""Parse bounce messages generated by Exim."""
def process(self, msg):
"""See `IBounceDetector`."""
all_failed = msg.get_all('x-failed-recipients', [])
# all_failed will contain string/unicode values, but the flufl.bounce
# API requires these to be bytes. We don't know the encoding, but
# assume it must be ascii, per the relevant RFCs.
return (NoTemporaryFailures,
set(address.encode('us-ascii')
for name, address in getaddresses(all_failed)))
flufl.bounce-2.3/flufl/bounce/_detectors/simplewarning.py 0000664 0001750 0001750 00000005511 12374411560 024176 0 ustar barry barry 0000000 0000000 # Copyright (C) 2001-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""Recognizes simple heuristically delimited warnings."""
__metaclass__ = type
__all__ = [
'SimpleWarning',
]
from flufl.bounce._detectors.simplematch import SimpleMatch
from flufl.bounce._detectors.simplematch import _c
from flufl.bounce.interfaces import NoPermanentFailures
# 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\S+@\S+)\s*')),
# MessageSwitch
(_c('Your message to:'),
_c('This is just a warning, you do not need to take any action'),
_c(r'\s*(?P\S+@\S+)\s*')),
# Symantec_AntiVirus_for_SMTP_Gateways
(_c('Your message with Subject:'),
_c('Delivery attempts will continue to be made'),
_c(r'\s*(?P\S+@\S+)\s*')),
# googlemail.com warning
(_c('Delivery to the following recipient has been delayed'),
_c('Message will be retried'),
_c(r'\s*(?P\S+@\S+)\s*')),
# Exchange warning message.
(_c('This is an advisory-only email'),
_c('has been postponed'),
_c('"(?P[^"]+)"')),
# kundenserver.de
(_c('not yet been delivered'),
_c('No action is required on your part'),
_c(r'\s*(?P\S+@[^>\s]+)>?\s*')),
# Next one goes here...
]
class SimpleWarning(SimpleMatch):
"""Recognizes simple heuristically delimited warnings."""
PATTERNS = PATTERNS
def process(self, msg):
"""See `SimpleMatch`."""
# Since these are warnings, they're classified as temporary failures.
# There are no permanent failures.
(temporary,
permanent_really_temporary) = super(SimpleWarning, self).process(msg)
return permanent_really_temporary, NoPermanentFailures
flufl.bounce-2.3/flufl/bounce/_detectors/yale.py 0000664 0001750 0001750 00000006314 12374411560 022253 0 ustar barry barry 0000000 0000000 # Copyright (C) 2000-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""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. :(
"""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'Yale',
]
import re
from email.utils import getaddresses
from enum import Enum
from io import BytesIO
from zope.interface import implementer
from flufl.bounce.interfaces import (
IBounceDetector, NoFailures, NoTemporaryFailures)
scre = re.compile(b'Message not delivered to the following', re.IGNORECASE)
ecre = re.compile(b'Error Detail', re.IGNORECASE)
acre = re.compile(b'\\s+(?P\\S+)\\s+')
class ParseState(Enum):
start = 0
intro_found = 1
@implementer(IBounceDetector)
class Yale:
"""Parse Yale's bounces (or what used to be)."""
def process(self, msg):
"""See `IBounceDetector`."""
if msg.is_multipart():
return NoFailures
try:
whofrom = getaddresses([msg.get('from', '')])[0][1]
if not whofrom:
return NoFailures
username, domain = whofrom.split('@', 1)
except (IndexError, ValueError):
return NoFailures
if username.lower() != 'mailer-daemon':
return NoFailures
parts = domain.split('.')
parts.reverse()
for part1, part2 in zip(parts, ('edu', 'yale')):
if part1 != part2:
return NoFailures
# 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 = set()
body = BytesIO(msg.get_payload(decode=True))
state = ParseState.start
for line in body:
if state is ParseState.start and scre.search(line):
state = ParseState.intro_found
elif state is ParseState.intro_found and ecre.search(line):
break
elif state is ParseState.intro_found:
mo = acre.search(line)
if mo:
names.add(mo.group('addr'))
# Now we have a bunch of names, these are either @yale.edu or
# @cs.yale.edu. Add them both.
addresses = set()
for name in names:
addresses.add(name + b'@yale.edu')
addresses.add(name + b'@cs.yale.edu')
return NoTemporaryFailures, addresses
flufl.bounce-2.3/flufl/bounce/_detectors/yahoo.py 0000664 0001750 0001750 00000005616 12374411560 022444 0 ustar barry barry 0000000 0000000 # Copyright (C) 1998-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""Yahoo! has its own weird format for bounces."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'Yahoo',
]
import re
from email.iterators import body_line_iterator
from email.utils import parseaddr
from enum import Enum
from zope.interface import implementer
from flufl.bounce.interfaces import (
IBounceDetector, NoFailures, NoTemporaryFailures)
tcre = (re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE),
re.compile(r'Sorry, we were unable to deliver your message to '
r'the following address(\(es\))?\.',
re.IGNORECASE),
)
acre = re.compile(r'<(?P[^>]*)>:')
ecre = (re.compile(r'--- Original message follows'),
re.compile(r'--- Below this line is a copy of the message'),
)
class _ParseState(Enum):
start = 0
tag_seen = 1
all_done = 2
@implementer(IBounceDetector)
class Yahoo:
"""Yahoo! bounce detection."""
def process(self, msg):
"""See `IBounceDetector`."""
# 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 NoFailures
addresses = set()
state = _ParseState.start
for line in body_line_iterator(msg):
line = line.strip()
if state is _ParseState.start:
for cre in tcre:
if cre.match(line):
state = _ParseState.tag_seen
break
elif state is _ParseState.tag_seen:
mo = acre.match(line)
if mo:
addresses.add(mo.group('addr').encode('us-ascii'))
continue
for cre in ecre:
mo = cre.match(line)
if mo:
# We're at the end of the error response.
state = _ParseState.all_done
break
if state is _ParseState.all_done:
break
return NoTemporaryFailures, addresses
flufl.bounce-2.3/flufl/bounce/_detectors/aol.py 0000664 0001750 0001750 00000004012 12374411560 022065 0 ustar barry barry 0000000 0000000 # Copyright (C) 2009-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""Recognizes a class of messages from AOL that report only Screen Name."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'AOL',
]
import re
from email.utils import parseaddr
from zope.interface import implementer
from flufl.bounce.interfaces import (
IBounceDetector, NoFailures, NoTemporaryFailures)
scre = re.compile(b'mail to the following recipients could not be delivered')
@implementer(IBounceDetector)
class AOL:
"""Recognizes a class of messages from AOL that report only Screen Name."""
def process(self, msg):
if msg.get_content_type() != 'text/plain':
return NoFailures
if not parseaddr(msg.get('from', ''))[1].lower().endswith('@aol.com'):
return NoFailures
addresses = set()
found = False
for line in msg.get_payload(decode=True).splitlines():
if scre.search(line):
found = True
continue
if found:
local = line.strip()
if local:
if re.search(b'\\s', local):
break
if b'@' in local:
addresses.add(local)
else:
addresses.add(local + b'@aol.com')
return NoTemporaryFailures, addresses
flufl.bounce-2.3/flufl/bounce/_detectors/dsn.py 0000664 0001750 0001750 00000010467 12374411560 022111 0 ustar barry barry 0000000 0000000 # Copyright (C) 1998-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""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 __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'DSN',
]
from email.iterators import typed_subpart_iterator
from email.utils import parseaddr
from zope.interface import implementer
from flufl.bounce.interfaces import IBounceDetector
@implementer(IBounceDetector)
class DSN:
"""Parse RFC 3464 (i.e. DSN) bounce formats."""
def process(self, msg):
"""See `IBounceDetector`."""
# Iterate over each message/delivery-status subpart.
failed_addresses = []
delayed_addresses = []
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():
address_set = None
# 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'):
address_set = delayed_addresses
elif action.startswith('fail'):
address_set = failed_addresses
else:
# 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.
address_set.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('>'):
address_set.append(param[1:-1])
# There may be Nones in the current set of failures, so filter those
# out of both sets. Also, for Python 3 compatibility, the API
# requires byte addresses.
return (
# First, the delayed, or temporary failures.
set(parseaddr(address)[1].encode('us-ascii')
for address in delayed_addresses
if address is not None),
# And now the failed or permanent failures.
set(parseaddr(address)[1].encode('us-ascii')
for address in failed_addresses
if address is not None)
)
flufl.bounce-2.3/flufl/bounce/_detectors/groupwise.py 0000664 0001750 0001750 00000004750 12374411560 023347 0 ustar barry barry 0000000 0000000 # Copyright (C) 1998-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""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)
"""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'GroupWise',
]
import re
from email.message import Message
from io import BytesIO
from zope.interface import implementer
from flufl.bounce.interfaces import (
IBounceDetector, NoFailures, NoTemporaryFailures)
acre = re.compile(b'<(?P[^>]*)>')
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
@implementer(IBounceDetector)
class GroupWise:
"""Parse Novell GroupWise and NTMail bounces."""
def process(self, msg):
"""See `IBounceDetector`."""
if msg.get_content_type() != 'multipart/mixed' or not msg['x-mailer']:
return NoFailures
addresses = set()
# Find the first text/plain part in the message.
text_plain = find_textplain(msg)
if text_plain is None:
return NoFailures
body = BytesIO(text_plain.get_payload(decode=True))
for line in body:
mo = acre.search(line)
if mo:
addresses.add(mo.group('addr'))
elif b'@' in line:
i = line.find(b' ')
if i == 0:
continue
if i < 0:
addresses.add(line)
else:
addresses.add(line[:i])
return NoTemporaryFailures, set(addresses)
flufl.bounce-2.3/flufl/bounce/_detectors/__init__.py 0000664 0001750 0001750 00000000000 12374411560 023042 0 ustar barry barry 0000000 0000000 flufl.bounce-2.3/flufl/bounce/_detectors/sina.py 0000664 0001750 0001750 00000003454 12374411560 022255 0 ustar barry barry 0000000 0000000 # Copyright (C) 2002-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""sina.com bounces"""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'Sina',
]
import re
from email.iterators import body_line_iterator
from zope.interface import implementer
from flufl.bounce.interfaces import (
IBounceDetector, NoFailures, NoTemporaryFailures)
acre = re.compile(r'<(?P[^>]*)>')
@implementer(IBounceDetector)
class Sina:
"""sina.com bounces"""
def process(self, msg):
"""See `IBounceDetector`."""
if msg.get('from', '').lower() != 'mailer-daemon@sina.com':
return NoFailures
if not msg.is_multipart():
return NoFailures
# The interesting bits are in the first text/plain multipart.
part = None
try:
part = msg.get_payload(0)
except IndexError:
pass
if not part:
return NoFailures
addresses = set()
for line in body_line_iterator(part):
mo = acre.match(line)
if mo:
addresses.add(mo.group('addr').encode('us-ascii'))
return NoTemporaryFailures, addresses
flufl.bounce-2.3/flufl/bounce/_detectors/llnl.py 0000664 0001750 0001750 00000002750 12374411560 022262 0 ustar barry barry 0000000 0000000 # Copyright (C) 2001-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""LLNL's custom Sendmail bounce message."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'LLNL',
]
import re
from email.iterators import body_line_iterator
from zope.interface import implementer
from flufl.bounce.interfaces import (
IBounceDetector, NoFailures, NoTemporaryFailures)
acre = re.compile(r',\s*(?P\S+@[^,]+),', re.IGNORECASE)
@implementer(IBounceDetector)
class LLNL:
"""LLNL's custom Sendmail bounce message."""
def process(self, msg):
"""See `IBounceDetector`."""
for line in body_line_iterator(msg):
mo = acre.search(line)
if mo:
address = mo.group('addr').encode('us-ascii')
return NoTemporaryFailures, set([address])
return NoFailures
flufl.bounce-2.3/flufl/bounce/_detectors/simplematch.py 0000664 0001750 0001750 00000023550 12374411560 023630 0 ustar barry barry 0000000 0000000 # Copyright (C) 1998-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""Recognizes simple heuristically delimited bounces."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'SimpleMatch',
]
import re
from email.iterators import body_line_iterator
from email.quoprimime import unquote
from enum import Enum
from zope.interface import implementer
from flufl.bounce.interfaces import IBounceDetector, NoTemporaryFailures
class ParseState(Enum):
start = 0
tag_seen = 1
def _unquote_match(match):
return unquote(match.group(0))
def _quopri_decode(address):
# Some addresses come back with quopri encoded spaces. This will decode
# them and strip the spaces. We can't use the undocumebted
# email.quoprimime.header_decode() because that also turns underscores
# into spaces, which is not good for us. Instead we'll use the
# undocumented email.quoprimime.unquote().
#
# For compatibility with Python 3, the API requires byte addresses.
unquoted = re.sub('=[a-fA-F0-9]{2}', _unquote_match, address)
return unquoted.encode('us-ascii').strip()
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[^>]*)>')),
# sz-sb.de, corridor.com, nfg.nl
(_c('the following addresses had'),
_c('transcript of session follows'),
_c(r'^ *(\(expanded from: )?(?P[^\s@]+@[^\s@>]+?)>?\)?\s*$')),
# robanal.demon.co.uk
(_c('this message was created automatically by mail delivery software'),
_c('original message follows'),
_c('rcpt to:\s*<(?P[^>]*)>')),
# s1.com (InterScan E-Mail VirusWall NT ???)
(_c('message from interscan e-mail viruswall nt'),
_c('end of message'),
_c('rcpt to:\s*<(?P[^>]*)>')),
# Smail
(_c('failed addresses follow:'),
_c('message text follows:'),
_c(r'\s*(?P\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[^>]*)>')),
# 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.*)$')),
# usa.net
(_c('Intended recipient:\s*(?P.*)$'),
_c('--------RETURNED MAIL FOLLOWS--------'),
_c('Intended recipient:\s*(?P.*)$')),
# hotpop.com
(_c('Undeliverable Address:\s*(?P.*)$'),
_c('Original message attached'),
_c('Undeliverable Address:\s*(?P.*)$')),
# Another demon.co.uk format
(_c('This message was created automatically by mail delivery'),
_c('^---- START OF RETURNED MESSAGE ----'),
_c("addressed to '(?P[^']*)'")),
# Prodigy.net full mailbox
(_c("User's mailbox is full:"),
_c('Unable to deliver mail.'),
_c("User's mailbox is full:\s*<(?P[^>]*)>")),
# Microsoft SMTPSVC
(_c('The email below could not be delivered to the following user:'),
_c('Old message:'),
_c('<(?P[^>]*)>')),
# 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[^>]*)>:')),
# googlemail.com
(_c('Delivery to the following recipient failed'),
_c('----- Original message -----'),
_c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')),
# kundenserver.de
(_c('A message that you sent could not be delivered'),
_c('^---'),
_c('<(?P[^>]*)>')),
# another kundenserver.de
(_c('A message that you sent could not be delivered'),
_c('^---'),
_c('^(?P[^\s@]+@[^\s@:]+):')),
# thehartford.com / songbird
(_c('Del(i|e)very to the following recipients (failed|was aborted)'),
# this one may or may not have the original message, but there's nothing
# unique to stop on, so stop on the first line of at least 3 characters
# that doesn't start with 'D' (to not stop immediately) and has no '@'.
# Also note that simple_30.txt contains an apparent misspelling in the
# MTA's DSN section.
_c('^[^D][^@]{2,}$'),
_c('^[\s*]*(?P[^\s@]+@[^\s@]+)\s*$')),
# and another thehartfod.com/hartfordlife.com
(_c('^Your message\s*$'),
_c('^because:'),
_c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')),
# kviv.be (InterScan NT)
(_c('^Unable to deliver message to'),
_c(r'\*+\s+End of message\s+\*+'),
_c('<(?P[^>]*)>')),
# earthlink.net supported domains
(_c('^Sorry, unable to deliver your message to'),
_c('^A copy of the original message'),
_c('\s*(?P[^\s@]+@[^\s@]+)\s+')),
# ademe.fr
(_c('^A message could not be delivered to:'),
_c('^Subject:'),
_c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')),
# andrew.ac.jp
(_c('^Invalid final delivery userid:'),
_c('^Original message follows.'),
_c('\s*(?P[^\s@]+@[^\s@]+)\s*$')),
# E500_SMTP_Mail_Service@lerctr.org
(_c('------ Failed Recipients ------'),
_c('-------- Returned Mail --------'),
_c('<(?P[^>]*)>')),
# cynergycom.net
(_c('A message that you sent could not be delivered'),
_c('^---'),
_c('(?P[^\s@]+@[^\s@)]+)')),
# LSMTP for Windows
(_c('^--> Error description:\s*$'),
_c('^Error-End:'),
_c('^Error-for:\s+(?P[^\s@]+@[^\s@]+)')),
# Qmail with a tri-language intro beginning in spanish
(_c('Your message could not be delivered'),
_c('^-'),
_c('<(?P[^>]*)>:')),
# socgen.com
(_c('Your message could not be delivered to'),
_c('^\s*$'),
_c('(?P[^\s@]+@[^\s@]+)')),
# dadoservice.it
(_c('Your message has encountered delivery problems'),
_c('Your message reads'),
_c('addressed to\s*(?P[^\s@]+@[^\s@)]+)')),
# gomaps.com
(_c('Did not reach the following recipient'),
_c('^\s*$'),
_c('\s(?P[^\s@]+@[^\s@]+)')),
# EYOU MTA SYSTEM
(_c('This is the deliver program at'),
_c('^-'),
_c('^(?P[^\s@]+@[^\s@<>]+)')),
# A non-standard qmail at ieo.it
(_c('this is the email server at'),
_c('^-'),
_c('\s(?P[^\s@]+@[^\s@]+)[\s,]')),
# pla.net.py (MDaemon.PRO ?)
(_c('- no such user here'),
_c('There is no user'),
_c('^(?P[^\s@]+@[^\s@]+)\s')),
# mxlogic.net
(_c('The following address failed:'),
_c('Included is a copy of the message header'),
_c('<(?P[^>]+)>')),
# fastdnsservers.com
(_c('The following recipient\(s\) could not be reached'),
_c('\s*Error Type'),
_c('^(?P[^\s@]+@[^\s@<>]+)')),
# xxx.com (simple_36.txt)
(_c('Could not deliver message to the following recipient'),
_c('\s*-- The header'),
_c('Failed Recipient: (?P[^\s@]+@[^\s@<>]+)')),
# mta1.service.uci.edu
(_c('Message not delivered to the following addresses'),
_c('Error detail'),
_c('\s*(?P[^\s@]+@[^\s@)]+)')),
# Dovecot LDA Over quota MDN (bogus - should be DSN).
(_c('^Your message'),
_c('^Reporting'),
_c('Your message to (?P[^\s<@]+@[^\s@>]+)>? was automatically'
' rejected')),
# mail.ru
(_c('A message that you sent was rejected'),
_c('This is a copy of your message'),
_c('\s(?P[^\s@]+@[^\s@]+)')),
# Next one goes here...
]
@implementer(IBounceDetector)
class SimpleMatch:
"""Recognizes simple heuristically delimited bounces."""
PATTERNS = PATTERNS
def process(self, msg):
"""See `IBounceDetector`."""
addresses = set()
# 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 self.PATTERNS:
state = ParseState.start
for line in body_line_iterator(msg):
if state is ParseState.start:
if scre.search(line):
state = ParseState.tag_seen
if state is ParseState.tag_seen:
mo = acre.search(line)
if mo:
address = mo.group('addr')
if address:
addresses.add(_quopri_decode(address))
elif ecre.search(line):
break
if len(addresses) > 0:
break
return NoTemporaryFailures, addresses
flufl.bounce-2.3/flufl/bounce/_detectors/smtp32.py 0000664 0001750 0001750 00000004544 12374411560 022454 0 ustar barry barry 0000000 0000000 # Copyright (C) 1998-2014 by Barry A. Warsaw
#
# This file is part of flufl.bounce
#
# flufl.bounce is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# flufl.bounce 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.bounce. If not, see .
"""Something which claims
X-Mailer:
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)
"""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'SMTP32',
]
import re
from email.iterators import body_line_iterator
from zope.interface import implementer
from flufl.bounce.interfaces import (
IBounceDetector, NoFailures, NoTemporaryFailures)
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
|delivery\ userid[^:]*:
)
\s* # space separator
(?P[^\s]*) # and finally, the address
''', re.IGNORECASE | re.VERBOSE)
@implementer(IBounceDetector)
class SMTP32:
"""Something which claims
X-Mailer:
"""
def process(self, msg):
mailer = msg.get('x-mailer', '')
if not mailer.startswith('