[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