[Zope3-checkins] CVS: Zope3/src/zope/app/mail - event.py:1.1.14.5 mailer.py:1.3.2.6 service.py:1.2.2.6

Albertas Agejevas alga@codeworks.lt
Mon, 23 Jun 2003 11:00:49 -0400


Update of /cvs-repository/Zope3/src/zope/app/mail
In directory cvs.zope.org:/tmp/cvs-serv26043/src/zope/app/mail

Modified Files:
      Tag: cw-mail-branch
	event.py mailer.py service.py 
Log Message:
Bummer. Wiped out my changes with a HEAD merge.  Here they are back.


=== Zope3/src/zope/app/mail/event.py 1.1.14.4 => 1.1.14.5 ===
--- Zope3/src/zope/app/mail/event.py:1.1.14.4	Mon Jun 23 10:20:12 2003
+++ Zope3/src/zope/app/mail/event.py	Mon Jun 23 11:00:49 2003
@@ -15,7 +15,9 @@
 
 $Id$
 """
-from zope.app.interfaces.mail import IMailSentEvent
+from zope.interface import implements
+from zope.app.interfaces.mail import IMailSentEvent, IMailErrorEvent
+__metaclass__ = type
 from zope.interface import implements
 
 
@@ -24,5 +26,15 @@
 
     implements(IMailSentEvent)
 
-    def __init__(self, mailer):
-        self.mailer = mailer 
+    def __init__(self, messageId):
+        self.messageId = messageId
+
+
+class MailErrorEvent:
+    __doc__ = IMailErrorEvent.__doc__
+
+    implements(IMailErrorEvent)
+
+    def __init__(self, messageId, errorMessage):
+        self.messageId = messageId
+        self.errorMessage = errorMessage


=== Zope3/src/zope/app/mail/mailer.py 1.3.2.5 => 1.3.2.6 ===
--- Zope3/src/zope/app/mail/mailer.py:1.3.2.5	Mon Jun 23 10:20:12 2003
+++ Zope3/src/zope/app/mail/mailer.py	Mon Jun 23 11:00:49 2003
@@ -11,62 +11,52 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-"""MailService Implementation
 
-Simple implementation of the MailService, Mailers and MailEvents.
+"""These are classes which abstract different channels an email
+message could be sent out by.
 
 $Id$
 """
-from smtplib import SMTP
 
-from zope.app.interfaces.mail import IMailer, IBatchMailer
-from zope.app.event import publish
-from zope.app.mail.event import MailSentEvent
 from zope.interface import implements
+from zope.app.interfaces.mail import ISendmailMailer, ISMTPMailer
+from os import popen
+from smtplib import SMTP
+
+__metaclass__ = type
+
+class SendmailMailer:
+
+    implements(ISendmailMailer)
+
+    # A hook for unit tests
+    popen = popen
+
+    def __init__(self, command="/usr/lib/sendmail -oem -oi -f %(from)s %(to)s"):
+        self.command = command
+
+    def send(self, fromaddr, toaddrs, message):
+        command = self.command % {'from': fromaddr, 'to': " ".join(toaddrs)}
+        f = self.popen(command, "w")
+        f.write(message)
+        f.close()
+
+class SMTPMailer:
 
+    implements(ISMTPMailer)
 
-class SimpleMailer:
-    __doc__ = IMailer.__doc__
+    smtp = SMTP
 
-    implements(IMailer)
+    def __init__(self, hostname='localhost', port=25,
+                 username=None, password=None):
+        self.hostname = hostname
+        self.port = port
+        self.username = username
+        self.password = password
 
-    def send(self, fromaddr, toaddrs, message,
-             hostname, port, username, password):
-        "See zope.app.interfaces.services.mail.IMailer"
-        server = SMTP(hostname, port)
-        server.set_debuglevel(0)
-        if username is not None and password is not None:
-            server.login(username, password)
-        server.sendmail(fromaddr, toaddrs, message)
-        server.quit()
-        publish(self, MailSentEvent(self))
-
-
-class BatchMailer:
-    __doc__ = IBatchMailer.__doc__
-
-    implements(IBatchMailer)
-
-    # See zope.app.interfaces.mail.IBatchMailer
-    batchDelay = 5000
-
-    # See zope.app.interfaces.mail.IBatchMailer
-    batchSize = 5
-
-    def send(self, fromaddr, toaddrs, message,
-             hostname, port, username, password):
-        "See zope.app.interfaces.mail.IMailer"
-        server = SMTP(hostname, port)
-        server.set_debuglevel(0)
-        if username is not None and password is not None:
-            server.login(username, password)
-        recv = list(toaddrs)
-        batch = []
-        while recv:
-            while len(batch) < self.batchSize and recv:
-                batch.append(recv.pop())
-            server.sendmail(fromaddr, batch, message)
-            batch = []
-            time.sleep(self.batchDelay/1000.0)
-        server.quit()
-        publish(self, MailSentEvent(self))
+    def send(self, fromaddr, toaddrs, message):
+        connection = self.smtp(self.hostname, str(self.port))
+        if self.username is not None and self.password is not None:
+            connection.login(self.username, self.password)
+        connection.sendmail(fromaddr, toaddrs, message)
+        connection.quit()


=== Zope3/src/zope/app/mail/service.py 1.2.2.5 => 1.2.2.6 ===
--- Zope3/src/zope/app/mail/service.py:1.2.2.5	Mon Jun 23 10:20:12 2003
+++ Zope3/src/zope/app/mail/service.py	Mon Jun 23 11:00:49 2003
@@ -11,59 +11,176 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-"""MailService Implementation
+"""Mail service implementation
 
 This module contains various implementations of MailServices.
 
 $Id$
 """
-from zope.app.interfaces.mail import IAsyncMailService
+import rfc822
+import threading
+import os.path
+import logging
+from os import listdir, unlink
+from cStringIO import StringIO
+from random import randrange
+from time import strftime
+from socket import gethostname
+from os import getpid
+from time import sleep
 from zope.interface import implements
-
-class AsyncMailService:
-    __doc__ = IAsyncMailService.__doc__
-
-    implements(IAsyncMailService)
-
-    # See zope.app.interfaces.services.mail.IMailService
-    hostname = u''
-
-    # See zope.app.interfaces.services.mail.IMailService
-    port = 25
-
-    # See zope.app.interfaces.services.mail.IMailService
-    username = None
-
-    # See zope.app.interfaces.services.mail.IMailService
-    password = None
-
-    def __init__(self):
-        """Initialize the object."""
-        self.__mailers = {}
-        self.__default_mailer = ''
-
-    def createMailer(self, name):
-        "See zope.app.interfaces.services.mail.IAsyncMailService"
-        return self.__mailers[name]()
-
-    def getMailerNames(self):
-        "See zope.app.interfaces.services.mail.IAsyncMailService"
-        return self.__mailers.keys()
-
-    def getDefaultMailerName(self):
-        "See zope.app.interfaces.services.mail.IAsyncMailService"
-        return self.__default_mailer
-
-    def send(self, fromaddr, toaddrs, message, mailer=None):
-        "See zope.app.interfaces.services.mail.IMailService"
-        if mailer is None:
-            mailer = self.createMailer(self.getDefaultMailerName())
-        # XXX: should be called in new thread:should we use thread or async?
-        mailer.send(fromaddr, toaddrs, message, self.hostname, self.port,
-                    self.username, self.password)
-
-    def provideMailer(self, name, klass, default=False):
-        """Add a new mailer to the service."""
-        self.__mailers[name] = klass
-        if default:
-            self.__default_mailer = name
+from zope.app.interfaces.mail import IDirectMailService, IQueuedMailService
+from zope.app.mail.maildir import Maildir
+from transaction.interfaces import IDataManager
+from transaction import get_transaction
+
+__metaclass__ = type
+
+class MailDataManager:
+    """XXX I need a docstring"""
+
+    implements(IDataManager)
+
+    def __init__(self, callable, args=(), onAbort=None):
+        self.callable = callable
+        self.args = args
+        self.onAbort = onAbort
+
+    def prepare(self, transaction):
+        pass
+
+    def abort(self, transaction):
+        if self.onAbort:
+            self.onAbort()
+
+    def commit(self, transaction):
+        self.callable(*self.args)
+
+    def savepoint(self, transaction):
+        pass
+
+
+class AbstractMailService:
+
+    def newMessageId(self):
+        """Generates a new message ID according to RFC 2822 rules"""
+        randmax = 0x7fffffff
+        left_part = '%s.%d.%d' % (strftime('%Y%m%d%H%M%S'),
+                                  getpid(),
+                                  randrange(0, randmax))
+        return "%s@%s" % (left_part, gethostname())
+
+    def send(self, fromaddr, toaddrs, message):
+        parser = rfc822.Message(StringIO(message))
+        messageid = parser.getheader('Message-Id')
+        if messageid:
+            if not messageid.startswith('<') or not messageid.endswith('>'):
+                raise ValueError('Malformed Message-Id header')
+            messageid = messageid[1:-1]
+        else:
+            messageid = self.newMessageId()
+            message = 'Message-Id: <%s>\n%s' % (messageid, message)
+        get_transaction().join(self.createDataManager(fromaddr, toaddrs, message))
+        return messageid
+
+
+class DirectMailService(AbstractMailService):
+    __doc__ = IDirectMailService.__doc__
+
+    implements(IDirectMailService)
+
+    def __init__(self, mailer):
+        self.mailer = mailer
+
+    def createDataManager(self, fromaddr, toaddrs, message):
+        return MailDataManager(self.mailer.send, args=(fromaddr, toaddrs, message))
+
+
+class QueuedMailService(AbstractMailService):
+    __doc__ = IQueuedMailService.__doc__
+
+    implements(IQueuedMailService)
+
+    def __init__(self, queuePath):
+        self._queuePath = queuePath
+
+    queuePath = property(lambda self: self._queuePath)
+
+    def createDataManager(self, fromaddr, toaddrs, message):
+        maildir = Maildir(self.queuePath, True)
+        msg = maildir.newMessage()
+        msg.write('X-Zope-From: %s\n' % fromaddr)
+        msg.write('X-Zope-To: %s\n' % ", ".join(toaddrs))
+        msg.write(message)
+        return MailDataManager(msg.commit, onAbort=msg.abort)
+
+class QueueProcessorThread(threading.Thread):
+    """This thread is started at configuration time from the
+    mail:queuedService directive handler.
+    """
+    log = logging.getLogger("QueueProcessorThread")
+
+    def setMaildir(self, maildir):
+        """Set the maildir.
+
+        This method is used just to provide a maildir stubs ."""
+        self.maildir = maildir
+
+    def setQueuePath(self, path):
+        self.maildir = Maildir(path)
+
+    def setMailer(self, mailer):
+        self.mailer = mailer
+
+    def _parseMessage(self, message):
+        """Extract fromaddr and toaddrs from the first two lines of
+        the message.
+
+        Returns a fromaddr string, a toaddrs tuple and the message
+        string.
+        """
+
+        fromaddr = ""
+        toaddrs = ()
+        rest = ""
+
+        try:
+            first, second, rest = message.split('\n', 2)
+        except ValueError:
+            return fromaddr, toaddrs, message
+
+        if first.startswith("X-Zope-From: "):
+            i = len("X-Zope-From: ")
+            fromaddr = first[i:]
+
+        if second.startswith("X-Zope-To: "):
+            i = len("X-Zope-To: ")
+            toaddrs = tuple(second[i:].split(", "))
+
+        return fromaddr, toaddrs, rest
+
+    def run(self, forever=True):
+        while True:
+            for filename in self.maildir:
+                try:
+                    file = open(filename)
+                    message = file.read()
+                    file.close()
+                    fromaddr, toaddrs, message = self._parseMessage(message)
+                    self.mailer.send(fromaddr, toaddrs, message)
+                    unlink(filename)
+                    # XXX maybe log the Message-Id of the message sent
+                    self.log.info("Mail from %s to %s sent.",
+                                  fromaddr, ", ".join(toaddrs))
+                    # Blanket except because we don't want this thread to ever die
+                except:
+                    # XXX maybe throw away erroring messages here?
+                    self.log.error("Error while sending mail from %s to %s.",
+                                   fromaddr, ", ".join(toaddrs), exc_info=1)
+            else:
+                if forever:
+                    sleep(3)
+
+            # A testing plug
+            if not forever:
+                break