diff --git a/setup.py b/setup.py index 36e5ee5..c93d8f5 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ EXTRAS_REQUIRE = { 'test': TESTS_REQUIRE, - #':sys_platform == "win32"': ['pywin32'], + # ':sys_platform == "win32"': ['pywin32'], # Because https://sourceforge.net/p/pywin32/bugs/680/ ':sys_platform == "win32"': ['pypiwin32'], 'docs': [ @@ -50,55 +50,56 @@ def read(*rnames): read('CHANGES.rst') ) -setup(name='zope.sendmail', - version='4.2.2.dev0', - url='https://github.com/zopefoundation/zope.sendmail', - license='ZPL 2.1', - description='Zope sendmail', - author='Zope Foundation and Contributors', - author_email='zope-dev@zope.org', - long_description=LONG_DESCRIPTION, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Zope Public License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Operating System :: OS Independent', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Communications :: Email', - 'Framework :: Zope :: 3', - ], - packages=find_packages('src'), - package_dir={'': 'src'}, - namespace_packages=['zope'], - extras_require=EXTRAS_REQUIRE, - install_requires=[ - 'setuptools', - 'transaction', - 'zope.i18nmessageid', - 'zope.interface', - 'zope.schema', - # it's only needed for vocabulary, zcml and tests - 'zope.component>=3.8.0', - # these are only needed for zcml - 'zope.configuration', - ], - tests_require=TESTS_REQUIRE, - test_suite='zope.sendmail.tests', - include_package_data=True, - zip_safe=False, - entry_points=""" +setup( + name='zope.sendmail', + version='4.2.2.dev0', + url='https://github.com/zopefoundation/zope.sendmail', + license='ZPL 2.1', + description='Zope sendmail', + author='Zope Foundation and Contributors', + author_email='zope-dev@zope.org', + long_description=LONG_DESCRIPTION, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Zope Public License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Operating System :: OS Independent', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Communications :: Email', + 'Framework :: Zope :: 3', + ], + packages=find_packages('src'), + package_dir={'': 'src'}, + namespace_packages=['zope'], + extras_require=EXTRAS_REQUIRE, + install_requires=[ + 'setuptools', + 'transaction', + 'zope.i18nmessageid', + 'zope.interface', + 'zope.schema', + # it's only needed for vocabulary, zcml and tests + 'zope.component>=3.8.0', + # these are only needed for zcml + 'zope.configuration', + ], + tests_require=TESTS_REQUIRE, + test_suite='zope.sendmail.tests', + include_package_data=True, + zip_safe=False, + entry_points=""" [console_scripts] zope-sendmail = zope.sendmail.queue:run - """ + """ ) diff --git a/src/zope/__init__.py b/src/zope/__init__.py index 2cdb0e4..656dc0f 100644 --- a/src/zope/__init__.py +++ b/src/zope/__init__.py @@ -1 +1 @@ -__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover +__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover diff --git a/src/zope/sendmail/delivery.py b/src/zope/sendmail/delivery.py index 5f4d8ce..c7b7b7b 100644 --- a/src/zope/sendmail/delivery.py +++ b/src/zope/sendmail/delivery.py @@ -34,10 +34,12 @@ # BBB: this import is needed for backward compatibility with older versions of # zope.sendmail which defined QueueProcessorThread in this module -from zope.sendmail.queue import QueueProcessorThread +from zope.sendmail.queue import QueueProcessorThread # noqa: F401 + log = logging.getLogger("MailDataManager") + @implementer(IDataManager) class MailDataManager(object): @@ -80,7 +82,7 @@ def tpc_vote(self, txn): def tpc_finish(self, txn): try: self.callable(*self.args) - except Exception as e: + except Exception: # Any exceptions here can cause database corruption. # Better to protect the data and potentially miss emails than # leave a database in an inconsistent state which requires a diff --git a/src/zope/sendmail/interfaces.py b/src/zope/sendmail/interfaces.py index 4cfbf51..14fa1a6 100644 --- a/src/zope/sendmail/interfaces.py +++ b/src/zope/sendmail/interfaces.py @@ -112,7 +112,7 @@ class IMailQueueProcessor(Interface): pollingInterval = Int( title=_(u"Polling Interval"), description=_(u"How often the queue is checked for new messages" - " (in milliseconds)"), + u" (in milliseconds)"), default=5000) mailer = Attribute("IMailer that is used for message delivery") @@ -134,12 +134,12 @@ def send(fromaddr, toaddrs, message): Messages are sent immediately. """ - + def abort(): """Abort sending the message for asynchronous subclasses.""" - + def vote(fromaddr, toaddrs, message): - """Raise an exception if there is a known reason why the message + """Raise an exception if there is a known reason why the message cannot be sent.""" @@ -253,4 +253,3 @@ def abort(): Calling ``abort()`` more than once is allowed. """ - diff --git a/src/zope/sendmail/maildir.py b/src/zope/sendmail/maildir.py index 918378f..bd3d8c6 100644 --- a/src/zope/sendmail/maildir.py +++ b/src/zope/sendmail/maildir.py @@ -25,6 +25,7 @@ IMaildirFactory, IMaildir, IMaildirMessageWriter from zope.sendmail._compat import text_type + @provider(IMaildirFactory) @implementer(IMaildir) class Maildir(object): @@ -91,7 +92,8 @@ def newMessage(self): random.randrange(randmax)) filename = join(subdir_tmp, unique) try: - fd = os.open(filename, os.O_CREAT|os.O_EXCL|os.O_WRONLY, 0o600) + fd = os.open(filename, os.O_CREAT | os.O_EXCL | os.O_WRONLY, + 0o600) except OSError as e: if e.errno != errno.EEXIST: raise @@ -119,7 +121,6 @@ def _encode_utf8(s): class MaildirMessageWriter(object): """See :class:`zope.sendmail.interfaces.IMaildirMessageWriter`""" - def __init__(self, fd, filename, new_filename): self._filename = filename self._new_filename = new_filename diff --git a/src/zope/sendmail/mailer.py b/src/zope/sendmail/mailer.py index 6d14d34..36ed96d 100644 --- a/src/zope/sendmail/mailer.py +++ b/src/zope/sendmail/mailer.py @@ -31,6 +31,7 @@ class _SMTPState(local): code = None response = None + @implementer(ISMTPMailer) class SMTPMailer(object): """Implementation of :class:`zope.sendmail.interfaces.ISMTPMailer`.""" @@ -107,8 +108,8 @@ def send(self, fromaddr, toaddrs, message): password = password.encode('utf-8') connection.login(username, password) elif self.username: - raise RuntimeError('Mailhost does not support ESMTP but a username ' - 'is configured') + raise RuntimeError( + 'Mailhost does not support ESMTP but a username is configured') try: connection.sendmail(fromaddr, toaddrs, message) diff --git a/src/zope/sendmail/queue.py b/src/zope/sendmail/queue.py index 2f21a24..9d0f8c4 100644 --- a/src/zope/sendmail/queue.py +++ b/src/zope/sendmail/queue.py @@ -22,7 +22,6 @@ import argparse import os import smtplib -import stat import threading import time import errno @@ -31,9 +30,11 @@ from zope.sendmail.mailer import SMTPMailer import sys -if sys.platform == 'win32': # pragma: no cover +if sys.platform == 'win32': # pragma: no cover import win32file - _os_link = lambda src, dst: win32file.CreateHardLink(dst, src, None) + + def _os_link(src, dst): + return win32file.CreateHardLink(dst, src, None) else: _os_link = os.link @@ -56,9 +57,9 @@ # Any error conditions not depicted on the diagram will provoke the catch-all # exception logging of the ``run`` method. # -# In the diagram the "message file" is the file in the maildir's "cur" directory -# that contains the message and "tmp file" is a hard link to the message file -# created in the maildir's "tmp" directory. +# In the diagram the "message file" is the file in the maildir's "cur" +# directory that contains the message and "tmp file" is a hard link to the +# message file created in the maildir's "tmp" directory. # # ( start trying to deliver a message ) # | @@ -101,6 +102,7 @@ # V | # ( message delivered )<---------+ + class QueueProcessorThread(threading.Thread): """This thread is started at configuration time from the `mail:queuedDelivery` directive handler if processorThread is True. @@ -113,8 +115,8 @@ class QueueProcessorThread(threading.Thread): mailer = None def __init__(self, interval=3.0): - threading.Thread.__init__(self, - name="zope.sendmail.queue.QueueProcessorThread") + threading.Thread.__init__( + self, name="zope.sendmail.queue.QueueProcessorThread") self.interval = interval self._lock = threading.Lock() self.setDaemon(True) @@ -208,8 +210,9 @@ def _process_one_file(self, filename): # comment above this class # find the age of the tmp file (if it exists) - mtime = self._action_if_exists(tmp_filename, - lambda fname: os.stat(fname)[stat.ST_MTIME]) + mtime = self._action_if_exists( + tmp_filename, + lambda fname: os.stat(fname).st_mtime) age = time.time() - mtime if mtime is not None else None # if the tmp file exists, check its age @@ -222,12 +225,13 @@ def _process_one_file(self, filename): try: os.unlink(tmp_filename) except OSError as e: - if e.errno == errno.ENOENT: # file does not exist + if e.errno == errno.ENOENT: # file does not exist # it looks like someone else removed the tmp # file, that's fine, we'll try to deliver the # message again later return - # XXX: we're silently ignoring the exception here. Is that right? + # XXX: we're silently ignoring the exception here. + # Is that right? # If permissions or something are not right, we'll fail # on _os_link later on. # if we get here, the file existed, but was too @@ -247,7 +251,7 @@ def _process_one_file(self, filename): try: os.utime(filename, None) except OSError as e: - if e.errno == errno.ENOENT: # file does not exist + if e.errno == errno.ENOENT: # file does not exist # someone removed the message before we could # touch it, no need to complain, we'll just keep # going @@ -257,15 +261,14 @@ def _process_one_file(self, filename): # creating this hard link will fail if another process is # also sending this message try: - #os.link(filename, tmp_filename) _os_link(filename, tmp_filename) except OSError as e: - if e.errno == errno.EEXIST: # file exists, *nix + if e.errno == errno.EEXIST: # file exists, *nix # it looks like someone else is sending this # message too; we'll try again later return # XXX: Silently ignoring all other errno - except Exception as e: # pragma: no cover + except Exception as e: # pragma: no cover if e[0] == 183 and e[1] == 'CreateHardLink': # file exists, win32 return @@ -299,7 +302,6 @@ def _process_one_file(self, filename): "Discarding email from %s to %s due to" " a permanent error: %s", fromaddr, ", ".join(toaddrs), str(e)) - #os.link(filename, rejected_filename) _os_link(filename, rejected_filename) else: # Log an error and retry later @@ -330,7 +332,6 @@ def _process_one_file(self, filename): "Error while sending mail : %s ", filename, exc_info=True) - def stop(self): self._stopped = True self._lock.acquire() @@ -364,55 +365,67 @@ class ConsoleApp(object): ] parser = argparse.ArgumentParser() - parser.add_argument('--daemon', action='store_true', - help=("Run in daemon mode, periodically checking queue " - "and sending messages. Default is to send all " - "messages in queue once and exit.")) - parser.add_argument('--interval', metavar='<#secs>', type=float, default=3, - help=("How often to check queue when in daemon mode. " - "Default is %(default)s seconds.")) - smtp_group = parser.add_argument_group("SMTP Server", - "Connection information for the SMTP server") - smtp_group.add_argument('--hostname', default='localhost', - help=("Name of SMTP host to use for delivery. Default is " - "%(default)s.")) - smtp_group.add_argument('--port', type=int, default=25, - help=("Which port on SMTP server to deliver mail to. " - "Default is %(default)s.")) - - auth_group = parser.add_argument_group("Authentication", - ("Authentication information for the SMTP server. " - "If one is provided, they must both be. One or both " - "can be provided in the --config file.")) - auth_group.add_argument('--username', - help=("Username to use to log in to SMTP server. Default " - "is none.")) - auth_group.add_argument('--password', - help=("Password to use to log in to SMTP server. Must be " - "specified if username is specified.")) + parser.add_argument( + '--daemon', action='store_true', + help=("Run in daemon mode, periodically checking queue " + "and sending messages. Default is to send all " + "messages in queue once and exit.")) + parser.add_argument( + '--interval', metavar='<#secs>', type=float, default=3, + help=("How often to check queue when in daemon mode. " + "Default is %(default)s seconds.")) + smtp_group = parser.add_argument_group( + "SMTP Server", + "Connection information for the SMTP server") + smtp_group.add_argument( + '--hostname', default='localhost', + help=("Name of SMTP host to use for delivery. Default is " + "%(default)s.")) + smtp_group.add_argument( + '--port', type=int, default=25, + help=("Which port on SMTP server to deliver mail to. " + "Default is %(default)s.")) + + auth_group = parser.add_argument_group( + "Authentication", + ("Authentication information for the SMTP server. " + "If one is provided, they must both be. One or both " + "can be provided in the --config file.")) + auth_group.add_argument( + '--username', + help=("Username to use to log in to SMTP server. Default " + "is none.")) + auth_group.add_argument( + '--password', + help=("Password to use to log in to SMTP server. Must be " + "specified if username is specified.")) del auth_group tls_group = smtp_group.add_mutually_exclusive_group() - tls_group.add_argument('--force-tls', action='store_true', - help=("Do not connect if TLS is not available. Not " - "enabled by default.")) - tls_group.add_argument('--no-tls', action='store_true', - help=("Do not use TLS even if is available. Not enabled " - "by default.")) + tls_group.add_argument( + '--force-tls', action='store_true', + help=("Do not connect if TLS is not available. Not " + "enabled by default.")) + tls_group.add_argument( + '--no-tls', action='store_true', + help=("Do not use TLS even if is available. Not enabled " + "by default.")) del tls_group del smtp_group - parser.add_argument('--config', metavar='', - type=argparse.FileType(), - help=("Get configuration from specified ini file; it must " - "contain a section [%s] that can contain the " - "following keys: %s. If you specify the queue path " - "in the ini file, you don't need to specify it on " - "the command line. With the exception of the queue path, " - "options specified in the ini file override options on the " - "command line." % (INI_SECTION, ', '.join(INI_NAMES)))) - parser.add_argument("maildir", default=None, nargs="?", - help=("The path to the mail queue directory." - "If not given, it must be found in the --config file." - "If given, this overrides a value in the --config file")) + parser.add_argument( + '--config', metavar='', + type=argparse.FileType(), + help=("Get configuration from specified ini file; it must " + "contain a section [%s] that can contain the " + "following keys: %s. If you specify the queue path " + "in the ini file, you don't need to specify it on " + "the command line. With the exception of the queue path, " + "options specified in the ini file override options on the " + "command line." % (INI_SECTION, ', '.join(INI_NAMES)))) + parser.add_argument( + "maildir", default=None, nargs="?", + help=("The path to the mail queue directory." + "If not given, it must be found in the --config file." + "If given, this overrides a value in the --config file")) daemon = False interval = 3 @@ -432,8 +445,9 @@ def __init__(self, argv=None, verbose=True): self.script_name = argv[0] self.verbose = verbose self._process_args(argv[1:]) - self.mailer = self.MailerKind(self.hostname, self.port, self.username, - self.password, self.no_tls, self.force_tls) + self.mailer = self.MailerKind( + self.hostname, self.port, self.username, self.password, + self.no_tls, self.force_tls) def main(self): queue = self.QueueProcessorKind(self.interval) @@ -479,6 +493,7 @@ def _load_config(self, path): self.no_tls = boolean(config.get(section, "no_tls")) self.queue_path = string_or_none(config.get(section, "queue_path")) + def run(argv=None): logging.basicConfig() app = ConsoleApp(argv) diff --git a/src/zope/sendmail/tests/test_delivery.py b/src/zope/sendmail/tests/test_delivery.py index b94d276..80c2da2 100644 --- a/src/zope/sendmail/tests/test_delivery.py +++ b/src/zope/sendmail/tests/test_delivery.py @@ -31,6 +31,7 @@ from zope.sendmail.delivery import AbstractMailDelivery from zope.sendmail.delivery import DirectMailDelivery + @implementer(IMailer) class MailerStub(object): @@ -54,13 +55,14 @@ def testInterface(self): self.assertEqual(manager.callable, object) self.assertEqual(manager.args, (1, 2)) # required by IDataManager - self.assertTrue(isinstance(manager.sortKey(), str)) + self.assertIsInstance(manager.sortKey(), str) def test_successful_commit(self): # Regression test for http://www.zope.org/Collectors/Zope3-dev/590 from zope.sendmail.delivery import MailDataManager _success = [] + def _on_success(*args): _success.append(args) @@ -76,7 +78,6 @@ def _on_abort(*args): manager.tpc_finish(xact) self.assertEqual(_success, [('foo', 'bar')]) - def test_unsuccessful_commit(self): # Regression test for http://www.zope.org/Collectors/Zope3-dev/590 from zope.sendmail.delivery import MailDataManager @@ -101,6 +102,11 @@ def _on_abort(*args): class TestAbstractMailDelivery(unittest.TestCase): + # Avoid DeprecationWarning for assertRaisesRegexp on Python 3 while + # coping with Python 2 not having the Regex spelling variant + assertRaisesRegex = getattr(unittest.TestCase, 'assertRaisesRegex', + unittest.TestCase.assertRaisesRegexp) + def test_bad_message_id(self): class Parser(object): def parsestr(self, s): @@ -112,8 +118,8 @@ def parsestr(self, s): email.parser.Parser = Parser delivery = AbstractMailDelivery() - with self.assertRaisesRegexp(ValueError, - "Malformed Message-Id header"): + with self.assertRaisesRegex(ValueError, + "Malformed Message-Id header"): delivery.send(None, None, None) @@ -148,7 +154,7 @@ def testSend(self): mailer.sent_messages = [] msgid = delivery.send(fromaddr, toaddrs, message) - self.assertTrue('@' in msgid) + self.assertIn('@', msgid) self.assertEqual(mailer.sent_messages, []) transaction.commit() self.assertEqual(len(mailer.sent_messages), 1) @@ -156,7 +162,7 @@ def testSend(self): self.assertEqual(mailer.sent_messages[0][1], toaddrs) self.assertTrue(mailer.sent_messages[0][2].endswith(message)) new_headers = mailer.sent_messages[0][2][:-len(message)] - self.assertTrue(new_headers.find('Message-Id: <%s>' % msgid) != -1) + self.assertIn('Message-Id: <%s>' % msgid, new_headers) mailer.sent_messages = [] msgid = delivery.send(fromaddr, toaddrs, opt_headers + message) @@ -226,7 +232,6 @@ def send(self): self.assertIn("does not provide a vote method", str(w[0])) - class MaildirWriterStub(object): data = '' @@ -278,6 +283,7 @@ def newMessage(self): self.msgs.append(m) return m + class WritableMaildirStub(MaildirStub): STUB_DEFAULT_MESSAGE_LINES = ( @@ -299,7 +305,8 @@ def __init__(self, test, *args, **kwargs): self.stub_directory = tempfile.mkdtemp(suffix=".test_maildir") test.addCleanup(shutil.rmtree, self.stub_directory) - def stub_createFile(self, filename="message", lines=STUB_DEFAULT_MESSAGE_LINES): + def stub_createFile(self, filename="message", + lines=STUB_DEFAULT_MESSAGE_LINES): """ Create a new file in the temporary directory. @@ -387,6 +394,7 @@ def send(self, fromaddr, toaddrs, message): abort = None + @implementer(IMailer) class SMTPResponseExceptionMailerStub(object): @@ -444,12 +452,14 @@ def testSend(self): MaildirWriterStub.commited_messages = [] msgid = delivery.send(fromaddr, toaddrs, message) - self.assertTrue('@' in msgid) + self.assertIn('@', msgid) self.assertEqual(MaildirWriterStub.commited_messages, []) self.assertEqual(MaildirWriterStub.aborted_messages, []) transaction.commit() self.assertEqual(len(MaildirWriterStub.commited_messages), 1) - self.assertTrue(MaildirWriterStub.commited_messages[0].endswith(message)) + self.assertTrue( + MaildirWriterStub.commited_messages[0].endswith(message) + ) new_headers = MaildirWriterStub.commited_messages[0][:-len(message)] self.assertIn('Message-Id: <%s>' % msgid, new_headers) self.assertIn('X-Zope-From: %s' % fromaddr, new_headers) @@ -463,7 +473,3 @@ def testSend(self): transaction.abort() self.assertEqual(MaildirWriterStub.commited_messages, []) self.assertEqual(len(MaildirWriterStub.aborted_messages), 1) - - -def test_suite(): - return unittest.defaultTestLoader.loadTestsFromName(__name__) diff --git a/src/zope/sendmail/tests/test_directives.py b/src/zope/sendmail/tests/test_directives.py index f0e6de8..c3e0b2d 100644 --- a/src/zope/sendmail/tests/test_directives.py +++ b/src/zope/sendmail/tests/test_directives.py @@ -34,10 +34,12 @@ class MaildirStub(object): pass + @implementer(IMailer) class Mailer(object): pass + class MockQueueProcessorThread(object): def setMailer(self, mailer): @@ -111,10 +113,3 @@ def test_zcml_without_registered_smtp_mailer(self): def test_zcml_without_registered_mailer(self): self._check_zcml_without_registration(self.testMailer, 'test.mailer') - - -def test_suite(): - return unittest.defaultTestLoader.loadTestsFromName(__name__) - -if __name__ == '__main__': - unittest.main() diff --git a/src/zope/sendmail/tests/test_maildir.py b/src/zope/sendmail/tests/test_maildir.py index 11cc419..606abd4 100644 --- a/src/zope/sendmail/tests/test_maildir.py +++ b/src/zope/sendmail/tests/test_maildir.py @@ -25,11 +25,13 @@ from zope.sendmail.maildir import Maildir from zope.sendmail.interfaces import IMaildirMessageWriter + class FakeSocketModule(object): def gethostname(self): return 'myhostname' + class FakeTimeModule(object): _timer = 1234500000 @@ -40,6 +42,7 @@ def time(self): def sleep(self, n): self._timer += n + class FakeOsPathModule(object): def __init__(self, files, dirs): @@ -81,7 +84,7 @@ class FakeOsModule(object): '/path/to/maildir/tmp/1234500000.4242.myhostname.*': stat.S_IFREG, '/path/to/maildir/tmp/1234500001.4242.myhostname.*': stat.S_IFREG, '/path/to/regularfile': stat.S_IFREG, - '/path/to/emptydirectory': stat.S_IFDIR, + '/path/to/emptydir': stat.S_IFDIR, } _listdir = { '/path/to/maildir/new': ['1', '2', '.svn'], @@ -129,10 +132,10 @@ def rename(self, old, new): def open(self, filename, flags, mode=0o777): if (flags & os.O_EXCL and flags & os.O_CREAT - and self.access(filename, 0)): + and self.access(filename, 0)): raise OSError(errno.EEXIST, 'file already exists') if not flags & os.O_CREAT and not self.access(filename, 0): - raise OSError('file not found') # pragma: no cover + raise OSError('file not found') # pragma: no cover fd = max(list(self._descriptors.keys()) + [2]) + 1 self._descriptors[fd] = filename, flags, mode return fd @@ -140,7 +143,7 @@ def open(self, filename, flags, mode=0o777): def fdopen(self, fd, mode='r'): try: filename, flags, _permissions = self._descriptors[fd] - except KeyError: # pragma: no cover + except KeyError: # pragma: no cover raise AssertionError('os.fdopen() called with an unknown' ' file descriptor') assert mode == 'w' @@ -210,8 +213,8 @@ def test_factory(self): self.assertRaises(ValueError, Maildir, '/path/to/regularfile', True) # Case 4: it is a directory, but not a maildir - self.assertRaises(ValueError, Maildir, '/path/to/emptydirectory', False) - self.assertRaises(ValueError, Maildir, '/path/to/emptydirectory', True) + self.assertRaises(ValueError, Maildir, '/path/to/emptydir', False) + self.assertRaises(ValueError, Maildir, '/path/to/emptydir', True) def test_iteration(self): m = Maildir('/path/to/maildir') @@ -230,6 +233,7 @@ def test_newMessage(self): def test_newMessage_error(self): m = Maildir('/path/to/maildir') + def open(*args): raise OSError(errno.EADDRINUSE, "") self.fake_os_module.open = open @@ -303,10 +307,3 @@ def test_message_writer_unicode(self): b'fe\xc3\xa8 fi\xc3\xa8 fo\xc3\xa8 fo\xc3\xb2') writer.close() self.assertTrue(writer._fd._closed) - - -def test_suite(): - return unittest.defaultTestLoader.loadTestsFromName(__name__) - -if __name__ == '__main__': - unittest.main() diff --git a/src/zope/sendmail/tests/test_mailer.py b/src/zope/sendmail/tests/test_mailer.py index 891d70d..5154772 100644 --- a/src/zope/sendmail/tests/test_mailer.py +++ b/src/zope/sendmail/tests/test_mailer.py @@ -26,6 +26,7 @@ # This works for both, Python 2 and 3. port_types = (int, str) + class SMTP(object): fail_on_quit = False @@ -85,12 +86,16 @@ class TestSMTPMailer(unittest.TestCase): SMTPClass = SMTP + # Avoid DeprecationWarning for assertRaisesRegexp on Python 3 while + # coping with Python 2 not having the Regex spelling variant + assertRaisesRegex = getattr(unittest.TestCase, 'assertRaisesRegex', + unittest.TestCase.assertRaisesRegexp) + def _makeSMTP(self, h, p): self.smtp = self.SMTPClass(h, p) self.smtp_hook(self.smtp) return self.smtp - def setUp(self, port=None): self.smtp = None if port is None: @@ -122,8 +127,10 @@ def test_send_multiple_same_mailer(self): # The mailer re-opens itself as needed when sending # multiple mails. smtps = [] + def hook(smtp): smtps.append(smtp) + self.smtp_hook = hook for run in (1, 2): fromaddr = 'me@example.com' + str(run) @@ -138,15 +145,15 @@ def hook(smtp): self.assertEqual(2, len(smtps)) - def test_send_multiple_threads(self): import threading results = [] + def run(): try: self.test_send_multiple_same_mailer() - except BaseException as e: # pragma: no cover + except BaseException as e: # pragma: no cover results.append(e) raise else: @@ -186,8 +193,8 @@ def test_send_auth_unicode(self): fromaddr = 'me@example.com' toaddrs = ('you@example.com', 'him@example.com') msgtext = 'Headers: headers\n\nbodybodybody\n-- \nsig\n' - self.mailer.username = u'f\u00f8\u00f8' # double o slash - self.mailer.password = u'\u00e9vil' # e acute + self.mailer.username = u'f\u00f8\u00f8' # double o slash + self.mailer.password = u'\u00e9vil' # e acute self.mailer.hostname = 'spamrelay' self.mailer.port = 31337 self.mailer.send(fromaddr, toaddrs, msgtext) @@ -198,8 +205,8 @@ def test_send_auth_nonascii(self): fromaddr = 'me@example.com' toaddrs = ('you@example.com', 'him@example.com') msgtext = 'Headers: headers\n\nbodybodybody\n-- \nsig\n' - self.mailer.username = b'f\xc3\xb8\xc3\xb8' # double o slash - self.mailer.password = b'\xc3\xa9vil' # e acute + self.mailer.username = b'f\xc3\xb8\xc3\xb8' # double o slash + self.mailer.password = b'\xc3\xa9vil' # e acute self.mailer.hostname = 'spamrelay' self.mailer.port = 31337 self.mailer.send(fromaddr, toaddrs, msgtext) @@ -220,7 +227,6 @@ def test_send_failQuit(self): self.assertTrue(not self.smtp.quitted) self.assertTrue(self.smtp.closed) - def test_vote_bad_connection(self): def hook(smtp): @@ -228,8 +234,8 @@ def hook(smtp): smtp.helo = lambda: (100, "Nope") self.smtp_hook = hook - with self.assertRaisesRegexp(RuntimeError, - "Error sending HELO to the SMTP server"): + with self.assertRaisesRegex(RuntimeError, + "Error sending HELO to the SMTP server"): self.mailer.vote(None, None, None) def test_abort_no_conn(self): @@ -238,8 +244,10 @@ def test_abort_no_conn(self): def test_abort_fails_call_close(self): class Conn(object): closed = False + def quit(self): raise SSLError() + def close(self): self.closed = True @@ -258,20 +266,22 @@ def has_extn(self, name): self.mailer.force_tls = True self.mailer.connection = Conn() - with self.assertRaisesRegexp(RuntimeError, - 'TLS is not available'): + with self.assertRaisesRegex(RuntimeError, + 'TLS is not available'): self.mailer.send(None, None, None) def test_send_no_esmtp_with_username(self): class Conn(object): does_esmtp = False + def has_extn(self, *args): return False self.mailer.connection = Conn() self.mailer.username = 'user' - with self.assertRaisesRegexp(RuntimeError, - "Mailhost does not support ESMTP but a username"): + with self.assertRaisesRegex( + RuntimeError, + "Mailhost does not support ESMTP but a username"): self.mailer.send(None, None, None) @@ -280,10 +290,8 @@ class TestSMTPMailerWithNoEHLO(TestSMTPMailer): SMTPClass = SMTPWithNoEHLO def test_send_auth(self): - self.skipTest("This test requires ESMTP, which we're intentionally not enabling") + self.skipTest( + "This test requires ESMTP, which we're intentionally not enabling") test_send_auth_unicode = test_send_auth test_send_auth_nonascii = test_send_auth - -def test_suite(): - return unittest.defaultTestLoader.loadTestsFromName(__name__) diff --git a/src/zope/sendmail/tests/test_queue.py b/src/zope/sendmail/tests/test_queue.py index 9baac58..a1a1aaa 100644 --- a/src/zope/sendmail/tests/test_queue.py +++ b/src/zope/sendmail/tests/test_queue.py @@ -42,6 +42,7 @@ class QPTesting(queue.QueueProcessorThread): def _makeMaildir(self, path): return WritableMaildirStub(self.test, path) + class SMTPRecipientsRefusedMailerStub(object): def __init__(self, recipients): @@ -51,6 +52,7 @@ def send(self, fromaddr, toaddrs, message): import smtplib raise smtplib.SMTPRecipientsRefused(self.recipients) + @contextmanager def patched(module, attr, func): orig = getattr(module, attr) @@ -60,6 +62,7 @@ def patched(module, attr, func): finally: setattr(module, attr, orig) + class TestQueueProcessorThread(unittest.TestCase): def setUp(self): @@ -76,10 +79,12 @@ def setUp(self): def _assertEmptyErrorLog(self): self.assertEqual(self.thread.log.errors, []) - def _assertErrorLog(self, - From=WritableMaildirStub.STUB_DEFAULT_MESSAGE_SENT[0], - to=", ".join(WritableMaildirStub.STUB_DEFAULT_MESSAGE_SENT[1]), - exception_kind=None): + def _assertErrorLog( + self, + From=WritableMaildirStub.STUB_DEFAULT_MESSAGE_SENT[0], + to=", ".join(WritableMaildirStub.STUB_DEFAULT_MESSAGE_SENT[1]), + exception_kind=None, + ): expected = ('Error while sending mail from %s to %s.', (From, to,), {'exc_info': True},) @@ -104,8 +109,9 @@ def _assertGenericErrorLog(self, filename="message", def _assertTmpMessagePathDoesNotExist(self, filename="message"): full_path = self.md.stub_getTmpFilename(filename) - self.assertFalse(os.path.exists(full_path), - "The temporary path '%s' should not exist" % full_path) + self.assertFalse( + os.path.exists(full_path), + "The temporary path '%s' should not exist" % full_path) def _assertTmpMessagePathExists(self, filename="message"): full_path = self.md.stub_getTmpFilename(filename) @@ -128,7 +134,8 @@ def _assertMessagePathDoesNotExist(self, filename="message"): "The path '%s' should not exist" % full_path) def test_makeMaildir_creates(self): - md = queue.QueueProcessorThread()._makeMaildir(os.path.join(self.dir, 'testing')) + md = queue.QueueProcessorThread()._makeMaildir( + os.path.join(self.dir, 'testing')) self.assertTrue(os.path.exists(md.path)) def test_threadName(self): @@ -220,8 +227,10 @@ def test_smtp_recipients_refused(self): def test_stop_while_running(self): test = self + class Maildir(object): count = 0 + def __iter__(self): return self @@ -232,7 +241,7 @@ def __next__(self): return raise AssertionError("Should have stopped") - next = __next__ # Python 2 + next = __next__ # Python 2 self.thread.setMaildir(Maildir()) self.thread.run() @@ -254,6 +263,7 @@ def test_tmpfile_cannot_stat(self): tmp_file = self.md.stub_createTmpFile() err = OSError(tmp_file) + def stat(fname): # Note that this interferes with debuggers self.assertEqual(fname, tmp_file) @@ -269,7 +279,6 @@ def stat(fname): # And we logged the random error self._assertGenericErrorLog(exception=err) - def test_tmpfile_too_old(self): self.md.stub_createFile() self.md.stub_createTmpFile() @@ -339,9 +348,9 @@ def utime(fname, atm): self._assertMessagePathExists() self._assertEmptyErrorLog() - def test_run_forever(self): import time + class DoneSleeping(Exception): pass @@ -354,8 +363,6 @@ def sleep(i): self.thread.run() - - test_ini = """[app:zope-sendmail] interval = 33 hostname = testhost @@ -367,6 +374,7 @@ def sleep(i): queue_path = hammer/dont/hurt/em """ + class TestConsoleApp(unittest.TestCase): def setUp(self): from zope.sendmail.delivery import QueuedMailDelivery @@ -386,7 +394,7 @@ def tearDown(self): def _make_one(self, cmdline): cmdline = cmdline.split() if isinstance(cmdline, str) else cmdline with patched(sys, 'stdout', self.stdout), \ - patched(sys, 'stderr', self.stderr): + patched(sys, 'stderr', self.stderr): return ConsoleApp(cmdline, verbose=False) def _get_output(self): @@ -418,9 +426,11 @@ def test_args_processing_no_queue_path(self): def test_args_processing_almost_all_options(self): # use (almost) all of the options - cmdline = "zope-sendmail --daemon --interval 7 --hostname foo --port 75 " \ - "--username chris --password rossi --force-tls " \ + cmdline = ( + "zope-sendmail --daemon --interval 7 --hostname foo --port 75 " + "--username chris --password rossi --force-tls " "%s" % self.dir + ) app = self._make_one(cmdline) self.assertEqual("zope-sendmail", app.script_name) self.assertEqual(self.dir, app.queue_path) @@ -447,8 +457,8 @@ def test_args_processing_username_without_password(self): with self.assertRaises(SystemExit): self._make_one(cmdline) - self.assertIn('Must use username and password together', self._get_output()) - + self.assertIn('Must use username and password together', + self._get_output()) def test_args_processing_force_tls_and_no_tls(self): # test force_tls and no_tls @@ -457,7 +467,8 @@ def test_args_processing_force_tls_and_no_tls(self): with self.assertRaises(SystemExit): self._make_one(cmdline) - self.assertIn('--no-tls: not allowed with argument', self._get_output()) + self.assertIn('--no-tls: not allowed with argument', + self._get_output()) def test_ini_parse(self): ini_path = os.path.join(self.dir, "zope-sendmail.ini") @@ -504,8 +515,8 @@ def test_run(self): cmdline = ['sendmail', self.dir] with patched(QPTesting, 'test', self), \ - patched(ConsoleApp, 'QueueProcessorKind', QPTesting), \ - patched(ConsoleApp, 'MailerKind', MailerStub): + patched(ConsoleApp, 'QueueProcessorKind', QPTesting), \ + patched(ConsoleApp, 'MailerKind', MailerStub): queue.run(cmdline) def test_help(self): @@ -516,6 +527,3 @@ def test_help(self): self.assertIn('usage', self._get_output()) self.assertEqual(exc.exception.code, 0) - -def test_suite(): - return unittest.defaultTestLoader.loadTestsFromName(__name__) diff --git a/src/zope/sendmail/tests/test_vocabulary.py b/src/zope/sendmail/tests/test_vocabulary.py index f6e9d03..b5e4742 100644 --- a/src/zope/sendmail/tests/test_vocabulary.py +++ b/src/zope/sendmail/tests/test_vocabulary.py @@ -16,10 +16,11 @@ import doctest import unittest + from zope.component.testing import PlacelessSetup -class MailDeliveryNamesTests(PlacelessSetup, - unittest.TestCase): + +class MailDeliveryNamesTests(PlacelessSetup, unittest.TestCase): _marker = object() diff --git a/src/zope/sendmail/vocabulary.py b/src/zope/sendmail/vocabulary.py index ff99b90..a1a1f94 100644 --- a/src/zope/sendmail/vocabulary.py +++ b/src/zope/sendmail/vocabulary.py @@ -21,6 +21,7 @@ from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm from zope.sendmail.interfaces import IMailDelivery + def MailDeliveryNames(context=None): """Vocabulary with names of mail delivery utilities @@ -52,4 +53,5 @@ def MailDeliveryNames(context=None): terms = [SimpleTerm(name) for name, util in utils] return SimpleVocabulary(terms) + directlyProvides(MailDeliveryNames, IVocabularyFactory) diff --git a/src/zope/sendmail/zcml.py b/src/zope/sendmail/zcml.py index c11275f..dd601b4 100644 --- a/src/zope/sendmail/zcml.py +++ b/src/zope/sendmail/zcml.py @@ -30,15 +30,17 @@ try: from zope.component.security import proxify from zope.security.zcml import Permission -except ImportError: # pragma: no cover +except ImportError: # pragma: no cover SECURITY_SUPPORT = False Permission = TextLine def _assertPermission(permission, interfaces, component): - raise ConfigurationError("security proxied components are not " - "supported because zope.security is not available") + raise ConfigurationError( + "security proxied components are not " + "supported because zope.security is not available") else: SECURITY_SUPPORT = True + def _assertPermission(permission, interfaces, component): return proxify(component, provides=interfaces, permission=permission) @@ -49,7 +51,7 @@ class IDeliveryDirective(Interface): name = TextLine( title=u"Name", - description=u'Specifies the Delivery name of the mail utility. '\ + description=u'Specifies the Delivery name of the mail utility. ' u'The default is "Mail".', default=u"Mail", required=False) @@ -81,6 +83,7 @@ class IQueuedDeliveryDirective(IDeliveryDirective): required=False, default=True) + def _get_mailer(mailer): try: return getUtility(IMailer, mailer) @@ -111,10 +114,12 @@ def createQueuedDelivery(): callable=createQueuedDelivery, args=()) + class IDirectDeliveryDirective(IDeliveryDirective): """This directive creates and registers a global direct mail utility. It should be only called once during startup.""" + def directDelivery(_context, mailer, permission=None, name="Mail"): def createDirectDelivery(): @@ -131,6 +136,7 @@ def createDirectDelivery(): callable=createDirectDelivery, args=()) + class IMailerDirective(Interface): """A generic directive registering a mailer for the mail utility.""" @@ -165,6 +171,7 @@ class ISMTPMailerDirective(IMailerDirective): description=u"A password for SMTP AUTH.", required=False) + def smtpMailer(_context, name, hostname="localhost", port="25", username=None, password=None): _context.action(