[CalendarServer-changes] [10703] CalendarServer/branches/users/sagen/usingqueue

source_changes at macosforge.org source_changes at macosforge.org
Tue Feb 12 13:06:06 PST 2013


Revision: 10703
          http://trac.calendarserver.org//changeset/10703
Author:   sagen at apple.com
Date:     2013-02-12 13:06:06 -0800 (Tue, 12 Feb 2013)
Log Message:
-----------
work in progress towards using queue

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/usingqueue/calendarserver/push/applepush.py
    CalendarServer/branches/users/sagen/usingqueue/calendarserver/tap/caldav.py
    CalendarServer/branches/users/sagen/usingqueue/calendarserver/tap/util.py
    CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/changeip_calendar.py
    CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/test/test_changeip.py
    CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/util.py
    CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/ienterprise.py
    CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/queue.py
    CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/test/test_queue.py
    CalendarServer/branches/users/sagen/usingqueue/twisted/plugins/caldav.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/customxml.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/resource.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/delivery.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/mailgateway.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/scheduler.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_mailgateway.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/ischedule/resource.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/upgrade.py
    CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/file.py
    CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/sql.py
    CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/sagen/usingqueue/txdav/common/icommondatastore.py

Added Paths:
-----------
    CalendarServer/branches/users/sagen/usingqueue/calendarserver/push/notifier.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/inbound.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/outbound.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/smtpsender.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_inbound.py
    CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_outbound.py

Modified: CalendarServer/branches/users/sagen/usingqueue/calendarserver/push/applepush.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/calendarserver/push/applepush.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/calendarserver/push/applepush.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -139,6 +139,16 @@
         return service
 
 
+    _theService = None
+
+    @classmethod
+    def getService(cls, settings, store, serverHostName):
+        if cls._theService is None:
+            cls._theService = cls.makeService(settings, store, serverHostName)
+            cls._theService.startService()
+        return cls._theService
+
+
     def startService(self):
         """
         In addition to starting the provider and feedback sub-services, start a

Added: CalendarServer/branches/users/sagen/usingqueue/calendarserver/push/notifier.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/calendarserver/push/notifier.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/usingqueue/calendarserver/push/notifier.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -0,0 +1,186 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Notification framework for Calendar Server
+"""
+
+from twext.python.log import LoggingMixIn, Logger
+
+from twisted.internet.defer import inlineCallbacks, succeed
+from twext.enterprise.dal.record import fromTable
+from twext.enterprise.queue import WorkItem
+from txdav.common.datastore.sql_tables import schema
+from twistedcaldav.config import config
+
+
+log = Logger()
+
+
+class PushNotificationWork(WorkItem, fromTable(schema.PUSH_NOTIFICATION_WORK)):
+
+    @inlineCallbacks
+    def doWork(self):
+        from calendarserver.push.applepush import ApplePushNotifierService
+        print "DO WORK, pushID:", self.pushID
+        store = self.transaction._store
+        settings = config.Notifications.Services.ApplePushNotifier
+        service = ApplePushNotifierService.getService(settings, store,
+            config.ServerHostName)
+        yield service.enqueue(None, self.pushID)
+
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Classes used within calendarserver itself
+#
+
+class Notifier(LoggingMixIn):
+    """
+    Provides a hook for sending change notifications to the
+    L{NotifierFactory}.
+    """
+
+    def __init__(self, notifierFactory, label="default", id=None, prefix=None):
+        self._notifierFactory = notifierFactory
+        self._ids = { label : self.normalizeID(id) }
+        self._notify = True
+        self._prefix = prefix
+
+    def normalizeID(self, id):
+        urn = "urn:uuid:"
+        try:
+            if id.startswith(urn):
+                return id[len(urn):]
+        except AttributeError:
+            pass
+        return id
+
+    def enableNotify(self, arg):
+        self.log_debug("enableNotify: %s" % (self._ids['default'][1],))
+        self._notify = True
+
+    def disableNotify(self):
+        self.log_debug("disableNotify: %s" % (self._ids['default'][1],))
+        self._notify = False
+
+    @inlineCallbacks
+    def notify(self):
+        for label in self._ids.iterkeys():
+            print "...label", label
+            id = self.getID(label=label)
+            print "...id", id
+            if id is not None:
+                if self._notify:
+                    self.log_debug("Notifications are enabled: %s %s" %
+                        (label, id))
+                    yield self._notifierFactory.send(id)
+                else:
+                    self.log_debug("Skipping notification for: %s" % (id,))
+
+    def clone(self, label="default", id=None):
+        newNotifier = self.__class__(self._notifierFactory)
+        newNotifier._ids = self._ids.copy()
+        newNotifier._ids[label] = id
+        newNotifier._prefix = self._prefix
+        return newNotifier
+
+    def addID(self, label="default", id=None):
+        self._ids[label] = self.normalizeID(id)
+
+    def getID(self, label="default"):
+        id = self._ids.get(label, None)
+        if self._prefix is None:
+            return id
+        else:
+            return "%s|%s" % (self._prefix, id)
+
+    def nodeName(self, label="default"):
+        id = self.getID(label=label)
+        return succeed(self._notifierFactory.pushKeyForId(id))
+
+
+class NotifierFactory(LoggingMixIn):
+    """
+    Notifier Factory
+
+    Creates Notifier instances and forwards notifications from them to the
+    work queue.
+    """
+
+    def __init__(self, store, hostname, reactor=None):
+        self.store = store
+        self.hostname = hostname
+
+        if reactor is None:
+            from twisted.internet import reactor
+        self.reactor = reactor
+
+    @inlineCallbacks
+    def send(self, id):
+        txn = self.store.newTransaction()
+        yield txn.enqueue(PushNotificationWork, pushID=id)
+        yield txn.commit()
+
+    def newNotifier(self, label="default", id=None, prefix=None):
+        return Notifier(self, label=label, id=id, prefix=prefix)
+
+    def pushKeyForId(self, id):
+        path = "/"
+
+        try:
+            prefix, id = id.split("|", 1)
+            path += "%s/" % (prefix,)
+        except ValueError:
+            # id has no prefix
+            pass
+
+        path += "%s/" % (self.hostname,)
+        if id:
+            path += "%s/" % (id,)
+        return path
+
+
+
+def getPubSubAPSConfiguration(id, config):
+    """
+    Returns the Apple push notification settings specific to the notifier
+    ID, which includes a prefix that is either "CalDAV" or "CardDAV"
+    """
+    try:
+        prefix, id = id.split("|", 1)
+    except ValueError:
+        # id has no prefix, so we can't look up APS config
+        return None
+
+    # If we are directly talking to apple push, advertise those settings
+    applePushSettings = config.Notifications.Services.ApplePushNotifier
+    if applePushSettings.Enabled:
+        settings = {}
+        settings["APSBundleID"] = applePushSettings[prefix]["Topic"]
+        if config.EnableSSL:
+            url = "https://%s:%s/%s" % (config.ServerHostName, config.SSLPort,
+                applePushSettings.SubscriptionURL)
+        else:
+            url = "http://%s:%s/%s" % (config.ServerHostName, config.HTTPPort,
+                applePushSettings.SubscriptionURL)
+        settings["SubscriptionURL"] = url
+        settings["SubscriptionRefreshIntervalSeconds"] = applePushSettings.SubscriptionRefreshIntervalSeconds
+        settings["APSEnvironment"] = applePushSettings.Environment
+        return settings
+
+    return None
+

Modified: CalendarServer/branches/users/sagen/usingqueue/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/calendarserver/tap/caldav.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/calendarserver/tap/caldav.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -70,7 +70,6 @@
 from twistedcaldav.config import ConfigurationError
 from twistedcaldav.config import config
 from twistedcaldav.localization import processLocalizationFiles
-from twistedcaldav.scheduling.imip.resource import IMIPReplyInboxResource
 from twistedcaldav import memcachepool
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
 from twistedcaldav.upgrade import UpgradeFileSystemFormatService, PostDBImportService
@@ -511,7 +510,6 @@
 
         - regular slave processes (CalDAV workers)
         - notifier
-        - mail gateway
     """
 
     def __init__(self, maker, monitor, dispenser, dispatcher, configPath,
@@ -540,51 +538,7 @@
             )
             self.monitor.addProcessObject(process, PARENT_ENVIRONMENT)
 
-        if (
-            config.Notifications.Enabled and
-            config.Notifications.InternalNotificationHost == "localhost"
-        ):
-            self.maker.log_info("Adding notification service")
 
-            notificationsArgv = [
-                sys.executable,
-                sys.argv[0],
-            ]
-            if config.UserName:
-                notificationsArgv.extend(("-u", config.UserName))
-            if config.GroupName:
-                notificationsArgv.extend(("-g", config.GroupName))
-            notificationsArgv.extend((
-                "--reactor=%s" % (config.Twisted.reactor,),
-                "-n", self.maker.notifierTapName,
-                "-f", self.configPath,
-            ))
-            self.monitor.addProcess("notifications", notificationsArgv,
-                env=PARENT_ENVIRONMENT)
-
-        if (
-            config.Scheduling.iMIP.Enabled and
-            config.Scheduling.iMIP.MailGatewayServer == "localhost"
-        ):
-            self.maker.log_info("Adding mail gateway service")
-
-            mailGatewayArgv = [
-                sys.executable,
-                sys.argv[0],
-            ]
-            if config.UserName:
-                mailGatewayArgv.extend(("-u", config.UserName))
-            if config.GroupName:
-                mailGatewayArgv.extend(("-g", config.GroupName))
-            mailGatewayArgv.extend((
-                "--reactor=%s" % (config.Twisted.reactor,),
-                "-n", self.maker.mailGatewayTapName,
-                "-f", self.configPath,
-            ))
-
-            self.monitor.addProcess("mailgateway", mailGatewayArgv,
-                               env=PARENT_ENVIRONMENT)
-
         if config.GroupCaching.Enabled and config.GroupCaching.EnableUpdater:
             self.maker.log_info("Adding group caching service")
 
@@ -667,7 +621,6 @@
     #
     # Default tap names
     #
-    mailGatewayTapName = "caldav_mailgateway"
     notifierTapName = "caldav_notifier"
     groupMembershipCacherTapName = "caldav_groupcacher"
 
@@ -801,9 +754,9 @@
         oldLogLevel = logLevelForNamespace(None)
         setLogLevelForNamespace(None, "info")
 
+        # Note: 'additional' was used for IMIP reply resource, and perhaps
+        # we can remove this
         additional = []
-        if config.Scheduling.iMIP.Enabled:
-            additional.append(("inbox", IMIPReplyInboxResource, [], ("digest",)))
 
         #
         # Configure the service
@@ -836,7 +789,7 @@
             from txdav.common.datastore.sql import CommonDataStore as SQLStore
             if isinstance(store, SQLStore):
                 def queueMasterAvailable(connectionFromMaster):
-                    store.queuer = connectionFromMaster
+                    store.queuer = store.queuer.transferProposalCallbacks(connectionFromMaster)
                 queueFactory = QueueWorkerFactory(store.newTransaction, schema,
                                                   queueMasterAvailable)
                 controlSocketClient.addFactory(_QUEUE_ROUTE, queueFactory)

Modified: CalendarServer/branches/users/sagen/usingqueue/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/calendarserver/tap/util.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/calendarserver/tap/util.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -57,7 +57,7 @@
 from twistedcaldav.directory.internal import InternalDirectoryService
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 from twistedcaldav.directory.wiki import WikiDirectoryService
-from twistedcaldav.notify import NotifierFactory, getPubSubConfiguration
+from calendarserver.push.notifier import NotifierFactory 
 from calendarserver.push.applepush import APNSubscriptionResource
 from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
 from twistedcaldav.resource import AuthenticationWrapper
@@ -223,11 +223,9 @@
     # Configure NotifierFactory
     #
     if config.Notifications.Enabled:
-        notifierFactory = NotifierFactory(
-            config.Notifications.InternalNotificationHost,
-            config.Notifications.InternalNotificationPort,
-            pubSubConfig=getPubSubConfiguration(config)
-        )
+        # FIXME: NotifierFactory needs reference to the store in order
+        # to get a txn in order to create a Work item
+        notifierFactory = NotifierFactory(None, config.ServerHostName)
     else:
         notifierFactory = None
     quota = config.UserQuota
@@ -239,7 +237,7 @@
         else:
             uri = "http://%s:%s" % (config.ServerHostName, config.HTTPPort,)
         attachments_uri = uri + "/calendars/__uids__/%(home)s/dropbox/%(dropbox_id)s/%(name)s"
-        return CommonSQLDataStore(
+        store = CommonSQLDataStore(
             txnFactory, notifierFactory,
             FilePath(config.AttachmentsRoot), attachments_uri,
             config.EnableCalDAV, config.EnableCardDAV,
@@ -256,11 +254,13 @@
             cacheExpireSeconds=config.QueryCaching.ExpireSeconds
         )
     else:
-        return CommonFileDataStore(
+        store = CommonFileDataStore(
             FilePath(config.DocumentRoot),
             notifierFactory, config.EnableCalDAV, config.EnableCardDAV,
             quota=quota
         )
+    notifierFactory.store = store
+    return store
 
 
 
@@ -268,7 +268,6 @@
     """
     Create an L{AggregateDirectoryService} from the given configuration.
     """
-
     #
     # Setup the Augment Service
     #
@@ -362,7 +361,7 @@
 
 
 
-def getRootResource(config, newStore, resources=None):
+def getRootResource(config, newStore, resources=None, directory=None):
     """
     Set up directory service and resource hierarchy based on config.
     Return root resource.
@@ -398,7 +397,8 @@
     directoryBackedAddressBookResourceClass = DirectoryBackedAddressBookResource
     apnSubscriptionResourceClass = APNSubscriptionResource
 
-    directory = directoryFromConfig(config)
+    if directory is None:
+        directory = directoryFromConfig(config)
 
     #
     # Setup the ProxyDB Service
@@ -706,6 +706,10 @@
         directory,
     )
 
+    # FIXME:  Storing a reference to the root resource on the store
+    # until scheduling no longer needs resource objects
+    newStore.rootResource = root
+
     return logWrapper
 
 

Modified: CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/changeip_calendar.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/changeip_calendar.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/changeip_calendar.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -98,9 +98,6 @@
     keys = (
         ("Authentication", "Wiki", "Hostname"),
         ("BindAddresses",),
-        ("Notifications", "Services", "XMPPNotifier", "Host"),
-        ("Notifications", "Services", "XMPPNotifier", "JID"),
-        ("Notifications", "Services", "XMPPNotifier", "ServiceAddress"),
         ("Scheduling", "iMIP", "Receiving", "Server"),
         ("Scheduling", "iMIP", "Sending", "Server"),
         ("Scheduling", "iMIP", "Sending", "Address"),

Modified: CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/test/test_changeip.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/test/test_changeip.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/test/test_changeip.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -35,14 +35,6 @@
                 "original_hostname",
             ],
             "ServerHostName" : "",
-            "Notifications" : {
-                "Services" : {
-                    "XMPPNotifier" : {
-                        "Host" : "original_hostname",
-                        "JID" : "com.apple.notificationuser at original_hostname",
-                    },
-                },
-            },
             "Scheduling" : {
                 "iMIP" : {
                     "Receiving" : {
@@ -74,14 +66,6 @@
                     "new_hostname",
                 ],
                 "ServerHostName" : "",
-                "Notifications" : {
-                    "Services" : {
-                        "XMPPNotifier" : {
-                            "Host" : "new_hostname",
-                            "JID" : "com.apple.notificationuser at new_hostname",
-                        },
-                    },
-                },
                 "Scheduling" : {
                     "iMIP" : {
                         "Receiving" : {

Modified: CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/util.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/calendarserver/tools/util.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -44,7 +44,7 @@
 from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.notify import NotifierFactory
+from calendarserver.push.notifier import NotifierFactory
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 
 from txdav.common.datastore.file import CommonDataStore
@@ -77,9 +77,10 @@
             if not hasattr(self, "_principalCollection"):
 
                 if config.Notifications.Enabled:
+                    # FIXME: NotifierFactory needs reference to the store in order
+                    # to get a txn in order to create a Work item
                     notifierFactory = NotifierFactory(
-                        config.Notifications.InternalNotificationHost,
-                        config.Notifications.InternalNotificationPort,
+                        None, config.ServerHostName,
                     )
                 else:
                     notifierFactory = None
@@ -87,6 +88,7 @@
                 # Need a data store
                 _newStore = CommonDataStore(FilePath(config.DocumentRoot), 
                     notifierFactory, True, False)
+                notifierFactory.store = _newStore
 
                 #
                 # Instantiating a DirectoryCalendarHomeProvisioningResource with a directory

Modified: CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/ienterprise.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/ienterprise.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/ienterprise.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -296,3 +296,16 @@
         @rtype: L{twext.enterprise.queue.WorkItem}
         """
 
+
+    def registerProposalCallback(self, callback):
+        """
+        Tells the IQueuer to call a callback method whenever a new WorkProposal
+        is created.
+
+        @param callback: a callable which accepts a single parameter, a
+            L{WorkProposal}
+        """
+
+    def transferProposalCallbacks(self, newQueuer):
+        """
+        """
\ No newline at end of file

Modified: CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/queue.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/queue.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/queue.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -742,6 +742,9 @@
                                                        boxReceiver, locator)
         self.transactionFactory = transactionFactory
         self.whenConnected = whenConnected
+        # FIXME: Glyph it appears WorkProposal expects this to have reactor...
+        from twisted.internet import reactor
+        self.reactor = reactor
 
 
     def startReceivingBoxes(self, sender):
@@ -1012,9 +1015,47 @@
         """
         return _cloneDeferred(self._whenCommitted)
 
+class _BaseQueuer(object):
+    """
+    """
+    implements(IQueuer)
 
+    def __init__(self):
+        super(_BaseQueuer, self).__init__()
+        self.proposalCallbacks = set()
 
-class PeerConnectionPool(MultiService, object):
+    def registerProposalCallback(self, callback):
+        self.proposalCallbacks.add(callback);
+
+    def transferProposalCallbacks(self, newQueuer):
+        newQueuer.proposalCallbacks = self.proposalCallbacks
+        return newQueuer
+
+    def enqueueWork(self, txn, workItemType, **kw):
+        """
+        There is some work to do.  Do it, someplace else, ideally in parallel.
+        Later, let the caller know that the work has been completed by firing a
+        L{Deferred}.
+
+        @param workItemType: The type of work item to be enqueued.
+        @type workItemType: A subtype of L{WorkItem}
+
+        @param kw: The parameters to construct a work item.
+        @type kw: keyword parameters to C{workItemType.create}, i.e.
+            C{workItemType.__init__}
+
+        @return: an object that can track the enqueuing and remote execution of
+            this work.
+        @rtype: L{WorkProposal}
+        """
+        wp = WorkProposal(self, txn, workItemType, kw)
+        wp._start()
+        for callback in self.proposalCallbacks:
+            callback(wp)
+        return wp
+
+
+class PeerConnectionPool(_BaseQueuer, MultiService, object):
     """
     Each node has a L{PeerConnectionPool} connecting it to all the other nodes
     currently active on the same database.
@@ -1153,28 +1194,8 @@
         return self.choosePerformer(onlyLocally=True).performWork(table, workID)
 
 
-    def enqueueWork(self, txn, workItemType, **kw):
-        """
-        There is some work to do.  Do it, someplace else, ideally in parallel.
-        Later, let the caller know that the work has been completed by firing a
-        L{Deferred}.
 
-        @param workItemType: The type of work item to be enqueued.
-        @type workItemType: A subtype of L{WorkItem}
 
-        @param kw: The parameters to construct a work item.
-        @type kw: keyword parameters to C{workItemType.create}, i.e.
-            C{workItemType.__init__}
-
-        @return: an object that can track the enqueuing and remote execution of
-            this work.
-        @rtype: L{WorkProposal}
-        """
-        wp = WorkProposal(self, txn, workItemType, kw)
-        wp._start()
-        return wp
-
-
     def allWorkItemTypes(self):
         """
         Load all the L{WorkItem} types that this node can process and return
@@ -1397,13 +1418,16 @@
 
 
 
-class LocalQueuer(object):
+
+
+class LocalQueuer(_BaseQueuer):
     """
     When work is enqueued with this queuer, it is just executed locally.
     """
     implements(IQueuer)
 
     def __init__(self, txnFactory, reactor=None):
+        super(LocalQueuer, self).__init__()
         self.txnFactory = txnFactory
         if reactor is None:
             from twisted.internet import reactor
@@ -1414,23 +1438,4 @@
         """
         Choose to perform the work locally.
         """
-        return LocalPerformer(self.txnFactory)
-
-
-    def enqueueWork(self, txn, workItemType, **kw):
-        """
-        Do this work in the local process.
-
-        @see: L{PeerConnectionPool.enqueueWork}
-
-        @return: a pseudo work proposal, since everything completes at the same
-            time.
-        @rtype: L{WorkProposal}
-        """
-        wp = WorkProposal(self, txn, workItemType, kw)
-        wp._start()
-        return wp
-
-
-
-
+        return LocalPerformer(self.txnFactory)
\ No newline at end of file

Modified: CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/test/test_queue.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/test/test_queue.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twext/enterprise/test/test_queue.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -54,6 +54,9 @@
 from zope.interface.verify import verifyObject
 from twisted.test.proto_helpers import StringTransport
 
+from twext.enterprise.queue import _BaseQueuer
+import twext.enterprise.queue
+
 class Clock(_Clock):
     """
     More careful L{IReactorTime} fake which mimics the exception behavior of
@@ -627,3 +630,27 @@
         self.assertEquals(rows, [[4321, 7]])
 
 
+class DummyProposal(object):
+
+    def __init__(self, *ignored):
+        pass
+
+    def _start(self):
+        pass
+
+class BaseQueuerTests(TestCase):
+
+    def setUp(self):
+        self.proposal = None
+        self.patch(twext.enterprise.queue, "WorkProposal", DummyProposal)
+
+    def _proposalCallback(self, proposal):
+        self.proposal = proposal
+
+    def test_proposalCallbacks(self):
+        queuer = _BaseQueuer()
+        queuer.registerProposalCallback(self._proposalCallback)
+        self.assertEqual(self.proposal, None)
+        queuer.enqueueWork(None, None)
+        self.assertNotEqual(self.proposal, None)
+

Modified: CalendarServer/branches/users/sagen/usingqueue/twisted/plugins/caldav.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twisted/plugins/caldav.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twisted/plugins/caldav.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -53,6 +53,4 @@
 
 
 TwistedCalDAV = TAP("calendarserver.tap.caldav.CalDAVServiceMaker")
-CalDAVNotifier = TAP("twistedcaldav.notify.NotificationServiceMaker")
-CalDAVMailGateway = TAP("twistedcaldav.scheduling.imip.mailgateway.MailGatewayServiceMaker")
 CalDAVGroupCacher = TAP("twistedcaldav.directory.directory.GroupMembershipCacherServiceMaker")

Modified: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/customxml.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/customxml.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -470,66 +470,8 @@
 
 
 
- at registerElement
-class PubSubXMPPURIProperty (WebDAVTextElement):
-    """
-    A calendar home property to indicate the pubsub XMPP URI to subscribe to
-    for notifications.
-    """
-    namespace = calendarserver_namespace
-    name = "xmpp-uri"
-    protected = True
-    hidden = True
 
 
-
- at registerElement
-class PubSubHeartbeatProperty (WebDAVElement):
-    """
-    A calendar home property to indicate the pubsub XMPP URI to subscribe to
-    for server heartbeats.
-    """
-    namespace = calendarserver_namespace
-    name = "xmpp-heartbeat"
-    protected = True
-    hidden = True
-    allowed_children = {
-        (calendarserver_namespace, "xmpp-heartbeat-uri")     : (1, 1),
-        (calendarserver_namespace, "xmpp-heartbeat-minutes") : (1, 1),
-    }
-
-
-
- at registerElement
-class PubSubHeartbeatURIProperty (WebDAVTextElement):
-    namespace = calendarserver_namespace
-    name = "xmpp-heartbeat-uri"
-    protected = True
-    hidden = True
-
-
-
- at registerElement
-class PubSubHeartbeatMinutesProperty (WebDAVTextElement):
-    namespace = calendarserver_namespace
-    name = "xmpp-heartbeat-minutes"
-    protected = True
-    hidden = True
-
-
-
- at registerElement
-class PubSubXMPPServerProperty (WebDAVTextElement):
-    """
-    A calendar home property to indicate the pubsub XMPP hostname to
-    contact for notifications.
-    """
-    namespace = calendarserver_namespace
-    name = "xmpp-server"
-    protected = True
-    hidden = True
-
-
 PrincipalPropertySearch.allowed_children[(calendarserver_namespace, "limit")] = (0, 1)
 PrincipalPropertySearch.allowed_attributes["type"] = False
 Match.allowed_attributes = {

Modified: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/resource.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/resource.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -75,10 +75,7 @@
 
 from twistedcaldav.icaldav import ICalDAVResource, ICalendarPrincipalResource
 from twistedcaldav.linkresource import LinkResource
-from twistedcaldav.notify import (
-    getPubSubConfiguration, getPubSubXMPPURI, getPubSubHeartbeatURI,
-    getPubSubAPSConfiguration,
-)
+from calendarserver.push.notifier import getPubSubAPSConfiguration
 from twistedcaldav.sharing import SharedCollectionMixin, SharedHomeMixin
 from twistedcaldav.util import normalizationLookup
 from twistedcaldav.vcard import Component as vComponent
@@ -2321,8 +2318,7 @@
 
         elif qname == (customxml.calendarserver_namespace, "push-transports"):
 
-            if (config.Notifications.Services.XMPPNotifier.Enabled or
-                config.Notifications.Services.ApplePushNotifier.Enabled):
+            if config.Notifications.Services.ApplePushNotifier.Enabled:
 
                 nodeName = (yield self._newStoreHome.nodeName())
                 if nodeName:
@@ -2352,78 +2348,18 @@
                                 )
                             )
 
-                        pubSubConfiguration = getPubSubConfiguration(config)
-                        if (pubSubConfiguration['enabled'] and
-                            pubSubConfiguration['xmpp-server']):
-                            children.append(
-                                customxml.PubSubTransportProperty(
-                                    customxml.PubSubXMPPServerProperty(
-                                        pubSubConfiguration['xmpp-server']
-                                    ),
-                                    customxml.PubSubXMPPURIProperty(
-                                        getPubSubXMPPURI(notifierID, pubSubConfiguration)
-                                    ),
-                                    type="XMPP",
-                                )
-                            )
 
                         returnValue(customxml.PubSubPushTransportsProperty(*children))
             returnValue(None)
 
         elif qname == (customxml.calendarserver_namespace, "pushkey"):
-            if (config.Notifications.Services.XMPPNotifier.Enabled or
-                config.Notifications.Services.AMPNotifier.Enabled or
+            if (config.Notifications.Services.AMPNotifier.Enabled or
                 config.Notifications.Services.ApplePushNotifier.Enabled):
                 nodeName = (yield self._newStoreHome.nodeName())
                 if nodeName:
                     returnValue(customxml.PubSubXMPPPushKeyProperty(nodeName))
             returnValue(None)
 
-        elif qname == (customxml.calendarserver_namespace, "xmpp-uri"):
-            if config.Notifications.Services.XMPPNotifier.Enabled:
-                nodeName = (yield self._newStoreHome.nodeName())
-                if nodeName:
-                    notifierID = self._newStoreHome.notifierID()
-                    if notifierID:
-                        pubSubConfiguration = getPubSubConfiguration(config)
-                        returnValue(customxml.PubSubXMPPURIProperty(
-                            getPubSubXMPPURI(notifierID, pubSubConfiguration)))
-
-            returnValue(None)
-
-        elif qname == (customxml.calendarserver_namespace, "xmpp-heartbeat-uri"):
-            if config.Notifications.Services.XMPPNotifier.Enabled:
-                # Look up node name not because we want to return it, but
-                # to see if XMPP server is actually responding.  If it comes
-                # back with an empty nodeName, don't advertise
-                # xmpp-heartbeat-uri
-                nodeName = (yield self._newStoreHome.nodeName())
-                if nodeName:
-                    pubSubConfiguration = getPubSubConfiguration(config)
-                    returnValue(
-                        customxml.PubSubHeartbeatProperty(
-                            customxml.PubSubHeartbeatURIProperty(
-                                getPubSubHeartbeatURI(pubSubConfiguration)
-                            ),
-                            customxml.PubSubHeartbeatMinutesProperty(
-                                str(pubSubConfiguration['heartrate'])
-                            )
-                        )
-                    )
-            returnValue(None)
-
-        elif qname == (customxml.calendarserver_namespace, "xmpp-server"):
-            if config.Notifications.Services.XMPPNotifier.Enabled:
-                # Look up node name not because we want to return it, but
-                # to see if XMPP server is actually responding.  If it comes
-                # back with an empty nodeName, don't advertise xmpp-server
-                nodeName = (yield self._newStoreHome.nodeName())
-                if nodeName:
-                    pubSubConfiguration = getPubSubConfiguration(config)
-                    returnValue(customxml.PubSubXMPPServerProperty(
-                        pubSubConfiguration['xmpp-server']))
-            returnValue(None)
-
         returnValue((yield super(CommonHomeResource, self).readProperty(property, request)))
 
 
@@ -2556,11 +2492,6 @@
             #caldavxml.DefaultAlarmVToDoDate.qname(),
 
         )
-        existing += (
-            (customxml.calendarserver_namespace, "xmpp-uri"),
-            (customxml.calendarserver_namespace, "xmpp-heartbeat-uri"),
-            (customxml.calendarserver_namespace, "xmpp-server"),
-        )
 
         if config.EnableManagedAttachments:
             existing += (

Modified: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/delivery.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/delivery.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/delivery.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -33,11 +33,15 @@
 from twistedcaldav.config import config
 from twistedcaldav.util import AuthorizedHTTPGetter
 from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.imip.outbound import IMIPInvitationWork
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
 from twext.internet.gaiendpoint import GAIEndpoint
 from twext.internet.adaptendpoint import connect
+from twistedcaldav.directory.util import transactionFromRequest
 
 
+
+
 __all__ = [
     "ScheduleViaIMip",
 ]
@@ -105,9 +109,11 @@
 
                     fromAddr = str(self.scheduler.originator.cuaddr)
 
-                    log.debug("POSTing iMIP message to gateway...  To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, caldata,))
-                    yield self.postToGateway(fromAddr, toAddr, caldata)
+                    txn = transactionFromRequest(self.scheduler.request, self.scheduler.request._newStoreTransaction.store)
+                    log.debug("Submitting iMIP message...  To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, caldata,))
+                    yield txn.enqueue(IMIPInvitationWork, fromAddr=fromAddr, toAddr=toAddr, icalendarText=caldata)
 
+
                 except Exception, e:
                     # Generated failed response for this recipient
                     log.debug("iMIP request %s failed for recipient %s: %s" % (self, recipient, e))

Added: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/inbound.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/inbound.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/inbound.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -0,0 +1,701 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Inbound IMIP mail handling for Calendar Server
+"""
+
+from calendarserver.tap.util import FakeRequest
+from calendarserver.tap.util import getRootResource
+import email.utils
+from twext.enterprise.dal.record import fromTable
+from twext.enterprise.queue import WorkItem
+from twext.python.log import Logger, LoggingMixIn
+from twisted.application import internet, service
+from twisted.internet import protocol, defer, ssl
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.mail import pop3client, imap4
+from twisted.mail.smtp import messageid
+from twistedcaldav.config import config
+from twistedcaldav.ical import Property, Component
+from twistedcaldav.scheduling.imip.scheduler import IMIPScheduler
+from twistedcaldav.scheduling.imip.smtpsender import SMTPSender
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from txdav.common.datastore.sql_tables import schema
+
+log = Logger()
+
+#
+# Monkey patch imap4.log so it doesn't emit useless logging,
+# specifically, "Unhandled unsolicited response" nonsense.
+#
+class IMAPLogger(Logger):
+    def emit(self, level, message, *args, **kwargs):
+        if message.startswith("Unhandled unsolicited response:"):
+            return
+
+        Logger.emit(self, level, message, *args, **kwargs)
+
+imap4.log = IMAPLogger()
+
+
+""" SCHEMA:
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+"""
+
+class IMIPReplyWork(WorkItem, fromTable(schema.IMIP_REPLY_WORK)):
+
+    @inlineCallbacks
+    def doWork(self):
+        store = self.transaction._store
+        rootResource = store.rootResource
+        calendar = Component.fromString(self.icalendarText)
+        yield injectMessage(rootResource, self.organizer, self.attendee,
+            calendar)
+
+
+class MailRetrievalService(service.MultiService):
+
+    def __init__(self, store, directory):
+        self.store = store
+        self.directory = directory
+        self.root = getRootResource(config, self.store)
+        service.MultiService.__init__(self)
+
+    def startService(self):
+        settings = config.Scheduling['iMIP']
+        if settings['Enabled']:
+            mailReceiver = MailReceiver(self.store, self.directory, self.root)
+            mailType = settings['Receiving']['Type']
+            if mailType.lower().startswith('pop'):
+                client = POP3Service(settings['Receiving'], mailReceiver)
+            else:
+                client = IMAP4Service(settings['Receiving'], mailReceiver)
+            client.setServiceParent(self)
+        else:
+            log.info("iMIP Service not enabled")
+        service.MultiService.startService(self)
+
+
+
+
+class MailReceiver(object):
+
+    NO_TOKEN = 0
+    UNKNOWN_TOKEN = 1
+    MALFORMED_TO_ADDRESS = 2
+    NO_ORGANIZER_ADDRESS = 3
+    REPLY_FORWARDED_TO_ORGANIZER = 4
+    INJECTION_SUBMITTED = 5
+
+    # What about purge( ) and lowercase( )
+    def __init__(self, store, directory, root):
+        self.store = store
+        self.directory = directory
+        self.root = root
+
+    def checkDSN(self, message):
+        # returns (isdsn, action, icalendar attachment)
+
+        report = deliveryStatus = calBody = None
+
+        for part in message.walk():
+            contentType = part.get_content_type()
+            if contentType == "multipart/report":
+                report = part
+                continue
+            elif contentType == "message/delivery-status":
+                deliveryStatus = part
+                continue
+            elif contentType == "message/rfc822":
+                #original = part
+                continue
+            elif contentType == "text/calendar":
+                calBody = part.get_payload(decode=True)
+                continue
+
+        if report is not None and deliveryStatus is not None:
+            # we have what appears to be a dsn
+
+            lines = str(deliveryStatus).split("\n")
+            for line in lines:
+                lower = line.lower()
+                if lower.startswith("action:"):
+                    # found action:
+                    action = lower.split(' ')[1]
+                    break
+            else:
+                action = None
+
+            return True, action, calBody
+
+        else:
+            # not a dsn
+            return False, None, None
+
+
+    def _extractToken(self, text):
+        try:
+            pre, _ignore_post = text.split('@')
+            pre, token = pre.split('+')
+            return token
+        except ValueError:
+            return None
+
+
+    @inlineCallbacks
+    def processDSN(self, calBody, msgId):
+        calendar = Component.fromString(calBody)
+        # Extract the token (from organizer property)
+        organizer = calendar.getOrganizer()
+        token = self._extractToken(organizer)
+        if not token:
+            log.error("Mail gateway can't find token in DSN %s" % (msgId,))
+            return
+
+        txn = self.store.newTransaction()
+        result = (yield txn.imipLookupByToken(token))
+        yield txn.commit()
+        try:
+            # Note the results are returned as utf-8 encoded strings
+            organizer, attendee, icaluid = result[0]
+        except:
+            # This isn't a token we recognize
+            log.error("Mail gateway found a token (%s) but didn't "
+                           "recognize it in message %s"
+                           % (token, msgId))
+            returnValue(self.UNKNOWN_TOKEN)
+
+        calendar.removeAllButOneAttendee(attendee)
+        calendar.getOrganizerProperty().setValue(organizer)
+        for comp in calendar.subcomponents():
+            if comp.name() == "VEVENT":
+                comp.addProperty(Property("REQUEST-STATUS",
+                    ["5.1", "Service unavailable"]))
+                break
+        else:
+            # no VEVENT in the calendar body.
+            # TODO: what to do in this case?
+            pass
+
+        log.warn("Mail gateway processing DSN %s" % (msgId,))
+        txn = self.store.newTransaction()
+        yield txn.enqueue(IMIPReplyWork, organizer=organizer, attendee=attendee,
+            icalendarText=str(calendar))
+        yield txn.commit()
+        returnValue(self.INJECTION_SUBMITTED)
+
+
+    @inlineCallbacks
+    def processReply(self, msg):
+        # extract the token from the To header
+        _ignore_name, addr = email.utils.parseaddr(msg['To'])
+        if addr:
+            # addr looks like: server_address+token at example.com
+            token = self._extractToken(addr)
+            if not token:
+                log.error("Mail gateway didn't find a token in message "
+                               "%s (%s)" % (msg['Message-ID'], msg['To']))
+                returnValue(self.NO_TOKEN)
+        else:
+            log.error("Mail gateway couldn't parse To: address (%s) in "
+                           "message %s" % (msg['To'], msg['Message-ID']))
+            returnValue(self.MALFORMED_TO_ADDRESS)
+
+        txn = self.store.newTransaction()
+        result = (yield txn.imipLookupByToken(token))
+        yield txn.commit()
+        try:
+            # Note the results are returned as utf-8 encoded strings
+            organizer, attendee, icaluid = result[0]
+        except:
+            # This isn't a token we recognize
+            log.error("Mail gateway found a token (%s) but didn't "
+                           "recognize it in message %s"
+                           % (token, msg['Message-ID']))
+            returnValue(self.UNKNOWN_TOKEN)
+
+        for part in msg.walk():
+            if part.get_content_type() == "text/calendar":
+                calBody = part.get_payload(decode=True)
+                break
+        else:
+            # No icalendar attachment
+            log.warn("Mail gateway didn't find an icalendar attachment "
+                          "in message %s" % (msg['Message-ID'],))
+
+            toAddr = None
+            fromAddr = attendee[7:]
+            if organizer.startswith("mailto:"):
+                toAddr = organizer[7:]
+            elif organizer.startswith("urn:uuid:"):
+                guid = organizer[9:]
+                record = self.directory.recordWithGUID(guid)
+                if record and record.emailAddresses:
+                    toAddr = list(record.emailAddresses)[0]
+
+            if toAddr is None:
+                log.error("Don't have an email address for the organizer; "
+                               "ignoring reply.")
+                returnValue(self.NO_ORGANIZER_ADDRESS)
+
+            settings = config.Scheduling["iMIP"]["Sending"]
+            smtpSender = SMTPSender(settings.Username, settings.Password,
+                settings.UseSSL, settings.Server, settings.Port)
+
+            del msg["From"]
+            msg["From"] = fromAddr
+            del msg["Reply-To"]
+            msg["Reply-To"] = fromAddr
+            del msg["To"]
+            msg["To"] = toAddr
+            log.warn("Mail gateway forwarding reply back to organizer")
+            yield smtpSender.sendMessage(fromAddr, toAddr, messageid(), msg)
+            returnValue(self.REPLY_FORWARDED_TO_ORGANIZER)
+
+        # Process the imip attachment; inject to calendar server
+
+        log.debug(calBody)
+        calendar = Component.fromString(calBody)
+        event = calendar.mainComponent()
+
+        calendar.removeAllButOneAttendee(attendee)
+        organizerProperty = calendar.getOrganizerProperty()
+        if organizerProperty is None:
+            # ORGANIZER is required per rfc2446 section 3.2.3
+            log.warn("Mail gateway didn't find an ORGANIZER in REPLY %s"
+                          % (msg['Message-ID'],))
+            event.addProperty(Property("ORGANIZER", organizer))
+        else:
+            organizerProperty.setValue(organizer)
+
+        if not calendar.getAttendees():
+            # The attendee we're expecting isn't there, so add it back
+            # with a SCHEDULE-STATUS of SERVICE_UNAVAILABLE.
+            # The organizer will then see that the reply was not successful.
+            attendeeProp = Property("ATTENDEE", attendee,
+                params={
+                    "SCHEDULE-STATUS": iTIPRequestStatus.SERVICE_UNAVAILABLE,
+                }
+            )
+            event.addProperty(attendeeProp)
+
+            # TODO: We have talked about sending an email to the reply-to
+            # at this point, to let them know that their reply was missing
+            # the appropriate ATTENDEE.  This will require a new localizable
+            # email template for the message.
+
+        txn = self.store.newTransaction()
+        yield txn.enqueue(IMIPReplyWork, organizer=organizer, attendee=attendee,
+            icalendarText=str(calendar))
+        yield txn.commit()
+        returnValue(self.INJECTION_SUBMITTED)
+
+
+    # returns a deferred
+    def inbound(self, message):
+
+        try:
+            msg = email.message_from_string(message)
+
+            isDSN, action, calBody = self.checkDSN(msg)
+            if isDSN:
+                if action == 'failed' and calBody:
+                    # This is a DSN we can handle
+                    return self.processDSN(calBody, msg['Message-ID'])
+                else:
+                    # It's a DSN without enough to go on
+                    log.error("Mail gateway can't process DSN %s"
+                                   % (msg['Message-ID'],))
+                    return succeed(None)
+
+            log.info("Mail gateway received message %s from %s to %s" %
+                (msg['Message-ID'], msg['From'], msg['To']))
+
+            return self.processReply(msg)
+
+        except Exception, e:
+            # Don't let a failure of any kind stop us
+            log.error("Failed to process message: %s" % (e,))
+        return succeed(None)
+
+ at inlineCallbacks
+def injectMessage(root, organizer, attendee, calendar):
+
+    request = FakeRequest(root, None, "/")
+    resource = root.getChild("principals")
+    scheduler = IMIPScheduler(request, resource)
+    scheduler.originator = attendee
+    scheduler.recipients = [organizer,]
+    scheduler.calendar = calendar
+
+    results = (yield scheduler.doScheduling())
+
+    if results.success_response == 200:
+        log.info("Successfully injected iMIP response from %s to %s" %
+            (attendee, organizer))
+    else:
+        # FIXME: include error?
+        log.error("Failed to inject iMIP response from %s to %s" %
+            (attendee, organizer))
+
+    returnValue(results)
+
+
+    
+
+
+#
+# POP3
+#
+
+class POP3Service(service.Service, LoggingMixIn):
+
+    def __init__(self, settings, mailReceiver):
+        if settings["UseSSL"]:
+            self.client = internet.SSLClient(settings["Server"],
+                settings["Port"],
+                POP3DownloadFactory(settings, mailReceiver),
+                ssl.ClientContextFactory())
+        else:
+            self.client = internet.TCPClient(settings["Server"],
+                settings["Port"],
+                POP3DownloadFactory(settings, mailReceiver))
+
+        self.mailReceiver = mailReceiver
+
+
+    def startService(self):
+        self.client.startService()
+
+
+    def stopService(self):
+        self.client.stopService()
+
+
+
+class POP3DownloadProtocol(pop3client.POP3Client, LoggingMixIn):
+    allowInsecureLogin = False
+
+    def serverGreeting(self, greeting):
+        self.log_debug("POP servergreeting")
+        pop3client.POP3Client.serverGreeting(self, greeting)
+        login = self.login(self.factory.settings["Username"],
+            self.factory.settings["Password"])
+        login.addCallback(self.cbLoggedIn)
+        login.addErrback(self.cbLoginFailed)
+
+
+    def cbLoginFailed(self, reason):
+        self.log_error("POP3 login failed for %s" %
+            (self.factory.settings["Username"],))
+        return self.quit()
+
+
+    def cbLoggedIn(self, result):
+        self.log_debug("POP loggedin")
+        return self.listSize().addCallback(self.cbGotMessageSizes)
+
+
+    def cbGotMessageSizes(self, sizes):
+        self.log_debug("POP gotmessagesizes")
+        downloads = []
+        for i in range(len(sizes)):
+            downloads.append(self.retrieve(i).addCallback(self.cbDownloaded, i))
+        return defer.DeferredList(downloads).addCallback(self.cbFinished)
+
+
+    def cbDownloaded(self, lines, id):
+        self.log_debug("POP downloaded message %d" % (id,))
+        self.factory.handleMessage("\r\n".join(lines))
+        self.log_debug("POP deleting message %d" % (id,))
+        self.delete(id)
+
+
+    def cbFinished(self, results):
+        self.log_debug("POP finished")
+        return self.quit()
+
+
+
+class POP3DownloadFactory(protocol.ClientFactory, LoggingMixIn):
+    protocol = POP3DownloadProtocol
+
+    def __init__(self, settings, mailReceiver, reactor=None):
+        self.settings = settings
+        self.mailReceiver = mailReceiver
+        if reactor is None:
+            from twisted.internet import reactor
+        self.reactor = reactor
+        self.nextPoll = None
+        self.noisy = False
+
+
+    def retry(self, connector=None):
+        # TODO: if connector is None:
+
+        if connector is None:
+            if self.connector is None:
+                self.log_error("No connector to retry")
+                return
+            else:
+                connector = self.connector
+
+        def reconnector():
+            self.nextPoll = None
+            connector.connect()
+
+        self.log_debug("Scheduling next POP3 poll")
+        self.nextPoll = self.reactor.callLater(self.settings["PollingSeconds"],
+            reconnector)
+
+
+    def clientConnectionLost(self, connector, reason):
+        self.connector = connector
+        self.log_debug("POP factory connection lost")
+        self.retry(connector)
+
+
+    def clientConnectionFailed(self, connector, reason):
+        self.connector = connector
+        self.log_info("POP factory connection failed")
+        self.retry(connector)
+
+
+    def handleMessage(self, message):
+        self.log_debug("POP factory handle message")
+        self.log_debug(message)
+
+        return self.mailReceiver.inbound(message)
+
+
+
+#
+# IMAP4
+#
+
+class IMAP4Service(service.Service):
+
+    def __init__(self, settings, mailReceiver):
+
+        if settings["UseSSL"]:
+            self.client = internet.SSLClient(settings["Server"],
+                settings["Port"],
+                IMAP4DownloadFactory(settings, mailReceiver),
+                ssl.ClientContextFactory())
+        else:
+            self.client = internet.TCPClient(settings["Server"],
+                settings["Port"],
+                IMAP4DownloadFactory(settings, mailReceiver))
+
+        self.mailReceiver = mailReceiver
+
+
+    def startService(self):
+        self.client.startService()
+
+
+    def stopService(self):
+        self.client.stopService()
+
+
+
+class IMAP4DownloadProtocol(imap4.IMAP4Client, LoggingMixIn):
+
+    def serverGreeting(self, capabilities):
+        self.log_debug("IMAP servergreeting")
+        return self.authenticate(self.factory.settings["Password"]
+            ).addCallback(self.cbLoggedIn
+            ).addErrback(self.ebAuthenticateFailed)
+
+
+    def ebLogError(self, error):
+        self.log_error("IMAP Error: %s" % (error,))
+
+
+    def ebAuthenticateFailed(self, reason):
+        self.log_debug("IMAP authenticate failed for %s, trying login" %
+            (self.factory.settings["Username"],))
+        return self.login(self.factory.settings["Username"],
+            self.factory.settings["Password"]
+            ).addCallback(self.cbLoggedIn
+            ).addErrback(self.ebLoginFailed)
+
+
+    def ebLoginFailed(self, reason):
+        self.log_error("IMAP login failed for %s" %
+            (self.factory.settings["Username"],))
+        self.transport.loseConnection()
+
+
+    def cbLoggedIn(self, result):
+        self.log_debug("IMAP logged in [%s]" % (self.state,))
+        self.select("Inbox").addCallback(self.cbInboxSelected)
+
+
+    def cbInboxSelected(self, result):
+        self.log_debug("IMAP Inbox selected [%s]" % (self.state,))
+        allMessages = imap4.MessageSet(1, None)
+        self.fetchUID(allMessages, True).addCallback(self.cbGotUIDs)
+
+
+    def cbGotUIDs(self, results):
+        self.log_debug("IMAP got uids [%s]" % (self.state,))
+        self.messageUIDs = [result['UID'] for result in results.values()]
+        self.messageCount = len(self.messageUIDs)
+        self.log_debug("IMAP Inbox has %d messages" % (self.messageCount,))
+        if self.messageCount:
+            self.fetchNextMessage()
+        else:
+            # No messages; close it out
+            self.close().addCallback(self.cbClosed)
+
+
+    def fetchNextMessage(self):
+        self.log_debug("IMAP in fetchnextmessage [%s]" % (self.state,))
+        if self.messageUIDs:
+            nextUID = self.messageUIDs.pop(0)
+            messageListToFetch = imap4.MessageSet(nextUID)
+            self.log_debug("Downloading message %d of %d (%s)" %
+                (self.messageCount - len(self.messageUIDs), self.messageCount,
+                nextUID))
+            self.fetchMessage(messageListToFetch, True).addCallback(
+                self.cbGotMessage, messageListToFetch).addErrback(
+                    self.ebLogError)
+        else:
+            self.log_debug("Seeing if anything new has arrived")
+            # Go back and see if any more messages have come in
+            self.expunge().addCallback(self.cbInboxSelected)
+
+
+    def cbGotMessage(self, results, messageList):
+        self.log_debug("IMAP in cbGotMessage [%s]" % (self.state,))
+        try:
+            messageData = results.values()[0]['RFC822']
+        except IndexError:
+            # results will be empty unless the "twistedmail-imap-flags-anywhere"
+            # patch from http://twistedmatrix.com/trac/ticket/1105 is applied
+            self.log_error("Skipping empty results -- apply twisted patch!")
+            self.fetchNextMessage()
+            return
+
+        d = self.factory.handleMessage(messageData)
+        if isinstance(d, defer.Deferred):
+            d.addCallback(self.cbFlagDeleted, messageList)
+        else:
+            # No deferred returned, so no need for addCallback( )
+            self.cbFlagDeleted(None, messageList)
+
+
+    def cbFlagDeleted(self, results, messageList):
+        self.addFlags(messageList, ("\\Deleted",),
+            uid=True).addCallback(self.cbMessageDeleted, messageList)
+
+
+    def cbMessageDeleted(self, results, messageList):
+        self.log_debug("IMAP in cbMessageDeleted [%s]" % (self.state,))
+        self.log_debug("Deleted message")
+        self.fetchNextMessage()
+
+
+    def cbClosed(self, results):
+        self.log_debug("IMAP in cbClosed [%s]" % (self.state,))
+        self.log_debug("Mailbox closed")
+        self.logout().addCallback(
+            lambda _: self.transport.loseConnection())
+
+
+    def rawDataReceived(self, data):
+        self.log_debug("RAW RECEIVED: %s" % (data,))
+        imap4.IMAP4Client.rawDataReceived(self, data)
+
+
+    def lineReceived(self, line):
+        self.log_debug("RECEIVED: %s" % (line,))
+        imap4.IMAP4Client.lineReceived(self, line)
+
+
+    def sendLine(self, line):
+        self.log_debug("SENDING: %s" % (line,))
+        imap4.IMAP4Client.sendLine(self, line)
+
+
+
+class IMAP4DownloadFactory(protocol.ClientFactory, LoggingMixIn):
+    protocol = IMAP4DownloadProtocol
+
+    def __init__(self, settings, mailReceiver, reactor=None):
+        self.log_debug("Setting up IMAPFactory")
+
+        self.settings = settings
+        self.mailReceiver = mailReceiver
+        if reactor is None:
+            from twisted.internet import reactor
+        self.reactor = reactor
+        self.noisy = False
+
+
+    def buildProtocol(self, addr):
+        p = protocol.ClientFactory.buildProtocol(self, addr)
+        username = self.settings["Username"]
+        p.registerAuthenticator(imap4.CramMD5ClientAuthenticator(username))
+        p.registerAuthenticator(imap4.LOGINAuthenticator(username))
+        p.registerAuthenticator(imap4.PLAINAuthenticator(username))
+        return p
+
+
+    def handleMessage(self, message):
+        self.log_debug("IMAP factory handle message")
+        self.log_debug(message)
+
+        return self.mailReceiver.inbound(message)
+
+
+    def retry(self, connector=None):
+        # TODO: if connector is None:
+
+        if connector is None:
+            if self.connector is None:
+                self.log_error("No connector to retry")
+                return
+            else:
+                connector = self.connector
+
+        def reconnector():
+            self.nextPoll = None
+            connector.connect()
+
+        self.log_debug("Scheduling next IMAP4 poll")
+        self.nextPoll = self.reactor.callLater(self.settings["PollingSeconds"],
+            reconnector)
+
+
+    def clientConnectionLost(self, connector, reason):
+        self.connector = connector
+        self.log_debug("IMAP factory connection lost")
+        self.retry(connector)
+
+
+    def clientConnectionFailed(self, connector, reason):
+        self.connector = connector
+        self.log_warn("IMAP factory connection failed")
+        self.retry(connector)

Modified: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/mailgateway.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/mailgateway.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/mailgateway.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -1,4 +1,3 @@
-# -*- test-case-name: twistedcaldav.test.test_mail -*-
 ##
 # Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
@@ -19,477 +18,18 @@
 Mail Gateway for Calendar Server
 """
 
-from __future__ import with_statement
-
-from cStringIO import StringIO
-
-from calendarserver.tap.util import getRootResource, directoryFromConfig
-
-from email.mime.image import MIMEImage
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-
-from pycalendar.datetime import PyCalendarDateTime
-from pycalendar.duration import PyCalendarDuration
-
-from twext.internet.adaptendpoint import connect
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.python.log import Logger, LoggingMixIn
-from twext.web2 import server
-from twext.web2.channel.http import HTTPFactory
-
-from twisted.application import internet, service
-from twisted.internet import protocol, defer, ssl, reactor as _reactor
-from twisted.internet.defer import succeed
-from twisted.mail import pop3client, imap4
-from twisted.mail.smtp import messageid, rfc822date, ESMTPSenderFactory
-from twisted.plugin import IPlugin
-from twisted.python.usage import Options, UsageError
-from twisted.web import client
-from twisted.web.microdom import Text as DOMText, Element as DOMElement
-from twisted.web.microdom import parseString
-from twisted.web.template import XMLString, TEMPLATE_NAMESPACE, Element, renderer, flattenString, tags
-
-from twistedcaldav import memcachepool
-from twistedcaldav.config import config
-from twistedcaldav.ical import Property, Component
-from twistedcaldav.localization import translationTo, _
-from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
-from twistedcaldav.scheduling.imip.resource import IMIPInvitationInboxResource
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.sql import AbstractSQLDatabase
-from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
-from twistedcaldav.util import AuthorizedHTTPGetter
-
-from zope.interface import implements
-
 import datetime
-import email.utils
 import os
-import urlparse
 import uuid
 
+from twext.python.log import Logger, LoggingMixIn
+from twistedcaldav.sql import AbstractSQLDatabase
 
-__all__ = [
-    "MailGatewayServiceMaker",
-    "MailGatewayTokensDatabase",
-    "MailHandler",
-]
 
-
 log = Logger()
 
-#
-# Monkey patch imap4.log so it doesn't emit useless logging,
-# specifically, "Unhandled unsolicited response" nonsense.
-#
-class IMAPLogger(Logger):
-    def emit(self, level, message, *args, **kwargs):
-        if message.startswith("Unhandled unsolicited response:"):
-            return
+# TODO Migration from sqlite -> store
 
-        Logger.emit(self, level, message, *args, **kwargs)
-
-imap4.log = IMAPLogger()
-
-#
-# Templates
-#
-
-plainCancelTemplate = u"""%(subject)s
-
-%(orgLabel)s: %(plainOrganizer)s
-%(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s
-%(timeLabel)s: %(timeInfo)s %(durationInfo)s
-"""
-
-plainInviteTemplate = u"""%(subject)s
-
-%(orgLabel)s: %(plainOrganizer)s
-%(locLabel)s: %(location)s
-%(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s
-%(timeLabel)s: %(timeInfo)s %(durationInfo)s
-%(descLabel)s: %(description)s
-%(urlLabel)s: %(url)s
-%(attLabel)s: %(plainAttendees)s
-"""
-
-
-htmlCancelTemplate = u"""<html>
-    <body><div>
-
-    <h1>%(subject)s</h1>
-    <p>
-    <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s
-    </p>
-    <p>
-    <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s
-    </p>
-    <p>
-    <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s
-    </p>
-    """.encode("utf-8")
-
-
-htmlInviteTemplate = u"""<html>
-    <body><div>
-    <p>%(inviteLabel)s</p>
-
-    <h1>%(summary)s</h1>
-    <p>
-    <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s
-    </p>
-    <p>
-    <h3>%(locLabel)s:</h3> %(location)s
-    </p>
-    <p>
-    <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s
-    </p>
-    <p>
-    <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s
-    </p>
-    <p>
-    <h3>%(descLabel)s:</h3> %(description)s
-    </p>
-    <p>
-    <h3>%(urlLabel)s:</h3> <a href="%(url)s">%(url)s</a>
-    </p>
-    <p>
-    <h3>%(attLabel)s:</h3> %(htmlAttendees)s
-    </p>
-    """.encode("utf-8")
-
-def _visit(document, node):
-    if isinstance(node, DOMText):
-        idx = node.parentNode.childNodes.index(node)
-        splitted = node.data.split("%(")
-        firstTextNode = document.createTextNode(splitted[0])
-        firstTextNode.parentNode = node.parentNode
-        replacements = [firstTextNode]
-        for moreText in splitted[1:]:
-            slotName, extra = moreText.split(')', 1)
-            extra = extra[1:]
-            slotElement = document.createElement('t:slot')
-            slotElement.setAttribute("name", slotName)
-            slotElement.parentNode = node.parentNode
-            textNode = document.createTextNode(extra)
-            textNode.parentNode = node.parentNode
-            replacements.append(slotElement)
-            replacements.append(textNode)
-        node.parentNode.childNodes[idx:idx + 1] = replacements
-
-    elif isinstance(node, DOMElement):
-        for attrName, attrVal in node.attributes.items():
-            if '%(' in attrVal:
-                del node.attributes[attrName]
-                elem = document.createElement('t:attr')
-                elem.setAttribute('name', attrName)
-                textNode = document.createTextNode(attrVal)
-                elem.appendChild(textNode)
-                node.appendChild(elem)
-
-
-
-def _walk(document, n):
-    _visit(document, n)
-    for subn in n.childNodes:
-        _walk(document, subn)
-
-
-
-def _fixup(data, rendererName):
-    document = parseString(data, beExtremelyLenient=True)
-    document.documentElement.setAttribute(
-        "xmlns:t", TEMPLATE_NAMESPACE
-    )
-    document.doctype = (
-        'html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" '
-        '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"'
-    )
-    document.documentElement.setAttribute(
-        "t:render", rendererName
-    )
-    _walk(document, document)
-    result = document.toxml()
-    return result
-
-
-
-class StringFormatTemplateLoader(object):
-    """
-    Loader for twisted.web.template that converts a template with %()s slots.
-    """
-    def __init__(self, fileFactory, rendererName):
-        """
-        @param fileFactory: a 1-argument callable which returns a file-like
-            object that contains the %()s-format template.
-
-        @param rendererName: the name of the renderer.
-
-        @type rendererName: C{str}
-        """
-        self.fileFactory = fileFactory
-        self.rendererName = rendererName
-
-
-    def load(self):
-        html = _fixup(self.fileFactory().read(), self.rendererName)
-        return XMLString(html).load()
-
-
-
-def localizedLabels(language, canceled, inviteState):
-    """
-    Generate localized labels for an email in the given language.
-
-    @param language: a 2-letter language code
-
-    @type language: C{str}
-
-    @return: a 2-tuple of (subjectFormatString, labelDict), where the first is a
-        format string for use in the subject, and the latter is a dictionary
-        with labels suitable for filling out HTML and plain-text templates.  All
-        values are C{str}s.
-    """
-    with translationTo(language):
-        if canceled:
-            subjectFormatString = _("Event canceled: %(summary)s")
-        elif inviteState == "new":
-            subjectFormatString = _("Event invitation: %(summary)s")
-        elif inviteState == "update":
-            subjectFormatString = _("Event update: %(summary)s")
-        else:
-            subjectFormatString = _("Event reply: %(summary)s")
-
-        if canceled:
-            inviteLabel = _("Event Canceled")
-        else:
-            if inviteState == "new":
-                inviteLabel = _("Event Invitation")
-            elif inviteState == "update":
-                inviteLabel = _("Event Update")
-            else:
-                inviteLabel = _("Event Reply")
-
-        labels = dict(
-            dateLabel=_("Date"),
-            timeLabel=_("Time"),
-            durationLabel=_("Duration"),
-            recurrenceLabel=_("Occurs"),
-            descLabel=_("Description"),
-            urlLabel=_("URL"),
-            orgLabel=_("Organizer"),
-            attLabel=_("Attendees"),
-            locLabel=_("Location"),
-            inviteLabel=inviteLabel,
-        )
-
-        # The translations we get back from gettext are utf-8 encoded
-        # strings, so convert to unicode
-        for key in labels.keys():
-            if isinstance(labels[key], str):
-                labels[key] = labels[key].decode("utf-8")
-
-    return subjectFormatString.decode("utf-8"), labels
-
-
-
-class MailGatewayOptions(Options):
-    """
-    Mail gateway service config
-    """
-    optParameters = [[
-        "config", "f", DEFAULT_CONFIG_FILE, "Path to configuration file."
-    ]]
-
-    def __init__(self, *args, **kwargs):
-        super(MailGatewayOptions, self).__init__(*args, **kwargs)
-
-        self.overrides = {}
-
-
-    def _coerceOption(self, configDict, key, value):
-        """
-        Coerce the given C{val} to type of C{configDict[key]}
-        """
-        if key in configDict:
-            if isinstance(configDict[key], bool):
-                value = value == "True"
-
-            elif isinstance(configDict[key], (int, float, long)):
-                value = type(configDict[key])(value)
-
-            elif isinstance(configDict[key], (list, tuple)):
-                value = value.split(',')
-
-            elif isinstance(configDict[key], dict):
-                raise UsageError(
-                    "Dict options not supported on the command line"
-                )
-
-            elif value == 'None':
-                value = None
-
-        return value
-
-
-    def _setOverride(self, configDict, path, value, overrideDict):
-        """
-        Set the value at path in configDict
-        """
-        key = path[0]
-
-        if len(path) == 1:
-            overrideDict[key] = self._coerceOption(configDict, key, value)
-            return
-
-        if key in configDict:
-            if not isinstance(configDict[key], dict):
-                raise UsageError(
-                    "Found intermediate path element that is not a dictionary"
-                )
-
-            if key not in overrideDict:
-                overrideDict[key] = {}
-
-            self._setOverride(
-                configDict[key], path[1:],
-                value, overrideDict[key]
-            )
-
-
-    def opt_option(self, option):
-        """
-        Set an option to override a value in the config file. True, False, int,
-        and float options are supported, as well as comma separated lists. Only
-        one option may be given for each --option flag, however multiple
-        --option flags may be specified.
-        """
-
-        if "=" in option:
-            path, value = option.split('=')
-            self._setOverride(
-                DEFAULT_CONFIG,
-                path.split('/'),
-                value,
-                self.overrides
-            )
-        else:
-            self.opt_option('%s=True' % (option,))
-
-    opt_o = opt_option
-
-    def postOptions(self):
-        config.load(self['config'])
-        config.updateDefaults(self.overrides)
-        self.parent['pidfile'] = None
-
-
-
-def injectionSettingsFromURL(url, config):
-    """
-    Given a url returned from server podding info (or None if not podding),
-    generate the url that should be used to inject an iMIP reply.  If the
-    url is None, then compute the url from config.
-    """
-    path = "inbox"
-    if url is None:
-        # Didn't get url from server podding configuration, so use caldavd.plist
-        if config.Scheduling.iMIP.MailGatewayServer == "localhost":
-            hostname = "localhost"
-        else:
-            hostname = config.ServerHostName
-        if config.EnableSSL:
-            useSSL = True
-            port = config.SSLPort
-        else:
-            useSSL = False
-            port = config.HTTPPort
-        scheme = "https:" if useSSL else "http:"
-        url = "%s//%s:%d/%s/" % (scheme, hostname, port, path)
-    else:
-        url = "%s/%s/" % (url.rstrip("/"), path)
-    return url
-
-
-
-def injectMessage(url, organizer, attendee, calendar, msgId, reactor=None):
-
-    if reactor is None:
-        reactor = _reactor
-
-    headers = {
-        'Content-Type' : 'text/calendar',
-        'Originator' : attendee,
-        'Recipient' : organizer,
-        config.Scheduling.iMIP.Header : config.Scheduling.iMIP.Password,
-    }
-
-    data = str(calendar)
-    url = injectionSettingsFromURL(url, config)
-    parsed = urlparse.urlparse(url)
-
-    log.debug("Injecting to %s: %s %s" % (url, str(headers), data))
-
-    factory = client.HTTPClientFactory(url, method='POST', headers=headers,
-        postdata=data, agent="iMIP gateway")
-
-    factory.noisy = False
-    factory.protocol = AuthorizedHTTPGetter
-
-    if parsed.scheme == "https":
-        connect(GAIEndpoint(reactor, parsed.hostname, parsed.port,
-                            ssl.ClientContextFactory()),
-                factory)
-    else:
-        connect(GAIEndpoint(reactor, parsed.hostname, parsed.port), factory)
-
-
-    def _success(result, msgId):
-        log.info("Mail gateway successfully injected message %s" % (msgId,))
-
-
-    def _failure(failure, msgId):
-        log.err("Mail gateway failed to inject message %s (Reason: %s)" %
-            (msgId, failure.getErrorMessage()))
-        log.debug("Failed calendar body: %s" % (str(calendar),))
-
-    factory.deferred.addCallback(_success, msgId).addErrback(_failure, msgId)
-    return factory.deferred
-
-
-
-def serverForOrganizer(directory, organizer):
-    """
-    Return the URL for the server hosting the organizer, or None if podding
-    is not enabled or organizer is hosted locally.
-    Raises ServerNotFound if we can't find the record for the organizer.
-    @param directory: service to look for organizer in
-    @type directory: L{DirectoryService}
-    @param organizer: CUA of organizer
-    @type organizer: C{str}
-    @return: string URL
-    """
-    record = directory.recordWithCalendarUserAddress(organizer)
-    if record is None:
-        log.warn("Can't find server for %s" % (organizer,))
-        raise ServerNotFound()
-
-    srvr = record.server()  # None means hosted locally
-    if srvr is None:
-        return None
-    else:
-        return srvr.uri
-
-
-
-class ServerNotFound(Exception):
-    """
-    Can't determine which server is hosting a given user
-    """
-
-
-
 class MailGatewayTokensDatabase(AbstractSQLDatabase, LoggingMixIn):
     """
     A database to maintain "plus-address" tokens for IMIP requests.
@@ -560,6 +100,15 @@
         else:
             return None
 
+    def getAllTokens(self):
+        results = list(
+            self._db_execute(
+                """
+                select TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP
+                """
+            )
+        )
+        return results
 
     def deleteToken(self, token):
         self._db_execute(
@@ -665,1220 +214,8 @@
         pass
 
 
-
-#
-# Service
-#
-
-class MailGatewayService(service.MultiService):
-
-    def startService(self):
-        """
-        Purge old database tokens -- doing this in startService so that
-        it happens after we've shed privileges
-        """
-        service.MultiService.startService(self)
-        mailer = getattr(self, "mailer", None)
-        if mailer is not None:
-            mailer.purge()
-            mailer.lowercase()
-
-
-
-class MailGatewayServiceMaker(LoggingMixIn):
-    implements(IPlugin, service.IServiceMaker)
-
-    tapname = "caldav_mailgateway"
-    description = "Mail Gateway"
-    options = MailGatewayOptions
-
-    def makeService(self, options):
-        try:
-            from setproctitle import setproctitle
-        except ImportError:
-            pass
-        else:
-            setproctitle("CalendarServer [Mail Gateway]")
-
-        memcachepool.installPools(
-            config.Memcached.Pools,
-            config.Memcached.MaxClients,
-        )
-
-        mailGatewayService = MailGatewayService()
-
-        settings = config.Scheduling['iMIP']
-        if settings['Enabled']:
-            mailer = MailHandler()
-
-            mailType = settings['Receiving']['Type']
-            if mailType.lower().startswith('pop'):
-                self.log_info("Starting Mail Gateway Service: POP3")
-                client = POP3Service(settings['Receiving'], mailer)
-            elif mailType.lower().startswith('imap'):
-                self.log_info("Starting Mail Gateway Service: IMAP4")
-                client = IMAP4Service(settings['Receiving'], mailer)
-            else:
-                # TODO: raise error?
-                self.log_error("Invalid iMIP type in configuration: %s" %
-                    (mailType,))
-                return mailGatewayService
-
-            client.setServiceParent(mailGatewayService)
-
-            # Set up /inbox -- server POSTs to it to send out iMIP invites
-            IScheduleService(settings, mailer).setServiceParent(
-                mailGatewayService
-            )
-
-        else:
-            mailer = None
-            self.log_info("Mail Gateway Service not enabled")
-
-        mailGatewayService.mailer = mailer
-        return mailGatewayService
-
-
-
-class IScheduleService(service.MultiService, LoggingMixIn):
-    """
-    ISchedule Inbox
-    """
-
-    def __init__(self, settings, mailer):
-        service.MultiService.__init__(self)
-        self.settings = settings
-        self.mailer = mailer
-
-        # Disable since we're only interested in /principals (for auth)
-        config.EnableCalDAV = False
-        config.EnableCardDAV = False
-
-        rootResource = getRootResource(
-            config,
-            "IGNORED", # no need for a store - no /calendars nor /addressbooks
-            resources=[
-                ("inbox", IMIPInvitationInboxResource, (mailer,), ("digest",)),
-            ]
-        )
-
-        self.factory = HTTPFactory(server.Site(rootResource))
-        self.server = internet.TCPServer(settings['MailGatewayPort'],
-            self.factory)
-        self.server.setServiceParent(self)
-
-
-
-class MailHandler(LoggingMixIn):
-
-    def __init__(self, dataRoot=None, directory=None):
-        if dataRoot is None:
-            dataRoot = config.DataRoot
-        if directory is None:
-            directory = directoryFromConfig(config)
-        self.db = MailGatewayTokensDatabase(dataRoot)
-        self.days = config.Scheduling['iMIP']['InvitationDaysToLive']
-        self.directory = directory
-
-
-    def purge(self):
-        """
-        Purge old database tokens
-        """
-        self.db.purgeOldTokens(datetime.date.today() -
-            datetime.timedelta(days=self.days))
-
-
-    def lowercase(self):
-        """
-        Convert all mailto: to lowercase
-        """
-        self.db.lowercase()
-
-
-    def checkDSN(self, message):
-        # returns (isDSN, Action, icalendar attachment)
-
-        report = deliveryStatus = calBody = None
-
-        for part in message.walk():
-            content_type = part.get_content_type()
-            if content_type == "multipart/report":
-                report = part
-                continue
-            elif content_type == "message/delivery-status":
-                deliveryStatus = part
-                continue
-            elif content_type == "message/rfc822":
-                #original = part
-                continue
-            elif content_type == "text/calendar":
-                calBody = part.get_payload(decode=True)
-                continue
-
-        if report is not None and deliveryStatus is not None:
-            # we have what appears to be a DSN
-
-            lines = str(deliveryStatus).split("\n")
-            for line in lines:
-                lower = line.lower()
-                if lower.startswith("action:"):
-                    # found Action:
-                    action = lower.split(' ')[1]
-                    break
-            else:
-                action = None
-
-            return True, action, calBody
-
-        else:
-            # Not a DSN
-            return False, None, None
-
-
-    def _extractToken(self, text):
-        try:
-            pre, _ignore_post = text.split('@')
-            pre, token = pre.split('+')
-            return token
-        except ValueError:
-            return None
-
-
-    def processDSN(self, calBody, msgId, fn):
-        calendar = Component.fromString(calBody)
-        # Extract the token (from organizer property)
-        organizer = calendar.getOrganizer()
-        token = self._extractToken(organizer)
-        if not token:
-            self.log_error("Mail gateway can't find token in DSN %s" % (msgId,))
-            return
-
-        result = self.db.lookupByToken(token)
-        if result is None:
-            # This isn't a token we recognize
-            self.log_error("Mail gateway found a token (%s) but didn't "
-                           "recognize it in DSN %s" % (token, msgId))
-            return
-
-        organizer, attendee, icaluid = result
-        organizer = str(organizer)
-        attendee = str(attendee)
-        icaluid = str(icaluid)
-        calendar.removeAllButOneAttendee(attendee)
-        calendar.getOrganizerProperty().setValue(organizer)
-        for comp in calendar.subcomponents():
-            if comp.name() == "VEVENT":
-                comp.addProperty(Property("REQUEST-STATUS",
-                    ["5.1", "Service unavailable"]))
-                break
-        else:
-            # no VEVENT in the calendar body.
-            # TODO: what to do in this case?
-            pass
-
-        try:
-            hostname = serverForOrganizer(self.directory, organizer)
-        except ServerNotFound:
-            # We can't determine which server hosts the organizer
-            self.log_error("Unable to determine which server hosts organizer %s"
-                % (organizer,))
-            return succeed(None)
-
-        self.log_warn("Mail gateway processing DSN %s to server %s" % (msgId, hostname))
-        return fn(hostname, organizer, attendee, calendar, msgId)
-
-
-    def processReply(self, msg, injectFunction, testMode=False):
-        # extract the token from the To header
-        _ignore_name, addr = email.utils.parseaddr(msg['To'])
-        if addr:
-            # addr looks like: server_address+token at example.com
-            token = self._extractToken(addr)
-            if not token:
-                self.log_error("Mail gateway didn't find a token in message "
-                               "%s (%s)" % (msg['Message-ID'], msg['To']))
-                return
-        else:
-            self.log_error("Mail gateway couldn't parse To: address (%s) in "
-                           "message %s" % (msg['To'], msg['Message-ID']))
-            return
-
-        result = self.db.lookupByToken(token)
-        if result is None:
-            # This isn't a token we recognize
-            self.log_error("Mail gateway found a token (%s) but didn't "
-                           "recognize it in message %s"
-                           % (token, msg['Message-ID']))
-            return
-
-        organizer, attendee, icaluid = result
-        organizer = str(organizer)
-        attendee = str(attendee)
-        icaluid = str(icaluid)
-
-        for part in msg.walk():
-            if part.get_content_type() == "text/calendar":
-                calBody = part.get_payload(decode=True)
-                break
-        else:
-            # No icalendar attachment
-            self.log_warn("Mail gateway didn't find an icalendar attachment "
-                          "in message %s" % (msg['Message-ID'],))
-
-            toAddr = None
-            fromAddr = attendee[7:]
-
-            if organizer.startswith("mailto:"):
-                toAddr = organizer[7:]
-            elif organizer.startswith("urn:uuid:"):
-                guid = organizer[9:]
-                record = self.directory.recordWithGUID(guid)
-                if record and record.emailAddresses:
-                    toAddr = list(record.emailAddresses)[0]
-
-            if toAddr is None:
-                self.log_error("Don't have an email address for the organizer; "
-                               "ignoring reply.")
-                return
-
-            if testMode:
-                return (toAddr, fromAddr)
-
-            settings = config.Scheduling["iMIP"]["Sending"]
-            if settings["UseSSL"]:
-                contextFactory = ssl.ClientContextFactory()
-            else:
-                contextFactory = None
-
-            deferred = defer.Deferred()
-            del msg["From"]
-            msg["From"] = fromAddr
-            del msg["Reply-To"]
-            msg["Reply-To"] = fromAddr
-            del msg["To"]
-            msg["To"] = toAddr
-            factory = ESMTPSenderFactory(
-                settings["Username"], settings["Password"],
-                fromAddr, toAddr,
-                # per http://trac.calendarserver.org/ticket/416 ...
-                StringIO(msg.as_string().replace("\r\n", "\n")),
-                deferred,
-                contextFactory=contextFactory,
-                requireAuthentication=False,
-                requireTransportSecurity=settings["UseSSL"],
-            )
-
-            self.log_warn("Mail gateway forwarding reply back to organizer")
-            connect(GAIEndpoint(_reactor, settings["Server"], settings["Port"]),
-                    factory)
-            return deferred
-
-        # Process the imip attachment; inject to calendar server
-
-        self.log_debug(calBody)
-        calendar = Component.fromString(calBody)
-        event = calendar.mainComponent()
-
-        calendar.removeAllButOneAttendee(attendee)
-        organizerProperty = calendar.getOrganizerProperty()
-        if organizerProperty is None:
-            # ORGANIZER is required per rfc2446 section 3.2.3
-            self.log_warn("Mail gateway didn't find an ORGANIZER in REPLY %s"
-                          % (msg['Message-ID'],))
-            event.addProperty(Property("ORGANIZER", organizer))
-        else:
-            organizerProperty.setValue(organizer)
-
-        if not calendar.getAttendees():
-            # The attendee we're expecting isn't there, so add it back
-            # with a SCHEDULE-STATUS of SERVICE_UNAVAILABLE.
-            # The organizer will then see that the reply was not successful.
-            attendeeProp = Property("ATTENDEE", attendee,
-                params={
-                    "SCHEDULE-STATUS": iTIPRequestStatus.SERVICE_UNAVAILABLE,
-                }
-            )
-            event.addProperty(attendeeProp)
-
-            # TODO: We have talked about sending an email to the reply-to
-            # at this point, to let them know that their reply was missing
-            # the appropriate ATTENDEE.  This will require a new localizable
-            # email template for the message.
-
-        try:
-            hostname = serverForOrganizer(self.directory, organizer)
-        except ServerNotFound:
-            # We can't determine which server hosts the organizer
-            self.log_error("Unable to determine which server hosts organizer %s"
-                % (organizer,))
-            return succeed(None)
-
-        return injectFunction(hostname, organizer, attendee, calendar,
-            msg['Message-ID'])
-
-
-    def inbound(self, message, fn=injectMessage):
-        try:
-            msg = email.message_from_string(message)
-
-            isDSN, action, calBody = self.checkDSN(msg)
-            if isDSN:
-                if action == 'failed' and calBody:
-                    # This is a DSN we can handle
-                    return self.processDSN(calBody, msg['Message-ID'], fn)
-                else:
-                    # It's a DSN without enough to go on
-                    self.log_error("Mail gateway can't process DSN %s"
-                                   % (msg['Message-ID'],))
-                    return
-
-            self.log_info("Mail gateway received message %s from %s to %s" %
-                (msg['Message-ID'], msg['From'], msg['To']))
-
-            return self.processReply(msg, fn)
-
-        except Exception, e:
-            # Don't let a failure of any kind stop us
-            self.log_error("Failed to process message: %s" % (e,))
-
-
-    def outbound(self, originator, recipient, calendar, language='en',
-                 send=True, onlyAfter=None):
-        # create token, send email
-
-        settings = config.Scheduling['iMIP']['Sending']
-
-        if onlyAfter is None:
-            duration = PyCalendarDuration(days=settings.SuppressionDays)
-            onlyAfter = PyCalendarDateTime.getNowUTC() - duration
-
-        component = calendar.masterComponent()
-        if component is None:
-            component = calendar.mainComponent(True)
-        icaluid = component.propertyValue("UID")
-        method = calendar.propertyValue("METHOD")
-
-        # Clean up the attendee list which is purely used within the human
-        # readable email message (not modifying the calendar body)
-        attendees = []
-        for attendeeProp in calendar.getAllAttendeeProperties():
-            cutype = attendeeProp.parameterValue("CUTYPE", "INDIVIDUAL")
-            if cutype == "INDIVIDUAL":
-                cn = attendeeProp.parameterValue("CN", None)
-                if cn is not None:
-                    cn = cn.decode("utf-8")
-                cuaddr = normalizeCUAddr(attendeeProp.value())
-                if cuaddr.startswith("mailto:"):
-                    mailto = cuaddr[7:]
-                    if not cn:
-                        cn = mailto
-                else:
-                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
-                    if emailAddress:
-                        mailto = emailAddress
-                    else:
-                        mailto = None
-
-                if cn or mailto:
-                    attendees.append((cn, mailto))
-
-        toAddr = recipient
-        if not recipient.lower().startswith("mailto:"):
-            raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP "
-                             "operation." % (recipient,))
-        recipient = recipient[7:]
-
-        if method != "REPLY":
-            # Invites and cancellations:
-
-            # Reuse or generate a token based on originator, toAddr, and
-            # event uid
-            token = self.db.getToken(originator, toAddr.lower(), icaluid)
-            if token is None:
-
-                # Because in the past the originator was sometimes in mailto:
-                # form, lookup an existing token by mailto: as well
-                organizerProperty = calendar.getOrganizerProperty()
-                organizerEmailAddress = organizerProperty.parameterValue("EMAIL", None)
-                if organizerEmailAddress is not None:
-                    token = self.db.getToken("mailto:%s" % (organizerEmailAddress.lower(),), toAddr.lower(), icaluid)
-
-            if token is None:
-                token = self.db.createToken(originator, toAddr.lower(), icaluid)
-                self.log_debug("Mail gateway created token %s for %s "
-                               "(originator), %s (recipient) and %s (icaluid)"
-                               % (token, originator, toAddr, icaluid))
-                inviteState = "new"
-
-            else:
-                self.log_debug("Mail gateway reusing token %s for %s "
-                               "(originator), %s (recipient) and %s (icaluid)"
-                               % (token, originator, toAddr, icaluid))
-                inviteState = "update"
-
-            fullServerAddress = settings['Address']
-            _ignore_name, serverAddress = email.utils.parseaddr(fullServerAddress)
-            pre, post = serverAddress.split('@')
-            addressWithToken = "%s+%s@%s" % (pre, token, post)
-
-            organizerProperty = calendar.getOrganizerProperty()
-            organizerEmailAddress = organizerProperty.parameterValue("EMAIL",
-                                                                     None)
-            organizerValue = organizerProperty.value()
-            organizerProperty.setValue("mailto:%s" % (addressWithToken,))
-
-            # If the organizer is also an attendee, update that attendee value
-            # to match
-            organizerAttendeeProperty = calendar.getAttendeeProperty(
-                [organizerValue])
-            if organizerAttendeeProperty is not None:
-                organizerAttendeeProperty.setValue("mailto:%s" %
-                                                   (addressWithToken,))
-
-            # The email's From will include the originator's real name email
-            # address if available.  Otherwise it will be the server's email
-            # address (without # + addressing)
-            if organizerEmailAddress:
-                orgEmail = fromAddr = organizerEmailAddress
-            else:
-                fromAddr = serverAddress
-                orgEmail = None
-            cn = calendar.getOrganizerProperty().parameterValue('CN', None)
-            if cn is None:
-                cn = u'Calendar Server'
-                orgCN = orgEmail
-            else:
-                orgCN = cn = cn.decode("utf-8")
-
-            # a unicode cn (rather than an encode string value) means the
-            # from address will get properly encoded per rfc2047 within the
-            # MIMEMultipart in generateEmail
-            formattedFrom = "%s <%s>" % (cn, fromAddr)
-
-            # Reply-to address will be the server+token address
-
-        else: # REPLY
-            inviteState = "reply"
-
-            # Look up the attendee property corresponding to the originator
-            # of this reply
-            originatorAttendeeProperty = calendar.getAttendeeProperty(
-                [originator])
-            formattedFrom = fromAddr = originator = ""
-            if originatorAttendeeProperty:
-                originatorAttendeeEmailAddress = (
-                    originatorAttendeeProperty.parameterValue("EMAIL", None)
-                )
-                if originatorAttendeeEmailAddress:
-                    formattedFrom = fromAddr = originator = (
-                        originatorAttendeeEmailAddress
-                    )
-
-            organizerMailto = str(calendar.getOrganizer())
-            if not organizerMailto.lower().startswith("mailto:"):
-                raise ValueError("ORGANIZER address '%s' must be mailto: "
-                                 "for REPLY." % (organizerMailto,))
-            orgEmail = organizerMailto[7:]
-
-            orgCN = calendar.getOrganizerProperty().parameterValue('CN', None)
-            addressWithToken = formattedFrom
-
-        # At the point we've created the token in the db, which we always
-        # want to do, but if this message is for an event completely in
-        # the past we don't want to actually send an email.
-        if not calendar.hasInstancesAfter(onlyAfter):
-            self.log_debug("Skipping IMIP message for old event")
-            return succeed(True)
-
-        # Now prevent any "internal" CUAs from being exposed by converting
-        # to mailto: if we have one
-        for attendeeProp in calendar.getAllAttendeeProperties():
-            cutype = attendeeProp.parameterValue('CUTYPE', None)
-            if cutype == "INDIVIDUAL":
-                cuaddr = normalizeCUAddr(attendeeProp.value())
-                if not cuaddr.startswith("mailto:"):
-                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
-                    if emailAddress:
-                        attendeeProp.setValue("mailto:%s" % (emailAddress,))
-
-        msgId, message = self.generateEmail(inviteState, calendar, orgEmail,
-            orgCN, attendees, formattedFrom, addressWithToken, recipient,
-            language=language)
-
-        if send:
-            self.log_debug("Sending: %s" % (message,))
-            def _success(result, msgId, fromAddr, toAddr):
-                self.log_info("Mail gateway sent message %s from %s to %s" %
-                    (msgId, fromAddr, toAddr))
-                return True
-
-            def _failure(failure, msgId, fromAddr, toAddr):
-                self.log_error("Mail gateway failed to send message %s from %s "
-                               "to %s (Reason: %s)" %
-                               (msgId, fromAddr, toAddr,
-                                failure.getErrorMessage()))
-                return False
-
-            deferred = defer.Deferred()
-
-            if settings["UseSSL"]:
-                contextFactory = ssl.ClientContextFactory()
-            else:
-                contextFactory = None
-
-            factory = ESMTPSenderFactory(
-                settings['Username'], settings['Password'],
-                fromAddr, toAddr, StringIO(str(message)), deferred,
-                contextFactory=contextFactory,
-                requireAuthentication=False,
-                requireTransportSecurity=settings["UseSSL"])
-
-            connect(GAIEndpoint(_reactor, settings["Server"], settings["Port"]),
-                    factory)
-            deferred.addCallback(_success, msgId, fromAddr, toAddr)
-            deferred.addErrback(_failure, msgId, fromAddr, toAddr)
-            return deferred
-        else:
-            return succeed((inviteState, calendar, orgEmail, orgCN, attendees,
-                formattedFrom, recipient, addressWithToken))
-
-
-    def getIconPath(self, details, canceled, language='en'):
-        iconDir = config.Scheduling.iMIP.MailIconsDirectory.rstrip("/")
-
-        if canceled:
-            iconName = "canceled.png"
-            iconPath = os.path.join(iconDir, iconName)
-            if os.path.exists(iconPath):
-                return iconPath
-            else:
-                return None
-
-        else:
-            month = int(details['month'])
-            day = int(details['day'])
-            with translationTo(language) as trans:
-                monthName = trans.monthAbbreviation(month)
-            iconName = "%02d.png" % (day,)
-            iconPath = os.path.join(iconDir, monthName.encode("utf-8"), iconName)
-            if not os.path.exists(iconPath):
-                # Try the generic (numeric) version
-                iconPath = os.path.join(iconDir, "%02d" % (month,), iconName)
-                if not os.path.exists(iconPath):
-                    return None
-            return iconPath
-
-
-    def generateEmail(self, inviteState, calendar, orgEmail, orgCN,
-                      attendees, fromAddress, replyToAddress, toAddress,
-                      language='en'):
-        """
-        Generate MIME text containing an iMIP invitation, cancellation, update
-        or reply.
-
-        @param inviteState: 'new', 'update', or 'reply'.
-
-        @type inviteState: C{str}
-
-        @param calendar: the iCalendar component to attach to the email.
-
-        @type calendar: L{twistedcaldav.ical.Component}
-
-        @param orgEmail: The email for the organizer, in C{localhost at domain}
-            format, or C{None} if the organizer has no email address.
-
-        @type orgEmail: C{str} or C{NoneType}
-
-        @param orgCN: Common name / display name for the organizer.
-
-        @type orgCN: C{unicode}
-
-        @param attendees: A C{list} of 2-C{tuple}s of (common name, email
-            address) similar to (orgEmail, orgCN).
-
-        @param fromAddress: the address to use in the C{From:} header of the
-            email.
-
-        @type fromAddress: C{str}
-
-        @param replyToAddress: the address to use in the C{Reply-To} header.
-
-        @type replyToAddress: C{str}
-
-        @param toAddress: the address to use in the C{To} header.
-
-        @type toAddress: C{str}
-
-        @param language: a 2-letter language code describing the target
-            language that the email should be generated in.
-
-        @type language: C{str}
-
-        @return: a 2-tuple of C{str}s: (message ID, message text).  The message
-            ID is the value of the C{Message-ID} header, and the message text is
-            the full MIME message, ready for transport over SMTP.
-        """
-
-        details = self.getEventDetails(calendar, language=language)
-        canceled = (calendar.propertyValue("METHOD") == "CANCEL")
-        iconPath = self.getIconPath(details, canceled, language=language)
-
-        subjectFormat, labels = localizedLabels(language, canceled, inviteState)
-        details.update(labels)
-
-        details['subject'] = subjectFormat % {'summary' : details['summary']}
-        details['iconName'] = iconName = "calicon.png"
-
-        plainText = self.renderPlainText(details, (orgCN, orgEmail),
-                                         attendees, canceled)
-
-        [addIcon, htmlText] = self.renderHTML(details, (orgCN, orgEmail),
-                                              attendees, canceled)
-
-        msg = MIMEMultipart()
-        msg["From"] = fromAddress
-        msg["Subject"] = details['subject']
-        msg["Reply-To"] = replyToAddress
-        msg["To"] = toAddress
-        msg["Date"] = rfc822date()
-        msgId = messageid()
-        msg["Message-ID"] = msgId
-
-        msgAlt = MIMEMultipart("alternative")
-        msg.attach(msgAlt)
-
-        # plain version
-        msgPlain = MIMEText(plainText, "plain", "UTF-8")
-        msgAlt.attach(msgPlain)
-
-        # html version
-        msgHtmlRelated = MIMEMultipart("related", type="text/html")
-        msgAlt.attach(msgHtmlRelated)
-
-        msgHtml = MIMEText(htmlText, "html", "UTF-8")
-        msgHtmlRelated.attach(msgHtml)
-
-        # an image for html version
-        if addIcon and iconPath != None and os.path.exists(iconPath):
-
-            with open(iconPath) as iconFile:
-                msgIcon = MIMEImage(iconFile.read(),
-                    _subtype='png;x-apple-mail-type=stationery;name="%s"' %
-                    (iconName,))
-
-            msgIcon.add_header("Content-ID", "<%s>" % (iconName,))
-            msgIcon.add_header("Content-Disposition", "inline;filename=%s" %
-                (iconName,))
-            msgHtmlRelated.attach(msgIcon)
-
-        calendarText = str(calendar)
-        # the icalendar attachment
-        self.log_debug("Mail gateway sending calendar body: %s"
-                       % (calendarText,))
-        msgIcal = MIMEText(calendarText, "calendar", "UTF-8")
-        method = calendar.propertyValue("METHOD").lower()
-        msgIcal.set_param("method", method)
-        msgIcal.add_header("Content-ID", "<invitation.ics>")
-        msgIcal.add_header("Content-Disposition",
-            "inline;filename=invitation.ics")
-        msg.attach(msgIcal)
-
-        return msgId, msg.as_string()
-
-
-    def renderPlainText(self, details, (orgCN, orgEmail), attendees, canceled):
-        """
-        Render text/plain message part based on invitation details and a flag
-        indicating whether the message is a cancellation.
-
-        @return: UTF-8 encoded text.
-
-        @rtype: C{str}
-        """
-        plainAttendeeList = []
-        for cn, mailto in attendees:
-            if cn:
-                plainAttendeeList.append(cn if not mailto else
-                    "%s <%s>" % (cn, mailto))
-            elif mailto:
-                plainAttendeeList.append("<%s>" % (mailto,))
-
-        details['plainAttendees'] = ", ".join(plainAttendeeList)
-
-        details['plainOrganizer'] = (orgCN if not orgEmail else
-            "%s <%s>" % (orgCN, orgEmail))
-
-        # plain text version
-        if canceled:
-            plainTemplate = plainCancelTemplate
-        else:
-            plainTemplate = plainInviteTemplate
-
-        return (plainTemplate % details).encode("UTF-8")
-
-
-    def renderHTML(self, details, organizer, attendees, canceled):
-        """
-        Render HTML message part based on invitation details and a flag
-        indicating whether the message is a cancellation.
-
-        @return: a 2-tuple of (should add icon (C{bool}), html text (C{str},
-            representing utf-8 encoded bytes)).  The first element indicates
-            whether the MIME generator needs to add a C{cid:} icon image part to
-            satisfy the HTML links.
-        """
-        orgCN, orgEmail = organizer
-
-        # TODO: htmlAttendees needs to be a separate element with a separate
-        # template fragment.  Luckily that fragment is the same regardless
-        # of the rest of the template.
-        htmlAttendees = []
-        first = True
-        for cn, mailto in attendees:
-            if not first:
-                htmlAttendees.append(u", ")
-            else:
-                first = False
-
-            if mailto:
-                if not cn:
-                    cn = mailto
-                htmlAttendees.append(
-                    tags.a(href="mailto:%s" % (mailto,))(cn)
-                )
-            else:
-                htmlAttendees.append(cn)
-
-        details['htmlAttendees'] = htmlAttendees
-
-        # TODO: htmlOrganizer is also some HTML that requires additional
-        # template stuff, and once again, it's just a 'mailto:'.
-        # tags.a(href="mailto:"+email)[cn]
-        if orgEmail:
-            details['htmlOrganizer'] = tags.a(href="mailto:%s" % (orgEmail,))(
-                orgCN)
-        else:
-            details['htmlOrganizer'] = orgCN
-
-        templateDir = config.Scheduling.iMIP.MailTemplatesDirectory.rstrip("/")
-        templateName = "cancel.html" if canceled else "invite.html"
-        templatePath = os.path.join(templateDir, templateName)
-
-        if not os.path.exists(templatePath):
-            # Fall back to built-in simple templates:
-            if canceled:
-                htmlTemplate = htmlCancelTemplate
-            else:
-                htmlTemplate = htmlInviteTemplate
-        else: # HTML template file exists
-
-            with open(templatePath) as templateFile:
-                htmlTemplate = templateFile.read()
-
-        class EmailElement(Element):
-            loader = StringFormatTemplateLoader(lambda : StringIO(htmlTemplate),
-                                                "email")
-
-            @renderer
-            def email(self, request, tag):
-                return tag.fillSlots(**details)
-
-        textCollector = []
-        flattenString(None, EmailElement()).addCallback(textCollector.append)
-        htmlText = textCollector[0]
-
-        # If the template refers to an icon in a cid: link, it needs to be added
-        # in the MIME.
-        addIcon = (htmlTemplate.find("cid:%(iconName)s") != -1)
-        return (addIcon, htmlText)
-
-
-    def getEventDetails(self, calendar, language='en'):
-        """
-        Create a dictionary mapping slot names - specifically: summary,
-        description, location, dateInfo, timeInfo, durationInfo, recurrenceInfo,
-        url - with localized string values that should be placed into the HTML
-        and plain-text templates.
-
-        @param calendar: a L{Component} upon which to base the language.
-        @type calendar: L{Component}
-
-        @param language: a 2-letter language code.
-        @type language: C{str}
-
-        @return: a mapping from template slot name to localized text.
-        @rtype: a C{dict} mapping C{bytes} to C{unicode}.
-        """
-
-        # Get the most appropriate component
-        component = calendar.masterComponent()
-        if component is None:
-            component = calendar.mainComponent(True)
-
-        results = {}
-
-        dtStart = component.propertyValue('DTSTART')
-        results['month'] = dtStart.getMonth()
-        results['day'] = dtStart.getDay()
-
-        for propertyToResult in ['summary', 'description', 'location', 'url']:
-            result = component.propertyValue(propertyToResult.upper())
-            if result is None:
-                result = u""
-            else:
-                result = result.decode('utf-8')
-            results[propertyToResult] = result
-
-        with translationTo(language) as trans:
-            results['dateInfo'] = trans.date(component).decode('utf-8')
-            results['timeInfo'], duration = (x.decode('utf-8') for x in trans.time(component))
-            results['durationInfo'] = u"(%s)" % (duration,) if duration else u""
-
-            for propertyName in ('RRULE', 'RDATE', 'EXRULE', 'EXDATE',
-                                 'RECURRENCE-ID'):
-                if component.hasProperty(propertyName):
-                    results['recurrenceInfo'] = _("(Repeating)").decode('utf-8')
-                    break
-            else:
-                results['recurrenceInfo'] = u""
-
-        return results
-
-
-
-#
-# POP3
-#
-
-class POP3Service(service.Service, LoggingMixIn):
-
-    def __init__(self, settings, mailer):
-        if settings["UseSSL"]:
-            self.client = internet.SSLClient(settings["Server"],
-                settings["Port"],
-                POP3DownloadFactory(settings, mailer),
-                ssl.ClientContextFactory())
-        else:
-            self.client = internet.TCPClient(settings["Server"],
-                settings["Port"],
-                POP3DownloadFactory(settings, mailer))
-
-        self.mailer = mailer
-
-
-    def startService(self):
-        self.client.startService()
-
-
-    def stopService(self):
-        self.client.stopService()
-
-
-
-class POP3DownloadProtocol(pop3client.POP3Client, LoggingMixIn):
-    allowInsecureLogin = False
-
-    def serverGreeting(self, greeting):
-        self.log_debug("POP servergreeting")
-        pop3client.POP3Client.serverGreeting(self, greeting)
-        login = self.login(self.factory.settings["Username"],
-            self.factory.settings["Password"])
-        login.addCallback(self.cbLoggedIn)
-        login.addErrback(self.cbLoginFailed)
-
-
-    def cbLoginFailed(self, reason):
-        self.log_error("POP3 login failed for %s" %
-            (self.factory.settings["Username"],))
-        return self.quit()
-
-
-    def cbLoggedIn(self, result):
-        self.log_debug("POP loggedin")
-        return self.listSize().addCallback(self.cbGotMessageSizes)
-
-
-    def cbGotMessageSizes(self, sizes):
-        self.log_debug("POP gotmessagesizes")
-        downloads = []
-        for i in range(len(sizes)):
-            downloads.append(self.retrieve(i).addCallback(self.cbDownloaded, i))
-        return defer.DeferredList(downloads).addCallback(self.cbFinished)
-
-
-    def cbDownloaded(self, lines, id):
-        self.log_debug("POP downloaded message %d" % (id,))
-        self.factory.handleMessage("\r\n".join(lines))
-        self.log_debug("POP deleting message %d" % (id,))
-        self.delete(id)
-
-
-    def cbFinished(self, results):
-        self.log_debug("POP finished")
-        return self.quit()
-
-
-
-class POP3DownloadFactory(protocol.ClientFactory, LoggingMixIn):
-    protocol = POP3DownloadProtocol
-
-    def __init__(self, settings, mailer, reactor=None):
-        self.settings = settings
-        self.mailer = mailer
-        if reactor is None:
-            from twisted.internet import reactor
-        self.reactor = reactor
-        self.nextPoll = None
-        self.noisy = False
-
-
-    def retry(self, connector=None):
-        # TODO: if connector is None:
-
-        if connector is None:
-            if self.connector is None:
-                self.log_error("No connector to retry")
-                return
-            else:
-                connector = self.connector
-
-        def reconnector():
-            self.nextPoll = None
-            connector.connect()
-
-        self.log_debug("Scheduling next POP3 poll")
-        self.nextPoll = self.reactor.callLater(self.settings["PollingSeconds"],
-            reconnector)
-
-
-    def clientConnectionLost(self, connector, reason):
-        self.connector = connector
-        self.log_debug("POP factory connection lost")
-        self.retry(connector)
-
-
-    def clientConnectionFailed(self, connector, reason):
-        self.connector = connector
-        self.log_info("POP factory connection failed")
-        self.retry(connector)
-
-
-    def handleMessage(self, message):
-        self.log_debug("POP factory handle message")
-        self.log_debug(message)
-
-        return self.mailer.inbound(message)
-
-
-
-#
-# IMAP4
-#
-
-class IMAP4Service(service.Service):
-
-    def __init__(self, settings, mailer):
-
-        if settings["UseSSL"]:
-            self.client = internet.SSLClient(settings["Server"],
-                settings["Port"],
-                IMAP4DownloadFactory(settings, mailer),
-                ssl.ClientContextFactory())
-        else:
-            self.client = internet.TCPClient(settings["Server"],
-                settings["Port"],
-                IMAP4DownloadFactory(settings, mailer))
-
-        self.mailer = mailer
-
-
-    def startService(self):
-        self.client.startService()
-
-
-    def stopService(self):
-        self.client.stopService()
-
-
-
-class IMAP4DownloadProtocol(imap4.IMAP4Client, LoggingMixIn):
-
-    def serverGreeting(self, capabilities):
-        self.log_debug("IMAP servergreeting")
-        return self.authenticate(self.factory.settings["Password"]
-            ).addCallback(self.cbLoggedIn
-            ).addErrback(self.ebAuthenticateFailed)
-
-
-    def ebLogError(self, error):
-        self.log_error("IMAP Error: %s" % (error,))
-
-
-    def ebAuthenticateFailed(self, reason):
-        self.log_debug("IMAP authenticate failed for %s, trying login" %
-            (self.factory.settings["Username"],))
-        return self.login(self.factory.settings["Username"],
-            self.factory.settings["Password"]
-            ).addCallback(self.cbLoggedIn
-            ).addErrback(self.ebLoginFailed)
-
-
-    def ebLoginFailed(self, reason):
-        self.log_error("IMAP login failed for %s" %
-            (self.factory.settings["Username"],))
-        self.transport.loseConnection()
-
-
-    def cbLoggedIn(self, result):
-        self.log_debug("IMAP logged in [%s]" % (self.state,))
-        self.select("Inbox").addCallback(self.cbInboxSelected)
-
-
-    def cbInboxSelected(self, result):
-        self.log_debug("IMAP Inbox selected [%s]" % (self.state,))
-        allMessages = imap4.MessageSet(1, None)
-        self.fetchUID(allMessages, True).addCallback(self.cbGotUIDs)
-
-
-    def cbGotUIDs(self, results):
-        self.log_debug("IMAP got uids [%s]" % (self.state,))
-        self.messageUIDs = [result['UID'] for result in results.values()]
-        self.messageCount = len(self.messageUIDs)
-        self.log_debug("IMAP Inbox has %d messages" % (self.messageCount,))
-        if self.messageCount:
-            self.fetchNextMessage()
-        else:
-            # No messages; close it out
-            self.close().addCallback(self.cbClosed)
-
-
-    def fetchNextMessage(self):
-        self.log_debug("IMAP in fetchnextmessage [%s]" % (self.state,))
-        if self.messageUIDs:
-            nextUID = self.messageUIDs.pop(0)
-            messageListToFetch = imap4.MessageSet(nextUID)
-            self.log_debug("Downloading message %d of %d (%s)" %
-                (self.messageCount - len(self.messageUIDs), self.messageCount,
-                nextUID))
-            self.fetchMessage(messageListToFetch, True).addCallback(
-                self.cbGotMessage, messageListToFetch).addErrback(
-                    self.ebLogError)
-        else:
-            self.log_debug("Seeing if anything new has arrived")
-            # Go back and see if any more messages have come in
-            self.expunge().addCallback(self.cbInboxSelected)
-
-
-    def cbGotMessage(self, results, messageList):
-        self.log_debug("IMAP in cbGotMessage [%s]" % (self.state,))
-        try:
-            messageData = results.values()[0]['RFC822']
-        except IndexError:
-            # results will be empty unless the "twistedmail-imap-flags-anywhere"
-            # patch from http://twistedmatrix.com/trac/ticket/1105 is applied
-            self.log_error("Skipping empty results -- apply twisted patch!")
-            self.fetchNextMessage()
-            return
-
-        d = self.factory.handleMessage(messageData)
-        if isinstance(d, defer.Deferred):
-            d.addCallback(self.cbFlagDeleted, messageList)
-        else:
-            # No deferred returned, so no need for addCallback( )
-            self.cbFlagDeleted(None, messageList)
-
-
-    def cbFlagDeleted(self, results, messageList):
-        self.addFlags(messageList, ("\\Deleted",),
-            uid=True).addCallback(self.cbMessageDeleted, messageList)
-
-
-    def cbMessageDeleted(self, results, messageList):
-        self.log_debug("IMAP in cbMessageDeleted [%s]" % (self.state,))
-        self.log_debug("Deleted message")
-        self.fetchNextMessage()
-
-
-    def cbClosed(self, results):
-        self.log_debug("IMAP in cbClosed [%s]" % (self.state,))
-        self.log_debug("Mailbox closed")
-        self.logout().addCallback(
-            lambda _: self.transport.loseConnection())
-
-
-    def rawDataReceived(self, data):
-        self.log_debug("RAW RECEIVED: %s" % (data,))
-        imap4.IMAP4Client.rawDataReceived(self, data)
-
-
-    def lineReceived(self, line):
-        self.log_debug("RECEIVED: %s" % (line,))
-        imap4.IMAP4Client.lineReceived(self, line)
-
-
-    def sendLine(self, line):
-        self.log_debug("SENDING: %s" % (line,))
-        imap4.IMAP4Client.sendLine(self, line)
-
-
-
-class IMAP4DownloadFactory(protocol.ClientFactory, LoggingMixIn):
-    protocol = IMAP4DownloadProtocol
-
-    def __init__(self, settings, mailer, reactor=None):
-        self.log_debug("Setting up IMAPFactory")
-
-        self.settings = settings
-        self.mailer = mailer
-        if reactor is None:
-            from twisted.internet import reactor
-        self.reactor = reactor
-        self.noisy = False
-
-
-    def buildProtocol(self, addr):
-        p = protocol.ClientFactory.buildProtocol(self, addr)
-        username = self.settings["Username"]
-        p.registerAuthenticator(imap4.CramMD5ClientAuthenticator(username))
-        p.registerAuthenticator(imap4.LOGINAuthenticator(username))
-        p.registerAuthenticator(imap4.PLAINAuthenticator(username))
-        return p
-
-
-    def handleMessage(self, message):
-        self.log_debug("IMAP factory handle message")
-        self.log_debug(message)
-
-        return self.mailer.inbound(message)
-
-
-    def retry(self, connector=None):
-        # TODO: if connector is None:
-
-        if connector is None:
-            if self.connector is None:
-                self.log_error("No connector to retry")
-                return
-            else:
-                connector = self.connector
-
-        def reconnector():
-            self.nextPoll = None
-            connector.connect()
-
-        self.log_debug("Scheduling next IMAP4 poll")
-        self.nextPoll = self.reactor.callLater(self.settings["PollingSeconds"],
-            reconnector)
-
-
-    def clientConnectionLost(self, connector, reason):
-        self.connector = connector
-        self.log_debug("IMAP factory connection lost")
-        self.retry(connector)
-
-
-    def clientConnectionFailed(self, connector, reason):
-        self.connector = connector
-        self.log_warn("IMAP factory connection failed")
-        self.retry(connector)
+ at inlineCallbacks
+def migrateTokensToStore(path, store):
+    oldDB = MailGatewayTokensDatabase(path)
+    txn = store.newTransaction()
+    yield txn.commit()

Added: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/outbound.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/outbound.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/outbound.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -0,0 +1,780 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Outbound IMIP mail handling for Calendar Server
+"""
+
+from __future__ import with_statement
+
+from cStringIO import StringIO
+import datetime
+import os
+
+from email.mime.image import MIMEImage
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+import email.utils
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.duration import PyCalendarDuration
+from twext.enterprise.dal.record import fromTable
+from twext.enterprise.queue import WorkItem
+from twext.python.log import Logger, LoggingMixIn
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.mail.smtp import messageid, rfc822date
+from twisted.web.microdom import Text as DOMText, Element as DOMElement
+from twisted.web.microdom import parseString
+from twisted.web.template import XMLString, TEMPLATE_NAMESPACE, Element, renderer, flattenString, tags
+from twistedcaldav.config import config
+from twistedcaldav.ical import Component
+from twistedcaldav.localization import translationTo, _
+from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
+from twistedcaldav.scheduling.imip.smtpsender import SMTPSender
+from txdav.common.datastore.sql_tables import schema
+
+
+
+log = Logger()
+
+
+""" SCHEMA:
+create sequence WORKITEM_SEQ;
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID         integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  FROM_ADDR       varchar(255) not null,
+  TO_ADDR         varchar(255) not null,
+  ICALENDAR_TEXT  text         not null
+);
+"""
+
+
+class IMIPInvitationWork(WorkItem, fromTable(schema.IMIP_INVITATION_WORK)):
+
+    mailSender = None
+
+    @classmethod
+    def getMailSender(cls):
+        if cls.mailSender is not None:
+            if config.Scheduling.iMIP.Enabled:
+                settings = config.Scheduling.iMIP.Sending
+                smtpSender = SMTPSender(settings.Username, settings.Password,
+                    settings.UseSSL, settings.Server, settings.Port)
+                cls.mailSender = MailSender(settings.Address,
+                    settings.SuppressionDays, smtpSender=smtpSender)
+        return cls.mailSender
+
+    @inlineCallbacks
+    def doWork(self):
+        print "XYZZY in doWork"
+        mailSender = self.getMailSender()
+        if mailSender is not None:
+            calendar = Component.fromString(self.icalendarText)
+            success = (yield mailSender.outbound(self.transaction,
+                self.fromAddr, self.toAddr, calendar))
+            if not success:
+                self.transaction.enqueue(IMIPInvitationWork,
+                    fromAddr=self.fromAddr, toAddr=self.toAddr,
+                    icalendarText=self.icalendarText,
+                    notBefore=(
+                        datetime.datetime.now() +
+                        datetime.timedelta(seconds=10)
+                    )
+                )
+
+#
+# Templates
+#
+
+plainCancelTemplate = u"""%(subject)s
+
+%(orgLabel)s: %(plainOrganizer)s
+%(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s
+%(timeLabel)s: %(timeInfo)s %(durationInfo)s
+"""
+
+plainInviteTemplate = u"""%(subject)s
+
+%(orgLabel)s: %(plainOrganizer)s
+%(locLabel)s: %(location)s
+%(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s
+%(timeLabel)s: %(timeInfo)s %(durationInfo)s
+%(descLabel)s: %(description)s
+%(urlLabel)s: %(url)s
+%(attLabel)s: %(plainAttendees)s
+"""
+
+
+htmlCancelTemplate = u"""<html>
+    <body><div>
+
+    <h1>%(subject)s</h1>
+    <p>
+    <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s
+    </p>
+    <p>
+    <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s
+    </p>
+    <p>
+    <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s
+    </p>
+    """.encode("utf-8")
+
+
+htmlInviteTemplate = u"""<html>
+    <body><div>
+    <p>%(inviteLabel)s</p>
+
+    <h1>%(summary)s</h1>
+    <p>
+    <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s
+    </p>
+    <p>
+    <h3>%(locLabel)s:</h3> %(location)s
+    </p>
+    <p>
+    <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s
+    </p>
+    <p>
+    <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s
+    </p>
+    <p>
+    <h3>%(descLabel)s:</h3> %(description)s
+    </p>
+    <p>
+    <h3>%(urlLabel)s:</h3> <a href="%(url)s">%(url)s</a>
+    </p>
+    <p>
+    <h3>%(attLabel)s:</h3> %(htmlAttendees)s
+    </p>
+    """.encode("utf-8")
+
+def _visit(document, node):
+    if isinstance(node, DOMText):
+        idx = node.parentNode.childNodes.index(node)
+        splitted = node.data.split("%(")
+        firstTextNode = document.createTextNode(splitted[0])
+        firstTextNode.parentNode = node.parentNode
+        replacements = [firstTextNode]
+        for moreText in splitted[1:]:
+            slotName, extra = moreText.split(')', 1)
+            extra = extra[1:]
+            slotElement = document.createElement('t:slot')
+            slotElement.setAttribute("name", slotName)
+            slotElement.parentNode = node.parentNode
+            textNode = document.createTextNode(extra)
+            textNode.parentNode = node.parentNode
+            replacements.append(slotElement)
+            replacements.append(textNode)
+        node.parentNode.childNodes[idx:idx + 1] = replacements
+
+    elif isinstance(node, DOMElement):
+        for attrName, attrVal in node.attributes.items():
+            if '%(' in attrVal:
+                del node.attributes[attrName]
+                elem = document.createElement('t:attr')
+                elem.setAttribute('name', attrName)
+                textNode = document.createTextNode(attrVal)
+                elem.appendChild(textNode)
+                node.appendChild(elem)
+
+
+
+def _walk(document, n):
+    _visit(document, n)
+    for subn in n.childNodes:
+        _walk(document, subn)
+
+
+
+def _fixup(data, rendererName):
+    document = parseString(data, beExtremelyLenient=True)
+    document.documentElement.setAttribute(
+        "xmlns:t", TEMPLATE_NAMESPACE
+    )
+    document.doctype = (
+        'html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" '
+        '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"'
+    )
+    document.documentElement.setAttribute(
+        "t:render", rendererName
+    )
+    _walk(document, document)
+    result = document.toxml()
+    return result
+
+
+
+class StringFormatTemplateLoader(object):
+    """
+    Loader for twisted.web.template that converts a template with %()s slots.
+    """
+    def __init__(self, fileFactory, rendererName):
+        """
+        @param fileFactory: a 1-argument callable which returns a file-like
+            object that contains the %()s-format template.
+
+        @param rendererName: the name of the renderer.
+
+        @type rendererName: C{str}
+        """
+        self.fileFactory = fileFactory
+        self.rendererName = rendererName
+
+
+    def load(self):
+        html = _fixup(self.fileFactory().read(), self.rendererName)
+        return XMLString(html).load()
+
+
+
+def localizedLabels(language, canceled, inviteState):
+    """
+    Generate localized labels for an email in the given language.
+
+    @param language: a 2-letter language code
+
+    @type language: C{str}
+
+    @return: a 2-tuple of (subjectFormatString, labelDict), where the first is a
+        format string for use in the subject, and the latter is a dictionary
+        with labels suitable for filling out HTML and plain-text templates.  All
+        values are C{str}s.
+    """
+    with translationTo(language):
+        if canceled:
+            subjectFormatString = _("Event canceled: %(summary)s")
+        elif inviteState == "new":
+            subjectFormatString = _("Event invitation: %(summary)s")
+        elif inviteState == "update":
+            subjectFormatString = _("Event update: %(summary)s")
+        else:
+            subjectFormatString = _("Event reply: %(summary)s")
+
+        if canceled:
+            inviteLabel = _("Event Canceled")
+        else:
+            if inviteState == "new":
+                inviteLabel = _("Event Invitation")
+            elif inviteState == "update":
+                inviteLabel = _("Event Update")
+            else:
+                inviteLabel = _("Event Reply")
+
+        labels = dict(
+            dateLabel=_("Date"),
+            timeLabel=_("Time"),
+            durationLabel=_("Duration"),
+            recurrenceLabel=_("Occurs"),
+            descLabel=_("Description"),
+            urlLabel=_("URL"),
+            orgLabel=_("Organizer"),
+            attLabel=_("Attendees"),
+            locLabel=_("Location"),
+            inviteLabel=inviteLabel,
+        )
+
+        # The translations we get back from gettext are utf-8 encoded
+        # strings, so convert to unicode
+        for key in labels.keys():
+            if isinstance(labels[key], str):
+                labels[key] = labels[key].decode("utf-8")
+
+    return subjectFormatString.decode("utf-8"), labels
+
+
+
+class MailSender(LoggingMixIn):
+
+    def __init__(self, address, suppressionDays, smtpSender):
+        self.address = address
+        self.suppressionDays = suppressionDays
+        self.smtpSender = smtpSender
+
+
+    @inlineCallbacks
+    def outbound(self, txn, originator, recipient, calendar, language='en',
+        onlyAfter=None):
+
+        if onlyAfter is None:
+            duration = PyCalendarDuration(days=self.suppressionDays)
+            onlyAfter = PyCalendarDateTime.getNowUTC() - duration
+
+        component = calendar.masterComponent()
+        if component is None:
+            component = calendar.mainComponent(True)
+        icaluid = component.propertyValue("UID")
+        method = calendar.propertyValue("METHOD")
+
+        # Clean up the attendee list which is purely used within the human
+        # readable email message (not modifying the calendar body)
+        attendees = []
+        for attendeeProp in calendar.getAllAttendeeProperties():
+            cutype = attendeeProp.parameterValue("CUTYPE", "INDIVIDUAL")
+            if cutype == "INDIVIDUAL":
+                cn = attendeeProp.parameterValue("CN", None)
+                if cn is not None:
+                    cn = cn.decode("utf-8")
+                cuaddr = normalizeCUAddr(attendeeProp.value())
+                if cuaddr.startswith("mailto:"):
+                    mailto = cuaddr[7:]
+                    if not cn:
+                        cn = mailto
+                else:
+                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
+                    if emailAddress:
+                        mailto = emailAddress
+                    else:
+                        mailto = None
+
+                if cn or mailto:
+                    attendees.append((cn, mailto))
+
+        toAddr = recipient
+        if not recipient.lower().startswith("mailto:"):
+            raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP "
+                             "operation." % (recipient,))
+        recipient = recipient[7:]
+
+        if method != "REPLY":
+            # Invites and cancellations:
+
+            # Reuse or generate a token based on originator, toAddr, and
+            # event uid
+            token = (yield txn.imipGetToken(originator, toAddr.lower(), icaluid))
+            if token is None:
+
+                # Because in the past the originator was sometimes in mailto:
+                # form, lookup an existing token by mailto: as well
+                organizerProperty = calendar.getOrganizerProperty()
+                organizerEmailAddress = organizerProperty.parameterValue("EMAIL", None)
+                if organizerEmailAddress is not None:
+                    token = (yield txn.imipGetToken("mailto:%s" % (organizerEmailAddress.lower(),), toAddr.lower(), icaluid))
+
+            if token is None:
+                token = (yield txn.imipCreateToken(originator, toAddr.lower(), icaluid))
+                self.log_debug("Mail gateway created token %s for %s "
+                               "(originator), %s (recipient) and %s (icaluid)"
+                               % (token, originator, toAddr, icaluid))
+                inviteState = "new"
+
+            else:
+                self.log_debug("Mail gateway reusing token %s for %s "
+                               "(originator), %s (recipient) and %s (icaluid)"
+                               % (token, originator, toAddr, icaluid))
+                inviteState = "update"
+
+            fullServerAddress = self.address
+            _ignore_name, serverAddress = email.utils.parseaddr(fullServerAddress)
+            pre, post = serverAddress.split('@')
+            addressWithToken = "%s+%s@%s" % (pre, token, post)
+
+            organizerProperty = calendar.getOrganizerProperty()
+            organizerEmailAddress = organizerProperty.parameterValue("EMAIL",
+                                                                     None)
+            organizerValue = organizerProperty.value()
+            organizerProperty.setValue("mailto:%s" % (addressWithToken,))
+
+            # If the organizer is also an attendee, update that attendee value
+            # to match
+            organizerAttendeeProperty = calendar.getAttendeeProperty(
+                [organizerValue])
+            if organizerAttendeeProperty is not None:
+                organizerAttendeeProperty.setValue("mailto:%s" %
+                                                   (addressWithToken,))
+
+            # The email's From will include the originator's real name email
+            # address if available.  Otherwise it will be the server's email
+            # address (without # + addressing)
+            if organizerEmailAddress:
+                orgEmail = fromAddr = organizerEmailAddress
+            else:
+                fromAddr = serverAddress
+                orgEmail = None
+            cn = calendar.getOrganizerProperty().parameterValue('CN', None)
+            if cn is None:
+                cn = u'Calendar Server'
+                orgCN = orgEmail
+            else:
+                orgCN = cn = cn.decode("utf-8")
+
+            # a unicode cn (rather than an encode string value) means the
+            # from address will get properly encoded per rfc2047 within the
+            # MIMEMultipart in generateEmail
+            formattedFrom = "%s <%s>" % (cn, fromAddr)
+
+            # Reply-to address will be the server+token address
+
+        else: # REPLY
+            inviteState = "reply"
+
+            # Look up the attendee property corresponding to the originator
+            # of this reply
+            originatorAttendeeProperty = calendar.getAttendeeProperty(
+                [originator])
+            formattedFrom = fromAddr = originator = ""
+            if originatorAttendeeProperty:
+                originatorAttendeeEmailAddress = (
+                    originatorAttendeeProperty.parameterValue("EMAIL", None)
+                )
+                if originatorAttendeeEmailAddress:
+                    formattedFrom = fromAddr = originator = (
+                        originatorAttendeeEmailAddress
+                    )
+
+            organizerMailto = str(calendar.getOrganizer())
+            if not organizerMailto.lower().startswith("mailto:"):
+                raise ValueError("ORGANIZER address '%s' must be mailto: "
+                                 "for REPLY." % (organizerMailto,))
+            orgEmail = organizerMailto[7:]
+
+            orgCN = calendar.getOrganizerProperty().parameterValue('CN', None)
+            addressWithToken = formattedFrom
+
+        # At the point we've created the token in the db, which we always
+        # want to do, but if this message is for an event completely in
+        # the past we don't want to actually send an email.
+        if not calendar.hasInstancesAfter(onlyAfter):
+            self.log_debug("Skipping IMIP message for old event")
+            returnValue(True)
+
+        # Now prevent any "internal" CUAs from being exposed by converting
+        # to mailto: if we have one
+        for attendeeProp in calendar.getAllAttendeeProperties():
+            cutype = attendeeProp.parameterValue('CUTYPE', None)
+            if cutype == "INDIVIDUAL":
+                cuaddr = normalizeCUAddr(attendeeProp.value())
+                if not cuaddr.startswith("mailto:"):
+                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
+                    if emailAddress:
+                        attendeeProp.setValue("mailto:%s" % (emailAddress,))
+
+        msgId, message = self.generateEmail(inviteState, calendar, orgEmail,
+            orgCN, attendees, formattedFrom, addressWithToken, recipient,
+            language=language)
+
+        try:
+            success = (yield self.smtpSender.sendMessage(fromAddr, toAddr,
+                msgId, message))
+            returnValue(success)
+        except:
+            returnValue(False)
+
+
+    def getIconPath(self, details, canceled, language='en'):
+        iconDir = config.Scheduling.iMIP.MailIconsDirectory.rstrip("/")
+
+        if canceled:
+            iconName = "canceled.png"
+            iconPath = os.path.join(iconDir, iconName)
+            if os.path.exists(iconPath):
+                return iconPath
+            else:
+                return None
+
+        else:
+            month = int(details['month'])
+            day = int(details['day'])
+            with translationTo(language) as trans:
+                monthName = trans.monthAbbreviation(month)
+            iconName = "%02d.png" % (day,)
+            iconPath = os.path.join(iconDir, monthName.encode("utf-8"), iconName)
+            if not os.path.exists(iconPath):
+                # Try the generic (numeric) version
+                iconPath = os.path.join(iconDir, "%02d" % (month,), iconName)
+                if not os.path.exists(iconPath):
+                    return None
+            return iconPath
+
+
+    def generateEmail(self, inviteState, calendar, orgEmail, orgCN,
+                      attendees, fromAddress, replyToAddress, toAddress,
+                      language='en'):
+        """
+        Generate MIME text containing an iMIP invitation, cancellation, update
+        or reply.
+
+        @param inviteState: 'new', 'update', or 'reply'.
+
+        @type inviteState: C{str}
+
+        @param calendar: the iCalendar component to attach to the email.
+
+        @type calendar: L{twistedcaldav.ical.Component}
+
+        @param orgEmail: The email for the organizer, in C{localhost at domain}
+            format, or C{None} if the organizer has no email address.
+
+        @type orgEmail: C{str} or C{NoneType}
+
+        @param orgCN: Common name / display name for the organizer.
+
+        @type orgCN: C{unicode}
+
+        @param attendees: A C{list} of 2-C{tuple}s of (common name, email
+            address) similar to (orgEmail, orgCN).
+
+        @param fromAddress: the address to use in the C{From:} header of the
+            email.
+
+        @type fromAddress: C{str}
+
+        @param replyToAddress: the address to use in the C{Reply-To} header.
+
+        @type replyToAddress: C{str}
+
+        @param toAddress: the address to use in the C{To} header.
+
+        @type toAddress: C{str}
+
+        @param language: a 2-letter language code describing the target
+            language that the email should be generated in.
+
+        @type language: C{str}
+
+        @return: a 2-tuple of C{str}s: (message ID, message text).  The message
+            ID is the value of the C{Message-ID} header, and the message text is
+            the full MIME message, ready for transport over SMTP.
+        """
+
+        details = self.getEventDetails(calendar, language=language)
+        canceled = (calendar.propertyValue("METHOD") == "CANCEL")
+        iconPath = self.getIconPath(details, canceled, language=language)
+
+        subjectFormat, labels = localizedLabels(language, canceled, inviteState)
+        details.update(labels)
+
+        details['subject'] = subjectFormat % {'summary' : details['summary']}
+        details['iconName'] = iconName = "calicon.png"
+
+        plainText = self.renderPlainText(details, (orgCN, orgEmail),
+                                         attendees, canceled)
+
+        [addIcon, htmlText] = self.renderHTML(details, (orgCN, orgEmail),
+                                              attendees, canceled)
+
+        msg = MIMEMultipart()
+        msg["From"] = fromAddress
+        msg["Subject"] = details['subject']
+        msg["Reply-To"] = replyToAddress
+        msg["To"] = toAddress
+        msg["Date"] = rfc822date()
+        msgId = messageid()
+        msg["Message-ID"] = msgId
+
+        msgAlt = MIMEMultipart("alternative")
+        msg.attach(msgAlt)
+
+        # plain version
+        msgPlain = MIMEText(plainText, "plain", "UTF-8")
+        msgAlt.attach(msgPlain)
+
+        # html version
+        msgHtmlRelated = MIMEMultipart("related", type="text/html")
+        msgAlt.attach(msgHtmlRelated)
+
+        msgHtml = MIMEText(htmlText, "html", "UTF-8")
+        msgHtmlRelated.attach(msgHtml)
+
+        # an image for html version
+        if addIcon and iconPath != None and os.path.exists(iconPath):
+
+            with open(iconPath) as iconFile:
+                msgIcon = MIMEImage(iconFile.read(),
+                    _subtype='png;x-apple-mail-type=stationery;name="%s"' %
+                    (iconName,))
+
+            msgIcon.add_header("Content-ID", "<%s>" % (iconName,))
+            msgIcon.add_header("Content-Disposition", "inline;filename=%s" %
+                (iconName,))
+            msgHtmlRelated.attach(msgIcon)
+
+        calendarText = str(calendar)
+        # the icalendar attachment
+        self.log_debug("Mail gateway sending calendar body: %s"
+                       % (calendarText,))
+        msgIcal = MIMEText(calendarText, "calendar", "UTF-8")
+        method = calendar.propertyValue("METHOD").lower()
+        msgIcal.set_param("method", method)
+        msgIcal.add_header("Content-ID", "<invitation.ics>")
+        msgIcal.add_header("Content-Disposition",
+            "inline;filename=invitation.ics")
+        msg.attach(msgIcal)
+
+        return msgId, msg.as_string()
+
+
+    def renderPlainText(self, details, (orgCN, orgEmail), attendees, canceled):
+        """
+        Render text/plain message part based on invitation details and a flag
+        indicating whether the message is a cancellation.
+
+        @return: UTF-8 encoded text.
+
+        @rtype: C{str}
+        """
+        plainAttendeeList = []
+        for cn, mailto in attendees:
+            if cn:
+                plainAttendeeList.append(cn if not mailto else
+                    "%s <%s>" % (cn, mailto))
+            elif mailto:
+                plainAttendeeList.append("<%s>" % (mailto,))
+
+        details['plainAttendees'] = ", ".join(plainAttendeeList)
+
+        details['plainOrganizer'] = (orgCN if not orgEmail else
+            "%s <%s>" % (orgCN, orgEmail))
+
+        # plain text version
+        if canceled:
+            plainTemplate = plainCancelTemplate
+        else:
+            plainTemplate = plainInviteTemplate
+
+        return (plainTemplate % details).encode("UTF-8")
+
+
+    def renderHTML(self, details, organizer, attendees, canceled):
+        """
+        Render HTML message part based on invitation details and a flag
+        indicating whether the message is a cancellation.
+
+        @return: a 2-tuple of (should add icon (C{bool}), html text (C{str},
+            representing utf-8 encoded bytes)).  The first element indicates
+            whether the MIME generator needs to add a C{cid:} icon image part to
+            satisfy the HTML links.
+        """
+        orgCN, orgEmail = organizer
+
+        # TODO: htmlAttendees needs to be a separate element with a separate
+        # template fragment.  Luckily that fragment is the same regardless
+        # of the rest of the template.
+        htmlAttendees = []
+        first = True
+        for cn, mailto in attendees:
+            if not first:
+                htmlAttendees.append(u", ")
+            else:
+                first = False
+
+            if mailto:
+                if not cn:
+                    cn = mailto
+                htmlAttendees.append(
+                    tags.a(href="mailto:%s" % (mailto,))(cn)
+                )
+            else:
+                htmlAttendees.append(cn)
+
+        details['htmlAttendees'] = htmlAttendees
+
+        # TODO: htmlOrganizer is also some HTML that requires additional
+        # template stuff, and once again, it's just a 'mailto:'.
+        # tags.a(href="mailto:"+email)[cn]
+        if orgEmail:
+            details['htmlOrganizer'] = tags.a(href="mailto:%s" % (orgEmail,))(
+                orgCN)
+        else:
+            details['htmlOrganizer'] = orgCN
+
+        templateDir = config.Scheduling.iMIP.MailTemplatesDirectory.rstrip("/")
+        templateName = "cancel.html" if canceled else "invite.html"
+        templatePath = os.path.join(templateDir, templateName)
+
+        if not os.path.exists(templatePath):
+            # Fall back to built-in simple templates:
+            if canceled:
+                htmlTemplate = htmlCancelTemplate
+            else:
+                htmlTemplate = htmlInviteTemplate
+        else: # HTML template file exists
+
+            with open(templatePath) as templateFile:
+                htmlTemplate = templateFile.read()
+
+        class EmailElement(Element):
+            loader = StringFormatTemplateLoader(lambda : StringIO(htmlTemplate),
+                                                "email")
+
+            @renderer
+            def email(self, request, tag):
+                return tag.fillSlots(**details)
+
+        textCollector = []
+        flattenString(None, EmailElement()).addCallback(textCollector.append)
+        htmlText = textCollector[0]
+
+        # If the template refers to an icon in a cid: link, it needs to be added
+        # in the MIME.
+        addIcon = (htmlTemplate.find("cid:%(iconName)s") != -1)
+        return (addIcon, htmlText)
+
+
+    def getEventDetails(self, calendar, language='en'):
+        """
+        Create a dictionary mapping slot names - specifically: summary,
+        description, location, dateInfo, timeInfo, durationInfo, recurrenceInfo,
+        url - with localized string values that should be placed into the HTML
+        and plain-text templates.
+
+        @param calendar: a L{Component} upon which to base the language.
+        @type calendar: L{Component}
+
+        @param language: a 2-letter language code.
+        @type language: C{str}
+
+        @return: a mapping from template slot name to localized text.
+        @rtype: a C{dict} mapping C{bytes} to C{unicode}.
+        """
+
+        # Get the most appropriate component
+        component = calendar.masterComponent()
+        if component is None:
+            component = calendar.mainComponent(True)
+
+        results = {}
+
+        dtStart = component.propertyValue('DTSTART')
+        results['month'] = dtStart.getMonth()
+        results['day'] = dtStart.getDay()
+
+        for propertyToResult in ['summary', 'description', 'location', 'url']:
+            result = component.propertyValue(propertyToResult.upper())
+            if result is None:
+                result = u""
+            else:
+                result = result.decode('utf-8')
+            results[propertyToResult] = result
+
+        with translationTo(language) as trans:
+            results['dateInfo'] = trans.date(component).decode('utf-8')
+            results['timeInfo'], duration = (x.decode('utf-8') for x in trans.time(component))
+            results['durationInfo'] = u"(%s)" % (duration,) if duration else u""
+
+            for propertyName in ('RRULE', 'RDATE', 'EXRULE', 'EXDATE',
+                                 'RECURRENCE-ID'):
+                if component.hasProperty(propertyName):
+                    results['recurrenceInfo'] = _("(Repeating)").decode('utf-8')
+                    break
+            else:
+                results['recurrenceInfo'] = u""
+
+        return results
+
+
+

Modified: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/scheduler.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/scheduler.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/scheduler.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -15,22 +15,18 @@
 ##
 
 from twext.python.log import Logger
-from twext.web2 import responsecode
 from twext.web2.dav.http import ErrorResponse
-from twext.web2.http import HTTPError
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import succeed
 from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.scheduling import addressmapping
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser
 from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser
-from twistedcaldav.scheduling.scheduler import RemoteScheduler, \
-    ScheduleResponseQueue
-import itertools
-import socket
+from twistedcaldav.scheduling.scheduler import RemoteScheduler
+from twistedcaldav.scheduling.scheduler import ScheduleResponseQueue
 
 
 """
-L{IMIPScheduler} - handles deliveries for scheduling messages being POSTed to the iMIP inbox.
+L{IMIPScheduler} - handles deliveries for scheduling messages retrieved via
+mail
 """
 
 __all__ = [
@@ -63,26 +59,14 @@
         pass
 
 
-    @inlineCallbacks
     def checkOriginator(self):
         """
-        Check the validity of the Originator header.
+        The originator always comes out of the tokens db
         """
+        self.originator = RemoteCalendarUser(self.originator)
+        return succeed(None)
 
-        # For remote requests we do not allow the originator to be a local user or one within our domain.
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
-        if originatorPrincipal or localUser:
-            log.err("Cannot use originator that is on this server: %s" % (self.originator,))
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["originator-denied"],
-                "Originator cannot be local to server",
-            ))
-        else:
-            self.originator = RemoteCalendarUser(self.originator)
 
-
     def checkOrganizerAsOriginator(self):
         pass
 
@@ -92,21 +76,4 @@
 
 
     def securityChecks(self):
-        """
-        Check that the connection is from the mail gateway
-        """
-        allowed = config.Scheduling['iMIP']['MailGatewayServer']
-        # Get the request IP and map to hostname.
-        clientip = self.request.remoteAddr.host
-        host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
-        for host in itertools.chain((host, clientip), aliases):
-            if host == allowed:
-                break
-        else:
-            log.err("Only %s is allowed to submit internal scheduling requests, not %s" % (allowed, host))
-            # TODO: verify this is the right response:
-            raise HTTPError(self.errorResponse(
-                responsecode.FORBIDDEN,
-                self.errorElements["originator-denied"],
-                "Originator server not allowed to send to this server",
-            ))
+        pass

Added: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/smtpsender.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/smtpsender.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/smtpsender.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -0,0 +1,75 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+SMTP sending utility
+"""
+
+from cStringIO import StringIO
+
+from twext.internet.adaptendpoint import connect
+from twext.internet.gaiendpoint import GAIEndpoint
+from twext.python.log import Logger
+from twisted.internet import defer, ssl, reactor as _reactor
+from twisted.mail.smtp import ESMTPSenderFactory
+
+log = Logger()
+
+class SMTPSender(object):
+
+    def __init__(self, username, password, useSSL, server, port):
+        self.username = username
+        self.password = password
+        self.useSSL = useSSL
+        self.server = server
+        self.port = port
+
+    def sendMessage(self, fromAddr, toAddr, msgId, message):
+
+        log.debug("Sending: %s" % (message,))
+        def _success(result, msgId, fromAddr, toAddr):
+            log.info("Sent IMIP message %s from %s to %s" %
+                (msgId, fromAddr, toAddr))
+            return True
+
+        def _failure(failure, msgId, fromAddr, toAddr):
+            log.error("Failed to send IMIP message %s from %s "
+                           "to %s (Reason: %s)" %
+                           (msgId, fromAddr, toAddr,
+                            failure.getErrorMessage()))
+            return False
+
+        deferred = defer.Deferred()
+
+        if self.useSSL:
+            contextFactory = ssl.ClientContextFactory()
+        else:
+            contextFactory = None
+
+        factory = ESMTPSenderFactory(
+            self.username, self.password,
+            fromAddr, toAddr,
+            # per http://trac.calendarserver.org/ticket/416 ...
+            StringIO(message.as_string().replace("\r\n", "\n")), deferred,
+            contextFactory=contextFactory,
+            requireAuthentication=False,
+            requireTransportSecurity=self.useSSL)
+
+        connect(GAIEndpoint(_reactor, self.server, self.port),
+                factory)
+        deferred.addCallback(_success, msgId, fromAddr, toAddr)
+        deferred.addErrback(_failure, msgId, fromAddr, toAddr)
+        return deferred

Added: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_inbound.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_inbound.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_inbound.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -0,0 +1,387 @@
+##
+# Copyright (c) 2008-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+import email
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.modules import getModule
+from twistedcaldav.ical import Component
+from twistedcaldav.scheduling.imip.inbound import MailReceiver, injectMessage
+from twistedcaldav.scheduling.imip.inbound import IMIPReplyWork
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.test.util import xmlFile
+from txdav.common.datastore.test.util import buildStore
+from calendarserver.tap.util import getRootResource
+from twistedcaldav.config import config
+
+def echo(*args):
+    return args
+
+class InboundTests(TestCase):
+
+    @inlineCallbacks
+    def setUp(self):
+        super(InboundTests, self).setUp()
+
+        self.store = yield buildStore(self, None)
+        self.patch(config.DirectoryService.params, "xmlFile", xmlFile)
+        self.root = getRootResource(config, self.store)
+        self.directory = self.root.getDirectory()
+        self.receiver = MailReceiver(self.store, self.directory, self.root)
+        module = getModule(__name__)
+        self.dataPath = module.filePath.sibling("data")
+
+    def dataFile(self, name):
+        """
+        Get the contents of a given data file from the 'data/mail' test
+        fixtures directory.
+        """
+        return self.dataPath.child(name).getContent()
+
+
+    def test_checkDSNFailure(self):
+
+        data = {
+            'good_reply' : (False, None, None),
+            'dsn_failure_no_original' : (True, 'failed', None),
+            'dsn_failure_no_ics' : (True, 'failed', None),
+            'dsn_failure_with_ics' : (True, 'failed', '''BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+PRODID:-//example Inc.//iCal 3.0//EN
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZNAME:PST
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZNAME:PDT
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
+DTSTART;TZID=US/Pacific:20080812T094500
+DTEND;TZID=US/Pacific:20080812T104500
+ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:user01 at exam
+ ple.com
+ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
+ CTION;CN=nonexistant at example.com:mailto:nonexistant at example.com
+CREATED:20080812T191857Z
+DTSTAMP:20080812T191932Z
+ORGANIZER;CN=User 01:mailto:xyzzy+8e16b897-d544-4217-88e9-a363d08
+ 46f6c at example.com
+SEQUENCE:2
+SUMMARY:New Event
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+'''),
+        }
+
+        for filename, expected in data.iteritems():
+            msg = email.message_from_string(self.dataFile(filename))
+            self.assertEquals(self.receiver.checkDSN(msg), expected)
+
+
+    @inlineCallbacks
+    def test_processDSN(self):
+
+        template = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+PRODID:-//example Inc.//iCal 3.0//EN
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZNAME:PDT
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZNAME:PST
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
+DTSTART;TZID=US/Pacific:20080812T094500
+DTEND;TZID=US/Pacific:20080812T104500
+ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:user01 at exam
+ ple.com
+ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
+ CTION;CN=nonexistant at example.com:mailto:nonexistant at example.com
+CREATED:20080812T191857Z
+DTSTAMP:20080812T191932Z
+ORGANIZER;CN=User 01:mailto:xyzzy+%s at example.com
+SEQUENCE:2
+SUMMARY:New Event
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+        # Make sure an unknown token is not processed
+        calBody = template % "bogus_token"
+        self.assertEquals(
+            (yield self.receiver.processDSN(calBody, "xyzzy", echo)),
+            MailReceiver.UNKNOWN_TOKEN
+        )
+
+        # Make sure a known token *is* processed
+        txn = self.store.newTransaction()
+        token = (yield txn.imipCreateToken(
+            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
+            "mailto:user02 at example.com",
+            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C"
+        ))
+        yield txn.commit()
+        calBody = template % token
+        result = (yield self.receiver.processDSN(calBody, "xyzzy", echo))
+        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)
+        # organizer, attendee, calendar, msgId = result
+
+        # self.assertEquals(organizer, 'urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500')
+        # self.assertEquals(attendee, 'mailto:user02 at example.com')
+        # self.assertEquals(str(calendar), """BEGIN:VCALENDAR
+        # VERSION:2.0
+        # CALSCALE:GREGORIAN
+        # METHOD:REQUEST
+        # PRODID:-//example Inc.//iCal 3.0//EN
+        # BEGIN:VTIMEZONE
+        # TZID:US/Pacific
+        # BEGIN:DAYLIGHT
+        # DTSTART:20070311T020000
+        # RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+        # TZNAME:PDT
+        # TZOFFSETFROM:-0800
+        # TZOFFSETTO:-0700
+        # END:DAYLIGHT
+        # BEGIN:STANDARD
+        # DTSTART:20071104T020000
+        # RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+        # TZNAME:PST
+        # TZOFFSETFROM:-0700
+        # TZOFFSETTO:-0800
+        # END:STANDARD
+        # END:VTIMEZONE
+        # BEGIN:VEVENT
+        # UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
+        # DTSTART;TZID=US/Pacific:20080812T094500
+        # DTEND;TZID=US/Pacific:20080812T104500
+        # CREATED:20080812T191857Z
+        # DTSTAMP:20080812T191932Z
+        # ORGANIZER;CN=User 01:urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500
+        # REQUEST-STATUS:5.1;Service unavailable
+        # SEQUENCE:2
+        # SUMMARY:New Event
+        # TRANSP:OPAQUE
+        # END:VEVENT
+        # END:VCALENDAR
+        # """.replace("\n", "\r\n"))
+        #         self.assertEquals(msgId, 'xyzzy')
+
+    @inlineCallbacks
+    def test_processReply(self):
+        msg = email.message_from_string(self.dataFile('good_reply'))
+
+        # Make sure an unknown token is not processed
+        result = (yield self.receiver.processReply(msg, echo))
+        self.assertEquals(result, MailReceiver.UNKNOWN_TOKEN)
+
+        # Make sure a known token *is* processed
+        txn = self.store.newTransaction()
+        yield txn.imipCreateToken(
+            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
+            "mailto:xyzzy at example.com",
+            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
+            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
+        )
+        yield txn.commit()
+
+        result = (yield self.receiver.processReply(msg, echo))
+        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)
+        # organizer, attendee, _ignore_calendar = result
+        # self.assertEquals(organizer,
+        #                   'urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500')
+        # self.assertEquals(attendee, 'mailto:xyzzy at example.com')
+
+
+
+    def test_processReplyMissingOrganizer(self):
+        msg = email.message_from_string(self.dataFile('reply_missing_organizer'))
+
+        # stick the token in the database first
+        txn = self.store.newTransaction()
+        yield txn.imipCreateToken(
+            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
+            "mailto:xyzzy at example.com",
+            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
+            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
+        )
+        yield txn.commit()
+
+        result = (yield self.receiver.processReply(msg, echo))
+        organizer, _ignore_attendee, calendar = result
+        organizerProp = calendar.mainComponent().getOrganizerProperty()
+        self.assertTrue(organizerProp is not None)
+        self.assertEquals(organizer,
+                          "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500")
+
+
+    def test_processReplyMissingAttendee(self):
+        msg = email.message_from_string(self.dataFile('reply_missing_attendee'))
+
+        txn = self.store.newTransaction()
+        yield txn.imipCreateToken(
+            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
+            "mailto:xyzzy at example.com",
+            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
+            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
+        )
+        yield txn.commit()
+
+        result = (yield self.receiver.processReply(msg, echo))
+        _ignore_organizer, attendee, calendar = result
+
+        # Since the expected attendee was missing, the reply processor should
+        # have added an attendee back in with a "5.1;Service unavailable"
+        # schedule-status
+        attendeeProp = calendar.mainComponent().getAttendeeProperty([attendee])
+        self.assertEquals(attendeeProp.parameterValue("SCHEDULE-STATUS"),
+                          iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+
+    def test_processReplyMissingAttachment(self):
+
+        msg = email.message_from_string(
+            self.dataFile('reply_missing_attachment')
+        )
+
+        # stick the token in the database first
+        txn = self.store.newTransaction()
+        yield txn.imipCreateToken(
+            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
+            "mailto:xyzzy at example.com",
+            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
+            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
+        )
+        yield txn.commit()
+
+        result = (yield self.receiver.processReply(msg, echo))
+        self.assertEquals(result, MailReceiver.REPLY_FORWARDED_TO_ORGANIZER)        
+
+    @inlineCallbacks
+    def test_injectMessage(self):
+
+        calendar = Component.fromString("""BEGIN:VCALENDAR
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+VERSION:2.0
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20130208T120000Z
+DTSTART:20180601T120000Z
+DTEND:20180601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:mailto:xyzzy at example.com;PARTSTAT=ACCEPTED
+END:VEVENT
+END:VCALENDAR
+""")
+
+        result = (yield injectMessage(
+                self.root,
+                "urn:uuid:user01",
+                "mailto:xyzzy at example.com",
+                calendar
+            )
+        )
+        self.assertEquals(
+            "1.2;Scheduling message has been delivered",
+            result.responses[0].children[1].toString()
+        )
+
+    @inlineCallbacks
+    def test_injectMessageWithError(self):
+
+        calendar = Component.fromString("""BEGIN:VCALENDAR
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+VERSION:2.0
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20130208T120000Z
+DTSTART:20180601T120000Z
+DTEND:20180601T130000Z
+ORGANIZER:urn:uuid:unknown_user
+ATTENDEE:mailto:xyzzy at example.com;PARTSTAT=ACCEPTED
+END:VEVENT
+END:VCALENDAR
+""")
+
+        result = (yield injectMessage(
+                self.root,
+                "urn:uuid:unknown_user",
+                "mailto:xyzzy at example.com",
+                calendar
+            )
+        )
+        self.assertEquals(
+            "3.7;Invalid Calendar User",
+            result.responses[0].children[1].toString()
+        )
+
+
+    @inlineCallbacks
+    def test_work(self):
+
+        calendar = """BEGIN:VCALENDAR
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+VERSION:2.0
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20130208T120000Z
+DTSTART:20180601T120000Z
+DTEND:20180601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:mailto:xyzzy at example.com;PARTSTAT=ACCEPTED
+END:VEVENT
+END:VCALENDAR
+"""
+        txn = self.store.newTransaction()
+        wp = (yield txn.enqueue(IMIPReplyWork,
+            organizer="urn:uuid:user01",
+            attendee="mailto:xyzzy at example.com",
+            icalendarText=calendar
+        ))
+        yield txn.commit()
+        yield wp.whenExecuted()

Modified: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_mailgateway.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_mailgateway.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -20,7 +20,6 @@
 from pycalendar.datetime import PyCalendarDateTime
 
 from twisted.internet.defer import inlineCallbacks
-from twisted.python.modules import getModule
 from twisted.web.template import Element, renderer, flattenString
 
 from twistedcaldav.config import config, ConfigDict
@@ -28,15 +27,17 @@
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.ical import Component
 from twistedcaldav.scheduling.imip.mailgateway import MailGatewayTokensDatabase
-from twistedcaldav.scheduling.imip.mailgateway import MailHandler
 from twistedcaldav.scheduling.imip.mailgateway import StringFormatTemplateLoader
 from twistedcaldav.scheduling.imip.mailgateway import injectionSettingsFromURL
 from twistedcaldav.scheduling.imip.mailgateway import serverForOrganizer
+from twistedcaldav.scheduling.imip.outbound import MailSender
 from twistedcaldav.scheduling.ischedule.localservers import Servers
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
 from twistedcaldav.test.util import TestCase
 from twistedcaldav.test.util import xmlFile, augmentsFile
 
+from txdav.common.datastore.test.util import buildStore
+
 import datetime
 import email
 import os
@@ -68,11 +69,33 @@
 END:VCALENDAR
 """
 
+class DummySender(object):
+
+    def __init__(self):
+        self.reset()
+        self.shouldSucceed = True
+
+    def reset(self):
+        self.fromAddr = None
+        self.toAddr = None
+        self.msgId = None
+        self.message = None
+
+    def sendMessage(self, fromAddr, toAddr, msgId, message):
+        self.fromAddr = fromAddr
+        self.toAddr = toAddr
+        self.msgId = msgId
+        self.message = message
+        return succeed(self.shouldSucceed)
+
+
 class MailHandlerTests(TestCase):
 
     def setUp(self):
         super(MailHandlerTests, self).setUp()
 
+        self.store = yield buildStore(self, None)
+
         self._setupServers(serverData)
         self.directory = XMLDirectoryService(
             {
@@ -81,11 +104,9 @@
                     augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
             }
         )
-        self.handler = MailHandler(dataRoot=":memory:", directory=self.directory)
-        module = getModule(__name__)
+        self.handler = MailSender("server at example.com", 7, DummySender())
         self.dataPath = module.filePath.sibling("data")
 
-
     def _setupServers(self, data):
         self.patch(config, "ServerHostName", "caldav1.example.com")
         self.patch(config, "HTTPPort", 8008)
@@ -135,6 +156,7 @@
         attendee = "mailto:you at example.com"
         icaluid = "123"
         pastDate = datetime.date(2009, 1, 1)
+        txn = self.store.newTransaction()
         self.handler.db._db_execute(
             """
             insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)

Added: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_outbound.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_outbound.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/imip/test/test_outbound.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -0,0 +1,559 @@
+##
+# Copyright (c) 2008-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from cStringIO import StringIO
+import os
+
+import email
+from pycalendar.datetime import PyCalendarDateTime
+from twisted.internet.defer import inlineCallbacks, succeed
+from twisted.web.template import Element, renderer, flattenString
+from twistedcaldav.config import config
+from twistedcaldav.directory import augment
+from twistedcaldav.directory.xmlfile import XMLDirectoryService
+from twistedcaldav.ical import Component
+from twistedcaldav.scheduling.imip.outbound import IMIPInvitationWork
+from twistedcaldav.scheduling.imip.outbound import MailSender
+from twistedcaldav.scheduling.imip.outbound import StringFormatTemplateLoader
+from twistedcaldav.test.util import TestCase, xmlFile, augmentsFile
+from txdav.common.datastore.test.util import buildStore
+
+initialInviteText = u"""BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:CFDD5E46-4F74-478A-9311-B3FF905449C3
+DTSTART:20200325T154500Z
+DTEND:20200325T164500Z
+ATTENDEE;CN=Th\xe9 Attendee;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRU
+ E:mailto:attendee at example.com
+ATTENDEE;CN=Th\xe9 Organizer;CUTYPE=INDIVIDUAL;EMAIL=organizer at example.com;P
+ ARTSTAT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
+ATTENDEE;CN=An Attendee without CUTYPE;EMAIL=nocutype at example.com;PARTSTAT=A
+ CCEPTED:urn:uuid:4DB528DC-3E60-44FA-9546-2A00FCDCFFAB
+ATTENDEE;EMAIL=nocn at example.com;PARTSTAT=ACCEPTED:urn:uuid:A592CF8B-4FC8-4E4
+ F-B543-B2F29A7EEB0B
+ORGANIZER;CN=Th\xe9 Organizer;EMAIL=organizer at example.com:urn:uuid:C3B38B00-
+ 4166-11DD-B22C-A07C87E02F6A
+SUMMARY:t\xe9sting outbound( )
+DESCRIPTION:awesome description with "<" and "&"
+END:VEVENT
+END:VCALENDAR
+"""
+
+ORGANIZER = "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A"
+ATTENDEE = "mailto:attendee at example.com"
+ICALUID = "CFDD5E46-4F74-478A-9311-B3FF905449C3"
+
+class DummySMTPSender(object):
+
+    def __init__(self):
+        self.reset()
+        self.shouldSucceed = True
+
+    def reset(self):
+        self.sendMessageCalled = False
+        self.fromAddr = None
+        self.toAddr = None
+        self.msgId = None
+        self.message = None
+
+    def sendMessage(self, fromAddr, toAddr, msgId, message):
+        self.sendMessageCalled = True
+        self.fromAddr = fromAddr
+        self.toAddr = toAddr
+        self.msgId = msgId
+        self.message = message
+        return succeed(self.shouldSucceed)
+
+
+class OutboundTests(TestCase):
+
+    @inlineCallbacks
+    def setUp(self):
+        self.store = yield buildStore(self, None)
+        self.directory = XMLDirectoryService(
+            {
+                'xmlFile' : xmlFile,
+                'augmentService' :
+                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
+            }
+        )
+        self.sender = MailSender("server at example.com", 7, DummySMTPSender())
+
+        def _getSender(ignored):
+            return self.sender
+        self.patch(IMIPInvitationWork, "getMailSender", _getSender)
+
+        self.wp = None
+        self.store.queuer.registerProposalCallback(self._proposalCallback)
+
+    def _proposalCallback(self, wp):
+        # print "New proposal", wp
+        self.wp = wp
+
+    @inlineCallbacks
+    def test_work(self):
+        txn = self.store.newTransaction()
+        wp = (yield txn.enqueue(IMIPInvitationWork,
+            fromAddr=ORGANIZER,
+            toAddr=ATTENDEE,
+            icalendarText=initialInviteText.replace("\n", "\r\n"),
+        ))
+        self.assertEquals(wp, self.wp)
+        yield txn.commit()
+        yield wp.whenExecuted()
+
+        txn = self.store.newTransaction()
+        token = (yield txn.imipGetToken(
+            ORGANIZER,
+            ATTENDEE,
+            ICALUID
+        ))
+        self.assertTrue(token)
+        organizer, attendee, icaluid = (yield txn.imipLookupByToken(token))[0]
+        yield txn.commit()
+        self.assertEquals(organizer, ORGANIZER)
+        self.assertEquals(attendee, ATTENDEE)
+        self.assertEquals(icaluid, ICALUID)
+
+    @inlineCallbacks
+    def test_workFailure(self):
+
+        def _fail(*args):
+            raise Exception
+
+        self.sender.smtpSender.shouldSucceed = False
+
+        txn = self.store.newTransaction()
+        wp = (yield txn.enqueue(IMIPInvitationWork,
+            fromAddr=ORGANIZER,
+            toAddr=ATTENDEE,
+            icalendarText=initialInviteText.replace("\n", "\r\n"),
+        ))
+        yield txn.commit()
+        yield wp.whenExecuted()
+        self.assertNotEquals(wp, self.wp)
+
+
+    def _interceptEmail(self, inviteState, calendar, orgEmail, orgCn,
+        attendees, fromAddress, replyToAddress, toAddress, language="en"):
+        self.inviteState = inviteState
+        self.calendar = calendar
+        self.orgEmail = orgEmail
+        self.orgCn = orgCn
+        self.attendees = attendees
+        self.fromAddress = fromAddress
+        self.replyToAddress = replyToAddress
+        self.toAddress = toAddress
+        self.language = language
+        self.results = self._actualGenerateEmail(inviteState, calendar,
+            orgEmail, orgCn, attendees, fromAddress, replyToAddress, toAddress,
+            language=language)
+        return self.results
+
+    @inlineCallbacks
+    def test_outbound(self):
+        """
+        Make sure outbound( ) stores tokens properly so they can be looked up
+        """
+
+        config.Scheduling.iMIP.Sending.Address = "server at example.com"
+        self.patch(config.Localization, "LocalesDirectory", os.path.join(os.path.dirname(__file__), "locales"))
+        self._actualGenerateEmail = self.sender.generateEmail
+        self.patch(self.sender, "generateEmail", self._interceptEmail)
+
+        data = (
+            # Initial invite
+            (
+                initialInviteText,
+                "CFDD5E46-4F74-478A-9311-B3FF905449C3",
+                "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
+                "mailto:attendee at example.com",
+                "new",
+                "organizer at example.com",
+                u"Th\xe9 Organizer",
+                [
+                    (u'Th\xe9 Attendee', u'attendee at example.com'),
+                    (u'Th\xe9 Organizer', u'organizer at example.com'),
+                    (u'An Attendee without CUTYPE', u'nocutype at example.com'),
+                    (None, u'nocn at example.com'),
+                ],
+                u"Th\xe9 Organizer <organizer at example.com>",
+                "=?utf-8?q?Th=C3=A9_Organizer_=3Corganizer=40example=2Ecom=3E?=",
+                "attendee at example.com",
+            ),
+
+            # Update
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:CFDD5E46-4F74-478A-9311-B3FF905449C3
+DTSTART:20100325T154500Z
+DTEND:20100325T164500Z
+ATTENDEE;CN=The Attendee;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:
+ mailto:attendee at example.com
+ATTENDEE;CN=The Organizer;CUTYPE=INDIVIDUAL;EMAIL=organizer at example.com;PAR
+ TSTAT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
+ORGANIZER;CN=The Organizer;EMAIL=organizer at example.com:urn:uuid:C3B38B00-41
+ 66-11DD-B22C-A07C87E02F6A
+SUMMARY:testing outbound( ) *update*
+END:VEVENT
+END:VCALENDAR
+""",
+                "CFDD5E46-4F74-478A-9311-B3FF905449C3",
+                "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
+                "mailto:attendee at example.com",
+                "update",
+                "organizer at example.com",
+                "The Organizer",
+                [
+                    (u'The Attendee', u'attendee at example.com'),
+                    (u'The Organizer', u'organizer at example.com')
+                ],
+                "The Organizer <organizer at example.com>",
+                "The Organizer <organizer at example.com>",
+                "attendee at example.com",
+            ),
+
+            # Reply
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+BEGIN:VEVENT
+UID:DFDD5E46-4F74-478A-9311-B3FF905449C4
+DTSTART:20100325T154500Z
+DTEND:20100325T164500Z
+ATTENDEE;CN=The Attendee;CUTYPE=INDIVIDUAL;EMAIL=attendee at example.com;PARTST
+ AT=ACCEPTED:urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A
+ORGANIZER;CN=The Organizer;EMAIL=organizer at example.com:mailto:organizer at exam
+ ple.com
+SUMMARY:testing outbound( ) *reply*
+END:VEVENT
+END:VCALENDAR
+""",
+                None,
+                "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A",
+                "mailto:organizer at example.com",
+                "reply",
+                "organizer at example.com",
+                "The Organizer",
+                [
+                    (u'The Attendee', u'attendee at example.com'),
+                ],
+                "attendee at example.com",
+                "attendee at example.com",
+                "organizer at example.com",
+            ),
+
+        )
+        for (inputCalendar, UID, inputOriginator, inputRecipient, inviteState,
+            outputOrganizerEmail, outputOrganizerName, outputAttendeeList,
+            outputFrom, encodedFrom, outputRecipient) in data:
+
+            txn = self.store.newTransaction()
+            yield self.sender.outbound(
+                txn,
+                inputOriginator,
+                inputRecipient,
+                Component.fromString(inputCalendar.replace("\n", "\r\n")),
+                language="ja",
+                onlyAfter=PyCalendarDateTime(2010, 1, 1, 0, 0, 0)
+            )
+            yield txn.commit()
+
+            msg = email.message_from_string(self.sender.smtpSender.message)
+            self.assertEquals(msg["From"], encodedFrom)
+            self.assertEquals(self.inviteState, inviteState)
+            self.assertEquals(self.orgEmail, outputOrganizerEmail)
+            self.assertEquals(self.orgCn, outputOrganizerName)
+            self.assertEquals(self.attendees, outputAttendeeList)
+            self.assertEquals(self.fromAddress, outputFrom)
+            self.assertEquals(self.toAddress, outputRecipient)
+
+            if UID: # The organizer is local, and server is sending to remote
+                    # attendee
+                txn = self.store.newTransaction()
+                token = (yield txn.imipGetToken(inputOriginator, inputRecipient,
+                    UID))
+                yield txn.commit()
+                self.assertNotEquals(token, None)
+                self.assertEquals(msg["Reply-To"],
+                    "server+%s at example.com" % (token,))
+
+                # Make sure attendee property for organizer exists and matches
+                # the CUA of the organizer property
+                orgValue = self.calendar.getOrganizerProperty().value()
+                self.assertEquals(
+                    orgValue,
+                    self.calendar.getAttendeeProperty([orgValue]).value()
+                )
+
+            else: # Reply only -- the attendee is local, and server is sending reply to remote organizer
+
+                self.assertEquals(msg["Reply-To"], self.fromAddress)
+
+            # Check that we don't send any messages for events completely in
+            # the past.
+            self.sender.smtpSender.reset()
+            txn = self.store.newTransaction()
+            yield self.sender.outbound(
+                txn,
+                inputOriginator,
+                inputRecipient,
+                Component.fromString(inputCalendar.replace("\n", "\r\n")),
+                onlyAfter=PyCalendarDateTime(2021, 1, 1, 0, 0, 0)
+            )
+            yield txn.commit()
+            self.assertFalse(self.sender.smtpSender.sendMessageCalled)
+
+
+    @inlineCallbacks
+    def test_tokens(self):
+        txn = self.store.newTransaction()
+        token = (yield txn.imipLookupByToken("xyzzy"))
+        yield txn.commit()
+        self.assertEquals(token, [])
+
+        txn = self.store.newTransaction()
+        token1 = (yield txn.imipCreateToken("organizer", "attendee", "icaluid"))
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        token2 = (yield txn.imipGetToken("organizer", "attendee", "icaluid"))
+        yield txn.commit()
+        self.assertEquals(token1, token2)
+
+        txn = self.store.newTransaction()
+        self.assertEquals((yield txn.imipLookupByToken(token1)),
+            [["organizer", "attendee", "icaluid"]])
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        yield txn.imipRemoveToken(token1)
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        self.assertEquals((yield txn.imipLookupByToken(token1)), [])
+        yield txn.commit()
+        
+ 
+    @inlineCallbacks
+    def test_mailtoTokens(self):
+        """
+        Make sure old mailto tokens are still honored
+        """
+
+        organizerEmail = "mailto:organizer at example.com"
+
+        # Explictly store a token with mailto: CUA for organizer
+        # (something that doesn't happen any more, but did in the past)
+        txn = self.store.newTransaction()
+        origToken = (yield txn.imipCreateToken(organizerEmail,
+            "mailto:attendee at example.com",
+            "CFDD5E46-4F74-478A-9311-B3FF905449C3"
+            )
+        )
+        yield txn.commit()
+
+        inputCalendar = initialInviteText
+        UID = "CFDD5E46-4F74-478A-9311-B3FF905449C3"
+        inputOriginator = "urn:uuid:C3B38B00-4166-11DD-B22C-A07C87E02F6A"
+        inputRecipient = "mailto:attendee at example.com"
+
+        txn = self.store.newTransaction()
+        yield self.sender.outbound(txn, inputOriginator, inputRecipient,
+            Component.fromString(inputCalendar.replace("\n", "\r\n")),
+            onlyAfter=PyCalendarDateTime(2010, 1, 1, 0, 0, 0))
+
+        # Verify we didn't create a new token...
+        txn = self.store.newTransaction()
+        token = (yield txn.imipGetToken(inputOriginator, inputRecipient, UID))
+        yield txn.commit()
+        self.assertEquals(token, None)
+
+        # But instead kept the old one...
+        txn = self.store.newTransaction()
+        token = (yield txn.imipGetToken(organizerEmail, inputRecipient, UID))
+        yield txn.commit()
+        self.assertEquals(token, origToken)
+
+
+    def generateSampleEmail(self):
+        """
+        Invoke L{MailHandler.generateEmail} and parse the result.
+        """
+        calendar = Component.fromString(initialInviteText)
+        msgID, msgTxt = self.sender.generateEmail(
+            inviteState='new',
+            calendar=calendar,
+            orgEmail=u"user01 at localhost",
+            orgCN=u"User Z\xe9ro One",
+            attendees=[(u"Us\xe9r One", "user01 at localhost"),
+                       (u"User 2", "user02 at localhost")],
+            fromAddress="user01 at localhost",
+            replyToAddress="imip-system at localhost",
+            toAddress="user03 at localhost",
+        )
+        message = email.message_from_string(msgTxt)
+        return msgID, message
+
+
+    def test_generateEmail(self):
+        """
+        L{MailHandler.generateEmail} generates a MIME-formatted email with a
+        text/plain part, a text/html part, and a text/calendar part.
+        """
+        msgID, message = self.generateSampleEmail()
+        self.assertEquals(message['Message-ID'], msgID)
+        expectedTypes = set(["text/plain", "text/html", "text/calendar"])
+        actualTypes = set([
+            part.get_content_type() for part in message.walk()
+            if part.get_content_type().startswith("text/")
+        ])
+        self.assertEquals(actualTypes, expectedTypes)
+
+
+    def test_emailEncoding(self):
+        """
+        L{MailHandler.generateEmail} will preserve any non-ASCII characters
+        present in the fields that it formats in the message body.
+        """
+        _ignore_msgID, message = self.generateSampleEmail()
+        textPart = partByType(message, "text/plain")
+        htmlPart = partByType(message, "text/html")
+
+        plainText = textPart.get_payload(decode=True).decode(
+            textPart.get_content_charset()
+        )
+        htmlText = htmlPart.get_payload(decode=True).decode(
+            htmlPart.get_content_charset()
+        )
+
+        self.assertIn(u"Us\u00e9r One", plainText)
+        self.assertIn(u'<a href="mailto:user01 at localhost">Us\u00e9r One</a>',
+                      htmlText)
+
+        # The same assertion, but with the organizer's form.
+        self.assertIn(
+            u'<a href="mailto:user01 at localhost">User Z\u00e9ro One</a>',
+            htmlText)
+
+
+    def test_emailQuoting(self):
+        """
+        L{MailHandler.generateEmail} will HTML-quote all relevant fields in the
+        HTML part, but not the text/plain part.
+        """
+        _ignore_msgID, message = self.generateSampleEmail()
+        htmlPart = partByType(message, "text/html").get_payload(decode=True)
+        plainPart = partByType(message, "text/plain").get_payload(decode=True)
+        expectedPlain = 'awesome description with "<" and "&"'
+        expectedHTML = expectedPlain.replace("&", "&amp;").replace("<", "&lt;")
+
+        self.assertIn(expectedPlain, plainPart)
+        self.assertIn(expectedHTML, htmlPart)
+
+
+    def test_stringFormatTemplateLoader(self):
+        """
+        L{StringFormatTemplateLoader.load} will convert a template with
+        C{%(x)s}-format slots by converting it to a template with C{<t:slot
+        name="x" />} slots, and a renderer on the document element named
+        according to the constructor argument.
+        """
+        class StubElement(Element):
+            loader = StringFormatTemplateLoader(
+                lambda : StringIO(
+                    "<test><alpha>%(slot1)s</alpha>%(other)s</test>"
+                ),
+                "testRenderHere"
+            )
+
+            @renderer
+            def testRenderHere(self, request, tag):
+                return tag.fillSlots(slot1="hello",
+                                     other="world")
+        result = []
+        flattenString(None, StubElement()).addCallback(result.append)
+        self.assertEquals(result,
+                          ["<test><alpha>hello</alpha>world</test>"])
+
+
+    def test_templateLoaderWithAttributes(self):
+        """
+        L{StringFormatTemplateLoader.load} will convert a template with
+        C{%(x)s}-format slots inside attributes into t:attr elements containing
+        t:slot slots.
+        """
+        class StubElement(Element):
+            loader = StringFormatTemplateLoader(
+                lambda : StringIO(
+                    '<test><alpha beta="before %(slot1)s after">inner</alpha>'
+                    '%(other)s</test>'
+                ),
+                "testRenderHere"
+            )
+
+            @renderer
+            def testRenderHere(self, request, tag):
+                return tag.fillSlots(slot1="hello",
+                                     other="world")
+        result = []
+        flattenString(None, StubElement()).addCallback(result.append)
+        self.assertEquals(result,
+                          ['<test><alpha beta="before hello after">'
+                           'inner</alpha>world</test>'])
+
+
+    def test_templateLoaderTagSoup(self):
+        """
+        L{StringFormatTemplateLoader.load} will convert a template with
+        C{%(x)s}-format slots into t:slot slots, and render a well-formed output
+        document, even if the input is malformed (i.e. missing necessary closing
+        tags).
+        """
+        class StubElement(Element):
+            loader = StringFormatTemplateLoader(
+                lambda : StringIO(
+                    '<test><alpha beta="before %(slot1)s after">inner</alpha>'
+                    '%(other)s'
+                ),
+                "testRenderHere"
+            )
+
+            @renderer
+            def testRenderHere(self, request, tag):
+                return tag.fillSlots(slot1="hello",
+                                     other="world")
+        result = []
+        flattenString(None, StubElement()).addCallback(result.append)
+        self.assertEquals(result,
+                          ['<test><alpha beta="before hello after">'
+                           'inner</alpha>world</test>'])
+
+def partByType(message, contentType):
+    """
+    Retrieve a MIME part from an L{email.message.Message} based on a content
+    type.
+    """
+    for part in message.walk():
+        if part.get_content_type() == contentType:
+            return part
+    raise KeyError(contentType)

Modified: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/ischedule/resource.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/scheduling/ischedule/resource.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -30,6 +30,7 @@
 from twistedcaldav.scheduling.ischedule.scheduler import IScheduleScheduler
 from txdav.xml import element as davxml
 import twistedcaldav.scheduling.ischedule.xml  as ischedulexml
+from twistedcaldav.directory.util import transactionFromRequest
 from twistedcaldav.scheduling.ischedule.dkim import ISCHEDULE_CAPABILITIES
 
 __all__ = [
@@ -211,7 +212,7 @@
         scheduler = IScheduleScheduler(request, self)
 
         # Need a transaction to work with
-        txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
+        txn = transactionFromRequest(request, self._newStore)
         request._newStoreTransaction = txn
 
         # Do the POST processing treating this as a non-local schedule

Modified: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/stdconfig.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/stdconfig.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -735,9 +735,6 @@
     "Notifications" : {
         "Enabled": False,
         "CoalesceSeconds" : 3,
-        "InternalNotificationHost" : "localhost",
-        "InternalNotificationPort" : 62309,
-        "BindAddress" : "127.0.0.1",
 
         "Services" : {
             "SimpleLineNotifier" : {
@@ -783,32 +780,6 @@
                 "EnableStaggering" : False,
                 "StaggerSeconds" : 3,
             },
-            "XMPPNotifier" : {
-                "Service" : "twistedcaldav.notify.XMPPNotifierService",
-                "Enabled" : False,
-                "Host" : "", # "xmpp.host.name"
-                "Port" : 5222,
-                "JID" : "", # "jid at xmpp.host.name/resource"
-                "Password" : "",
-                "ServiceAddress" : "", # "pubsub.xmpp.host.name"
-                "CalDAV" : {
-                    "APSBundleID" : "",
-                    "SubscriptionURL" : "",
-                    "APSEnvironment" : "PRODUCTION",
-                },
-                "CardDAV" : {
-                    "APSBundleID" : "",
-                    "SubscriptionURL" : "",
-                    "APSEnvironment" : "PRODUCTION",
-                },
-                "NodeConfiguration" : {
-                    "pubsub#deliver_payloads" : "1",
-                    "pubsub#persist_items" : "1",
-                },
-                "KeepAliveSeconds" : 120,
-                "HeartbeatMinutes" : 30,
-                "AllowedJIDs": [],
-            },
         }
     },
 
@@ -1426,38 +1397,9 @@
                     # The password doesn't exist in the keychain.
                     log.info("%s APN certificate passphrase not found in keychain" % (protocol,))
 
-        if (
-            service["Service"] == "twistedcaldav.notify.XMPPNotifierService" and
-            service["Enabled"]
-        ):
-            # If we already have the password, don't fetch it again
-            if service["Password"]:
-                continue
 
-            # Get password from keychain.  If not there, fall back to what
-            # is in the plist.
-            try:
-                password = getPasswordFromKeychain(service["JID"])
-                service["Password"] = password
-                log.info("XMPP password retreived from keychain")
-            except KeychainAccessError:
-                # The system doesn't support keychain
-                pass
-            except KeychainPasswordNotFound:
-                # The password doesn't exist in the keychain.
-                log.info("XMPP password not found in keychain")
 
-            # Check for empty fields
-            for key, value in service.iteritems():
-                if not value and key not in (
-                    "AllowedJIDs", "HeartbeatMinutes", "Password",
-                    "SubscriptionURL", "APSBundleID"
-                ):
-                    raise ConfigurationError("Invalid %s for XMPPNotifierService: %r"
-                                             % (key, value))
 
-
-
 def _updateScheduling(configDict, reloading=False):
     #
     # Scheduling
@@ -1638,12 +1580,10 @@
     re-fetched after the process has shed privileges
     """
     iMIP = configDict.Scheduling.iMIP
-    XMPP = configDict.Notifications.Services.XMPPNotifier
     preserved = {
         "iMIPPassword" : iMIP.Password,
         "MailSendingPassword" : iMIP.Sending.Password,
         "MailReceivingPassword" : iMIP.Receiving.Password,
-        "XMPPPassword" : XMPP.Password,
     }
     return preserved
 
@@ -1655,11 +1595,9 @@
     re-fetched after the process has shed privileges
     """
     iMIP = configDict.Scheduling.iMIP
-    XMPP = configDict.Notifications.Services.XMPPNotifier
     iMIP.Password = preserved["iMIPPassword"]
     iMIP.Sending.Password = preserved["MailSendingPassword"]
     iMIP.Receiving.Password = preserved["MailReceivingPassword"]
-    XMPP.Password = preserved["XMPPPassword"]
 
 
 config.addResetHooks(_preserveConfig, _restoreConfig)

Modified: CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/upgrade.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/upgrade.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/twistedcaldav/upgrade.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -46,6 +46,7 @@
 from twistedcaldav.ical import Component
 from twistedcaldav.scheduling.cuaddress import LocalCalendarUser
 from twistedcaldav.scheduling.imip.mailgateway import MailGatewayTokensDatabase
+from twistedcaldav.scheduling.imip.inbound import MailRetrievalService
 from twistedcaldav.scheduling.scheduler import DirectScheduler
 from twistedcaldav.util import normalizationLookup
 
@@ -1048,6 +1049,8 @@
         Start the service.
         """
 
+        directory = directoryFromConfig(self.config)
+
         # Load proxy assignments from XML if specified
         if self.config.ProxyLoadFromFile:
             proxydbClass = namedClass(self.config.ProxyDBService.type)
@@ -1063,7 +1066,6 @@
             if proxydb is None:
                 proxydbClass = namedClass(self.config.ProxyDBService.type)
                 proxydb = proxydbClass(**self.config.ProxyDBService.params)
-            directory = directoryFromConfig(self.config)
 
             updater = GroupMembershipCacheUpdater(proxydb,
                 directory, self.config.GroupCaching.ExpireSeconds,
@@ -1082,7 +1084,14 @@
         yield self.processInboxItems()
         self.store.setMigrating(False)
 
+        # Initiate inbound mail handling
+        if self.config.Scheduling.iMIP.Enabled:
+            log.debug("Initializing inbound mail retrieval")
+            inboundMailService = MailRetrievalService(self.store, directory)
+            inboundMailService.setServiceParent(self.parent)
 
+
+
     @inlineCallbacks
     def processInboxItems(self):
         """

Modified: CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/file.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/file.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -337,6 +337,26 @@
         return NotImplementedError
 
 
+    def imipCreateToken(self, token):
+        return NotImplementedError
+
+
+    def imipLookupByToken(self, token):
+        return NotImplementedError
+
+
+    def imipGetToken(self, organizer, attendee, icaluid):
+        return NotImplementedError
+
+
+    def imipRemoveToken(self, token):
+        return NotImplementedError
+
+
+    def purgeOldIMIPTokens(self, olderThan):
+        return NotImplementedError
+
+
     def isNotifiedAlready(self, obj):
         return obj in self._notifiedAlready
 

Modified: CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/sql.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/sql.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -64,7 +64,8 @@
 from txdav.common.icommondatastore import HomeChildNameNotAllowedError, \
     HomeChildNameAlreadyExistsError, NoSuchHomeChildError, \
     ObjectResourceNameNotAllowedError, ObjectResourceNameAlreadyExistsError, \
-    NoSuchObjectResourceError, AllRetriesFailed, InvalidSubscriptionValues
+    NoSuchObjectResourceError, AllRetriesFailed, InvalidSubscriptionValues, \
+    InvalidIMIPTokenValues
 from txdav.common.inotifications import INotificationCollection, \
     INotificationObject
 
@@ -716,6 +717,105 @@
         return self._apnSubscriptionsBySubscriberQuery.on(self, subscriberGUID=guid)
 
 
+    # Create IMIP token
+
+    @classproperty
+    def _insertIMIPTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Insert({imip.TOKEN: Parameter("token"),
+                       imip.ORGANIZER: Parameter("organizer"),
+                       imip.ATTENDEE: Parameter("attendee"),
+                       imip.ICALUID: Parameter("icaluid"),
+                      })
+
+    @inlineCallbacks
+    def imipCreateToken(self, organizer, attendee, icaluid, token=None):
+        if not (organizer and attendee and icaluid):
+            raise InvalidIMIPTokenValues()
+
+        if token is None:
+            token = str(uuid4())
+
+        try:
+            yield self._insertIMIPTokenQuery.on(self,
+                token=token, organizer=organizer, attendee=attendee,
+                icaluid=icaluid)
+        except Exception:
+            # TODO: is it okay if someone else created the same row just now?
+            pass
+        returnValue(token)
+
+    # Lookup IMIP organizer+attendee+icaluid for token
+
+    @classproperty
+    def _selectIMIPTokenByTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Select([imip.ORGANIZER, imip.ATTENDEE, imip.ICALUID], From=imip,
+                      Where=(imip.TOKEN == Parameter("token")))
+
+    def imipLookupByToken(self, token):
+        return self._selectIMIPTokenByTokenQuery.on(self, token=token)
+
+    # Lookup IMIP token for organizer+attendee+icaluid
+
+    @classproperty
+    def _selectIMIPTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Select([imip.TOKEN], From=imip,
+                      Where=(imip.ORGANIZER == Parameter("organizer")).And(
+                             imip.ATTENDEE == Parameter("attendee")).And(
+                             imip.ICALUID == Parameter("icaluid")))
+    @classproperty
+    def _updateIMIPTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Update({imip.ACCESSED: utcNowSQL,},
+                      Where=(imip.ORGANIZER == Parameter("organizer")).And(
+                             imip.ATTENDEE == Parameter("attendee")).And(
+                             imip.ICALUID == Parameter("icaluid")))
+
+
+    @inlineCallbacks
+    def imipGetToken(self, organizer, attendee, icaluid):
+        row = (yield self._selectIMIPTokenQuery.on(self, organizer=organizer,
+            attendee=attendee, icaluid=icaluid))
+        if row:
+            token = row[0][0]
+            # update the timestamp
+            yield self._updateIMIPTokenQuery.on(self, organizer=organizer,
+                attendee=attendee, icaluid=icaluid)
+        else:
+            token = None
+        returnValue(token)
+
+    # Remove IMIP token
+
+    @classproperty
+    def _removeIMIPTokenQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Delete(From=imip,
+                      Where=(imip.TOKEN == Parameter("token")))
+
+    def imipRemoveToken(self, token):
+        return self._removeIMIPTokenQuery.on(self, token=token)
+
+    # Purge old IMIP tokens
+
+    @classproperty
+    def _purgeOldIMIPTokensQuery(cls): #@NoSelf
+        imip = schema.IMIP_TOKENS
+        return Delete(From=imip,
+                      Where=(imip.ACCESSED < Parameter("olderThan")))
+
+    def purgeOldIMIPTokens(self, olderThan):
+        """
+        @type olderThan: datetime
+        """
+        return self._purgeOldIMIPTokensQuery.on(self,
+            olderThan=olderThan)
+
+    # End of IMIP
+
+
     def preCommit(self, operation):
         """
         Run things before C{commit}.  (Note: only provided by SQL

Modified: CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/sql_schema/current.sql	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/txdav/common/datastore/sql_schema/current.sql	2013-02-12 21:06:06 UTC (rev 10703)
@@ -492,7 +492,73 @@
 create index APN_SUBSCRIPTIONS_RESOURCE_KEY
    on APN_SUBSCRIPTIONS(RESOURCE_KEY);
 
+-----------------
+-- IMIP Tokens --
+-----------------
 
+create table IMIP_TOKENS (
+  TOKEN                         varchar(255) not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALUID                       varchar(255) not null,
+  ACCESSED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+   on IMIP_TOKENS(TOKEN);
+
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+-- create table IMIP_POLLING_WORK (
+--   WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+--   NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+-- );
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  PUSH_ID                       varchar(255) not null
+);
+
+
 --------------------
 -- Schema Version --
 --------------------

Modified: CalendarServer/branches/users/sagen/usingqueue/txdav/common/icommondatastore.py
===================================================================
--- CalendarServer/branches/users/sagen/usingqueue/txdav/common/icommondatastore.py	2013-02-12 21:01:19 UTC (rev 10702)
+++ CalendarServer/branches/users/sagen/usingqueue/txdav/common/icommondatastore.py	2013-02-12 21:06:06 UTC (rev 10703)
@@ -148,6 +148,13 @@
     Invalid APN subscription values passed in.
     """
 
+# IMIP Tokens
+
+class InvalidIMIPTokenValues(ValueError):
+    """
+    Invalid IMIP token values passed in.
+    """
+
 #
 # Interfaces
 #
@@ -240,7 +247,29 @@
         @return: tuples of (token, key, timestamp, userAgent, ipAddr)
         """
 
+    def imipCreateToken(token):
+        """
+        """
 
+    def imipLookupByToken(token):
+        """
+        """
+
+
+    def imipGetToken(organizer, attendee, icaluid):
+        """
+        """
+
+
+    def imipRemoveToken(token):
+        """
+        """
+
+
+    def purgeOldIMIPTokens(olderThan):
+        """
+        """
+
 class IShareableCollection(Interface):
     """
     A collection resource which may be shared.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130212/30718bee/attachment-0001.html>


More information about the calendarserver-changes mailing list