[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