summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2014-02-20 22:05:56 -0500
committerThomas Bruederli <bruederli@kolabsys.com>2014-02-20 22:05:56 -0500
commitf6bd847a037038ff85f72da1ffca251b82400320 (patch)
treed07fb83d2f42071c10f4ad1fd5c96db4dbd7486c
parent6dd440aa974ad159d2d5666ff8370de6cafe5bdf (diff)
downloadpykolab-f6bd847a037038ff85f72da1ffca251b82400320.tar.gz
Ignore event updates for delegated resource collections; clean-up debug logging
-rw-r--r--tests/functional/test_wallace/test_005_resource_add.py58
-rw-r--r--tests/functional/test_wallace/test_005_resource_invitation.py231
-rw-r--r--wallace/module_resources.py50
3 files changed, 252 insertions, 87 deletions
diff --git a/tests/functional/test_wallace/test_005_resource_add.py b/tests/functional/test_wallace/test_005_resource_add.py
new file mode 100644
index 0000000..2de60fb
--- /dev/null
+++ b/tests/functional/test_wallace/test_005_resource_add.py
@@ -0,0 +1,58 @@
+import time
+import pykolab
+
+from pykolab import wap_client
+from pykolab.auth import Auth
+from pykolab.imap import IMAP
+from wallace import module_resources
+from twisted.trial import unittest
+
+import tests.functional.resource_func as funcs
+
+conf = pykolab.getConf()
+
+class TestResourceAdd(unittest.TestCase):
+
+ @classmethod
+ def setUp(self):
+ from tests.functional.purge_users import purge_users
+ #purge_users()
+
+ self.john = {
+ 'local': 'john.doe',
+ 'domain': 'example.org'
+ }
+
+ from tests.functional.user_add import user_add
+ #user_add("John", "Doe")
+
+ funcs.purge_resources()
+ self.audi = funcs.resource_add("car", "Audi A4")
+ self.passat = funcs.resource_add("car", "VW Passat")
+ self.boxter = funcs.resource_add("car", "Porsche Boxter S")
+ self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ])
+
+ from tests.functional.synchronize import synchronize_once
+ synchronize_once()
+
+ @classmethod
+ def tearDown(self):
+ from tests.functional.purge_users import purge_users
+ #funcs.purge_resources()
+ #purge_users()
+
+ def test_001_resource_created(self):
+ resource = module_resources.resource_record_from_email_address(self.audi['mail'])
+ self.assertEqual(len(resource), 1)
+ self.assertEqual(resource[0], self.audi['dn'])
+
+ collection = module_resources.resource_record_from_email_address(self.cars['mail'])
+ self.assertEqual(len(collection), 1)
+ self.assertEqual(collection[0], self.cars['dn'])
+
+ def test_002_resource_collection(self):
+ auth = Auth()
+ auth.connect()
+ attrs = auth.get_entry_attributes(None, self.cars['dn'], ['*'])
+ self.assertIn('groupofuniquenames', attrs['objectclass'])
+ self.assertEqual(len(attrs['uniquemember']), 3)
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 8d6803d..5afde3c 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -18,27 +18,7 @@ import tests.functional.resource_func as funcs
conf = pykolab.getConf()
-itip_invitation = """MIME-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="=_c8894dbdb8baeedacae836230e3436fd"
-From: "Doe, John" <john.doe@example.org>
-Date: Tue, 25 Feb 2014 13:54:14 +0100
-Message-ID: <240fe7ae7e139129e9eb95213c1016d7@example.org>
-User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
-To: %s
-Subject: "test" has been created
-
---=_c8894dbdb8baeedacae836230e3436fd
-Content-Type: text/plain; charset=UTF-8; format=flowed
-Content-Transfer-Encoding: quoted-printable
-
-*test*
-
---=_c8894dbdb8baeedacae836230e3436fd
-Content-Type: text/calendar; charset=UTF-8; method=REQUEST; name=event.ics
-Content-Disposition: attachment; filename=event.ics
-Content-Transfer-Encoding: 8bit
-
+itip_invitation = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
@@ -56,30 +36,9 @@ ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:ma
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
---=_c8894dbdb8baeedacae836230e3436fd--
"""
-itip_update = """MIME-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="=_c8894dbdb8baeedacae836230e3436fd"
-From: "Doe, John" <john.doe@example.org>
-Date: Tue, 25 Feb 2014 13:54:14 +0100
-Message-ID: <240fe7ae7e139129e9eb95213c1016d7@example.org>
-User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
-To: %s
-Subject: "test" has been updated
-
---=_c8894dbdb8baeedacae836230e3436fd
-Content-Type: text/plain; charset=UTF-8; format=flowed
-Content-Transfer-Encoding: quoted-printable
-
-*test* updated
-
---=_c8894dbdb8baeedacae836230e3436fd
-Content-Type: text/calendar; charset=UTF-8; method=REQUEST; name=event.ics
-Content-Disposition: attachment; filename=event.ics
-Content-Transfer-Encoding: 8bit
-
+itip_update = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
@@ -98,18 +57,35 @@ ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:ma
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
---=_c8894dbdb8baeedacae836230e3436fd--
"""
-itip_cancellation = """Return-Path: <john.doe@example.org>
-Content-Type: text/calendar; method=CANCEL; charset=UTF-8
-Content-Transfer-Encoding: quoted-printable
-To: %s
-From: john.doe@example.org
-Date: Mon, 24 Feb 2014 11:27:28 +0100
-Message-ID: <1a3aa8995e83dd24cf9247e538ac91ff@example.org>
-Subject: "test" cancelled
+itip_delegated = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.0-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:%s
+DTSTAMP;VALUE=DATE-TIME:20140227T141939Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/London:%s
+DTEND;VALUE=DATE-TIME;TZID=Europe/London:%s
+SUMMARY:test
+SEQUENCE:4
+ATTENDEE;CN=Company Cars;PARTSTAT=DELEGATED;ROLE=NON-PARTICIPANT;CUTYPE=IND
+ IVIDUAL;RSVP=TRUE;DELEGATED-TO=resource-car-audia4@example.org:mailto:reso
+ urce-collection-companycars@example.org
+ATTENDEE;CN=Audi A4;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUA
+ L;RSVP=TRUE;DELEGATED-FROM=resource-collection-companycars@example.org:mai
+ lto:resource-car-audia4@example.org
+ORGANIZER;CN=:mailto:john.doe@example.org
+DESCRIPTION:Sent to %s
+END:VEVENT
+END:VCALENDAR
+"""
+itip_cancellation = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
@@ -118,19 +94,65 @@ METHOD:CANCEL
BEGIN:VEVENT
UID:%s
DTSTAMP:20140218T1254140
-DTSTART;TZID=3DEurope/London:20120713T100000
-DTEND;TZID=3DEurope/London:20120713T110000
+DTSTART;TZID=Europe/London:20120713T100000
+DTEND;TZID=Europe/London:20120713T110000
SUMMARY:test
DESCRIPTION:test
-ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org
-ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DACCEPTED;RSVP=3DTRUE:mailt=
-o:%s
+ORGANIZER;CN="Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE:mailt=
+ o:%s
TRANSP:OPAQUE
SEQUENCE:3
END:VEVENT
END:VCALENDAR
"""
+itip_allday = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:%s
+DTSTAMP:20140213T1254140
+DTSTART;VALUE=DATE:%s
+DTEND;VALUE=DATE:%s
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN="Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:%s
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+
+mime_message = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe@example.org>
+Date: Tue, 25 Feb 2014 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c1016d7@example.org>
+User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
+To: %s
+Subject: "test"
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+*test*
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/calendar; charset=UTF-8; method=REQUEST; name=event.ics
+Content-Disposition: attachment; filename=event.ics
+Content-Transfer-Encoding: 8bit
+
+%s
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
class TestResourceInvitation(unittest.TestCase):
john = None
@@ -167,37 +189,44 @@ class TestResourceInvitation(unittest.TestCase):
from tests.functional.synchronize import synchronize_once
synchronize_once()
- def send_message(self, msg_source, to_addr, from_addr=None):
+ def send_message(self, itip_payload, to_addr, from_addr=None):
if from_addr is None:
from_addr = self.john['mail']
smtp = smtplib.SMTP('localhost', 10026)
- smtp.sendmail(from_addr, to_addr, msg_source)
+ smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, itip_payload))
- def send_itip_invitation(self, resource_email, start=None):
+ def send_itip_invitation(self, resource_email, start=None, allday=False):
if start is None:
start = datetime.datetime.now()
uid = str(uuid.uuid4())
- end = start + datetime.timedelta(hours=4)
- self.send_message(itip_invitation % (
- resource_email,
+
+ if allday:
+ template = itip_allday
+ end = start + datetime.timedelta(days=1)
+ date_format = '%Y%m%d'
+ else:
+ end = start + datetime.timedelta(hours=4)
+ template = itip_invitation
+ date_format = '%Y%m%dT%H%M%S'
+
+ self.send_message(template % (
uid,
- start.strftime('%Y%m%dT%H%M%S'),
- end.strftime('%Y%m%dT%H%M%S'),
+ start.strftime(date_format),
+ end.strftime(date_format),
resource_email
),
resource_email)
return uid
- def send_itip_update(self, resource_email, uid, start=None):
+ def send_itip_update(self, resource_email, uid, start=None, template=None):
if start is None:
start = datetime.datetime.now()
end = start + datetime.timedelta(hours=4)
- self.send_message(itip_update % (
- resource_email,
+ self.send_message((template if template is not None else itip_update) % (
uid,
start.strftime('%Y%m%dT%H%M%S'),
end.strftime('%Y%m%dT%H%M%S'),
@@ -209,7 +238,6 @@ class TestResourceInvitation(unittest.TestCase):
def send_itip_cancel(self, resource_email, uid):
self.send_message(itip_cancellation % (
- resource_email,
uid,
resource_email
),
@@ -290,6 +318,18 @@ class TestResourceInvitation(unittest.TestCase):
imap.imap.m.expunge()
imap.disconnect()
+ time.sleep(1)
+
+
+ def find_resource_by_email(self, email):
+ resource = None
+ if (email.find(self.audi['mail']) >= 0):
+ resource = self.audi
+ if (email.find(self.passat['mail']) >= 0):
+ resource = self.passat
+ if (email.find(self.boxter['mail']) >= 0):
+ resource = self.boxter
+ return resource
def test_001_resource_from_email_address(self):
@@ -331,7 +371,7 @@ class TestResourceInvitation(unittest.TestCase):
accept = self.check_message_received("Reservation Request for test was ACCEPTED")
self.assertIsInstance(accept, email.message.Message)
- delegatee = self.passat if accept['from'].find(self.passat['mail']) >= 0 else self.boxter
+ delegatee = self.find_resource_by_email(accept['from'])
self.assertIn(delegatee['mail'], accept['from'])
# check booking in the delegatee's resource calendar
@@ -343,13 +383,15 @@ class TestResourceInvitation(unittest.TestCase):
def test_005_rescheduling_reservation(self):
- uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,5,1, 10,0,0))
+ self.purge_mailbox(self.john['mailbox'])
+
+ uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,4,1, 10,0,0))
response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
self.purge_mailbox(self.john['mailbox'])
- self.send_itip_update(self.audi['mail'], uid, datetime.datetime(2014,5,1, 12,0,0)) # conflict with myself
+ self.send_itip_update(self.audi['mail'], uid, datetime.datetime(2014,4,1, 12,0,0)) # conflict with myself
response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
@@ -361,6 +403,8 @@ class TestResourceInvitation(unittest.TestCase):
def test_006_cancelling_revervation(self):
+ self.purge_mailbox(self.john['mailbox'])
+
uid = self.send_itip_invitation(self.boxter['mail'], datetime.datetime(2014,5,1, 10,0,0))
self.assertIsInstance(self.check_resource_calendar_event(self.boxter['kolabtargetfolder'], uid), pykolab.xml.Event)
@@ -374,3 +418,46 @@ class TestResourceInvitation(unittest.TestCase):
response = self.check_message_received("Reservation Request for test was ACCEPTED", self.boxter['mail'])
self.assertIsInstance(response, email.message.Message)
+
+
+ def test_007_update_delegated(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ dt = datetime.datetime(2014,8,1, 12,0,0)
+ uid = self.send_itip_invitation(self.cars['mail'], dt)
+
+ # wait for accept notification
+ accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ self.assertIsInstance(accept, email.message.Message)
+ delegatee = self.find_resource_by_email(accept['from'])
+
+ # send update message to all attendees (collection and delegatee)
+ self.purge_mailbox(self.john['mailbox'])
+ update_template = itip_delegated.replace("resource-car-audia4@example.org", delegatee['mail'])
+ self.send_itip_update(self.cars['mail'], uid, dt, template=update_template)
+ self.send_itip_update(delegatee['mail'], uid, dt, template=update_template)
+
+ # get response from delegatee
+ accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ self.assertIsInstance(accept, email.message.Message)
+ self.assertIn(delegatee['mail'], accept['from'])
+
+ # no delegation response on updates
+ self.assertEqual(self.check_message_received("Reservation Request for test was DELEGATED", self.cars['mail']), None)
+
+
+ def test_008_allday_reservation(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2), True)
+
+ accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ self.assertIsInstance(accept, email.message.Message)
+
+ event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertIsInstance(event.get_start(), datetime.date)
+
+ uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2, 16,0,0))
+ response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+ self.assertIsInstance(response, email.message.Message)
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index b718ac4..6aaf64a 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -35,6 +35,7 @@ from email.utils import getaddresses
import modules
import pykolab
+import kolabformat
from pykolab.auth import Auth
from pykolab.conf import Conf
@@ -229,19 +230,31 @@ def execute(*args, **kw):
else:
resources[resource_dn] = resource_attrs
- log.debug(_("Resources: %r, %r") % (resource_dns, resources), level=8)
+ log.debug(_("Resources: %r; %r") % (resource_dns, resources), level=8)
- # process CANCEL messages
done = False
+ receiving_resource = resources[resource_dns[0]]
+
for itip_event in itip_events:
- if itip_event['method'] == "CANCEL":
+ try:
+ receiving_attendee = itip_event['xml'].get_attendee_by_email(receiving_resource['mail'])
+ log.debug(_("Receiving Resource: %r; %r") % (receiving_resource, receiving_attendee), level=9)
+ except Exception, e:
+ log.error("Could not find envelope attendee: %r" % (e))
+ continue
+
+ # ignore updates and cancellations to resource collections who already delegated the event
+ if receiving_attendee.get_delegated_to().size() > 0 or receiving_attendee.get_role() == kolabformat.NonParticipant:
+ done = True
+ log.debug(_("Recipient %r is non-participant, ignoring message") % (receiving_resource['mail']), level=8)
+
+ # process CANCEL messages
+ if not done and itip_event['method'] == "CANCEL":
for resource in resource_dns:
if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
delete_resource_event(itip_event['uid'], resources[resource])
- # TODO: handle cancellations sent to resource collections. Really?
-
done = True
if done:
@@ -263,14 +276,14 @@ def execute(*args, **kw):
# sets the 'conflicting' flag and adds a list of conflicting events found
try:
- read_resource_calendar(resources[resource], itip_events)
+ num_messages = read_resource_calendar(resources[resource], itip_events)
except Exception, e:
log.error(_("Failed to read resource calendar for %r: %r") % (resource, e))
continue
end = time.time()
- log.debug(_("start: %r, end: %r, total: %r") % (start, end, (end-start)), level=1)
+ log.debug(_("start: %r, end: %r, total: %r, messages: %d") % (start, end, (end-start), num_messages), level=9)
# For each resource (collections are first!)
@@ -335,7 +348,7 @@ def execute(*args, **kw):
for uid in resources[resource]['existing_events']:
delete_resource_event(uid, resources[resource])
- log.debug(_("Accept invitation for individual resource %r / %r") % (resource, resources[resource]['mail']), level=9)
+ log.debug(_("Accept invitation for individual resource %r / %r") % (resource, resources[resource]['mail']), level=8)
accept_reservation_request(itip_event, resources[resource])
done = True
@@ -349,7 +362,7 @@ def execute(*args, **kw):
# Randomly selects a target resource from the resource collection.
_target_resource = resources[original_resource['uniquemember'][random.randint(0,(len(original_resource['uniquemember'])-1))]]
- log.debug(_("Delegate invitation for resource collection %r to %r") % (original_resource['mail'], _target_resource['mail']), level=9)
+ log.debug(_("Delegate invitation for resource collection %r to %r") % (original_resource['mail'], _target_resource['mail']), level=8)
if original_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
#
@@ -395,6 +408,8 @@ def read_resource_calendar(resource_rec, itip_events):
imap.imap.m.select(mailbox)
typ, data = imap.imap.m.search(None, 'ALL')
+ num_messages = len(data[0].split())
+
for num in data[0].split():
# For efficiency, makes the routine non-deterministic
if resource_rec['conflict']:
@@ -456,7 +471,7 @@ def read_resource_calendar(resource_rec, itip_events):
resource_rec['conflicting_events'].append(event.get_uid())
resource_rec['conflict'] = True
- return resource_rec['conflict']
+ return num_messages
def accept_reservation_request(itip_event, resource, delegator=None):
@@ -570,7 +585,7 @@ def itip_events_from_message(message):
# Get the itip_payload
itip_payload = part.get_payload(decode=True)
- log.debug(_("Raw iTip payload: %s") % (itip_payload))
+ log.debug(_("Raw iTip payload: %s") % (itip_payload), level=9)
# Python iCalendar prior to 3.0 uses "from_string".
if hasattr(icalendar.Calendar, 'from_ical'):
@@ -588,13 +603,14 @@ def itip_events_from_message(message):
itip = {}
if c['uid'] in seen_uids:
- log.debug(_("Duplicate iTip event: %s") % (c['uid']))
+ log.debug(_("Duplicate iTip event: %s") % (c['uid']), level=9)
continue
# From the event, take the following properties:
#
# - method
# - uid
+ # - sequence
# - start
# - end (if any)
# - duration (if any)
@@ -607,6 +623,7 @@ def itip_events_from_message(message):
itip['uid'] = str(c['uid'])
itip['method'] = str(cal['method']).upper()
+ itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0
if c.has_key('dtstart'):
itip['start'] = c['dtstart']
@@ -629,7 +646,12 @@ def itip_events_from_message(message):
itip['resources'] = c['resources']
itip['raw'] = itip_payload
- itip['xml'] = event_from_ical(c.to_ical())
+
+ try:
+ itip['xml'] = event_from_ical(c.to_ical())
+ except Exception, e:
+ log.error("event_from_ical() exception: %r" % (e))
+ continue
itip_events.append(itip)
@@ -798,8 +820,6 @@ def resource_records_from_itip_events(itip_events, recipient_email=None):
log.debug(_("The following resources are being referred to in the " + \
"iTip: %r") % (resource_records), level=8)
- auth.disconnect()
-
return resource_records