[CalendarServer-changes] [2939] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Thu Sep 4 11:22:25 PDT 2008


Revision: 2939
          http://trac.macosforge.org/projects/calendarserver/changeset/2939
Author:   sagen at apple.com
Date:     2008-09-04 11:22:25 -0700 (Thu, 04 Sep 2008)
Log Message:
-----------
Now the XMPP notifier won't try to publish the same node simultaneously; also handle incoming imip replies where the calendar body is missing ORGANIZER

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/mail.py
    CalendarServer/trunk/twistedcaldav/notify.py
    CalendarServer/trunk/twistedcaldav/test/test_mail.py
    CalendarServer/trunk/twistedcaldav/test/test_notify.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/test/data/mail/reply_missing_organizer

Modified: CalendarServer/trunk/twistedcaldav/mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/mail.py	2008-09-04 02:34:38 UTC (rev 2938)
+++ CalendarServer/trunk/twistedcaldav/mail.py	2008-09-04 18:22:25 UTC (rev 2939)
@@ -612,7 +612,12 @@
         organizer = str(organizer)
         attendee = str(attendee)
         calendar.removeAllButOneAttendee(attendee)
-        calendar.getOrganizerProperty().setValue(organizer)
+        organizerProperty = calendar.getOrganizerProperty()
+        if organizerProperty is None:
+            # this calendar body is incomplete, skip it
+            self.log_error("Mail gateway didn't find an organizer in message %s" % (msg['Message-ID'],))
+            return
+        organizerProperty.setValue(organizer)
         return fn(organizer, attendee, calendar, msg['Message-ID'])
 
 

Modified: CalendarServer/trunk/twistedcaldav/notify.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/notify.py	2008-09-04 02:34:38 UTC (rev 2938)
+++ CalendarServer/trunk/twistedcaldav/notify.py	2008-09-04 18:22:25 UTC (rev 2939)
@@ -43,7 +43,8 @@
 from twisted.python.reflect import namedClass
 from twisted.words.protocols.jabber import xmlstream
 from twisted.words.protocols.jabber.jid import JID
-from twisted.words.protocols.jabber.client import BasicAuthenticator, IQ
+from twisted.words.protocols.jabber.client import BasicAuthenticator
+from twisted.words.protocols.jabber.xmlstream import IQ
 from twisted.words.xish import domish
 from twistedcaldav.log import LoggingMixIn
 from twistedcaldav.config import config, parseConfig, defaultConfig
@@ -452,8 +453,6 @@
 
 
 
-
-
 class XMPPNotifier(LoggingMixIn):
     """
     XMPP Notifier
@@ -502,24 +501,46 @@
         self.doRoster = roster
 
         self.roster = {}
+        self.outstanding = {}
 
+    def lockNode(self, nodeName):
+        if self.outstanding.has_key(nodeName):
+            return False
+        else:
+            self.outstanding[nodeName] = 1
+            return True
+
+    def unlockNode(self, failure, nodeName):
+        try:
+            del self.outstanding[nodeName]
+        except KeyError:
+            pass
+
     def sendHeartbeat(self):
         if self.doHeartbeat and self.xmlStream is not None:
-            self.enqueue("")
+            self.enqueue("", lock=False)
             self.reactor.callLater(self.settings['HeartbeatSeconds'],
                 self.sendHeartbeat)
 
-    def enqueue(self, uri):
+    def enqueue(self, uri, lock=True):
         if self.xmlStream is not None:
             # Convert uri to node
             nodeName = self.uriToNodeName(uri)
-            self.publishNode(nodeName)
+            self.publishNode(nodeName, lock=lock)
 
     def uriToNodeName(self, uri):
         return getPubSubPath(uri, getPubSubConfiguration(self.config))
 
-    def publishNode(self, nodeName):
-        if self.xmlStream is not None:
+    def publishNode(self, nodeName, lock=True):
+        if self.xmlStream is None:
+            # We lost our connection
+            self.unlockNode(None, nodeName)
+            return
+
+        try:
+            if lock and not self.lockNode(nodeName):
+                return
+
             iq = IQ(self.xmlStream)
             pubsubElement = iq.addElement('pubsub', defaultUri=self.pubsubNS)
             publishElement = pubsubElement.addElement('publish')
@@ -528,59 +549,92 @@
             payloadElement = itemElement.addElement('plistfrag',
                 defaultUri='plist-apple')
             self.sendDebug("Publishing (%s)" % (nodeName,), iq)
-            iq.addCallback(self.responseFromPublish, nodeName)
-            iq.send(to=self.settings['ServiceAddress'])
+            d = iq.send(to=self.settings['ServiceAddress'])
+            d.addCallback(self.publishNodeSuccess, nodeName)
+            d.addErrback(self.publishNodeFailure, nodeName)
+        except:
+            self.unlockNode(None, nodeName)
+            raise
 
-    def responseFromPublish(self, nodeName, iq):
-        if iq['type'] == 'result':
-            self.sendDebug("Node publish successful (%s)" % (nodeName,), iq)
-        else:
+    def publishNodeSuccess(self, iq, nodeName):
+        self.unlockNode(None, nodeName)
+        self.sendDebug("Node publish successful (%s)" % (nodeName,), iq)
+
+    def publishNodeFailure(self, result, nodeName):
+        try:
+            iq = result.value.getElement()
             self.log_error("PubSub node publish error: %s" %
                 (iq.toXml().encode('ascii', 'replace')),)
             self.sendDebug("Node publish failed (%s)" % (nodeName,), iq)
 
-            errorElement = None
-            pubsubElement = None
-            for child in iq.elements():
-                if child.name == 'error':
-                    errorElement = child
-                if child.name == 'pubsub':
-                    pubsubElement = child
-
-            if errorElement:
-                if errorElement['code'] == '400':
+            if iq.name == "error":
+                if iq['code'] == '400':
                     self.requestConfigurationForm(nodeName)
 
-                elif errorElement['code'] == '404':
+                elif iq['code'] == '404':
                     self.createNode(nodeName)
+            else:
+                # Don't know how to proceed
+                self.unlockNode(None, nodeName)
+        except:
+            self.unlockNode(None, nodeName)
+            raise
 
     def createNode(self, nodeName):
-        if self.xmlStream is not None:
+        if self.xmlStream is None:
+            # We lost our connection
+            self.unlockNode(None, nodeName)
+            return
+
+        try:
             iq = IQ(self.xmlStream)
             pubsubElement = iq.addElement('pubsub', defaultUri=self.pubsubNS)
             child = pubsubElement.addElement('create')
             child['node'] = nodeName
-            iq.addCallback(self.responseFromCreate, nodeName)
-            iq.send(to=self.settings['ServiceAddress'])
+            d = iq.send(to=self.settings['ServiceAddress'])
+            d.addCallback(self.createNodeSuccess, nodeName)
+            d.addErrback(self.createNodeFailure, nodeName)
+        except:
+            self.unlockNode(None, nodeName)
+            raise
 
-    def responseFromCreate(self, nodeName, iq):
-        if iq['type'] == 'result':
+    def createNodeSuccess(self, iq, nodeName):
+        try:
             self.sendDebug("Node creation successful (%s)" % (nodeName,), iq)
             # now time to configure; fetch the form
             self.requestConfigurationForm(nodeName)
-        else:
+        except:
+            self.unlockNode(None, nodeName)
+            raise
+
+    def createNodeFailure(self, result, nodeName):
+        # If we get here we're giving up
+        try:
+            iq = result.value.getElement()
             self.log_error("PubSub node creation error: %s" %
                 (iq.toXml().encode('ascii', 'replace')),)
             self.sendError("Node creation failed (%s)" % (nodeName,), iq)
+        finally:
+            self.unlockNode(None, nodeName)
 
     def requestConfigurationForm(self, nodeName):
-        if self.xmlStream is not None:
+        if self.xmlStream is None:
+            # We lost our connection
+            self.unlockNode(None, nodeName)
+            return
+
+        try:
             iq = IQ(self.xmlStream, type='get')
-            child = iq.addElement('pubsub', defaultUri=self.pubsubNS+"#owner")
+            child = iq.addElement('pubsub',
+                defaultUri=self.pubsubNS+"#owner")
             child = child.addElement('configure')
             child['node'] = nodeName
-            iq.addCallback(self.responseFromConfigurationForm, nodeName)
-            iq.send(to=self.settings['ServiceAddress'])
+            d = iq.send(to=self.settings['ServiceAddress'])
+            d.addCallback(self.requestConfigurationFormSuccess, nodeName)
+            d.addErrback(self.requestConfigurationFormFailure, nodeName)
+        except:
+            self.unlockNode(None, nodeName)
+            raise
 
     def _getChild(self, element, name):
         for child in element.elements():
@@ -588,8 +642,13 @@
                 return child
         return None
 
-    def responseFromConfigurationForm(self, nodeName, iq):
-        if iq['type'] == 'result':
+    def requestConfigurationFormSuccess(self, iq, nodeName):
+        if self.xmlStream is None:
+            # We lost our connection
+            self.unlockNode(None, nodeName)
+            return
+
+        try:
             self.sendDebug("Received configuration form (%s)" % (nodeName,), iq)
             pubsubElement = self._getChild(iq, 'pubsub')
             if pubsubElement:
@@ -621,27 +680,55 @@
                                         valueElement = filledField.addElement('value')
                                         valueElement.addContent(value)
                                         # filledForm.addChild(field)
-                        filledIq.addCallback(self.responseFromConfiguration,
-                            nodeName)
                         self.sendDebug("Sending configuration form (%s)"
                                        % (nodeName,), filledIq)
-                        filledIq.send(to=self.settings['ServiceAddress'])
-        else:
+                        d = filledIq.send(to=self.settings['ServiceAddress'])
+                        d.addCallback(self.configurationSuccess, nodeName)
+                        d.addErrback(self.configurationFailure, nodeName)
+                        return
+
+            # Couldn't process configuration form, give up
+            self.unlockNode(None, nodeName)
+
+        except:
+            # Couldn't process configuration form, give up
+            self.unlockNode(None, nodeName)
+            raise
+
+    def requestConfigurationFormFailure(self, result, nodeName):
+        # If we get here we're giving up
+        try:
+            iq = result.value.getElement()
             self.log_error("PubSub configuration form request error: %s" %
                 (iq.toXml().encode('ascii', 'replace')),)
-            self.sendError("Failed to receive configuration form (%s)" % (nodeName,), iq)
+            self.sendError("Failed to receive configuration form (%s)" %
+                (nodeName,), iq)
+        finally:
+            self.unlockNode(None, nodeName)
 
+    def configurationSuccess(self, iq, nodeName):
+        if self.xmlStream is None:
+            # We lost our connection
+            self.unlockNode(None, nodeName)
+            return
 
-    def responseFromConfiguration(self, nodeName, iq):
-        if iq['type'] == 'result':
+        try:
             self.log_debug("PubSub node %s is configured" % (nodeName,))
             self.sendDebug("Configured node (%s)" % (nodeName,), iq)
-            self.publishNode(nodeName)
+            self.publishNode(nodeName, lock=False)
+        except:
+            self.unlockNode(None, nodeName)
+            raise
 
-        else:
+    def configurationFailure(self, result, nodeName):
+        # If we get here we're giving up
+        try:
+            iq = result.value.getElement()
             self.log_error("PubSub node configuration error: %s" %
                 (iq.toXml().encode('ascii', 'replace')),)
             self.sendError("Failed to configure node (%s)" % (nodeName,), iq)
+        finally:
+            self.unlockNode(None, nodeName)
 
 
     def requestRoster(self):
@@ -649,8 +736,8 @@
             self.roster = {}
             rosterIq = IQ(self.xmlStream, type='get')
             rosterIq.addElement("query", "jabber:iq:roster")
-            rosterIq.addCallback(self.handleRoster)
-            rosterIq.send()
+            d = rosterIq.send()
+            d.addCallback(self.handleRoster)
 
     def allowedInRoster(self, jid):
         for pattern in self.settings.get("AllowedJIDs", []):
@@ -765,7 +852,7 @@
             if frm in self.roster:
                 txt = str(body).lower()
                 if txt == "help":
-                    response = "debug on, debug off, roster, hammer <count>"
+                    response = "debug on, debug off, roster, publish <nodename>, hammer <count>"
                 elif txt == "roster":
                     response = "Roster: %s" % (str(self.roster),)
                 elif txt == "debug on":
@@ -774,6 +861,16 @@
                 elif txt == "debug off":
                     self.roster[frm]['debug'] = False
                     response = "Debugging off"
+                elif txt == "outstanding":
+                    response = "Outstanding: %s" % (str(self.outstanding),)
+                elif txt.startswith("publish"):
+                    try:
+                        publish, nodeName = txt.split()
+                    except ValueError:
+                        response = "Please phrase it like 'publish nodename'"
+                    else:
+                        response = "Publishing node %s" % (nodeName,)
+                        self.reactor.callLater(1, self.publishNode, nodeName)
                 elif txt.startswith("hammer"):
                     try:
                         hammer, count = txt.split()

Added: CalendarServer/trunk/twistedcaldav/test/data/mail/reply_missing_organizer
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/data/mail/reply_missing_organizer	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/data/mail/reply_missing_organizer	2008-09-04 18:22:25 UTC (rev 2939)
@@ -0,0 +1,89 @@
+Return-path: <xyzzy at example.com>
+Received: from hemlock.example.com ([17.128.115.180])
+ by mail4.example.com (Sun Java(tm) System Messaging Server 6.3-6.03 (built Mar
+ 14 2008; 32bit)) with ESMTP id <0K5I00I8I8G50QC0 at mail4.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Original-recipient:
+ rfc822;ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+Received: from relay14.example.com ([17.128.113.52])
+ by hemlock.example.com (Sun Java System Messaging Server 6.2-8.04 (built Feb 28
+ 2007)) with ESMTP id <0K5I004UB8G5UR90 at hemlock.example.com> for
+ ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com
+ (ORCPT ical-living-on+2Bd7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com); Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from relay14.example.com (unknown [127.0.0.1])
+	by relay14.example.com (Symantec Mail Security) with ESMTP id 6A5EF28087	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:17 -0700 (PDT)
+Received: from [17.83.208.154] (unknown [17.83.208.154])
+	(using TLSv1 with cipher AES128-SHA (128/128 bits))
+	(No client certificate requested)	by relay14.example.com (example SCV relay)
+ with ESMTP id CAEE22808B	for
+ <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>; Tue,
+ 12 Aug 2008 13:19:16 -0700 (PDT)
+Date: Tue, 12 Aug 2008 13:19:14 -0700
+From: plugh xyzzy <xyzzy at example.com>
+Subject: Event accepted: New Event
+To: User 01 <ical-living-on+d7cdf68d-8b73-4df1-ad3b-f08002fb285f at example.com>
+Message-id: <1983F777-BE86-4B98-881E-06D938E60920 at example.com>
+MIME-version: 1.0 (example Message framework v928.1)
+X-Mailer: example Mail (2.928.1)
+Content-type: multipart/alternative; boundary=example-Mail-1--253014167
+X-Mail-Calendar-Part: Yes
+X-Brightmail-Tracker: AAAAAA==
+
+
+--example-Mail-1--253014167
+Content-Type: text/plain;
+	charset=US-ASCII;
+	format=flowed;
+	delsp=yes
+Content-Transfer-Encoding: 7bit
+
+plugh xyzzy has accepted your iCal event invitation to the event: New  
+Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).
+--example-Mail-1--253014167
+Content-Type: multipart/mixed;
+	boundary=example-Mail-2--253014167
+
+
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "><font face="Helvetica" size="3" style="font: 12.0px Helvetica">plugh xyzzy has accepted your iCal event invitation to the event: New Event, scheduled for August 12, 2008 at 10:00 AM (US/Pacific).</font></div></div></body></html>
+--example-Mail-2--253014167
+Content-Disposition: attachment;
+	filename=iCal-20080812-131911.ics
+Content-Type: text/calendar;
+	x-unix-mode=0644;
+	name="iCal-20080812-131911.ics"
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR=0D=0APRODID:-//example=20Inc.//iCal=203.0//EN=0D=0A=
+CALSCALE:GREGORIAN=0D=0AVERSION:2.0=0D=0AMETHOD:REPLY=0D=0A=
+BEGIN:VTIMEZONE=0D=0ATZID:US/Pacific=0D=0ABEGIN:DAYLIGHT=0D=0A=
+TZOFFSETFROM:-0800=0D=0ATZOFFSETTO:-0700=0D=0ADTSTART:20070311T020000=0D=0A=
+RRULE:FREQ=3DYEARLY;BYMONTH=3D3;BYDAY=3D2SU=0D=0ATZNAME:PDT=0D=0A=
+END:DAYLIGHT=0D=0ABEGIN:STANDARD=0D=0ATZOFFSETFROM:-0700=0D=0A=
+TZOFFSETTO:-0800=0D=0ADTSTART:20071104T020000=0D=0A=
+RRULE:FREQ=3DYEARLY;BYMONTH=3D11;BYDAY=3D1SU=0D=0ATZNAME:PST=0D=0A=
+END:STANDARD=0D=0AEND:VTIMEZONE=0D=0ABEGIN:VEVENT=0D=0ASEQUENCE:7=0D=0A=
+DTSTART;TZID=3DUS/Pacific:20080812T100000=0D=0A=
+UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C=0D=0A
+DTSTAMP:20080812T201911Z=0D=0ASUMMARY:New=20=
+Event=0D=0A=
+CREATED:20080812T201906Z=0D=0ADTEND;TZID=3DUS/Pacific:20080812T110000=0D=0A=
+END:VEVENT=0D=0AEND:VCALENDAR=0D=0A=
+
+--example-Mail-2--253014167
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><div><div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; "></div></div></body></html>
+--example-Mail-2--253014167--
+
+--example-Mail-1--253014167--

Modified: CalendarServer/trunk/twistedcaldav/test/test_mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_mail.py	2008-09-04 02:34:38 UTC (rev 2938)
+++ CalendarServer/trunk/twistedcaldav/test/test_mail.py	2008-09-04 18:22:25 UTC (rev 2939)
@@ -88,7 +88,17 @@
         self.assertEquals(attendee, 'mailto:xyzzy at example.com')
         self.assertEquals(msgId, '<1983F777-BE86-4B98-881E-06D938E60920 at example.com>')
 
+    def test_processReplyMissingOrganizer(self):
+        msg = email.message_from_string(
+            file(os.path.join(self.dataDir, 'reply_missing_organizer')).read()
+        )
+        # stick the token in the database first
+        self.handler.db.createToken("mailto:user01 at example.com", "mailto:xyzzy at example.com", token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
 
+        result = self.handler.processReply(msg, echo)
+        self.assertEquals(result, None)
+
+
 class MailGatewayTokensDatabaseTests(TestCase):
 
     def setUp(self):

Modified: CalendarServer/trunk/twistedcaldav/test/test_notify.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_notify.py	2008-09-04 02:34:38 UTC (rev 2938)
+++ CalendarServer/trunk/twistedcaldav/test/test_notify.py	2008-09-04 18:22:25 UTC (rev 2939)
@@ -17,6 +17,7 @@
 from twisted.trial.unittest import TestCase
 from twisted.internet.task import Clock
 from twisted.words.protocols.jabber.client import IQ
+from twisted.words.protocols.jabber.error import StanzaError
 from twistedcaldav.notify import *
 from twistedcaldav import config as config_mod
 from twistedcaldav.config import Config
@@ -321,6 +322,11 @@
         pass
 
 
+class StubFailure(object):
+
+    def __init__(self, value):
+        self.value = value
+
 class XMPPNotifierTests(TestCase):
 
     xmppEnabledConfig = Config(config_mod.defaultConfig)
@@ -366,11 +372,9 @@
         self.assertEquals(iq.name, "iq")
 
     def test_publishReponse400(self):
-        response = IQ(self.xmlStream, type='error')
-        errorElement = response.addElement('error')
-        errorElement['code'] = '400'
+        failure = StubFailure(StanzaError('bad-request'))
         self.assertEquals(len(self.xmlStream.elements), 1)
-        self.notifier.responseFromPublish("testNodeName", response)
+        self.notifier.publishNodeFailure(failure, "testNodeName")
         self.assertEquals(len(self.xmlStream.elements), 2)
         iq = self.xmlStream.elements[1]
         self.assertEquals(iq.name, "iq")
@@ -386,11 +390,9 @@
 
 
     def test_publishReponse404(self):
-        response = IQ(self.xmlStream, type='error')
-        errorElement = response.addElement('error')
-        errorElement['code'] = '404'
         self.assertEquals(len(self.xmlStream.elements), 1)
-        self.notifier.responseFromPublish("testNodeName", response)
+        failure = StubFailure(StanzaError('item-not-found'))
+        self.notifier.publishNodeFailure(failure, "testNodeName")
         self.assertEquals(len(self.xmlStream.elements), 2)
         iq = self.xmlStream.elements[1]
         self.assertEquals(iq.name, "iq")
@@ -435,7 +437,7 @@
             fieldElement.addElement('value', content=field[1])
 
         self.assertEquals(len(self.xmlStream.elements), 1)
-        self.notifier.responseFromConfigurationForm("testNodeName", response)
+        self.notifier.requestConfigurationFormSuccess(response, "testNodeName")
         self.assertEquals(len(self.xmlStream.elements), 2)
 
         iq = self.xmlStream.elements[1]
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080904/012e8b61/attachment-0001.html 


More information about the calendarserver-changes mailing list