[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 ===