[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