5 commits - pykolab/xml tests/unit wallace/module_resources.py

Thomas Brüderli bruederli at kolabsys.com
Mon Feb 24 20:00:29 CET 2014


 pykolab/xml/attendee.py                  |    2 
 pykolab/xml/event.py                     |   10 
 tests/unit/test-003-event.py             |   14 -
 tests/unit/test-011-wallace_resources.py |  362 +++++++++++++++++++++++++++++++
 wallace/module_resources.py              |  140 ++++++-----
 5 files changed, 454 insertions(+), 74 deletions(-)

New commits:
commit f64c8d5e4162ccd034e4ab8fe7a3760b0282d1f0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 24 19:52:49 2014 +0100

    Add tests for recent improvements to libkolab event wrapper

diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 8f76397..1a38fad 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -2,6 +2,7 @@ import datetime
 import pytz
 import sys
 import unittest
+import kolabformat
 
 from pykolab.xml import Attendee
 from pykolab.xml import Event
@@ -40,8 +41,8 @@ class TestEventXML(unittest.TestCase):
         self.assertIsInstance(self.event.get_attendees(), list)
         self.assertEqual(len(self.event.get_attendees()), 1)
 
-    def test_005_attendee_add_name(self):
-        self.event.add_attendee("jane at doe.org", "Doe, Jane")
+    def test_005_attendee_add_name_and_props(self):
+        self.event.add_attendee("jane at doe.org", "Doe, Jane", role="OPTIONAL", cutype="RESOURCE")
         self.assertIsInstance(self.event.get_attendees(), list)
         self.assertEqual(len(self.event.get_attendees()), 2)
 
@@ -52,6 +53,10 @@ class TestEventXML(unittest.TestCase):
         self.assertIsInstance(self.event.get_attendee_by_email("jane at doe.org"), Attendee)
         self.assertIsInstance(self.event.get_attendee("jane at doe.org"), Attendee)
 
+    def test_007_get_attendee_props(self):
+        self.assertEqual(self.event.get_attendee("jane at doe.org").get_cutype(), kolabformat.CutypeResource)
+        self.assertEqual(self.event.get_attendee("jane at doe.org").get_role(), kolabformat.Optional)
+
     def test_007_get_nonexistent_attendee_by_email(self):
         self.assertRaises(ValueError, self.event.get_attendee_by_email, "nosuchattendee at invalid.domain")
         self.assertRaises(ValueError, self.event.get_attendee, "nosuchattendee at invalid.domain")
@@ -77,7 +82,10 @@ class TestEventXML(unittest.TestCase):
         self.event.delegate("jane at doe.org", "max at imum.com")
 
     def test_013_delegatee_is_now_attendee(self):
-        self.assertIsInstance(self.event.get_attendee("max at imum.com"), Attendee)
+        delegatee = self.event.get_attendee("max at imum.com")
+        self.assertIsInstance(delegatee, Attendee)
+        self.assertEqual(delegatee.get_role(), kolabformat.Optional)
+        self.assertEqual(delegatee.get_cutype(), kolabformat.CutypeResource)
 
     def test_014_delegate_attendee_adds(self):
         self.assertEqual(len(self.event.get_attendee("jane at doe.org").get_delegated_to()), 1)


commit 881f6847bcf45a21ce8ea691d8ede1190393bcc8
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 24 19:51:45 2014 +0100

    Copy delegator's role and cutype to delegatees

diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 98b823d..15f1c1a 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -78,6 +78,8 @@ class Attendee(kolabformat.Attendee):
             if not isinstance(delegator, Attendee):
                 raise ValueError, _("Not a valid attendee")
             else:
+                self.set_role(delegator.get_role())
+                self.set_cutype(delegator.get_cutype())
                 crefs.append(delegator.contactreference)
 
         if len(crefs) == 0:


commit a5730c9eec6c25882e2e9b7a6ef1db28cfabf20b
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 24 19:50:56 2014 +0100

    Pass attendee's cutype argument to object constructor; add method=XXX to iTip message part

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 6d3669c..9145a45 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -45,7 +45,7 @@ class Event(object):
         self.uid = self.get_uid()
 
     def add_attendee(self, email, name=None, rsvp=False, role=None, participant_status=None, cutype="INDIVIDUAL"):
-        attendee = Attendee(email, name, rsvp, role, participant_status)
+        attendee = Attendee(email, name, rsvp, role, participant_status, cutype)
         self._attendees.append(attendee)
         self.event.setAttendees(self._attendees)
 
@@ -667,7 +667,7 @@ class Event(object):
                     params = {}
 
                 if params.has_key('CN'):
-                    name = params['CN']
+                    name = str(params['CN'])
                 else:
                     name = None
 
@@ -968,8 +968,8 @@ class Event(object):
 
         msg.attach( MIMEText(text) )
 
-        part = MIMEBase('text', "calendar")
-        part.set_charset('UTF-8')
+        part = MIMEBase('text', 'calendar', charset='UTF-8', method=method)
+        del part['MIME-Version']  # mime parts don't need this
 
         # TODO: Should allow for localization
         msg["Subject"] = "Meeting Request %s" % (participant_status)
@@ -977,7 +977,7 @@ class Event(object):
         part.set_payload(self.as_string_itip(method=method))
 
         part.add_header('Content-Disposition', 'attachment; filename="event.ics"')
-        part.replace_header('Content-Transfer-Encoding', '8bit')
+        part.add_header('Content-Transfer-Encoding', '8bit')
 
         msg.attach(part)
 


commit 87e2d218a0851fe2a87d167ee9a6ee7e2ff6c6d0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 24 18:53:10 2014 +0100

    Make wallace/module_resources pass the unit tests
    - support non-multipart iTip messages
    - fix sending of delegated itip replies
    - simplify attendee finding

diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 7d45216..8b0fccb 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -452,93 +452,100 @@ def itip_events_from_message(message):
     """
     # Placeholder for any itip_events found in the message.
     itip_events = []
+    seen_uids = []
 
     # iTip methods we are actually interested in. Other methods will be ignored.
     itip_methods = [ "REQUEST", "REPLY", "ADD", "CANCEL" ]
 
-    # TODO: Are all iTip messages multipart? RFC 6047, section 2.4 states "A
+    # Are all iTip messages multipart? No! RFC 6047, section 2.4 states "A
     # MIME body part containing content information that conforms to this
     # document MUST have (...)" but does not state whether an iTip message must
     # therefore also be multipart.
-    if message.is_multipart():
-        # Check each part
-        for part in message.walk():
-
-            # The iTip part MUST be Content-Type: text/calendar (RFC 6047,
-            # section 2.4)
-            if part.get_content_type() == "text/calendar":
-                if not part.get_param('method') in itip_methods:
-                    log.error(
-                            _("Method %r not really interesting for us.") % (
-                                    part.get_param('method')
-                                )
-                        )
 
-                # Get the itip_payload
-                itip_payload = part.get_payload(decode=True)
+    # Check each part
+    for part in message.walk():
 
-                log.debug(_("Raw iTip payload: %s") % (itip_payload))
+        # The iTip part MUST be Content-Type: text/calendar (RFC 6047, section 2.4)
+        # But in real word, other mime-types are used as well
+        if part.get_content_type() in [ "text/calendar", "text/x-vcalendar", "application/ics" ]:
+            if not part.get_param('method') in itip_methods:
+                log.error(
+                        _("Method %r not really interesting for us.") % (
+                                part.get_param('method')
+                            )
+                    )
 
-                # Python iCalendar prior to 3.0 uses "from_string".
-                if hasattr(icalendar.Calendar, 'from_ical'):
-                    cal = icalendar.Calendar.from_ical(itip_payload)
-                elif hasattr(icalendar.Calendar, 'from_string'):
-                    cal = icalendar.Calendar.from_string(itip_payload)
+            # Get the itip_payload
+            itip_payload = part.get_payload(decode=True)
 
-                # If we can't read it, we're out
-                else:
-                    log.error(_("Could not read iTip from message."))
-                    return []
+            log.debug(_("Raw iTip payload: %s") % (itip_payload))
 
-                for c in cal.walk():
-                    if c.name == "VEVENT":
-                        itip = {}
+            # Python iCalendar prior to 3.0 uses "from_string".
+            if hasattr(icalendar.Calendar, 'from_ical'):
+                cal = icalendar.Calendar.from_ical(itip_payload)
+            elif hasattr(icalendar.Calendar, 'from_string'):
+                cal = icalendar.Calendar.from_string(itip_payload)
 
-                        # From the event, take the following properties:
-                        #
-                        # - start
-                        # - end (if any)
-                        # - duration (if any)
-                        # - organizer
-                        # - attendees (if any)
-                        # - resources (if any)
-                        # - TODO: recurrence rules (if any)
-                        #   Where are these stored actually?
-                        #
+            # If we can't read it, we're out
+            else:
+                log.error(_("Could not read iTip from message."))
+                return []
+
+            for c in cal.walk():
+                if c.name == "VEVENT":
+                    itip = {}
+
+                    if c['uid'] in seen_uids:
+                        log.debug(_("Duplicate iTip event: %s") % (c['uid']))
+                        continue
 
-                        if c.has_key('dtstart'):
-                            itip['start'] = c['dtstart']
-                        else:
-                            log.error(_("iTip event without a start"))
-                            continue
+                    # From the event, take the following properties:
+                    #
+                    # - start
+                    # - end (if any)
+                    # - duration (if any)
+                    # - organizer
+                    # - attendees (if any)
+                    # - resources (if any)
+                    # - TODO: recurrence rules (if any)
+                    #   Where are these stored actually?
+                    #
 
-                        if c.has_key('dtend'):
-                            itip['end'] = c['dtend']
+                    if c.has_key('dtstart'):
+                        itip['start'] = c['dtstart']
+                    else:
+                        log.error(_("iTip event without a start"))
+                        continue
 
-                        if c.has_key('duration'):
-                            itip['duration'] = c['duration']
+                    if c.has_key('dtend'):
+                        itip['end'] = c['dtend']
 
-                        itip['organizer'] = c['organizer']
+                    if c.has_key('duration'):
+                        itip['duration'] = c['duration']
 
-                        itip['attendees'] = c['attendee']
+                    itip['organizer'] = c['organizer']
 
-                        if c.has_key('resources'):
-                            itip['resources'] = c['resources']
+                    itip['attendees'] = c['attendee']
 
-                        itip['raw'] = itip_payload
-                        itip['xml'] = event_from_ical(c.to_ical())
+                    if c.has_key('resources'):
+                        itip['resources'] = c['resources']
 
-                        itip_events.append(itip)
+                    itip['raw'] = itip_payload
+                    itip['xml'] = event_from_ical(c.to_ical())
 
-                    # end if c.name == "VEVENT"
+                    itip_events.append(itip)
 
-                # end for c in cal.walk()
+                    seen_uids.append(c['uid'])
 
-            # end if part.get_content_type() == "text/calendar"
+                # end if c.name == "VEVENT"
 
-        # end for part in message.walk()
+            # end for c in cal.walk()
 
-    else: # if message.is_multipart()
+        # end if part.get_content_type() == "text/calendar"
+
+    # end for part in message.walk()
+
+    if not len(itip_events) and not message.is_multipart():
         log.debug(_("Message is not an iTip message (non-multipart message)"), level=5)
 
     return itip_events
@@ -741,20 +748,21 @@ def send_response(from_address, itip_events):
         itip_events = [ itip_events ]
 
     for itip_event in itip_events:
-        attendee = [a for a in itip_event['xml'].get_attendees() if a.get_email() == from_address][0]
+        attendee = itip_event['xml'].get_attendee(from_address)
         participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)
 
         if participant_status == "DELEGATED":
             # Extra actions to take
-            delegator = [a for a in itip_event['xml'].get_attendees() if a.get_email() == from_address][0]
+            delegator = itip_event['xml'].get_attendee(from_address)
             delegatee = [a for a in itip_event['xml'].get_attendees() if from_address in [b.email() for b in a.get_delegated_from()]][0]
 
-            itip_event['xml'].event.setAttendees([ delegator, delegatee ])
-
             message = itip_event['xml'].to_message_itip(delegatee.get_email(), method="REPLY", participant_status=itip_event['xml'].get_ical_attendee_participant_status(delegatee))
             smtp.sendmail(message['From'], message['To'], message.as_string())
 
-        itip_event['xml'].event.setAttendees([a for a in itip_event['xml'].get_attendees() if a.get_email() == from_address])
+            # restore list of attendees after to_message_itip()
+            itip_event['xml']._attendees = [ delegator, delegatee ]
+            itip_event['xml'].event.setAttendees(itip_event['xml']._attendees)
+            participant_status = "DELEGATED"
 
         message = itip_event['xml'].to_message_itip(from_address, method="REPLY", participant_status=participant_status)
         smtp.sendmail(message['From'], message['To'], message.as_string())


commit 99ba571d7c17f1c6966bce6aafe0d8d4a268837e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 24 18:47:01 2014 +0100

    Add basic unit tests for wallace/module_resources

diff --git a/tests/unit/test-011-wallace_resources.py b/tests/unit/test-011-wallace_resources.py
new file mode 100644
index 0000000..106c2f3
--- /dev/null
+++ b/tests/unit/test-011-wallace_resources.py
@@ -0,0 +1,362 @@
+import pykolab
+import logging
+
+from icalendar import Calendar
+from email import message
+from email import message_from_string
+from wallace import module_resources
+from twisted.trial import unittest
+
+# define some iTip MIME messages
+
+itip_multipart = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe at example.org>
+Date: Fri, 13 Jul 2012 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c1016d7 at example.org>
+User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
+To: resource-collection-car at example.org
+Subject: "test" has been updated
+
+--=_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: quoted-printable
+
+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:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+o:resource-collection-car at example.org
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+itip_non_multipart = """Return-Path: <john.doe at example.org>
+Sender: john.doe at example.org
+Content-Type: text/calendar; method=REQUEST; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+To: resource-collection-car at example.org
+From: john.doe at example.org
+Date: Mon, 24 Feb 2014 11:27:28 +0100
+Message-ID: <1a3aa8995e83dd24cf9247e538ac913a at example.org>
+Subject: test
+
+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:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DACCEPTED;RSVP=3DTRUE:mailt=
+o:resource-collection-car at example.org
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_google_multipart = """MIME-Version: 1.0
+Message-ID: <001a11c2ad84243e0604f3246bae at google.com>
+Date: Mon, 24 Feb 2014 10:27:28 +0000
+Subject: =?ISO-8859-1?Q?Invitation=3A_iTip_from_Apple_=40_Mon_Feb_24=2C_2014_12pm_?=
+	=?ISO-8859-1?Q?=2D_1pm_=28Tom_=26_T=E4m=29?=
+From: "john.doe" <john.doe at gmail.com>
+To: <john.sample at example.org>
+Content-Type: multipart/mixed; boundary=001a11c2ad84243df004f3246bad
+
+--001a11c2ad84243df004f3246bad
+Content-Type: multipart/alternative; boundary=001a11c2ad84243dec04f3246bab
+
+--001a11c2ad84243dec04f3246bab
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes
+
+<some text content here>
+
+--001a11c2ad84243dec04f3246bab
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+<div style=3D""><!-- some HTML message content here --></div>
+--001a11c2ad84243dec04f3246bab
+Content-Type: text/calendar; charset=UTF-8; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20140224T110000Z
+DTEND:20140224T120000Z
+DTSTAMP:20140224T102728Z
+ORGANIZER:mailto:kepjllr6mcq7d0959u4cdc7000 at group.calendar.google.com
+UID:0BE2F640-5814-47C9-ABAE-E7E959204E76
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;X-NUM-GUESTS=0:mailto:kepjllr6mcq7d0959u4cdc7000 at group.calendar.google.com
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
+ TRUE;CN=John Sample;X-NUM-GUESTS=0:mailto:john.sample at example.org
+CREATED:20140224T102728Z
+DESCRIPTION:Testing Multipart structure\\nView your event at http://www.goog
+ le.com/calendar/event?action=VIEW&eid=XzYxMTRhY2k2Nm9xMzBiOWw3MG9qOGI5azZ0M
+ WppYmExODkwa2FiYTU2dDJqaWQ5cDY4bzM4aDluNm8gdGhvbWFzQGJyb3RoZXJsaS5jaA&tok=N
+ TIja2VwamxscjZtY3E3ZDA5NTl1NGNkYzcwMDBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbTkz
+ NTcyYTU2YmUwNWMxNjY0Zjc3OTU0MzhmMDcwY2FhN2NjZjIzYWM&ctz=Europe/Zurich&hl=en
+ .
+LAST-MODIFIED:20140224T102728Z
+LOCATION:
+SEQUENCE:5
+STATUS:CONFIRMED
+SUMMARY:iTip from Apple
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--001a11c2ad84243dec04f3246bab--
+--001a11c2ad84243df004f3246bad
+Content-Type: application/ics; name="invite.ics"
+Content-Disposition: attachment; filename="invite.ics"
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw
+LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT
+VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMjI0VDExMDAwMFoNCkRURU5EOjIwMTQwMjI0
+VDEyMDAwMFoNCkRUU1RBTVA6MjAxNDAyMjRUMTAyNzI4Wg0KT1JHQU5JWkVSOm1haWx0bzprZXBq
+bGxyNm1jcTdkMDk1OXU0Y2RjNzAwMEBncm91cC5jYWxlbmRhci5nb29nbGUuY29tDQpVSUQ6MEJF
+MkY2NDAtNTgxNC00N0M5LUFCQUUtRTdFOTU5MjA0RTc2DQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJ
+RFVBTDtST0xFPVJFUS1QQVJUSUNJUEFOVDtQQVJUU1RBVD1BQ0NFUFRFRDtSU1ZQPVRSVUUNCiA7
+WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmtlcGpsbHI2bWNxN2QwOTU5dTRjZGM3MDAwQGdyb3VwLmNh
+bGVuZGFyLmdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFMO1JPTEU9UkVRLVBB
+UlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7WC1OVU0tR1VFU1RT
+PTA6bWFpbHRvOnRob21hc0Bicm90aGVybGkuY2gNCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM
+O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7
+Q049VGhvbWFzIEJydWVkZXJsaTtYLU5VTS1HVUVTVFM9MDptYWlsdG86cm91bmRjdWJlQGdtYWls
+LmNvbQ0KQ1JFQVRFRDoyMDE0MDIyNFQxMDI3MjhaDQpERVNDUklQVElPTjpUZXN0aW5nIE11bHRp
+cGFydCBzdHJ1Y3R1cmVcblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vd3d3Lmdvb2cNCiBsZS5j
+b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPVh6WXhNVFJoWTJrMk5tOXhNekJpT1d3
+M01HOXFPR0k1YXpaME0NCiBXcHBZbUV4T0Rrd2EyRmlZVFUyZERKcWFXUTVjRFk0YnpNNGFEbHVO
+bThnZEdodmJXRnpRR0p5YjNSb1pYSnNhUzVqYUEmdG9rPU4NCiBUSWphMlZ3YW14c2NqWnRZM0Uz
+WkRBNU5UbDFOR05rWXpjd01EQkFaM0p2ZFhBdVkyRnNaVzVrWVhJdVoyOXZaMnhsTG1OdmJUa3oN
+CiBOVGN5WVRVMlltVXdOV014TmpZMFpqYzNPVFUwTXpobU1EY3dZMkZoTjJOalpqSXpZV00mY3R6
+PUV1cm9wZS9adXJpY2gmaGw9ZW4NCiAuDQpMQVNULU1PRElGSUVEOjIwMTQwMjI0VDEwMjcyOFoN
+CkxPQ0FUSU9OOg0KU0VRVUVOQ0U6NQ0KU1RBVFVTOkNPTkZJUk1FRA0KU1VNTUFSWTppVGlwIGZy
+b20gQXBwbGUNClRSQU5TUDpPUEFRVUUNCkVORDpWRVZFTlQNCkVORDpWQ0FMRU5EQVINCg==
+--001a11c2ad84243df004f3246bad--
+"""
+
+itip_application_ics = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe at example.org>
+Date: Fri, 13 Jul 2012 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c101622 at example.org>
+User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
+To: resource-collection-car at example.org
+Subject: "test" has been updated
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=UTF-8; format=flowed
+
+<some text here>
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: application/ics; charset=UTF-8; method=REQUEST;
+ name=event.ics
+Content-Transfer-Encoding: quoted-printable
+
+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:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+o:resource-collection-car at example.org
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+itip_empty = """MIME-Version: 1.0
+Date: Fri, 17 Jan 2014 13:51:50 +0100
+From: <john.doe at example.org>
+User-Agent: Roundcube Webmail/0.9.5
+To: john.sample at example.org
+Subject: "test" has been sent
+Message-ID: <52D92766.5040508 at somedomain.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+Message plain text goes here...
+"""
+
+
+conf = pykolab.getConf()
+
+if not hasattr(conf, 'defaults'):
+    conf.finalize_conf()
+
+class TestWallaceResources(unittest.TestCase):
+
+    def setUp(self):
+        # monkey-patch the pykolab.auth module to check API calls
+        # without actually connecting to LDAP
+        self.patch(pykolab.auth.Auth, "connect", self._mock_nop)
+        self.patch(pykolab.auth.Auth, "disconnect", self._mock_nop)
+        self.patch(pykolab.auth.Auth, "find_resource", self._mock_find_resource)
+
+        # intercept calls to smtplib.SMTP.sendmail()
+        import smtplib
+        self.patch(smtplib.SMTP, "__init__", self._mock_smtp_init)
+        self.patch(smtplib.SMTP, "quit", self._mock_nop)
+        self.patch(smtplib.SMTP, "sendmail", self._mock_smtp_sendmail)
+
+        self.smtplog = [];
+
+    def _mock_nop(self, domain=None):
+        pass
+
+    def _mock_find_resource(self, address):
+        (prefix, domain) = address.split('@')
+        entry_dn = "uid=" + prefix + ",dc=" + ",dc=".join(domain.split('.'))
+        return [ entry_dn ];
+
+    def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0):
+        pass
+
+    def _mock_smtp_sendmail(self, from_addr, to_addr, message, mail_options=None, rcpt_options=None):
+        self.smtplog.append((from_addr, to_addr, message))
+
+    def _get_ics_part(self, message):
+        ics_part = None
+        for part in message.walk():
+            if part.get_content_type() == 'text/calendar':
+                ics_part = part
+
+        return ics_part
+
+    def _get_ical(self, ics):
+        if hasattr(Calendar, 'from_ical'):
+            cal = Calendar.from_ical(ics)
+        elif hasattr(Calendar, 'from_string'):
+            cal = Calendar.from_string(ics)
+
+        for e in cal.walk():
+            if e.name == "VEVENT":
+                return e
+
+        return None
+
+    def test_001_itip_events_from_message(self):
+        itips1 = module_resources.itip_events_from_message(message_from_string(itip_multipart))
+        self.assertEqual(len(itips1), 1, "Multipart iTip message with text/calendar")
+
+        itips2 = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))
+        self.assertEqual(len(itips2), 1, "Detect non-multipart iTip messages")
+
+        itips3 = module_resources.itip_events_from_message(message_from_string(itip_application_ics))
+        self.assertEqual(len(itips3), 1, "Multipart iTip message with application/ics attachment")
+
+        itips4 = module_resources.itip_events_from_message(message_from_string(itip_google_multipart))
+        self.assertEqual(len(itips4), 1, "Multipart iTip message from Google")
+
+        itips5 = module_resources.itip_events_from_message(message_from_string(itip_empty))
+        self.assertEqual(len(itips5), 0, "Simple plain text message")
+
+
+    def test_002_resource_record_from_email_address(self):
+        res = module_resources.resource_record_from_email_address("doe at example.org")
+        # assert call to (pathced) pykolab.auth.Auth.find_resource()
+        self.assertEqual(len(res), 1);
+        self.assertEqual("uid=doe,dc=example,dc=org", res[0]);
+
+
+    def test_003_resource_records_from_itip_events(self):
+        itips = module_resources.itip_events_from_message(message_from_string(itip_multipart))
+        res = module_resources.resource_records_from_itip_events(itips)
+        self.assertEqual(len(res), 1);
+        self.assertEqual("uid=resource-collection-car,dc=example,dc=org", res[0]);
+
+
+    def test_004_send_response_accept(self):
+        itip_event = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))
+        module_resources.send_response("resource-collection-car at example.org", itip_event)
+
+        self.assertEqual(len(self.smtplog), 1);
+        self.assertEqual("resource-collection-car at example.org", self.smtplog[0][0])
+        self.assertEqual("john.doe at example.org", self.smtplog[0][1])
+
+        response = message_from_string(self.smtplog[0][2])
+        self.assertIn("ACCEPTED", response['subject'], "Participant status in message subject")
+        self.assertTrue(response.is_multipart())
+
+        # find ics part of the response
+        ics_part = self._get_ics_part(response)
+        self.assertIsInstance(ics_part, message.Message)
+        self.assertEqual(ics_part.get_param('method'), "REPLY")
+
+
+    def test_005_send_response_delegate(self):
+        # delegate resource-collection-car at example.org => resource-car-audi-a4 at example.org
+        itip_event = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))[0]
+        itip_event['xml'].delegate('resource-collection-car at example.org', 'resource-car-audi-a4 at example.org')
+        itip_event['xml'].set_attendee_participant_status(itip_event['xml'].get_attendee('resource-car-audi-a4 at example.org'), "ACCEPTED")
+
+        module_resources.send_response("resource-collection-car at example.org", itip_event)
+
+        self.assertEqual(len(self.smtplog), 2);
+        self.assertEqual("resource-car-audi-a4 at example.org", self.smtplog[0][0])
+        self.assertEqual("resource-collection-car at example.org", self.smtplog[1][0])
+
+        # delegated resource responds ACCEPTED
+        response1 = message_from_string(self.smtplog[0][2])
+        ical1 = self._get_ical(self._get_ics_part(response1).get_payload(decode=True))
+        self.assertIn("ACCEPTED", response1['subject'], "Participant status in message subject")
+        self.assertEqual(ical1['attendee'], "MAILTO:resource-car-audi-a4 at example.org")
+
+        # resource collection responds DELEGATED
+        response2 = message_from_string(self.smtplog[1][2])
+        ical2 = self._get_ical(self._get_ics_part(response2).get_payload(decode=True))
+        self.assertIn("DELEGATED", response2['subject'], "Delegation message subject")
+        self.assertEqual(ical2['attendee'], "MAILTO:resource-collection-car at example.org")
+        self.assertEqual(ical2['attendee'].params['PARTSTAT'], "DELEGATED")
+




More information about the commits mailing list