[CalendarServer-changes] [2768] CalendarServer/branches/users/sagen/mailgateway-implicit-2745/ twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Mon Aug 4 17:49:35 PDT 2008


Revision: 2768
          http://trac.macosforge.org/projects/calendarserver/changeset/2768
Author:   sagen at apple.com
Date:     2008-08-04 17:49:35 -0700 (Mon, 04 Aug 2008)
Log Message:
-----------
Checkpoint: round-trips of imip messages is working

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/mail.py
    CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/scheduling/imip.py
    CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/tap.py

Modified: CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/mail.py
===================================================================
--- CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/mail.py	2008-08-01 20:47:47 UTC (rev 2767)
+++ CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/mail.py	2008-08-05 00:49:35 UTC (rev 2768)
@@ -28,6 +28,7 @@
 from twisted.application import internet, service
 from twisted.python.usage import Options, UsageError
 from twisted.python.reflect import namedClass
+from twisted.mail.smtp import messageid, rfc822date, sendmail
 from twistedcaldav.log import LoggingMixIn
 from twistedcaldav import ical
 from twistedcaldav.resource import CalDAVResource
@@ -35,7 +36,13 @@
 from twistedcaldav.config import config, parseConfig, defaultConfig
 from twistedcaldav.sql import AbstractSQLDatabase
 from zope.interface import Interface, implements
-import email, uuid
+import email, email.utils
+import uuid
+import os
+import cStringIO
+import datetime
+import base64
+import MimeWriter
 
 __all__ = [
     "IMIPInboxResource",
@@ -212,19 +219,23 @@
 
 
 
-def injectMessage(reactor, useSSL, host, port, path, originator, recipient,
-    calendar):
+def injectMessage(organizer, attendee, calendar, reactor=None):
 
+    if reactor is None:
+        from twisted.internet import reactor
+
     headers = {
         'Content-Type' : 'text/calendar',
-        'Originator' : originator,
-        'Recipient' : recipient,
+        'Originator' : attendee,
+        'Recipient' : organizer,
     }
 
-    # TODO: use token to look up actual organizer for substitution
-    calendar.getOrganizerProperty().setValue("mailto:user01 at example.com")
     data = str(calendar)
 
+    useSSL = False
+    host = "localhost"
+    port = 8008
+    path = "email-inbox"
     scheme = "https:" if useSSL else "http:"
     url = "%s//%s:%d/%s/" % (scheme, host, port, path)
 
@@ -247,7 +258,7 @@
 
     Token Database:
 
-    ROW: TOKEN, ORGANIZER
+    ROW: TOKEN, ORGANIZER, ATTENDEE
 
     """
 
@@ -260,15 +271,42 @@
         path = os.path.join(path, MailGatewayTokensDatabase.dbFilename)
         super(MailGatewayTokensDatabase, self).__init__(path, True)
 
-    def createToken(self, organizer):
-        token = uuid.uuid4()
+        # TODO: generate random number for use in token creation
+
+    def createToken(self, organizer, attendee):
+        token = str(uuid.uuid4())
         self._db_execute(
             """
-            insert into TOKENS (TOKEN, ORGANIZER)
-            values (:1, :2)
-            """, token, organizer
+            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE)
+            values (:1, :2, :3)
+            """, token, organizer, attendee
         )
+        return token
 
+    def lookupByToken(self, token):
+        results = list(
+            self._db_execute(
+                """
+                select ORGANIZER, ATTENDEE from TOKENS
+                where TOKEN = :1
+                """, token
+            )
+        )
+
+        if len(results) != 1:
+            return None
+
+        return results[0]
+
+    def getToken(self, organizer, attendee):
+        token = self._db_value_for_sql(
+            """
+            select TOKEN from TOKENS
+            where ORGANIZER = :1 and ATTENDEE = :2
+            """, organizer, attendee
+        )
+        return token
+
     def deleteToken(self, token):
         self._db_execute(
             """
@@ -301,7 +339,8 @@
             """
             create table TOKENS (
                 TOKEN       text,
-                ORGANIZER   text
+                ORGANIZER   text,
+                ATTENDEE    text
             )
             """
         )
@@ -337,12 +376,14 @@
 
         multiService = service.MultiService()
 
+        mailer = MailHandler()
+
         for settings in config.MailGateway["Services"]:
             if settings["Enabled"]:
-                client = namedClass(settings["Service"])(settings)
+                client = namedClass(settings["Service"])(settings, mailer)
                 client.setServiceParent(multiService)
 
-        IScheduleService().setServiceParent(multiService)
+        IScheduleService(mailer).setServiceParent(multiService)
 
         return multiService
 
@@ -352,10 +393,11 @@
 #
 class IScheduleService(service.Service, LoggingMixIn):
 
-    def __init__(self): # TODO: settings
+    def __init__(self, mailer): # TODO: settings
+        self.mailer = mailer
         root = resource.Resource()
         root.putChild('', self.HomePage())
-        root.putChild('inbox', self.IScheduleInbox())
+        root.putChild('email-inbox', self.IScheduleInbox(mailer))
         self.site = server.Site(root)
         self.server = internet.TCPServer(62311, self.site)
 
@@ -377,6 +419,10 @@
 
     class IScheduleInbox(resource.Resource):
 
+        def __init__(self, mailer):
+            resource.Resource.__init__(self)
+            self.mailer = mailer
+
         def render_GET(self, request):
             return """
             <html>
@@ -386,29 +432,252 @@
             """
 
         def render_POST(self, request):
+            # Compute token, add to db, generate email and send it
+            calendar = ical.Component.fromString(request.content.read())
+            headers = request.getAllHeaders()
+            self.mailer.outbound(headers['originator'], headers['recipient'],
+                calendar)
+
+            # TODO: what to return?
             return """
             <html>
             <head><title>ISchedule Inbox</title></head>
-            <body>POSTED ISchedule Inbox</body>
+            <body>ISchedule Inbox</body>
             </html>
             """
 
+class MailHandler(LoggingMixIn):
 
+    def __init__(self):
+        self.db = MailGatewayTokensDatabase(config.DataRoot)
 
+    @inlineCallbacks
+    def inbound(self, token, calendar):
+        # process mail messages from POP or IMAP, inject to calendar server
+        result = self.db.lookupByToken(token)
+        if result is None:
+            # This isn't a token we recognize
+            self.error("Received a token I don't recognize: %s" % (token,))
+            return
+        organizer, attendee = result
+        organizer = str(organizer)
+        attendee = str(attendee)
+        calendar.getOrganizerProperty().setValue(organizer)
+        yield injectMessage(organizer, attendee, calendar)
+
+
+    @inlineCallbacks
+    def outbound(self, organizer, attendee, calendar):
+        # create token, send email
+        token = self.db.getToken(organizer, attendee)
+        if token is None:
+            token = self.db.createToken(organizer, attendee)
+
+        calendar.getOrganizerProperty().setValue("mailto:SERVER_ADDRESS+%s at example.com" % (token,))
+        # TODO: grab server email address from config
+
+        message = self._generateTemplateMessage(calendar)
+
+        # The email's From: will be the calendar server's address (without
+        # + addressing), while the Reply-To: will be the organizer's email
+        # address.
+        if not organizer.startswith("mailto:"):
+            raise ValueError("ORGANIZER address '%s' must be mailto: for iMIP operation." % (organizer,))
+        organizer = organizer[7:]
+        fromAddr = "SERVER_ADDRESS at example.com"
+        message = message.replace("${fromaddress}", fromAddr)
+        message = message.replace("${replytoaddress}", organizer)
+        
+        if not attendee.startswith("mailto:"):
+            raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (attendee,))
+        attendee = attendee[7:]
+        sendit = message.replace("${toaddress}", attendee)
+        yield sendmail('smtp.example.com', fromAddr, attendee, sendit, port=25)
+
+
+    def _generateTemplateMessage(self, calendar):
+
+        caldata = str(calendar)
+        data = cStringIO.StringIO()
+        writer = MimeWriter.MimeWriter(data)
+    
+        writer.addheader("From", "${fromaddress}")
+        writer.addheader("Reply-To", "${replytoaddress}")
+        writer.addheader("To", "${toaddress}")
+        writer.addheader("Date", rfc822date())
+        writer.addheader("Subject", "DO NOT REPLY: calendar invitation test")
+        writer.addheader("Message-ID", messageid())
+        writer.addheader("Mime-Version", "1.0")
+        writer.flushheaders()
+    
+        writer.startmultipartbody("mixed")
+    
+        # message body
+        part = writer.nextpart()
+        body = part.startbody("text/plain")
+        body.write("Hi, You've been invited to a cool event by CalendarServer's new iMIP processor.  %s " % (self._generateCalendarSummary(calendar),))
+    
+        part = writer.nextpart()
+        encoding = "7bit"
+        for i in caldata:
+            if ord(i) > 127:
+                encoding = "base64"
+                caldata = base64.encodestring(caldata)
+                break
+        part.addheader("Content-Transfer-Encoding", encoding)
+        body = part.startbody("text/calendar; charset=utf-8")
+        body.write(caldata.replace("\r", ""))
+    
+        # finish
+        writer.lastpart()
+
+        return data.getvalue()
+
+    def _generateCalendarSummary(self, calendar):
+
+        # Get the most appropriate component
+        component = calendar.masterComponent()
+        if component is None:
+            component = calendar.mainComponent(True)
+            
+        organizer = component.getOrganizerProperty()
+        if "CN" in organizer.params():
+            organizer = "%s <%s>" % (organizer.params()["CN"][0], organizer.value(),)
+        else:
+            organizer = organizer.value()
+            
+        dtinfo = self._getDateTimeInfo(component)
+        
+        summary = component.propertyValue("SUMMARY")
+        if summary is None:
+            summary = ""
+
+        description = component.propertyValue("DESCRIPTION")
+        if description is None:
+            description = ""
+
+        return """---- Begin Calendar Event Summary ----
+
+Organizer:   %s
+Summary:     %s
+%sDescription: %s
+
+----  End Calendar Event Summary  ----
+""" % (organizer, summary, dtinfo, description,)
+
+    def _getDateTimeInfo(self, component):
+        
+        dtstart = component.propertyNativeValue("DTSTART")
+        tzid_start = component.getProperty("DTSTART").params().get("TZID", "UTC")
+
+        dtend = component.propertyNativeValue("DTEND")
+        if dtend:
+            tzid_end = component.getProperty("DTEND").params().get("TZID", "UTC")
+            duration = dtend - dtstart
+        else:
+            duration = component.propertyNativeValue("DURATION")
+            if duration:
+                dtend = dtstart + duration
+                tzid_end = tzid_start
+            else:
+                if isinstance(dtstart, datetime.date):
+                    dtend = None
+                    duration = datetime.timedelta(days=1)
+                else:
+                    dtend = dtstart + datetime.timedelta(days=1)
+                    dtend.hour = dtend.minute = dtend.second = 0
+                    duration = dtend - dtstart
+        result = "Starts:      %s\n" % (self._getDateTimeText(dtstart, tzid_start),)
+        if dtend is not None:
+            result += "Ends:        %s\n" % (self._getDateTimeText(dtend, tzid_end),)
+        result += "Duration:    %s\n" % (self._getDurationText(duration),)
+        
+        if not isinstance(dtstart, datetime.datetime):
+            result += "All Day\n"
+        
+        for property_name in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",):
+            if component.hasProperty(property_name):
+                result += "Recurring\n"
+                break
+            
+        return result
+
+    def _getDateTimeText(self, dtvalue, tzid):
+        
+        if isinstance(dtvalue, datetime.datetime):
+            timeformat = "%A, %B %e, %Y %I:%M %p"
+        elif isinstance(dtvalue, datetime.date):
+            timeformat = "%A, %B %e, %Y"
+            tzid = ""
+        if tzid:
+            tzid = " (%s)" % (tzid,)
+
+        return "%s%s" % (dtvalue.strftime(timeformat), tzid,)
+        
+    def _getDurationText(self, duration):
+        
+        result = ""
+        if duration.days > 0:
+            result += "%d %s" % (
+                duration.days,
+                self._pluralize(duration.days, "day", "days")
+            )
+
+        hours = duration.seconds / 3600
+        minutes = divmod(duration.seconds / 60, 60)[1]
+        seconds = divmod(duration.seconds, 60)[1]
+        
+        if hours > 0:
+            if result:
+                result += ", "
+            result += "%d %s" % (
+                hours,
+                self._pluralize(hours, "hour", "hours")
+            )
+        
+        if minutes > 0:
+            if result:
+                result += ", "
+            result += "%d %s" % (
+                minutes,
+                self._pluralize(minutes, "minute", "minutes")
+            )
+        
+        if seconds > 0:
+            if result:
+                result += ", "
+            result += "%d %s" % (
+                seconds,
+                self._pluralize(seconds, "second", "seconds")
+            )
+
+        return result
+
+    def _pluralize(self, number, unit1, unitS):
+        return unit1 if number == 1 else unitS
+
+
+
+
+
+
 #
 # POP3
 #
 
 class POP3Service(service.Service, LoggingMixIn):
 
-    def __init__(self, settings):
+    def __init__(self, settings, mailer):
         if settings["UseSSL"]:
             self.client = internet.SSLClient(settings["Host"], settings["Port"],
-                POP3DownloadFactory(settings), ssl.ClientContextFactory())
+                POP3DownloadFactory(settings, mailer),
+                ssl.ClientContextFactory())
         else:
             self.client = internet.TCPClient(settings["Host"], settings["Port"],
-                POP3DownloadFactory(settings))
+                POP3DownloadFactory(settings, mailer))
 
+        self.mailer = mailer
+
     def startService(self):
         self.client.startService()
 
@@ -457,8 +726,9 @@
 class POP3DownloadFactory(protocol.ClientFactory, LoggingMixIn):
     protocol = POP3DownloadProtocol
 
-    def __init__(self, settings, reactor=None):
+    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
@@ -498,34 +768,56 @@
         self.log_info("POP factory handle message")
         self.log_info(message)
         parsedMessage = email.message_from_string(message)
-        # TODO: messages can be handed off here...
+
+        # TODO: make sure this is an email message we want to handle
+
+        # extract the token from the To header
+        name, addr = email.utils.parseaddr(parsedMessage['To'])
+        if addr:
+            # addr looks like: SERVER_ADDRESS+token at example.com
+            try:
+                pre, post = addr.split('@')
+                pre, token = pre.split('+')
+            except ValueError:
+                # TODO: handle this error
+                return
+        else:
+            # TODO: handle this error
+            return
+
         for part in parsedMessage.walk():
             if part.get_content_type() == "text/calendar":
                 calBody = part.get_payload(decode=True)
-                self.log_info(calBody)
-                calendar = ical.Component.fromString(calBody)
-                yield injectMessage(self.reactor, False, "localhost", 8008,
-                    "email-inbox", "mailto:ORGANIZER at HOST.NAME",
-                    "mailto:user01 at example.com", calendar)
+                break
+        else:
+            # TODO: handle this condition
+            # No icalendear attachment
+            return
 
+        self.log_info(calBody)
+        calendar = ical.Component.fromString(calBody)
+        yield self.mailer.inbound(token, calendar)
 
 
 
+
 #
 # IMAP4
 #
 
 class IMAP4Service(service.Service):
 
-    def __init__(self, settings):
+    def __init__(self, settings, mailer):
 
         if settings["UseSSL"]:
             self.client = internet.SSLClient(settings["Host"], settings["Port"],
-                IMAP4DownloadFactory(settings), ssl.ClientContextFactory())
+                IMAP4DownloadFactory(settings, mailer),
+                ssl.ClientContextFactory())
         else:
             self.client = internet.TCPClient(settings["Host"], settings["Port"],
-                IMAP4DownloadFactory(settings))
+                IMAP4DownloadFactory(settings, mailer))
 
+        self.mailer = mailer
 
     def startService(self):
         self.client.startService()
@@ -615,10 +907,11 @@
 class IMAP4DownloadFactory(protocol.ClientFactory, LoggingMixIn):
     protocol = IMAP4DownloadProtocol
 
-    def __init__(self, settings, reactor=None):
+    def __init__(self, settings, mailer, reactor=None):
         self.log_info("Setting up IMAPFactory")
 
         self.settings = settings
+        self.mailer = mailer
         if reactor is None:
             from twisted.internet import reactor
         self.reactor = reactor
@@ -658,230 +951,3 @@
         self.connector = connector
         self.log_info("IMAP factory connection failed")
         self.retry(connector)
-
-
-
-
-"""
-class ScheduleViaIMip(DeliveryService):
-    
-    @classmethod
-    def serviceType(cls):
-        return DeliveryService.serviceType_imip
-
-    @inlineCallbacks
-    def generateSchedulingResponses(self):
-        
-        # Generate an HTTP client request
-        try:
-            # We do not do freebusy requests via iMIP
-            if self.freebusy:
-                raise ValueError("iMIP VFREEBUSY REQUESTs not supported.")
-
-            # Copy the ical component because we need to substitute the
-            # calendar server's sepcial email address for ORGANIZER
-            itip = self.scheduler.calendar.duplicate( )
-            itip.getOrganizerProperty().setValue("mailto:SERVER_ADDRESS+TOKEN at HOST.NAME")
-            # TODO: generate +address token
-            # TODO: grab server email address from config
-
-            message = self._generateTemplateMessage(itip)
-
-            # The email's From: will be the calendar server's address (without
-            # + addressing), while the Reply-To: will be the organizer's email
-            # address.
-            cuAddr = self.scheduler.originator.cuaddr
-            if not cuAddr.startswith("mailto:"):
-                raise ValueError("ORGANIZER address '%s' must be mailto: for iMIP operation." % (fromAddr,))
-            cuAddr = cuAddr[7:]
-            fromAddr = "SERVER_ADDRESS at HOST.NAME"
-            message = message.replace("${fromaddress}", fromAddr)
-            message = message.replace("${replytoaddress}", cuAddr)
-            
-            for recipient in self.recipients:
-                try:
-                    toAddr = recipient.cuaddr
-                    if not toAddr.startswith("mailto:"):
-                        raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (toAddr,))
-                    toAddr = toAddr[7:]
-                    sendit = message.replace("${toaddress}", toAddr)
-                    log.debug("Sending iMIP message To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, sendit,))
-                    yield sendmail(config.Scheduling[self.serviceType()]["Sending"]["Server"], fromAddr, toAddr, sendit, port=config.Scheduling[self.serviceType()]["Sending"]["Port"])
-        
-                except Exception, e:
-                    # Generated failed response for this recipient
-                    log.err("Could not do server-to-imip request : %s %s" % (self, e))
-                    err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
-                    self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
-                
-                else:
-                    self.responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
-
-        except Exception, e:
-            # Generated failed responses for each recipient
-            log.err("Could not do server-to-imip request : %s %s" % (self, e))
-            for recipient in self.recipients:
-                err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
-                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
-
-    def _generateTemplateMessage(self, calendar):
-
-        caldata = str(calendar)
-        data = cStringIO.StringIO()
-        writer = MimeWriter.MimeWriter(data)
-    
-        writer.addheader("From", "${fromaddress}")
-        writer.addheader("Reply-To", "${replytoaddress}")
-        writer.addheader("To", "${toaddress}")
-        writer.addheader("Date", rfc822date())
-        writer.addheader("Subject", "DO NOT REPLY: calendar invitation test")
-        writer.addheader("Message-ID", messageid())
-        writer.addheader("Mime-Version", "1.0")
-        writer.flushheaders()
-    
-        writer.startmultipartbody("mixed")
-    
-        # message body
-        part = writer.nextpart()
-        body = part.startbody("text/plain")
-        body.write("Hi, You've been invited to a cool event by CalendarServer's new iMIP processor.  %s " % (self._generateCalendarSummary(calendar),))
-    
-        part = writer.nextpart()
-        encoding = "7bit"
-        for i in caldata:
-            if ord(i) > 127:
-                encoding = "base64"
-                caldata = base64.encodestring(caldata)
-                break
-        part.addheader("Content-Transfer-Encoding", encoding)
-        body = part.startbody("text/calendar; charset=utf-8")
-        body.write(caldata.replace("\r", ""))
-    
-        # finish
-        writer.lastpart()
-
-        return data.getvalue()
-
-    def _generateCalendarSummary(self, calendar):
-
-        # Get the most appropriate component
-        component = calendar.masterComponent()
-        if component is None:
-            component = calendar.mainComponent(True)
-            
-        organizer = component.getOrganizerProperty()
-        if "CN" in organizer.params():
-            organizer = "%s <%s>" % (organizer.params()["CN"][0], organizer.value(),)
-        else:
-            organizer = organizer.value()
-            
-        dtinfo = self._getDateTimeInfo(component)
-        
-        summary = component.propertyValue("SUMMARY")
-        if summary is None:
-            summary = ""
-
-        description = component.propertyValue("DESCRIPTION")
-        if description is None:
-            description = ""
-
-        return "---- Begin Calendar Event Summary ----
-
-Organizer:   %s
-Summary:     %s
-%sDescription: %s
-
-----  End Calendar Event Summary  ----
-" % (organizer, summary, dtinfo, description,)
-
-    def _getDateTimeInfo(self, component):
-        
-        dtstart = component.propertyNativeValue("DTSTART")
-        tzid_start = component.getProperty("DTSTART").params().get("TZID", "UTC")
-
-        dtend = component.propertyNativeValue("DTEND")
-        if dtend:
-            tzid_end = component.getProperty("DTEND").params().get("TZID", "UTC")
-            duration = dtend - dtstart
-        else:
-            duration = component.propertyNativeValue("DURATION")
-            if duration:
-                dtend = dtstart + duration
-                tzid_end = tzid_start
-            else:
-                if isinstance(dtstart, datetime.date):
-                    dtend = None
-                    duration = datetime.timedelta(days=1)
-                else:
-                    dtend = dtstart + datetime.timedelta(days=1)
-                    dtend.hour = dtend.minute = dtend.second = 0
-                    duration = dtend - dtstart
-        result = "Starts:      %s\n" % (self._getDateTimeText(dtstart, tzid_start),)
-        if dtend is not None:
-            result += "Ends:        %s\n" % (self._getDateTimeText(dtend, tzid_end),)
-        result += "Duration:    %s\n" % (self._getDurationText(duration),)
-        
-        if not isinstance(dtstart, datetime.datetime):
-            result += "All Day\n"
-        
-        for property_name in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",):
-            if component.hasProperty(property_name):
-                result += "Recurring\n"
-                break
-            
-        return result
-
-    def _getDateTimeText(self, dtvalue, tzid):
-        
-        if isinstance(dtvalue, datetime.datetime):
-            timeformat = "%A, %B %e, %Y %I:%M %p"
-        elif isinstance(dtvalue, datetime.date):
-            timeformat = "%A, %B %e, %Y"
-            tzid = ""
-        if tzid:
-            tzid = " (%s)" % (tzid,)
-
-        return "%s%s" % (dtvalue.strftime(timeformat), tzid,)
-        
-    def _getDurationText(self, duration):
-        
-        result = ""
-        if duration.days > 0:
-            result += "%d %s" % (
-                duration.days,
-                self._pluralize(duration.days, "day", "days")
-            )
-
-        hours = duration.seconds / 3600
-        minutes = divmod(duration.seconds / 60, 60)[1]
-        seconds = divmod(duration.seconds, 60)[1]
-        
-        if hours > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                hours,
-                self._pluralize(hours, "hour", "hours")
-            )
-        
-        if minutes > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                minutes,
-                self._pluralize(minutes, "minute", "minutes")
-            )
-        
-        if seconds > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                seconds,
-                self._pluralize(seconds, "second", "seconds")
-            )
-
-        return result
-
-    def _pluralize(self, number, unit1, unitS):
-        return unit1 if number == 1 else unitS
-"""

Modified: CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/scheduling/imip.py
===================================================================
--- CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/scheduling/imip.py	2008-08-01 20:47:47 UTC (rev 2767)
+++ CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/scheduling/imip.py	2008-08-05 00:49:35 UTC (rev 2768)
@@ -25,6 +25,7 @@
 from twisted.web2 import responsecode
 from twisted.web2.dav.http import ErrorResponse
 from twisted.web2.http import HTTPError
+from twisted.web import client
 
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
@@ -61,35 +62,18 @@
             if self.freebusy:
                 raise ValueError("iMIP VFREEBUSY REQUESTs not supported.")
 
-            # Copy the ical component because we need to substitute the
-            # calendar server's sepcial email address for ORGANIZER
-            itip = self.scheduler.calendar.duplicate( )
-            itip.getOrganizerProperty().setValue("mailto:SERVER_ADDRESS+TOKEN at HOST.NAME")
-            # TODO: generate +address token
-            # TODO: grab server email address from config
+            caldata = str(self.scheduler.calendar)
 
-            message = self._generateTemplateMessage(itip)
-
-            # The email's From: will be the calendar server's address (without
-            # + addressing), while the Reply-To: will be the organizer's email
-            # address.
-            cuAddr = self.scheduler.originator.cuaddr
-            if not cuAddr.startswith("mailto:"):
-                raise ValueError("ORGANIZER address '%s' must be mailto: for iMIP operation." % (fromAddr,))
-            cuAddr = cuAddr[7:]
-            fromAddr = "SERVER_ADDRESS at HOST.NAME"
-            message = message.replace("${fromaddress}", fromAddr)
-            message = message.replace("${replytoaddress}", cuAddr)
-            
             for recipient in self.recipients:
                 try:
-                    toAddr = recipient.cuaddr
+                    toAddr = str(recipient.cuaddr)
                     if not toAddr.startswith("mailto:"):
                         raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (toAddr,))
-                    toAddr = toAddr[7:]
-                    sendit = message.replace("${toaddress}", toAddr)
-                    log.debug("Sending iMIP message To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, sendit,))
-                    yield sendmail(config.Scheduling[self.serviceType()]["Sending"]["Server"], fromAddr, toAddr, sendit, port=config.Scheduling[self.serviceType()]["Sending"]["Port"])
+
+                    fromAddr = str(self.scheduler.calendar.getOrganizer())
+
+                    log.debug("POSTing iMIP message to gateway...  To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, caldata,))
+                    yield self.postToGateway(fromAddr, toAddr, caldata)
         
                 except Exception, e:
                     # Generated failed response for this recipient
@@ -107,167 +91,18 @@
                 err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
                 self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
 
-    def _generateTemplateMessage(self, calendar):
+    def postToGateway(self, fromAddr, toAddr, caldata, reactor=None):
+        if reactor is None:
+            from twisted.internet import reactor
 
-        caldata = str(calendar)
-        data = cStringIO.StringIO()
-        writer = MimeWriter.MimeWriter(data)
-    
-        writer.addheader("From", "${fromaddress}")
-        writer.addheader("Reply-To", "${replytoaddress}")
-        writer.addheader("To", "${toaddress}")
-        writer.addheader("Date", rfc822date())
-        writer.addheader("Subject", "DO NOT REPLY: calendar invitation test")
-        writer.addheader("Message-ID", messageid())
-        writer.addheader("Mime-Version", "1.0")
-        writer.flushheaders()
-    
-        writer.startmultipartbody("mixed")
-    
-        # message body
-        part = writer.nextpart()
-        body = part.startbody("text/plain")
-        body.write("""Hi,
-You've been invited to a cool event by CalendarServer's new iMIP processor.
+        url = "http://localhost:62311/email-inbox"
+        headers = {
+            'Content-Type' : 'text/calendar',
+            'Originator' : fromAddr,
+            'Recipient' : toAddr,
+        }
+        factory = client.HTTPClientFactory(url, method='POST', headers=headers,
+            postdata=caldata)
+        reactor.connectTCP("localhost", 62311, factory)
+        return factory.deferred
 
-%s
-""" % (self._generateCalendarSummary(calendar),))
-    
-        part = writer.nextpart()
-        encoding = "7bit"
-        for i in caldata:
-            if ord(i) > 127:
-                encoding = "base64"
-                caldata = base64.encodestring(caldata)
-                break
-        part.addheader("Content-Transfer-Encoding", encoding)
-        body = part.startbody("text/calendar; charset=utf-8")
-        body.write(caldata.replace("\r", ""))
-    
-        # finish
-        writer.lastpart()
-
-        return data.getvalue()
-
-    def _generateCalendarSummary(self, calendar):
-
-        # Get the most appropriate component
-        component = calendar.masterComponent()
-        if component is None:
-            component = calendar.mainComponent(True)
-            
-        organizer = component.getOrganizerProperty()
-        if "CN" in organizer.params():
-            organizer = "%s <%s>" % (organizer.params()["CN"][0], organizer.value(),)
-        else:
-            organizer = organizer.value()
-            
-        dtinfo = self._getDateTimeInfo(component)
-        
-        summary = component.propertyValue("SUMMARY")
-        if summary is None:
-            summary = ""
-
-        description = component.propertyValue("DESCRIPTION")
-        if description is None:
-            description = ""
-
-        return """---- Begin Calendar Event Summary ----
-
-Organizer:   %s
-Summary:     %s
-%sDescription: %s
-
-----  End Calendar Event Summary  ----
-""" % (organizer, summary, dtinfo, description,)
-
-    def _getDateTimeInfo(self, component):
-        
-        dtstart = component.propertyNativeValue("DTSTART")
-        tzid_start = component.getProperty("DTSTART").params().get("TZID", "UTC")
-
-        dtend = component.propertyNativeValue("DTEND")
-        if dtend:
-            tzid_end = component.getProperty("DTEND").params().get("TZID", "UTC")
-            duration = dtend - dtstart
-        else:
-            duration = component.propertyNativeValue("DURATION")
-            if duration:
-                dtend = dtstart + duration
-                tzid_end = tzid_start
-            else:
-                if isinstance(dtstart, datetime.date):
-                    dtend = None
-                    duration = datetime.timedelta(days=1)
-                else:
-                    dtend = dtstart + datetime.timedelta(days=1)
-                    dtend.hour = dtend.minute = dtend.second = 0
-                    duration = dtend - dtstart
-        result = "Starts:      %s\n" % (self._getDateTimeText(dtstart, tzid_start),)
-        if dtend is not None:
-            result += "Ends:        %s\n" % (self._getDateTimeText(dtend, tzid_end),)
-        result += "Duration:    %s\n" % (self._getDurationText(duration),)
-        
-        if not isinstance(dtstart, datetime.datetime):
-            result += "All Day\n"
-        
-        for property_name in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",):
-            if component.hasProperty(property_name):
-                result += "Recurring\n"
-                break
-            
-        return result
-
-    def _getDateTimeText(self, dtvalue, tzid):
-        
-        if isinstance(dtvalue, datetime.datetime):
-            timeformat = "%A, %B %e, %Y %I:%M %p"
-        elif isinstance(dtvalue, datetime.date):
-            timeformat = "%A, %B %e, %Y"
-            tzid = ""
-        if tzid:
-            tzid = " (%s)" % (tzid,)
-
-        return "%s%s" % (dtvalue.strftime(timeformat), tzid,)
-        
-    def _getDurationText(self, duration):
-        
-        result = ""
-        if duration.days > 0:
-            result += "%d %s" % (
-                duration.days,
-                self._pluralize(duration.days, "day", "days")
-            )
-
-        hours = duration.seconds / 3600
-        minutes = divmod(duration.seconds / 60, 60)[1]
-        seconds = divmod(duration.seconds, 60)[1]
-        
-        if hours > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                hours,
-                self._pluralize(hours, "hour", "hours")
-            )
-        
-        if minutes > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                minutes,
-                self._pluralize(minutes, "minute", "minutes")
-            )
-        
-        if seconds > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                seconds,
-                self._pluralize(seconds, "second", "seconds")
-            )
-
-        return result
-
-    def _pluralize(self, number, unit1, unitS):
-        return unit1 if number == 1 else unitS

Modified: CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/tap.py	2008-08-01 20:47:47 UTC (rev 2767)
+++ CalendarServer/branches/users/sagen/mailgateway-implicit-2745/twistedcaldav/tap.py	2008-08-05 00:49:35 UTC (rev 2768)
@@ -542,7 +542,7 @@
         # IMIP delivery resource
         #
         imipInbox = self.imipResourceClass(
-            os.path.join(config.DocumentRoot, 'imip-inbox'),
+            os.path.join(config.DocumentRoot, 'email-inbox'),
             root,
         )
         root.putChild('email-inbox', imipInbox)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080804/26cc43a3/attachment-0001.html 


More information about the calendarserver-changes mailing list