[Zope3-checkins] CVS: Zope3/src/zope/app/mail - configure.zcml:1.2 event.py:1.3 mailer.py:1.5 meta.zcml:1.2 metaconfigure.py:1.3 service.py:1.4 mail.py:NONE

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


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

Modified Files:
	configure.zcml event.py mailer.py meta.zcml metaconfigure.py 
	service.py 
Removed Files:
	mail.py 
Log Message:
Transactionaly safe QueuedMailService merged into head.
If you want to play with it, uncomment the example tag in
zope/app/mail/configure.zcml .


=== Zope3/src/zope/app/mail/configure.zcml 1.1 => 1.2 ===
--- Zope3/src/zope/app/mail/configure.zcml:1.1	Wed Apr 16 09:45:43 2003
+++ Zope3/src/zope/app/mail/configure.zcml	Mon Jun 23 11:45:39 2003
@@ -1,21 +1,25 @@
 <zopeConfigure
    xmlns="http://namespaces.zope.org/zope"
-   xmlns:service="http://namespaces.zope.org/service"
    xmlns:mail="http://namespaces.zope.org/mail"
    >
 
-<serviceType id="Mail" 
-	     interface="zope.app.interfaces.mail.IMailService" />
+  <serviceType id="Mail"
+	       interface="zope.app.interfaces.mail.IMailService" />
 
-<mail:mailservice name="Mail"
-    hostname="localhost" port="25"
-    class=".service.AsyncMailService" 
-    permission="zope.Public"/>
+  <permission id="zope.SendMail" 
+              title="Send out mail with arbitrary from and to addresses" />
 
-<mail:mailer name="SimpleMailer" class=".mailer.SimpleMailer" 
-             serviceType="Mail" default="True" /> 
+  <mail:sendmailMailer name="sendmail" />
 
-<mail:mailer name="BatchMailer" class=".mailer.BatchMailer" 
-             serviceType="Mail" /> 
+  <mail:smtpMailer name="smtp" hostname="localhost" port="25" />
+
+  <!--
+    To send mail, uncomment the following directive and be sure to
+    create the queue directory.
+
+  <mail:queuedService permission="zope.SendMail"
+                      queuePath="./queue"
+                      mailer="smtp" />
+   -->
 
 </zopeConfigure>


=== Zope3/src/zope/app/mail/event.py 1.2 => 1.3 ===
--- Zope3/src/zope/app/mail/event.py:1.2	Fri Jun  6 15:29:03 2003
+++ Zope3/src/zope/app/mail/event.py	Mon Jun 23 11:45:39 2003
@@ -15,14 +15,26 @@
 
 $Id$
 """
-from zope.app.interfaces.mail import IMailSentEvent
+from zope.app.interfaces.mail import IMailSentEvent, IMailErrorEvent
 from zope.interface import implements
 
+__metaclass__ = type
+
 
 class MailSentEvent:
     __doc__ = IMailSentEvent.__doc__
 
     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.4 => 1.5 ===
--- Zope3/src/zope/app/mail/mailer.py:1.4	Fri Jun  6 15:29:03 2003
+++ Zope3/src/zope/app/mail/mailer.py	Mon Jun 23 11:45:39 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/meta.zcml 1.1 => 1.2 ===
--- Zope3/src/zope/app/mail/meta.zcml:1.1	Wed Apr 16 09:45:43 2003
+++ Zope3/src/zope/app/mail/meta.zcml	Mon Jun 23 11:45:39 2003
@@ -1,93 +1,123 @@
 <zopeConfigure xmlns="http://namespaces.zope.org/zope">
-  
+
   <directives namespace="http://namespaces.zope.org/mail">
 
-    <directive name="mailservice" handler=".metaconfigure.mailservice">
+    <directive name="queuedService" handler=".metaconfigure.queuedService">
 
       <description>
-        This directive creates and registers a global mail service. It should
-        be only called once during startup. 
+        This directive creates and registers a global queued mail service. It
+        should be only called once during startup. 
       </description>
 
       <attribute name="name" required="no">
         <description>
-	  Specifies the Service name of the mail service. The default is
+          Specifies the Service name of the mail service. The default is
           "Mail".
         </description>
       </attribute>
 
       <attribute name="permission" required="yes">
         <description>
-	  Defines the permission that is required to use this object.
+          Defines the permission that is required to use this object.
         </description>
       </attribute>
 
-      <attribute name="class" required="yes">
+      <attribute name="queuePath" required="yes">
         <description>
-	  Class of the Mail Service.
+          Defines the path for the queue directory.
         </description>
       </attribute>
 
-      <attribute name="hostname" required="no">
+      <attribute name="mailer" required="yes">
         <description>
-	  Name of the server that is used to send the mail. Default is set to
-	  'localhost'.
+          The id of the mailer used by this service.
         </description>
       </attribute>
 
-      <attribute name="port" required="no">
+    </directive>
+
+    <directive name="directService" handler=".metaconfigure.directService">
+
+      <description>
+        This directive creates and registers a global direct mail service. It
+        should be only called once during startup. 
+      </description>
+
+      <attribute name="name" required="no">
         <description>
-	  Port on the server that is used to send the mail. Default is set to
-	  to the standard port '25'.
+          Specifies the Service name of the mail service. The default is
+          "Mail".
         </description>
       </attribute>
 
-      <attribute name="username" required="no">
+      <attribute name="permission" required="yes">
         <description>
-	  Some SMTP servers support authentication. If no username is given,
-          then the Mail Service will not try to use authentication.
+          Defines the permission that is required to use this object.
         </description>
       </attribute>
 
-      <attribute name="password" required="no">
+      <attribute name="mailer" required="yes">
+        <description>
+          Defines the mailer to be used for sending mail.
+        </description>
+      </attribute>
+
+    </directive>
+
+    <directive name="sendmailMailer" handler=".metaconfigure.sendmailMailer">
+
+      <description>
+        Registers a new Sendmail mailer.
+      </description>
+
+      <attribute name="id" required="yes">
+        <description>
+          Id of the mailer.
+        </description>
+      </attribute>
+
+      <attribute name="command" required="no">
         <description>
-	  Password that is used for authentication. Makes only sense in
-	  combination with username. 
+        A template command for sending out mail, containing %(from)s
+        and %(to)s for respective addresses.
         </description>
       </attribute>
 
     </directive>
 
-    <directive name="mailer" handler=".metaconfigure.mailer">
+    <directive name="smtpMailer" handler=".metaconfigure.smtpMailer">
 
       <description>
-        Registers a new mailer class wiht the global translation service.
+        Registers a new SMTP mailer.
       </description>
 
-      <attribute name="name" required="yes">
+      <attribute name="id" required="yes">
         <description>
-	  Name of the mailer class under which it is registered in the global
-          mail service.
+          Name of the mailer.
         </description>
       </attribute>
 
-      <attribute name="class" required="yes">
+      <attribute name="hostname" required="no">
         <description>
-	  The class representing this object.
+        Hostname of the SMTP host.
         </description>
       </attribute>
 
-      <attribute name="serviceType" required="no">
+      <attribute name="port" required="no">
         <description>
-	  Specifies the service type for which the mailer should be
-          registered. The default is "Mail".
+        Port of the SMTP server.
         </description>
       </attribute>
 
-      <attribute name="default" required="no">
+      <attribute name="username" required="no">
+        <description>
+        A username for SMTP AUTH.
+        </description>
+      </attribute>
+
+      <attribute name="password" required="no">
         <description>
-	  Specifies whether this mailer is the default mailer. The default
-	  value for the 'default' attribute is "False".
+        A password for SMTP AUTH.
         </description>
       </attribute>
 
@@ -95,4 +125,4 @@
 
   </directives>
 
-</zopeConfigure>
\ No newline at end of file
+</zopeConfigure>


=== Zope3/src/zope/app/mail/metaconfigure.py 1.2 => 1.3 ===
--- Zope3/src/zope/app/mail/metaconfigure.py:1.2	Mon May 19 06:03:37 2003
+++ Zope3/src/zope/app/mail/metaconfigure.py	Mon Jun 23 11:45:39 2003
@@ -15,44 +15,102 @@
 
 $Id$
 """
+
 from zope.component import getService
 from zope.configuration.action import Action
+from zope.configuration.exceptions import ConfigurationError
 from zope.app.component.metaconfigure import provideService
+from zope.app.mail.service import QueuedMailService, DirectMailService
+from zope.app.mail.service import QueueProcessorThread
+from zope.app.mail.mailer import SendmailMailer, SMTPMailer
+
+
+def queuedService(_context, permission, queuePath, mailer, name="Mail"):
+    # XXX what if queuePath is relative?  I'd like to make it absolute here,
+    # but should it be relative to $CWD or $INSTANCE_HOME (if there is one
+    # in Zope 3)?
+
+    def createQueuedService():
+        component = QueuedMailService(queuePath)
+        provideService(name, component, permission)
+
+        thread = QueueProcessorThread()
+        thread.setMailer(getMailer(mailer))
+        thread.setQueuePath(queuePath)
+        thread.setDaemon(True)
+        thread.start()
 
+    return [
+        Action(
+            discriminator = ('service', name),
+            callable = createQueuedService,
+            args = (),
+            )
+        ]
 
-def mailservice(_context, class_, permission, name="Mail",
-                hostname="localhost", port=25, username=None, password=None):
+def directService(_context, permission, mailer, name="Mail"):
 
-    component = _context.resolve(class_)()
-    component.hostname = hostname
-    component.port = int(port)
-    component.username = username
-    component.password = password
+    def makeService():
+        mailer_component = queryMailer(mailer)
+        if mailer_component is None:
+            raise ConfigurationError("Mailer %r is not defined" % mailer)
+        component = DirectMailService(mailer_component)
+        provideService(name, component, permission)
 
     return [
         Action(
             discriminator = ('service', name),
-            callable = provideService,
-            args = (name, component, permission),
+            callable = makeService,
+            args = (),
             )
         ]
 
 
-def mailer(_context, name, class_, serviceType="Mail", default=False):
-    klass = _context.resolve(class_)
-
-    if default == "True":
-        default = True
-
-    def register(serviceType, name, klass, default):
-        mailservice = getService(None, serviceType)
-        mailservice.provideMailer(name, klass, default)
+def sendmailMailer(_context, id,
+                   command="/usr/lib/sendmail -oem -oi -f %(from)s %(to)s"):
+    return [Action(discriminator=('mailer', id),
+                   callable=provideMailer,
+                   args=(id, SendmailMailer(command)),)
+        ]
 
 
-    return [
-        Action(
-             discriminator = ('mailer', name),
-             callable = register,
-             args = (serviceType, name, klass, default)
-             )
+def smtpMailer(_context, id, hostname="localhost", port="25",
+               username=None, password=None):
+    return [Action(discriminator=('mailer', id),
+                   callable=provideMailer,
+                   args=(id, SMTPMailer(hostname, port,
+                                          username, password)),)
         ]
+
+# Example of mailer configuration:
+#
+#   def smtp(_context, id, hostname, port):
+#       component = SMTPMailer(hostname, port)
+#       if queryMailer(id) is not None:
+#           raise ConfigurationError("Redefinition of mailer %r" % id)
+#       provideMailer(id, component)
+#       return []
+#
+# or is it better to make mailer registration an Action?  But that won't work,
+# because queryMailer will get called during directive processing, before any
+# actions are run.
+
+
+mailerRegistry = {}
+queryMailer = mailerRegistry.get
+provideMailer = mailerRegistry.__setitem__
+
+def getMailer(mailer):
+    result = queryMailer(mailer)
+    if result is None:
+        raise ConfigurationError("Mailer lookup failed")
+    return result
+
+# Register our cleanup with Testing.CleanUp to make writing unit tests simpler.
+try:
+    from zope.testing.cleanup import addCleanUp
+except ImportError:
+    pass
+else:
+    addCleanUp(mailerRegistry.clear)
+    del addCleanUp


=== Zope3/src/zope/app/mail/service.py 1.3 => 1.4 ===
--- Zope3/src/zope/app/mail/service.py:1.3	Fri Jun  6 15:29:03 2003
+++ Zope3/src/zope/app/mail/service.py	Mon Jun 23 11:45:39 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

=== Removed File Zope3/src/zope/app/mail/mail.py ===