[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("&", "&").replace("<", "<")
+
+ 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