[Zope3-checkins] CVS: Zope3/src/zope/app/mail - __init__.py:1.1 configure.zcml:1.1 event.py:1.1 mail.py:1.1 mailer.py:1.1 meta.zcml:1.1 metaconfigure.py:1.1 service.py:1.1

Stephan Richter srichter@cbu.edu
Wed, 16 Apr 2003 09:45:44 -0400


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

Added Files:
	__init__.py configure.zcml event.py mail.py mailer.py 
	meta.zcml metaconfigure.py service.py 
Log Message:
This is a first stab at a flexible facility to send out mail. I discussed
the API with Barry a bit, before writing this code. 

Goals:

  - Keep the actual IMailService interface as simple as possible.

  - Allow flexible mechanisms to send mail.

  - Be able to implement and register you own mail sending policies. 

Implementation:

  - The global mail service is an asynchronous mail sender, which means 
    it does not block the request. Once it is done, it will send an event
    to which interested parties can subscribe.

  - The AsyncMailService registers and works with Mailer objects, which 
    encode the policy of the mailing. I supplied a SimpleMailer, which has
    no specific policies and a BatchMailer, which allows to send the 
    messages in batches, so that the SMTP server will not overload. I 
    currently do not have any of the mailers in action, but I am going to
    add MailService support to the zwiki product for testing purposes soon.

  - MailService will make heavy use of the EventService for publishing 
    status updates, which is necessary due to its async nature. Currently 
    I have only one event, but I plan to add more soon.

To Do:

  - I lied about the AsyncMailService to be async! :-) I actually have not 
    implemented the async behavior yet; I need help from other people for
    this feature. Should I use thread or asyncore, and how do I use either 
    one. I am not really knowledgable in this area.

  - As mentioned above, add more events.

  - Come up with advanced unit tests for the BatchMailer.

  - Put the MailService-specific ZCML directive into a higher up ZCML file,
    since it contains server-specific information, similar to the ones for 
    HTTP and FTP.


=== Added File Zope3/src/zope/app/mail/__init__.py ===


=== Added File Zope3/src/zope/app/mail/configure.zcml ===
<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" />

<mail:mailservice name="Mail"
    hostname="localhost" port="25"
    class=".service.AsyncMailService" 
    permission="zope.Public"/>

<mail:mailer name="SimpleMailer" class=".mailer.SimpleMailer" 
             serviceType="Mail" default="True" /> 

<mail:mailer name="BatchMailer" class=".mailer.BatchMailer" 
             serviceType="Mail" /> 

</zopeConfigure>


=== Added File Zope3/src/zope/app/mail/event.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Collection of possible Mail Events.

$Id: event.py,v 1.1 2003/04/16 13:45:43 srichter Exp $
"""
from zope.app.interfaces.mail import IMailSentEvent


class MailSentEvent:
    __doc__ = IMailSentEvent.__doc__

    __implements__ =  IMailSentEvent

    def __init__(self, mailer):
        self.mailer = mailer 


=== Added File Zope3/src/zope/app/mail/mail.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""MailService Implementation

Simple implementation of the MailService, Mailers and MailEvents.

$Id: mail.py,v 1.1 2003/04/16 13:45:43 srichter Exp $
"""
from smtplib import SMTP

from zope.app.interfaces.mail import IAsyncMailService, IMailer, IMailSentEvent
from zope.app.event import publish

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


class SimpleMailer:
    __doc__ = IMailer.__doc__

    __implements__ = IMailer

    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 MailSentEvent:
    __doc__ = IMailSentEvent.__doc__

    __implements__ =  IMailSentEvent

    def __init__(self, mailer):
        self.mailer = mailer 


=== Added File Zope3/src/zope/app/mail/mailer.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""A c

Simple implementation of the MailService, Mailers and MailEvents.

$Id: mailer.py,v 1.1 2003/04/16 13:45:43 srichter Exp $
"""
from smtplib import SMTP
from time import sleep

from zope.app.interfaces.mail import IMailer, IBatchMailer
from zope.app.event import publish
from zope.app.mail.event import MailSentEvent


class SimpleMailer:
    __doc__ = IMailer.__doc__

    __implements__ = IMailer

    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))


=== Added File Zope3/src/zope/app/mail/meta.zcml ===
<zopeConfigure xmlns="http://namespaces.zope.org/zope">
  
  <directives namespace="http://namespaces.zope.org/mail">

    <directive name="mailservice" handler=".metaconfigure.mailservice">

      <description>
        This directive creates and registers a global 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
          "Mail".
        </description>
      </attribute>

      <attribute name="permission" required="yes">
        <description>
	  Defines the permission that is required to use this object.
        </description>
      </attribute>

      <attribute name="class" required="yes">
        <description>
	  Class of the Mail Service.
        </description>
      </attribute>

      <attribute name="hostname" required="no">
        <description>
	  Name of the server that is used to send the mail. Default is set to
	  'localhost'.
        </description>
      </attribute>

      <attribute name="port" required="no">
        <description>
	  Port on the server that is used to send the mail. Default is set to
	  to the standard port '25'.
        </description>
      </attribute>

      <attribute name="username" required="no">
        <description>
	  Some SMTP servers support authentication. If no username is given,
          then the Mail Service will not try to use authentication.
        </description>
      </attribute>

      <attribute name="password" required="no">
        <description>
	  Password that is used for authentication. Makes only sense in
	  combination with username. 
        </description>
      </attribute>

    </directive>

    <directive name="mailer" handler=".metaconfigure.mailer">

      <description>
        Registers a new mailer class wiht the global translation service.
      </description>

      <attribute name="name" required="yes">
        <description>
	  Name of the mailer class under which it is registered in the global
          mail service.
        </description>
      </attribute>

      <attribute name="class" required="yes">
        <description>
	  The class representing this object.
        </description>
      </attribute>

      <attribute name="serviceType" required="no">
        <description>
	  Specifies the service type for which the mailer should be
          registered. The default is "Mail".
        </description>
      </attribute>

      <attribute name="default" required="no">
        <description>
	  Specifies whether this mailer is the default mailer. The default
	  value for the 'default' attribute is "False".
        </description>
      </attribute>

    </directive>

  </directives>

</zopeConfigure>

=== Added File Zope3/src/zope/app/mail/metaconfigure.py ===
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
# 
##############################################################################
"""mail ZCML Namespace handler 

$Id: metaconfigure.py,v 1.1 2003/04/16 13:45:43 srichter Exp $
"""
from zope.component import getService
from zope.configuration.action import Action
from zope.app.component.metaconfigure import provideService


def mailservice(_context, class_, permission, name="Mail",
                hostname="localhost", port=25, username=None, password=None):

    component = _context.resolve(class_)()
    component.hostname = hostname
    component.port = int(port)
    component.username = username
    component.password = password

    return [
        Action(
            discriminator = ('service', name),
            callable = provideService,
            args = (name, component, permission),
            )
        ]


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)
        

    return [
        Action(
             discriminator = ('mailer', name),
             callable = register,
             args = (serviceType, name, klass, default)
             )
        ]


=== Added File Zope3/src/zope/app/mail/service.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""MailService Implementation

This module contains various implementations of MailServices.

$Id: service.py,v 1.1 2003/04/16 13:45:43 srichter Exp $
"""
from zope.app.interfaces.mail import IAsyncMailService 

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