[Checkins] SVN: zope.sendmail/branches/grantma-retryfixes/ -
Restructured SMTP mailer and QueueProcessorThread so that all
Matthew Grant
grantma at anathoth.gen.nz
Sun Mar 9 03:59:46 EDT 2008
Log message for revision 84551:
- Restructured SMTP mailer and QueueProcessorThread so that all
SMTP error logic is in the mailer. Clears the way for another
mailer for /usr/sbin/sendmail command line can be used with
QueueProcessorThread.
- Added ability for QueueProcessorThread so that it can handle temporary
failures in delivery to its smart host - ie administrator reconfiguring
mailserver, mail server reboot/restart
- Formatted log messages in a consistent fashion so that they can be grepped
out of z3.log
- Added maildir message filename to log messages as message id - allows
easy analysis/triage of mail message sending problems
- Added cleaning of lock links to QueueProcessorThread so that messages can be
sent immediately on Zope3 restart.
- Added pollingInterval (ms), cleanLockLinks (boolean), and retryInterval
(seconds) configure options to configure.zcml.
Changed:
U zope.sendmail/branches/grantma-retryfixes/CHANGES.txt
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/configure.zcml
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/delivery.py
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/interfaces.py
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/maildir.py
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/mailer.py
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/mail.zcml
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_delivery.py
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_maildir.py
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_mailer.py
U zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/zcml.py
-=-
Modified: zope.sendmail/branches/grantma-retryfixes/CHANGES.txt
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/CHANGES.txt 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/CHANGES.txt 2008-03-09 07:59:46 UTC (rev 84551)
@@ -1,6 +1,21 @@
Change history
~~~~~~~~~~~~~~
+- Restructured SMTP mailer and QueueProcessorThread so that all
+ SMTP error logic is in the mailer. Clears the way for another
+ mailer for /usr/sbin/sendmail command line can be used with
+ QueueProcessorThread.
+- Added ability for QueueProcessorThread so that it can handle temporary
+ failures in delivery to its smart host - ie administrator reconfiguring
+ mailserver, mail server reboot/restart
+- Formatted log messages in a consistent fashion so that they can be grepped
+ out of z3.log
+- Added maildir message filename to log messages as message id - allows
+ easy analysis/triage of mail message sending problems
+- Added cleaning of lock links to QueueProcessorThread so that messages can be
+ sent immediately on Zope3 restart.
+- Added pollingInterval (ms), cleanLockLinks (boolean), and retryInterval
+ (seconds) configure options to configure.zcml.
3.5.0b1 (unreleased)
--------------------
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/configure.zcml
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/configure.zcml 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/configure.zcml 2008-03-09 07:59:46 UTC (rev 84551)
@@ -14,11 +14,18 @@
<!--
To send mail, uncomment the following directive and be sure to
- create the queue directory.
+ create the queue directory.
+
+ Other parameters sepcify the polling interval, and the retry
+ interval used when a temporary failure is detected. Lock links
+ can also be cleared on server start.
<mail:queuedDelivery permission="zope.SendMail"
queuePath="./queue"
- mailer="smtp" />
+ retryInterval="300"
+ pollingInterval="3000"
+ cleanLockLinks="False"
+ mailer="smtp" />
-->
<interface interface="zope.sendmail.interfaces.IMailDelivery" />
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/delivery.py
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/delivery.py 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/delivery.py 2008-03-09 07:59:46 UTC (rev 84551)
@@ -24,7 +24,6 @@
import os
import os.path
import rfc822
-import smtplib
import stat
import threading
import time
@@ -33,8 +32,12 @@
from time import strftime
from socket import gethostname
-from zope.interface import implements
+from zope.interface import implements, providedBy
+from zope.interface.exceptions import DoesNotImplement
from zope.sendmail.interfaces import IDirectMailDelivery, IQueuedMailDelivery
+from zope.sendmail.interfaces import ISMTPMailer, IMailer
+from zope.sendmail.interfaces import MailerTemporaryFailureException
+from zope.sendmail.interfaces import MailerPermanentFailureException
from zope.sendmail.maildir import Maildir
from transaction.interfaces import IDataManager
import transaction
@@ -46,6 +49,15 @@
# messages sent.
MAX_SEND_TIME = 60*60*3
+# Prefixes for messages being processed in the queue
+# This one is the lock link prefix for when a message
+# is being sent - also edit this in maildir.py if changed...
+SENDING_MSG_LOCK_PREFIX = '.sending-'
+# This is the rejected message prefix
+REJECTED_MSG_PREFIX = '.rejected-'
+
+
+
class MailDataManager(object):
implements(IDataManager)
@@ -124,7 +136,7 @@
def createDataManager(self, fromaddr, toaddrs, message):
return MailDataManager(self.mailer.send,
- args=(fromaddr, toaddrs, message))
+ args=(fromaddr, toaddrs, message, 'direct_delivery'))
class QueuedMailDelivery(AbstractMailDelivery):
@@ -209,9 +221,15 @@
__stopped = False
interval = 3.0 # process queue every X second
- def __init__(self, interval=3.0):
+ def __init__(self,
+ interval=3.0,
+ retry_interval=300.0,
+ clean_lock_links=False):
threading.Thread.__init__(self)
self.interval = interval
+ self.retry_interval = retry_interval
+ self.clean_lock_links = clean_lock_links
+ self.test_results = {}
def setMaildir(self, maildir):
"""Set the maildir.
@@ -223,6 +241,8 @@
self.maildir = Maildir(path, True)
def setMailer(self, mailer):
+ if not(IMailer.providedBy(mailer)):
+ raise (DoesNotImplement)
self.mailer = mailer
def _parseMessage(self, message):
@@ -252,8 +272,45 @@
return fromaddr, toaddrs, rest
+ def _unlinkFile(self, filename):
+ """Unlink the message file """
+ try:
+ os.unlink(filename)
+ except OSError, e:
+ if e.errno == 2: # file does not exist
+ # someone else unlinked the file; oh well
+ pass
+ else:
+ # something bad happend, log it
+ raise
+
+ def _queueRetryWait(self, tmp_filename, forever):
+ """Implements Retry Wait if there is an SMTP Connection
+ Failure or error 4xx due to machine load etc
+ """
+ # Clean up by unlinking lock link
+ self._unlinkFile(tmp_filename)
+ # Wait specified retry interval in stages of self.interval
+ count = self.retry_interval
+ while(count > 0 and not self.__stopped):
+ if forever:
+ time.sleep(self.interval)
+ count -= self.interval
+ # Plug for test routines so that we know we got here
+ if not forever:
+ self.test_results['_queueRetryWait'] \
+ = "Retry timeout: %s count: %s" \
+ % (str(self.retry_interval), str(count))
+
+
def run(self, forever=True):
atexit.register(self.stop)
+ # Clean .sending- lock files from queue
+ if self.clean_lock_links:
+ self.maildir._cleanLockLinks()
+ # Set up logger for mailer
+ if hasattr(self.mailer, 'set_logger'):
+ self.mailer.set_logger(self.log)
while not self.__stopped:
for filename in self.maildir:
# if we are asked to stop while sending messages, do so
@@ -263,8 +320,9 @@
fromaddr = ''
toaddrs = ()
head, tail = os.path.split(filename)
- tmp_filename = os.path.join(head, '.sending-' + tail)
- rejected_filename = os.path.join(head, '.rejected-' + tail)
+ tmp_filename = os.path.join(head, SENDING_MSG_LOCK_PREFIX + tail)
+ rejected_filename = os.path.join(head, REJECTED_MSG_PREFIX + tail)
+ message_id = os.path.basename(filename)
try:
# perform a series of operations in an attempt to ensure
# that no two threads/processes send this message
@@ -339,53 +397,46 @@
file.close()
fromaddr, toaddrs, message = self._parseMessage(message)
try:
- self.mailer.send(fromaddr, toaddrs, message)
- except smtplib.SMTPResponseException, e:
- if 500 <= e.smtp_code <= 599:
- # permanent error, ditch the message
- self.log.error(
- "Discarding email from %s to %s due to"
- " a permanent error: %s",
- fromaddr, ", ".join(toaddrs), str(e))
- os.link(filename, rejected_filename)
- else:
- # Log an error and retry later
- raise
+ sentaddrs = self.mailer.send(fromaddr,
+ toaddrs,
+ message,
+ message_id)
+ except MailerTemporaryFailureException, e:
+ self._queueRetryWait(tmp_filename, forever)
+ # We break as we don't want to send message later
+ break;
+ except MailerPermanentFailureException, e:
+ os.link(filename, rejected_filename)
+ sentaddrs = []
- try:
- os.unlink(filename)
- except OSError, e:
- if e.errno == 2: # file does not exist
- # someone else unlinked the file; oh well
- pass
- else:
- # something bad happend, log it
- raise
+ # Unlink message file
+ self._unlinkFile(filename)
- try:
- os.unlink(tmp_filename)
- except OSError, e:
- if e.errno == 2: # file does not exist
- # someone else unlinked the file; oh well
- pass
- else:
- # something bad happend, log it
- raise
+ # Unlink the lock file
+ self._unlinkFile(tmp_filename)
# TODO: maybe log the Message-Id of the message sent
- self.log.info("Mail from %s to %s sent.",
- fromaddr, ", ".join(toaddrs))
+ if len(sentaddrs) > 0:
+ self.log.info("%s - mail sent, Sender: %s, Rcpt: %s,",
+ message_id,
+ fromaddr,
+ ", ".join(sentaddrs))
# Blanket except because we don't want
# this thread to ever die
except:
if fromaddr != '' or toaddrs != ():
self.log.error(
- "Error while sending mail from %s to %s.",
- fromaddr, ", ".join(toaddrs), exc_info=True)
+ "%s - Error while sending mail, Sender: %s,"
+ " Rcpt: %s,",
+ message_id,
+ fromaddr,
+ ", ".join(toaddrs),
+ exc_info=True)
else:
self.log.error(
- "Error while sending mail : %s ",
- filename, exc_info=True)
+ "%s - Error while sending mail.",
+ message_id,
+ exc_info=True)
else:
if forever:
time.sleep(self.interval)
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/interfaces.py
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/interfaces.py 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/interfaces.py 2008-03-09 07:59:46 UTC (rev 84551)
@@ -54,7 +54,8 @@
"""
__docformat__ = 'restructuredtext'
-from zope.interface import Interface, Attribute
+from zope.interface import Interface, Attribute, implements
+from zope.interface.common.interfaces import IException
from zope.schema import TextLine, Int, Password, Bool
from zope.i18nmessageid import MessageFactory
@@ -108,7 +109,7 @@
class IMailQueueProcessor(Interface):
- """A mail queue processor that delivers queueud messages asynchronously.
+ """A mail queue processor that delivers queued messages asynchronously.
"""
queuePath = TextLine(
@@ -119,15 +120,79 @@
title=_(u"Polling Interval"),
description=_(u"How often the queue is checked for new messages"
" (in milliseconds)"),
- default=5000)
+ default=3000)
mailer = Attribute("IMailer that is used for message delivery")
+ cleanLockFiles = Bool(
+ title=_(u"Clean Lock Files"),
+ description=_(u"Clean stale lock files from queue before processing"
+ " start."),
+ default=False)
+ retryInterval = Int(
+ title=_(u"Retry Interval"),
+ description=_(u"Retry time after connection failure or SMTP error 4xx."
+ " (in seconds)"),
+ default=300)
+
+#
+# Exception classes for use within Zope Sendmail, for use of Mailers
+#
+class IMailerFailureException(IException):
+ """Failure in sending mail"""
+ pass
+
+class MailerFailureException(Exception):
+ """Failure in sending mail"""
+
+ implements(IMailerFailureException)
+
+ def __init__(self, message="Failure in sending mail"):
+ self.message = message
+ self.args = (message,)
+
+
+class IMailerTemporaryFailureException(IMailerFailureException):
+ """Temporary failure in sending mail - retry later"""
+ pass
+
+class MailerTemporaryFailureException(MailerFailureException):
+ """Temporary failure in sending mail - retry later"""
+
+ implements(IMailerTemporaryFailureException)
+
+ def __init__(self, message="Temporary failure in sending mail - retry later"):
+ self.message = message
+ self.args = (message,)
+
+
+class IMailerPermanentFailureException(IMailerFailureException):
+ """Permanent failure in sending mail - take reject action"""
+ pass
+
+class MailerPermanentFailureException(MailerFailureException):
+ """Permanent failure in sending mail - take reject action"""
+
+ implements(IMailerPermanentFailureException)
+
+ def __init__(self, message="Permanent failure in sending mail - take reject action"):
+ self.message = message
+ self.args = (message,)
+
+
class IMailer(Interface):
- """Mailer handles synchronous mail delivery."""
+ """Mailer handles synchronous mail delivery.
- def send(fromaddr, toaddrs, message):
+ Mailer can raise the exceptions
+
+ MailerPermanentFailure
+ MailerTemporaryFailure
+
+ to indicate to sending process what action to take.
+ """
+
+ def send(fromaddr, toaddrs, message, message_id):
"""Send an email message.
`fromaddr` is the sender address (unicode string),
@@ -138,12 +203,18 @@
2822. It should contain at least Date, From, To, and Message-Id
headers.
+ `message_id` is an id for the message, typically a filename.
+
Messages are sent immediatelly.
Dispatches an `IMailSentEvent` on successful delivery, otherwise an
`IMailErrorEvent`.
"""
+ def set_logger(logger):
+ """Set the log object for the Mailer - this is for use by
+ QueueProcessorThread to hand a logging object to the mailer
+ """
class ISMTPMailer(IMailer):
"""A mailer that delivers mail to a relay host via SMTP."""
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/maildir.py
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/maildir.py 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/maildir.py 2008-03-09 07:59:46 UTC (rev 84551)
@@ -28,6 +28,7 @@
from zope.sendmail.interfaces import \
IMaildirFactory, IMaildir, IMaildirMessageWriter
+SENDING_MSG_LOCK_PREFIX = '.sending-'
class Maildir(object):
"""See `zope.sendmail.interfaces.IMaildir`"""
@@ -73,6 +74,30 @@
if not x.startswith('.')]
return iter(new_messages + cur_messages)
+ def _cleanLockLinks(self):
+ """Clean the maildir of any .sending-* lock files"""
+ # Fish inside the Maildir queue directory to get
+ # .sending-* files
+ # Get The links
+ join = os.path.join
+ subdir_cur = join(self.path, 'cur')
+ subdir_new = join(self.path, 'new')
+ lock_links = [join(subdir_new, x) for x in os.listdir(subdir_new)
+ if x.startswith(SENDING_MSG_LOCK_PREFIX)]
+ lock_links += [join(subdir_cur, x) for x in os.listdir(subdir_cur)
+ if x.startswith(SENDING_MSG_LOCK_PREFIX)]
+ # Remove any links
+ for link in lock_links:
+ try:
+ os.unlink(link)
+ except OSError, e:
+ if e.errno == 2: # file does not exist
+ # someone else unlinked the file; oh well
+ pass
+ else:
+ # something bad happend
+ raise
+
def newMessage(self):
"See `zope.sendmail.interfaces.IMaildir`"
# NOTE: http://www.qmail.org/man/man5/maildir.html says, that the first
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/mailer.py
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/mailer.py 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/mailer.py 2008-03-09 07:59:46 UTC (rev 84551)
@@ -20,18 +20,26 @@
__docformat__ = 'restructuredtext'
import socket
-from smtplib import SMTP
+import smtplib
+import logging
from zope.interface import implements
-from zope.sendmail.interfaces import ISMTPMailer
+from zope.interface.exceptions import DoesNotImplement
+from zope.sendmail.interfaces import (ISMTPMailer,
+ MailerTemporaryFailureException,
+ MailerPermanentFailureException)
+SMTP_ERR_MEDIUM_LOG_MSG = '%s - SMTP Error: %s - %s, %s'
+SMTP_ERR_SERVER_LOG_MSG = '%s - SMTP server %s:%s - %s'
+SMTP_ERR_LOG_MSG = '%s - SMTP Error: %s - %s, Sender: %s, Rcpt: %s'
+
have_ssl = hasattr(socket, 'ssl')
class SMTPMailer(object):
implements(ISMTPMailer)
- smtp = SMTP
+ smtp = smtplib.SMTP
def __init__(self, hostname='localhost', port=25,
username=None, password=None, no_tls=False, force_tls=False):
@@ -41,22 +49,105 @@
self.password = password
self.force_tls = force_tls
self.no_tls = no_tls
+ self.logger = None
- def send(self, fromaddr, toaddrs, message):
- connection = self.smtp(self.hostname, str(self.port))
+ def set_logger(self, logger):
+ if not isinstance(logger, logging.Logger):
+ raise DoesNotImplement('Invalid logger given')
+ self.logger = logger
+ def _log(self, log_level, *args):
+ if self.logger is None:
+ return
+ # This is not elegant, but it can be fixed later
+ if log_level == 'debug':
+ self.logger.debug(*args)
+ elif log_level =='info':
+ self.logger.info(*args)
+ elif log_level =='error':
+ self.logger.error(*args)
+ elif log_level =='warning':
+ self.logger.warning(*args)
+
+ def _handle_smtp_error(self,
+ smtp_code,
+ smtp_error,
+ fromaddr,
+ toaddrs,
+ message_id):
+ """Process results of an SMTP error
+ returns True to indicate break needed"""
+ if 500 <= smtp_code <= 599:
+ # permanent error, ditch the message
+ self._log('warning',
+ SMTP_ERR_LOG_MSG,
+ message_id,
+ str(smtp_code),
+ smtp_error,
+ fromaddr,
+ ", ".join(toaddrs))
+ raise MailerPermanentFailureException()
+ elif 400 <= smtp_code <= 499:
+ # Temporary error
+ self._log('warning',
+ SMTP_ERR_LOG_MSG,
+ message_id,
+ str(smtp_code),
+ smtp_error,
+ fromaddr,
+ ", ".join(toaddrs))
+ # temporary failure, go and sleep for
+ # retry_interval
+ raise MailerTemporaryFailureException()
+ else:
+ self._log('warning',
+ SMTP_ERR_LOG_MSG,
+ message_id,
+ str(smtp_code),
+ smtp_error,
+ fromaddr,
+ ", ".join(toaddrs))
+ raise MailerTemporaryFailureException()
+
+ def send(self, fromaddr, toaddrs, message, message_id):
+ try:
+ connection = self.smtp(self.hostname, str(self.port))
+ except socket.error, e:
+ self._log('info',
+ "%s - SMTP server %s:%s - could not connect(),"
+ " %s.",
+ message_id,
+ self.hostname,
+ str(self.port),
+ str(e),)
+ # temporary failure, go and sleep for
+ # retry_interval
+ raise MailerTemporaryFailureException()
+
# send EHLO
code, response = connection.ehlo()
if code < 200 or code >= 300:
code, response = connection.helo()
if code < 200 or code >= 300:
- raise RuntimeError('Error sending HELO to the SMTP server '
- '(code=%s, response=%s)' % (code, response))
+ self._log('warning',
+ SMTP_ERR_MEDIUM_LOG_MSG,
+ message_id,
+ code,
+ str(response),
+ 'error sending HELO')
+ raise MailerTemporaryFailureException()
# encryption support
- have_tls = connection.has_extn('starttls')
+ have_tls = connection.has_extn('starttls')
if not have_tls and self.force_tls:
- raise RuntimeError('TLS is not available but TLS is required')
+ error_str = 'TLS is not available but TLS is required'
+ self._log('warning',
+ SMTP_ERR_SERVER_LOG_MSG,
+ message_id,
+ self.hostname,
+ self.port,
+ error_str)
+ raise MaileriTemporaryFailure(error_str)
if have_tls and have_ssl and not self.no_tls:
connection.starttls()
@@ -66,8 +157,93 @@
if self.username is not None and self.password is not None:
connection.login(self.username, self.password)
elif self.username:
- raise RuntimeError('Mailhost does not support ESMTP but a username '
- 'is configured')
+ error_str = 'Mailhost does not support ESMTP but a username ' \
+ 'is configured'
+ self._log('warning',
+ SMTP_ERR_SERVER_LOG_MSG,
+ message_id,
+ self.hostname,
+ self.port,
+ error_str)
+ raise MailerTemporaryFailureException(error_str)
- connection.sendmail(fromaddr, toaddrs, message)
+ try:
+ send_errors = connection.sendmail(fromaddr, toaddrs, message)
+ except smtplib.SMTPSenderRefused, e:
+ self._log('warning',
+ SMTP_ERR_LOG_MSG,
+ message_id,
+ str(e.smtp_code),
+ e.smtp_error,
+ e.sender,
+ ", ".join(toaddrs))
+ # temporary failure, go and sleep for
+ # retry_interval
+ raise MailerTemporaryFailureException()
+ except (smtplib.SMTPAuthenticationError,
+ smtplib.SMTPConnectError,
+ smtplib.SMTPDataError,
+ smtplib.SMTPHeloError,
+ smtplib.SMTPResponseException), e:
+ self._handle_smtp_error(e.smtp_code,
+ e.smtp_error,
+ fromaddr,
+ toaddrs,
+ message_id)
+ except smtplib.SMTPServerDisconnected, e:
+ self._log('info',
+ SMTP_ERR_SERVER_LOG_MSG,
+ message_id,
+ self.hostname,
+ str(self.port),
+ str(e))
+ # temporary failure, go and sleep for
+ # retry_interval
+ raise MailerTemporaryFailureException()
+ except smtplib.SMTPRecipientsRefused, e:
+ # This exception is raised because no recipients
+ # were acceptable - lets take the most common error
+ # code and proceed with that
+ freq = {}
+ for result in e.recipients.values():
+ if freq.has_key(result):
+ freq[result] += 1
+ else:
+ freq[result] = 1
+ max_ = 0
+ for result in freq.keys():
+ if freq[result] > max_:
+ most_common = result
+ max_ = freq[result]
+ (smtp_code, smtp_error) = most_common
+ self._handle_smtp_error(smtp_code,
+ smtp_error,
+ fromaddr,
+ toaddrs,
+ message_id)
+ except smtplib.SMTPException, e:
+ # Permanent SMTP failure
+ self._log('warning',
+ '%s - SMTP failure: %s',
+ message_id,
+ str(e))
+ raise MailerPermanentFailureException()
+
connection.quit()
+
+ # Log ANY errors
+ if send_errors is not None:
+ sentaddrs = [x for x in toaddrs
+ if x not in send_errors.keys()]
+ for address in send_errors.keys():
+ self._log('warning',
+ SMTP_ERR_LOG_MSG,
+ message_id,
+ str(send_errors[address][0]),
+ send_errors[address][1],
+ fromaddr,
+ address)
+ else:
+ sentaddrs = list(toaddrs)
+
+ return sentaddrs
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/mail.zcml
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/mail.zcml 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/mail.zcml 2008-03-09 07:59:46 UTC (rev 84551)
@@ -7,6 +7,9 @@
name="Mail"
queuePath="path/to/tmp/mailbox"
mailer="test.smtp"
+ retryInterval="255"
+ pollingInterval="3000"
+ cleanLockLinks="False"
permission="zope.Public" />
<mail:directDelivery
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_delivery.py
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_delivery.py 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_delivery.py 2008-03-09 07:59:46 UTC (rev 84551)
@@ -21,6 +21,7 @@
import os.path
import shutil
import smtplib
+from socket import error as socket_error
from tempfile import mkdtemp
from unittest import TestCase, TestSuite, makeSuite
@@ -28,7 +29,9 @@
from zope.testing import doctest
from zope.interface import implements
from zope.interface.verify import verifyObject
-from zope.sendmail.interfaces import IMailer
+from zope.sendmail.interfaces import IMailer, ISMTPMailer
+from zope.sendmail.interfaces import MailerTemporaryFailureException
+from zope.sendmail.interfaces import MailerPermanentFailureException
class MailerStub(object):
@@ -37,8 +40,9 @@
def __init__(self, *args, **kw):
self.sent_messages = []
- def send(self, fromaddr, toaddrs, message):
+ def send(self, fromaddr, toaddrs, message, message_id):
self.sent_messages.append((fromaddr, toaddrs, message))
+ return toaddrs
class TestMailDataManager(TestCase):
@@ -186,10 +190,14 @@
self.create = create
self.msgs = []
self.files = []
+ self.cleaned_lock_links = False
def __iter__(self):
return iter(self.files)
+ def _cleanLockLinks(self):
+ self.cleaned_lock_links = True
+
def newMessage(self):
m = MaildirWriterStub()
self.msgs.append(m)
@@ -200,6 +208,7 @@
def __init__(self):
self.infos = []
+ self.warnings = []
self.errors = []
def getLogger(name):
@@ -208,6 +217,9 @@
def error(self, msg, *args, **kwargs):
self.errors.append((msg, args, kwargs))
+ def warning(self, msg, *args, **kwargs):
+ self.warnings.append((msg, args, kwargs))
+
def info(self, msg, *args, **kwargs):
self.infos.append((msg, args, kwargs))
@@ -222,20 +234,30 @@
def __init__(self, *args, **kw):
pass
- def send(self, fromaddr, toaddrs, message):
+ def send(self, fromaddr, toaddrs, message, message_id):
raise BizzarreMailError("bad things happened while sending mail")
-class SMTPResponseExceptionMailerStub(object):
+class MailerPermanentFailureExceptionMailerStub(object):
implements(IMailer)
- def __init__(self, code):
- self.code = code
+ def __init__(self, msg='Permanent failure'):
+ self.msg = msg
- def send(self, fromaddr, toaddrs, message):
- raise smtplib.SMTPResponseException(self.code, 'Serious Error')
+ def send(self, fromaddr, toaddrs, message, message_id):
+ raise MailerPermanentFailureException(self.msg)
+class MailerTemporaryFailureExceptionMailerStub(object):
+
+ implements(IMailer)
+ def __init__(self, msg='Temporary failure'):
+ self.msg = msg
+
+ def send(self, fromaddr, toaddrs, message, message_id):
+ raise MailerTemporaryFailureException(self.msg)
+
+
class TestQueuedMailDelivery(TestCase):
def setUp(self):
@@ -330,8 +352,44 @@
self.assertEquals(t, ('bar at example.com', 'baz at example.com'))
self.assertEquals(m, msg)
+ def test_unlink(self):
+ self.thread.log = LoggerStub() # Clean log
+ self.filename = os.path.join(self.dir, 'message')
+ self.tmp_filename = os.path.join(self.dir, '.sending-message')
+ temp = open(self.filename, "w+b")
+ temp.write('X-Zope-From: foo at example.com\n'
+ 'X-Zope-To: bar at example.com, baz at example.com\n'
+ 'Header: value\n\nBody\n')
+ temp.close()
+ self.md.files.append(self.filename)
+ os.link(self.filename, self.tmp_filename)
+ self.thread._unlinkFile(self.tmp_filename)
+ self.failUnless(os.path.exists(self.filename))
+ self.failIf(os.path.exists(self.tmp_filename), 'File exists')
+
+ def test_queueRetryWait(self):
+ self.thread.log = LoggerStub() # Clean log
+ self.filename = os.path.join(self.dir, 'message')
+ self.tmp_filename = os.path.join(self.dir, '.sending-message')
+ temp = open(self.filename, "w+b")
+ temp.write('X-Zope-From: foo at example.com\n'
+ 'X-Zope-To: bar at example.com, baz at example.com\n'
+ 'Header: value\n\nBody\n')
+ temp.close()
+ self.md.files.append(self.filename)
+ os.link(self.filename, self.tmp_filename)
+ self.thread._queueRetryWait(self.tmp_filename, forever=False)
+ self.failUnless(os.path.exists(self.filename))
+ self.failIf(os.path.exists(self.tmp_filename), 'File exists')
+ # Check that 5 minute wait is happening
+ self.assertEquals(self.thread.test_results,
+ {'_queueRetryWait':
+ 'Retry timeout: 300.0 count: 0.0'})
+
def test_deliveration(self):
+ self.thread.log = LoggerStub() # Clean log
self.filename = os.path.join(self.dir, 'message')
+ self.tmp_filename = os.path.join(self.dir, '.sending-message')
temp = open(self.filename, "w+b")
temp.write('X-Zope-From: foo at example.com\n'
'X-Zope-To: bar at example.com, baz at example.com\n'
@@ -344,13 +402,15 @@
('bar at example.com', 'baz at example.com'),
'Header: value\n\nBody\n')])
self.failIf(os.path.exists(self.filename), 'File exists')
+ self.failIf(os.path.exists(self.tmp_filename), 'File exists')
self.assertEquals(self.thread.log.infos,
- [('Mail from %s to %s sent.',
- ('foo at example.com',
+ [('%s - mail sent, Sender: %s, Rcpt: %s,',
+ ('message', 'foo at example.com',
'bar at example.com, baz at example.com'),
{})])
def test_error_logging(self):
+ self.thread.log = LoggerStub() # Clean log
self.thread.setMailer(BrokenMailerStub())
self.filename = os.path.join(self.dir, 'message')
temp = open(self.filename, "w+b")
@@ -361,15 +421,18 @@
self.md.files.append(self.filename)
self.thread.run(forever=False)
self.assertEquals(self.thread.log.errors,
- [('Error while sending mail from %s to %s.',
- ('foo at example.com',
- 'bar at example.com, baz at example.com'),
- {'exc_info': 1})])
+ [('%s - Error while sending mail, Sender: %s,'
+ ' Rcpt: %s,',
+ ('message', 'foo at example.com',
+ 'bar at example.com, baz at example.com'),
+ {'exc_info': True})])
- def test_smtp_response_error_transient(self):
+ def test_mailer_temporary_failure(self):
# Test a transient error
- self.thread.setMailer(SMTPResponseExceptionMailerStub(451))
+ self.thread.log = LoggerStub() # Clean log
+ self.thread.setMailer(MailerTemporaryFailureExceptionMailerStub())
self.filename = os.path.join(self.dir, 'message')
+ self.tmp_filename = os.path.join(self.dir, '.sending-message')
temp = open(self.filename, "w+b")
temp.write('X-Zope-From: foo at example.com\n'
'X-Zope-To: bar at example.com, baz at example.com\n'
@@ -377,19 +440,20 @@
temp.close()
self.md.files.append(self.filename)
self.thread.run(forever=False)
-
- # File must remail were it was, so it will be retried
+ # File must remain were it was, so it will be retried
self.failUnless(os.path.exists(self.filename))
- self.assertEquals(self.thread.log.errors,
- [('Error while sending mail from %s to %s.',
- ('foo at example.com',
- 'bar at example.com, baz at example.com'),
- {'exc_info': 1})])
+ self.failIf(os.path.exists(self.tmp_filename), 'File exists')
+ # Check that 5 minute wait is happening
+ self.assertEquals(self.thread.test_results,
+ {'_queueRetryWait':
+ 'Retry timeout: 300.0 count: 0.0'})
- def test_smtp_response_error_permanent(self):
+ def test_mailer_permanent_failure(self):
# Test a permanent error
- self.thread.setMailer(SMTPResponseExceptionMailerStub(550))
+ self.thread.log = LoggerStub() # Clean log
+ self.thread.setMailer(MailerPermanentFailureExceptionMailerStub())
self.filename = os.path.join(self.dir, 'message')
+ self.tmp_filename = os.path.join(self.dir, '.sending-message')
temp = open(self.filename, "w+b")
temp.write('X-Zope-From: foo at example.com\n'
'X-Zope-To: bar at example.com, baz at example.com\n'
@@ -397,19 +461,30 @@
temp.close()
self.md.files.append(self.filename)
self.thread.run(forever=False)
-
# File must be moved aside
- self.failIf(os.path.exists(self.filename))
+ self.failIf(os.path.exists(self.filename), 'File exists')
+ self.failIf(os.path.exists(self.tmp_filename), 'File exists')
self.failUnless(os.path.exists(os.path.join(self.dir,
'.rejected-message')))
- self.assertEquals(self.thread.log.errors,
- [('Discarding email from %s to %s due to a '
- 'permanent error: %s',
- ('foo at example.com',
- 'bar at example.com, baz at example.com',
- "(550, 'Serious Error')"), {})])
+ def test_zzz_qptCleanLockLinks(self):
+ from zope.sendmail.delivery import QueueProcessorThread
+ self.thread = QueueProcessorThread(clean_lock_links=True)
+ self.thread.log = LoggerStub()
+ self.thread.setMaildir(self.md)
+ self.thread.setMailer(self.mailer)
+ self.filename = os.path.join(self.dir, 'message')
+ self.tmp_filename = os.path.join(self.dir, '.sending-message')
+ temp = open(self.filename, "w+b")
+ temp.write('X-Zope-From: foo at example.com\n'
+ 'X-Zope-To: bar at example.com, baz at example.com\n'
+ 'Header: value\n\nBody\n')
+ temp.close()
+ self.md.files.append(self.filename)
+ self.thread.run(forever=False)
+ self.assertEquals(self.thread.maildir.cleaned_lock_links, True)
+
def test_suite():
return TestSuite((
makeSuite(TestMailDataManager),
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_maildir.py
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_maildir.py 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_maildir.py 2008-03-09 07:59:46 UTC (rev 84551)
@@ -76,8 +76,8 @@
'/path/to/emptydirectory': stat.S_IFDIR,
}
_listdir = {
- '/path/to/maildir/new': ['1', '2', '.svn'],
- '/path/to/maildir/cur': ['2', '1', '.tmp'],
+ '/path/to/maildir/new': ['1', '2', '.svn', '.sending-2'],
+ '/path/to/maildir/cur': ['2', '1', '.tmp', '.sending-1'],
'/path/to/maildir/tmp': ['1', '2', '.ignore'],
}
@@ -229,6 +229,15 @@
'/path/to/maildir/new/1',
'/path/to/maildir/new/2'])
+ def test_cleanLockLinks(self):
+ from zope.sendmail.maildir import Maildir
+ filename1 = '/path/to/maildir/new/.sending-2'
+ filename2 = '/path/to/maildir/cur/.sending-1'
+ m = Maildir('/path/to/maildir')
+ m._cleanLockLinks()
+ self.assert_(filename1 in self.fake_os_module._removed_files)
+ self.assert_(filename2 in self.fake_os_module._removed_files)
+
def test_newMessage(self):
from zope.sendmail.maildir import Maildir
from zope.sendmail.interfaces import IMaildirMessageWriter
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_mailer.py
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_mailer.py 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/tests/test_mailer.py 2008-03-09 07:59:46 UTC (rev 84551)
@@ -16,31 +16,60 @@
$Id$
"""
+import socket
+import unittest
+import smtplib
+import logging
from StringIO import StringIO
+
from zope.interface.verify import verifyObject
-from zope.sendmail.interfaces import ISMTPMailer
+
+from zope.sendmail.interfaces import (ISMTPMailer,
+ MailerTemporaryFailureException,
+ MailerPermanentFailureException)
from zope.sendmail.mailer import SMTPMailer
-import socket
-import unittest
+from zope.sendmail.tests.test_delivery import LoggerStub
class TestSMTPMailer(unittest.TestCase):
- def setUp(self, port=None):
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ self.test_kwargs = {'fromaddr': 'me at example.com',
+ 'toaddrs': ('you at example.com', 'him at example.com'),
+ 'message': 'Headers: headers\n\nbodybodybody\n-- \nsig\n',
+ 'message_id': 'dummy_file_name_XXX345YZ'}
+
+ def setUp(self, port=None, exception=None, exception_args=None,
+ send_errors=None):
global SMTP
class SMTP(object):
+ _exception = None
+ _exception_args = None
+ _send_errors = None
def __init__(myself, h, p):
myself.hostname = h
myself.port = p
+ self.smtp = myself
if type(p) == type(u""):
raise socket.error("Int or String expected")
- self.smtp = myself
+ if p == '0':
+ raise socket.error("Emulated connect() failure")
def sendmail(self, f, t, m):
self.fromaddr = f
self.toaddrs = t
self.msgtext = m
+ if hasattr(self, '_exception'):
+ if self._exception and issubclass(self._exception, Exception):
+ if hasattr(self, '_exception_args') \
+ and type(self._exception_args) is tuple:
+ raise self._exception(*self._exception_args)
+ else:
+ raise self._exception('Crazy Arguments WANTED!')
+ if self._send_errors:
+ return self._send_errors
def login(self, username, password):
self.username = username
@@ -65,45 +94,180 @@
else:
self.mailer = SMTPMailer(u'localhost', port)
self.mailer.smtp = SMTP
+ self.mailer.logger = LoggerStub()
+ SMTP._exception = exception
+ SMTP._exception_args = exception_args
+ SMTP._send_errors = send_errors
def test_interface(self):
verifyObject(ISMTPMailer, self.mailer)
+ def test_set_logger(self):
+ # Do this with throw away instances...
+ test_mailer = SMTPMailer()
+ log_object = logging.getLogger('test_logger')
+ test_mailer.set_logger(log_object)
+ self.assertEquals(isinstance(test_mailer.logger, logging.Logger), True)
+
def test_send(self):
for run in (1,2):
if run == 2:
self.setUp(u'25')
- fromaddr = 'me at example.com'
- toaddrs = ('you at example.com', 'him at example.com')
- msgtext = 'Headers: headers\n\nbodybodybody\n-- \nsig\n'
- self.mailer.send(fromaddr, toaddrs, msgtext)
- self.assertEquals(self.smtp.fromaddr, fromaddr)
- self.assertEquals(self.smtp.toaddrs, toaddrs)
- self.assertEquals(self.smtp.msgtext, msgtext)
+ else:
+ self.setUp()
+ td = self.test_kwargs
+ result = self.mailer.send(**self.test_kwargs)
+ self.assertEquals(self.smtp.fromaddr, td['fromaddr'])
+ self.assertEquals(self.smtp.toaddrs, td['toaddrs'])
+ self.assertEquals(self.smtp.msgtext, td['message'])
self.assert_(self.smtp.quit)
+ self.assertEquals(result, ['you at example.com', 'him at example.com'])
+ def test_mailer_no_connect(self):
+ # set up test value to raise socket.error exception
+ self.setUp('0')
+ try:
+ self.mailer.send(**self.test_kwargs)
+ except MailerTemporaryFailureException:
+ pass
+ self.assertEquals(self.mailer.logger.infos,
+ [('%s - SMTP server %s:%s - could not connect(), %s.',
+ ('dummy_file_name_XXX345YZ', u'localhost', '0',
+ 'Emulated connect() failure'), {})])
+
+ def test_mailer_smtp_data_error(self):
+ self.setUp(exception=smtplib.SMTPDataError,
+ exception_args=(471, 'SMTP Data Error'))
+ try:
+ self.mailer.send(**self.test_kwargs)
+ except MailerTemporaryFailureException:
+ pass
+ self.assertEquals(self.mailer.logger.warnings,
+ [('%s - SMTP Error: %s - %s, Sender: %s, Rcpt: %s',
+ ('dummy_file_name_XXX345YZ', '471', 'SMTP Data Error',
+ 'me at example.com', 'you at example.com, him at example.com'),
+ {})])
+
+
+ def test_mailer_smtp_really_bad_error(self):
+ self.setUp(exception=smtplib.SMTPResponseException,
+ exception_args=(550, 'SMTP Really Bad Error'))
+ try:
+ self.mailer.send(**self.test_kwargs)
+ except MailerPermanentFailureException:
+ pass
+ self.assertEquals(self.mailer.logger.warnings,
+ [('%s - SMTP Error: %s - %s, Sender: %s, Rcpt: %s',
+ ('dummy_file_name_XXX345YZ', '550', 'SMTP Really Bad Error',
+ 'me at example.com', 'you at example.com, him at example.com'),
+ {})])
+
+ def test_mailer_smtp_crazy_error(self):
+ self.setUp(exception=smtplib.SMTPResponseException,
+ exception_args=(200, 'SMTP Crazy Error'))
+ try:
+ self.mailer.send(**self.test_kwargs)
+ except MailerTemporaryFailureException:
+ pass
+ self.assertEquals(self.mailer.logger.warnings,
+ [('%s - SMTP Error: %s - %s, Sender: %s, Rcpt: %s',
+ ('dummy_file_name_XXX345YZ', '200', 'SMTP Crazy Error',
+ 'me at example.com', 'you at example.com, him at example.com'),
+ {})])
+
+ def test_mailer_smtp_server_disconnected(self):
+ self.setUp(exception=smtplib.SMTPServerDisconnected,
+ exception_args=('TCP RST - unexpected dissconnection',))
+ try:
+ self.mailer.send(**self.test_kwargs)
+ except MailerTemporaryFailureException:
+ pass
+ self.assertEquals(self.mailer.logger.infos,
+ [('%s - SMTP server %s:%s - %s',
+ ('dummy_file_name_XXX345YZ',
+ 'localhost', '25',
+ 'TCP RST - unexpected dissconnection'),
+ {})])
+
+ def test_mailer_smtp_sender_refused(self):
+ self.setUp(exception=smtplib.SMTPSenderRefused,
+ exception_args=(550, 'SMTP Sender Refused',
+ 'iamasender at bogus.com'))
+ try:
+ self.mailer.send(**self.test_kwargs)
+ except MailerTemporaryFailureException:
+ pass
+ self.assertEquals(self.mailer.logger.warnings,
+ [('%s - SMTP Error: %s - %s, Sender: %s, Rcpt: %s',
+ ('dummy_file_name_XXX345YZ', '550', 'SMTP Sender Refused',
+ 'iamasender at bogus.com', 'you at example.com, him at example.com'),
+ {})])
+
+ def test_mailer_smtp_recipients_refused(self):
+ self.setUp(exception=smtplib.SMTPRecipientsRefused,
+ exception_args=({'you at example.com': (451, 'SMTP Recipient A Refused'),
+ 'him at example.com': (450, 'SMTP Recipient B Refused')},))
+ try:
+ self.mailer.send(**self.test_kwargs)
+ except MailerTemporaryFailureException:
+ pass
+ self.assertEquals(self.mailer.logger.warnings,
+ [('%s - SMTP Error: %s - %s, Sender: %s, Rcpt: %s',
+ ('dummy_file_name_XXX345YZ', '450', 'SMTP Recipient B Refused',
+ 'me at example.com', 'you at example.com, him at example.com'),
+ {})])
+
+ def test_mailer_smtp_exception(self):
+ self.setUp(exception=smtplib.SMTPException,
+ exception_args=('SMTP Permanent Failure',))
+ try:
+ self.mailer.send(**self.test_kwargs)
+ except MailerPermanentFailureException:
+ pass
+ self.assertEquals(self.mailer.logger.warnings,
+ [('%s - SMTP failure: %s',
+ ('dummy_file_name_XXX345YZ',
+ 'SMTP Permanent Failure'),
+ {})])
+
+ def test_mailer_partial_send_failure(self):
+ self.setUp(send_errors={'you at example.com': (550, 'User unknown')})
+ td = self.test_kwargs
+ result = self.mailer.send(**self.test_kwargs)
+ self.assertEquals(self.smtp.fromaddr, td['fromaddr'])
+ self.assertEquals(self.smtp.toaddrs, td['toaddrs'])
+ self.assertEquals(self.smtp.msgtext, td['message'])
+ self.assert_(self.smtp.quit)
+ self.assertEquals(self.mailer.logger.warnings,
+ [('%s - SMTP Error: %s - %s, Sender: %s, Rcpt: %s',
+ ('dummy_file_name_XXX345YZ', '550', 'User unknown',
+ 'me at example.com', 'you at example.com'),
+ {})])
+ self.assertEquals(result, ['him at example.com'])
+
def test_send_auth(self):
- fromaddr = 'me at example.com'
- toaddrs = ('you at example.com', 'him at example.com')
- msgtext = 'Headers: headers\n\nbodybodybody\n-- \nsig\n'
+ self.setUp()
self.mailer.username = 'foo'
self.mailer.password = 'evil'
self.mailer.hostname = 'spamrelay'
self.mailer.port = 31337
- self.mailer.send(fromaddr, toaddrs, msgtext)
+ td = self.test_kwargs
+ result = self.mailer.send(**self.test_kwargs)
self.assertEquals(self.smtp.username, 'foo')
self.assertEquals(self.smtp.password, 'evil')
self.assertEquals(self.smtp.hostname, 'spamrelay')
self.assertEquals(self.smtp.port, '31337')
- self.assertEquals(self.smtp.fromaddr, fromaddr)
- self.assertEquals(self.smtp.toaddrs, toaddrs)
- self.assertEquals(self.smtp.msgtext, msgtext)
+ self.assertEquals(self.smtp.fromaddr, td['fromaddr'])
+ self.assertEquals(self.smtp.toaddrs, td['toaddrs'])
+ self.assertEquals(self.smtp.msgtext, td['message'])
self.assert_(self.smtp.quit)
+ self.assertEquals(result, ['you at example.com', 'him at example.com'])
class TestSMTPMailerWithNoEHLO(TestSMTPMailer):
- def setUp(self, port=None):
+ def setUp(self, port=None, exception=None, exception_args=None,
+ send_errors=None):
class SMTPWithNoEHLO(SMTP):
does_esmtp = False
@@ -111,9 +275,11 @@
def __init__(myself, h, p):
myself.hostname = h
myself.port = p
+ self.smtp = myself
if type(p) == type(u""):
raise socket.error("Int or String expected")
- self.smtp = myself
+ if p == '0':
+ raise socket.error("Emulated connect() failure")
def helo(self):
return (200, 'Hello, I am your stupid MTA mock')
@@ -127,6 +293,10 @@
else:
self.mailer = SMTPMailer(u'localhost', port)
self.mailer.smtp = SMTPWithNoEHLO
+ self.mailer.logger = LoggerStub()
+ SMTPWithNoEHLO._exception = exception
+ SMTPWithNoEHLO._exception_args = exception_args
+ SMTPWithNoEHLO._send_errors = send_errors
def test_send_auth(self):
# This test requires ESMTP, which we're intentionally not enabling
Modified: zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/zcml.py
===================================================================
--- zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/zcml.py 2008-03-09 07:15:59 UTC (rev 84550)
+++ zope.sendmail/branches/grantma-retryfixes/src/zope/sendmail/zcml.py 2008-03-09 07:59:46 UTC (rev 84551)
@@ -22,7 +22,7 @@
from zope.configuration.fields import Path
from zope.configuration.exceptions import ConfigurationError
from zope.interface import Interface
-from zope.schema import TextLine, BytesLine, Int
+from zope.schema import TextLine, BytesLine, Int, Bool
from zope.security.checker import InterfaceChecker, CheckerPublic
from zope.security.zcml import Permission
@@ -70,8 +70,36 @@
description=u"Defines the path for the queue directory.",
required=True)
-def queuedDelivery(_context, permission, queuePath, mailer, name="Mail"):
+ pollingInterval = Int(
+ title=u'Polling Interval',
+ description=u'Defines the polling interval for queue processing in'
+ ' milliseconds',
+ default=3000,
+ required=False)
+ retryInterval = Int(
+ title=u'Retry Interval',
+ description=u'Defines the retry interval for queue processing in'
+ ' the event of a temporary error in seconds',
+ default=300,
+ required=False)
+
+ cleanLockLinks = Bool(
+ title=u'Clean Lock Links',
+ description=u'Whether or not to clean up .sending-* lock links in the'
+ ' queue on processing thread start',
+ default=False,
+ required=False)
+
+def queuedDelivery(_context,
+ permission,
+ queuePath,
+ mailer,
+ pollingInterval=3000,
+ retryInterval=300,
+ cleanLockLinks=False,
+ name="Mail"):
+
def createQueuedDelivery():
delivery = QueuedMailDelivery(queuePath)
delivery = _assertPermission(permission, IMailDelivery, delivery)
@@ -80,17 +108,19 @@
mailerObject = queryUtility(IMailer, mailer)
if mailerObject is None:
- raise ConfigurationError("Mailer %r is not defined" %mailer)
+ raise ConfigurationError("Mailer %r is not defined" % mailer)
- thread = QueueProcessorThread()
+ thread = QueueProcessorThread(interval=pollingInterval/1000,
+ retry_interval=retryInterval,
+ clean_lock_links=cleanLockLinks)
thread.setMailer(mailerObject)
thread.setQueuePath(queuePath)
thread.start()
_context.action(
- discriminator = ('delivery', name),
- callable = createQueuedDelivery,
- args = () )
+ discriminator=('delivery', name),
+ callable=createQueuedDelivery,
+ args=() )
class IDirectDeliveryDirective(IDeliveryDirective):
"""This directive creates and registers a global direct mail utility. It
More information about the Checkins
mailing list