Пример #1
0
    def _transferAttendees(self, old_comp, new_comp, ignore_attendee_value):
        """
        Transfer Attendee PARTSTAT from old component to new component.

        @param old_comp: existing server calendar component
        @type old_comp: L{Component}
        @param new_comp: new calendar component
        @type new_comp: L{Component}
        @param ignore_attendee_value: Attendee to ignore
        @type ignore_attendee_value: C{str}
        """

        # Create map of ATTENDEEs in old component
        old_attendees = {}
        for attendee in old_comp.properties("ATTENDEE"):
            value = normalizeCUAddr(attendee.value())
            if value == ignore_attendee_value:
                continue
            old_attendees[value] = attendee

        for new_attendee in new_comp.properties("ATTENDEE"):

            # Whenever SCHEDULE-FORCE-SEND is explicitly set by the Organizer we assume the Organizer
            # is deliberately overwriting PARTSTAT
            if new_attendee.parameterValue("SCHEDULE-FORCE-SEND", "") == "REQUEST":
                continue

            # Transfer parameters from any old Attendees found
            value = normalizeCUAddr(new_attendee.value())
            old_attendee = old_attendees.get(value)
            if old_attendee:
                self._transferParameter(old_attendee, new_attendee, "PARTSTAT")
                self._transferParameter(old_attendee, new_attendee, "RSVP")
                self._transferParameter(old_attendee, new_attendee, "SCHEDULE-STATUS")
Пример #2
0
    def _transferAttendees(self, old_comp, new_comp, ignore_attendee_value):
        """
        Transfer Attendee PARTSTAT from old component to new component.

        @param old_comp: existing server calendar component
        @type old_comp: L{Component}
        @param new_comp: new calendar component
        @type new_comp: L{Component}
        @param ignore_attendee_value: Attendee to ignore
        @type ignore_attendee_value: C{str}
        """

        # Create map of ATTENDEEs in old component
        old_attendees = {}
        for attendee in old_comp.properties("ATTENDEE"):
            value = normalizeCUAddr(attendee.value())
            if value == ignore_attendee_value:
                continue
            old_attendees[value] = attendee

        for new_attendee in new_comp.properties("ATTENDEE"):

            # Whenever SCHEDULE-FORCE-SEND is explicitly set by the Organizer we assume the Organizer
            # is deliberately overwriting PARTSTAT
            if new_attendee.parameterValue("SCHEDULE-FORCE-SEND", "") == "REQUEST":
                continue

            # Transfer parameters from any old Attendees found
            value = normalizeCUAddr(new_attendee.value())
            old_attendee = old_attendees.get(value)
            if old_attendee:
                self._transferParameter(old_attendee, new_attendee, "PARTSTAT")
                self._transferParameter(old_attendee, new_attendee, "RSVP")
                self._transferParameter(old_attendee, new_attendee, "SCHEDULE-STATUS")
Пример #3
0
    def doImplicitAttendeeUpdate(self):
        """
        An iTIP message has been sent by to an attendee by the organizer. We need to update the attendee state
        based on the nature of the iTIP message.
        """

        # Do security check: ORGANZIER in iTIP MUST match existing resource value
        if self.recipient_calendar:
            existing_organizer = self.recipient_calendar.getOrganizer()
            existing_organizer = normalizeCUAddr(existing_organizer) if existing_organizer else ""
            new_organizer = normalizeCUAddr(self.message.getOrganizer())
            new_organizer = normalizeCUAddr(new_organizer) if new_organizer else ""
            if existing_organizer != new_organizer:
                # Additional check - if the existing organizer is missing and the originator
                # is local to the server - then allow the change
                if not (existing_organizer == "" and self.originator.hosted()):
                    log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                    raise ImplicitProcessorException("5.3;Organizer change not allowed")

        # Handle splitting of data early so we can preserve per-attendee data
        if self.message.hasProperty("X-CALENDARSERVER-SPLIT-OLDER-UID"):
            if config.Scheduling.Options.Splitting.Enabled:
                # Tell the existing resource to split
                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' splitting UID: '%s'" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                split = (yield self.doImplicitAttendeeSplit())
                if split:
                    returnValue((True, False, False, None,))
            else:
                self.message.removeProperty("X-CALENDARSERVER-SPLIT-OLDER-UID")
                self.message.removeProperty("X-CALENDARSERVER-SPLIT-RID")

        elif self.message.hasProperty("X-CALENDARSERVER-SPLIT-NEWER-UID"):
            if config.Scheduling.Options.Splitting.Enabled:
                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - split already done" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                returnValue((True, False, False, None,))
            else:
                self.message.removeProperty("X-CALENDARSERVER-SPLIT-OLDER-UID")
                self.message.removeProperty("X-CALENDARSERVER-SPLIT-RID")

        # Different based on method
        if self.method == "REQUEST":
            result = (yield self.doImplicitAttendeeRequest())
        elif self.method == "CANCEL":
            result = (yield self.doImplicitAttendeeCancel())
        elif self.method == "ADD":
            # TODO: implement ADD
            result = (False, False, False, None)
        else:
            # NB We should never get here as we will have rejected unsupported METHODs earlier.
            result = (True, True, False, None,)

        returnValue(result)
Пример #4
0
def cuAddressConverter(origCUAddr):
    """ Converts calendar user addresses to OD-compatible form """

    cua = normalizeCUAddr(origCUAddr)

    if cua.startswith("urn:x-uid:"):
        return "uid", cua[10:]

    elif cua.startswith("urn:uuid:"):
        return "guid", uuid.UUID(cua[9:])

    elif cua.startswith("mailto:"):
        return "emailAddresses", cua[7:]

    elif cua.startswith("/") or cua.startswith("http"):
        ignored, collection, id = cua.rsplit("/", 2)
        if collection == "__uids__":
            return "uid", id
        else:
            return "recordName", id

    else:
        raise ValueError(
            "Invalid calendar user address format: %s" %
            (origCUAddr,)
        )
Пример #5
0
 def _organizerMerge(self):
     """
     Merge changes to ATTENDEE properties in oldcalendar into newcalendar.
     """
     organizer = normalizeCUAddr(
         self.newcalendar.masterComponent().propertyValue("ORGANIZER"))
     self._doSmartMerge(organizer, True)
Пример #6
0
    def recordWithCalendarUserAddress(self, address, timeoutSeconds=None):
        address = normalizeCUAddr(address)
        record = None

        if config.Scheduling.Options.FakeResourceLocationEmail:
            if address.startswith("mailto:") and address.endswith(
                    "@do_not_reply"):
                try:
                    address = "urn:x-uid:{}".format(
                        address[7:-13].decode("hex"))
                except Exception:
                    try:
                        address = "urn:uuid:{}".format(address[7:-13])
                    except Exception as e:
                        log.error(
                            "Invalid @do_not_reply cu-address: '{address}' {exc}",
                            address=address,
                            exc=e)
                        address = ""

        if address.startswith("urn:x-uid:"):
            uid = address[10:]
            record = yield self.recordWithUID(uid,
                                              timeoutSeconds=timeoutSeconds)

        elif address.startswith("urn:uuid:"):
            try:
                guid = uuid.UUID(address[9:])
            except ValueError:
                log.info("Invalid GUID: {guid}", guid=address[9:])
                returnValue(None)
            record = yield self.recordWithGUID(guid,
                                               timeoutSeconds=timeoutSeconds)

        elif address.startswith("mailto:"):
            records = yield self.recordsWithEmailAddress(
                address[7:], limitResults=1, timeoutSeconds=timeoutSeconds)
            record = records[0] if records else None

        elif address.startswith("/principals/"):
            parts = address.split("/")
            if len(parts) == 4:
                if parts[2] == "__uids__":
                    uid = parts[3]
                    record = yield self.recordWithUID(
                        uid, timeoutSeconds=timeoutSeconds)
                else:
                    recordType = self.oldNameToRecordType(parts[2])
                    record = yield self.recordWithShortName(
                        recordType, parts[3], timeoutSeconds=timeoutSeconds)

        if record:
            if record.hasCalendars or (config.GroupAttendees.Enabled
                                       and record.recordType
                                       == BaseRecordType.group):
                returnValue(record)

        returnValue(None)
Пример #7
0
def convertCUAsToMailto(comp):
    """
    Replace non-mailto: CUAs with mailto: CUAs where possible (i.e. there is an
    EMAIL parameter value attached)
    """
    for attendeeProp in itertools.chain(comp.getAllAttendeeProperties(), [comp.getOrganizerProperty()]):
        cuaddr = normalizeCUAddr(attendeeProp.value())
        if not cuaddr.startswith("mailto:"):
            emailAddress = attendeeProp.parameterValue("EMAIL", None)
            if emailAddress:
                attendeeProp.setValue("mailto:%s" % (emailAddress,))
                attendeeProp.removeParameter("EMAIL")
Пример #8
0
    def recordWithCalendarUserAddress(
        self, address, timeoutSeconds=None
    ):
        address = normalizeCUAddr(address)
        record = None

        if address.startswith("urn:x-uid:"):
            uid = address[10:]
            record = yield self.recordWithUID(
                uid, timeoutSeconds=timeoutSeconds
            )

        elif address.startswith("urn:uuid:"):
            try:
                guid = uuid.UUID(address[9:])
            except ValueError:
                log.info("Invalid GUID: {guid}", guid=address[9:])
                returnValue(None)
            record = yield self.recordWithGUID(
                guid, timeoutSeconds=timeoutSeconds
            )

        elif address.startswith("mailto:"):
            records = yield self.recordsWithEmailAddress(
                address[7:], limitResults=1, timeoutSeconds=timeoutSeconds
            )
            record = records[0] if records else None

        elif address.startswith("/principals/"):
            parts = address.split("/")
            if len(parts) == 4:
                if parts[2] == "__uids__":
                    uid = parts[3]
                    record = yield self.recordWithUID(
                        uid, timeoutSeconds=timeoutSeconds
                    )
                else:
                    recordType = self.oldNameToRecordType(parts[2])
                    record = yield self.recordWithShortName(
                        recordType, parts[3], timeoutSeconds=timeoutSeconds
                    )

        if record:
            if record.hasCalendars or (
                config.GroupAttendees.Enabled and
                record.recordType == BaseRecordType.group
            ):
                returnValue(record)

        returnValue(None)
Пример #9
0
    def recordWithCalendarUserAddress(self, address, timeoutSeconds=None):
        address = normalizeCUAddr(address)
        record = None

        if address.startswith("urn:x-uid:"):
            uid = address[10:]
            record = yield self.recordWithUID(uid,
                                              timeoutSeconds=timeoutSeconds)

        elif address.startswith("urn:uuid:"):
            try:
                guid = uuid.UUID(address[9:])
            except ValueError:
                log.info("Invalid GUID: {guid}", guid=address[9:])
                returnValue(None)
            record = yield self.recordWithGUID(guid,
                                               timeoutSeconds=timeoutSeconds)

        elif address.startswith("mailto:"):
            records = yield self.recordsWithEmailAddress(
                address[7:], limitResults=1, timeoutSeconds=timeoutSeconds)
            record = records[0] if records else None

        elif address.startswith("/principals/"):
            parts = address.split("/")
            if len(parts) == 4:
                if parts[2] == "__uids__":
                    uid = parts[3]
                    record = yield self.recordWithUID(
                        uid, timeoutSeconds=timeoutSeconds)
                else:
                    recordType = self.oldNameToRecordType(parts[2])
                    record = yield self.recordWithShortName(
                        recordType, parts[3], timeoutSeconds=timeoutSeconds)

        if record:
            if record.hasCalendars or (config.GroupAttendees.Enabled
                                       and record.recordType
                                       == BaseRecordType.group):
                returnValue(record)

        returnValue(None)
Пример #10
0
def cuAddressConverter(origCUAddr):
    """ Converts calendar user addresses to OD-compatible form """

    cua = normalizeCUAddr(origCUAddr)

    if cua.startswith("urn:x-uid:"):
        return "uid", cua[10:]

    elif cua.startswith("urn:uuid:"):
        return "guid", uuid.UUID(cua[9:])

    elif cua.startswith("mailto:"):
        return "emailAddresses", cua[7:]

    elif cua.startswith("/") or cua.startswith("http"):
        ignored, collection, id = cua.rsplit("/", 2)
        if collection == "__uids__":
            return "uid", id
        else:
            return "recordName", id

    else:
        raise ValueError("Invalid calendar user address format: %s" %
                         (origCUAddr, ))
Пример #11
0
    def outbound(self, txn, originator, recipient, calendar, onlyAfter=None):
        """
        Generates and sends an outbound IMIP message.

        @param txn: the transaction to use for looking up/creating tokens
        @type txn: L{CommonStoreTransaction}
        """

        if onlyAfter is None:
            duration = Duration(days=self.suppressionDays)
            onlyAfter = DateTime.getNowUTC() - duration

        icaluid = calendar.resourceUID()
        method = calendar.propertyValue("METHOD")

        # Clean up the attendee list which is purely used within the human
        # readable email message (not modifying the calendar body)
        attendees = []
        for attendeeProp in calendar.getAllAttendeeProperties():
            cutype = attendeeProp.parameterValue("CUTYPE", "INDIVIDUAL")
            if cutype == "INDIVIDUAL":
                cn = attendeeProp.parameterValue("CN", None)
                if cn is not None:
                    cn = cn.decode("utf-8")
                cuaddr = normalizeCUAddr(attendeeProp.value())
                if cuaddr.startswith("mailto:"):
                    mailto = cuaddr[7:]
                    if not cn:
                        cn = mailto
                else:
                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
                    if emailAddress:
                        mailto = emailAddress
                    else:
                        mailto = None

                if cn or mailto:
                    attendees.append((cn, mailto))

        toAddr = recipient
        if not recipient.lower().startswith("mailto:"):
            raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP "
                             "operation." % (recipient,))
        recipient = recipient[7:]

        if method != "REPLY":
            # Invites and cancellations:

            # Reuse or generate a token based on originator, toAddr, and
            # event uid
            record = (yield txn.imipGetToken(originator, toAddr.lower(), icaluid))
            if record is None:

                # Because in the past the originator was sometimes in mailto:
                # form, lookup an existing token by mailto: as well
                organizerProperty = calendar.getOrganizerProperty()
                organizerEmailAddress = organizerProperty.parameterValue("EMAIL", None)
                if organizerEmailAddress is not None:
                    record = (yield txn.imipGetToken("mailto:%s" % (organizerEmailAddress.lower(),), toAddr.lower(), icaluid))

            if record is None:
                record = (yield txn.imipCreateToken(originator, toAddr.lower(), icaluid))
                self.log.debug("Mail gateway created token %s for %s "
                               "(originator), %s (recipient) and %s (icaluid)"
                               % (record.token, originator, toAddr, icaluid))
                inviteState = "new"

            else:
                self.log.debug("Mail gateway reusing token %s for %s "
                               "(originator), %s (recipient) and %s (icaluid)"
                               % (record.token, originator, toAddr, icaluid))
                inviteState = "update"
            token = record.token

            fullServerAddress = self.address
            _ignore_name, serverAddress = email.utils.parseaddr(fullServerAddress)
            pre, post = serverAddress.split('@')
            addressWithToken = "%s+%s@%s" % (pre, token, post)

            organizerProperty = calendar.getOrganizerProperty()
            organizerEmailAddress = organizerProperty.parameterValue("EMAIL",
                                                                     None)
            organizerValue = organizerProperty.value()
            organizerProperty.setValue("mailto:%s" % (addressWithToken,))

            # If the organizer is also an attendee, update that attendee value
            # to match
            organizerAttendeeProperty = calendar.getAttendeeProperty(
                [organizerValue])
            if organizerAttendeeProperty is not None:
                organizerAttendeeProperty.setValue("mailto:%s" %
                                                   (addressWithToken,))

            # The email's From will include the originator's real name email
            # address if available.  Otherwise it will be the server's email
            # address (without # + addressing)
            if organizerEmailAddress:
                orgEmail = fromAddr = organizerEmailAddress
            else:
                fromAddr = serverAddress
                orgEmail = None
            cn = calendar.getOrganizerProperty().parameterValue('CN', None)
            if cn is None:
                cn = u'Calendar Server'
                orgCN = orgEmail
            else:
                orgCN = cn = cn.decode("utf-8")

            # a unicode cn (rather than an encode string value) means the
            # from address will get properly encoded per rfc2047 within the
            # MIMEMultipart in generateEmail
            formattedFrom = "%s <%s>" % (cn, fromAddr)

            # Reply-to address will be the server+token address

        else: # REPLY
            inviteState = "reply"

            # Look up the attendee property corresponding to the originator
            # of this reply
            originatorAttendeeProperty = calendar.getAttendeeProperty(
                [originator])
            formattedFrom = fromAddr = originator = ""
            if originatorAttendeeProperty:
                originatorAttendeeEmailAddress = (
                    originatorAttendeeProperty.parameterValue("EMAIL", None)
                )
                if originatorAttendeeEmailAddress:
                    formattedFrom = fromAddr = originator = (
                        originatorAttendeeEmailAddress
                    )

            organizerMailto = str(calendar.getOrganizer())
            if not organizerMailto.lower().startswith("mailto:"):
                raise ValueError("ORGANIZER address '%s' must be mailto: "
                                 "for REPLY." % (organizerMailto,))
            orgEmail = organizerMailto[7:]

            orgCN = calendar.getOrganizerProperty().parameterValue('CN', None)
            if orgCN:
                orgCN = orgCN.decode("utf-8")
            addressWithToken = formattedFrom

        # At the point we've created the token in the db, which we always
        # want to do, but if this message is for an event completely in
        # the past we don't want to actually send an email.
        if not calendar.hasInstancesAfter(onlyAfter):
            self.log.debug("Skipping IMIP message for old event")
            returnValue(True)

        # Now prevent any "internal" CUAs from being exposed by converting
        # to mailto: if we have one
        for attendeeProp in calendar.getAllAttendeeProperties():
            cutype = attendeeProp.parameterValue('CUTYPE', None)
            if cutype == "INDIVIDUAL":
                cuaddr = normalizeCUAddr(attendeeProp.value())
                if not cuaddr.startswith("mailto:"):
                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
                    if emailAddress:
                        attendeeProp.setValue("mailto:%s" % (emailAddress,))

        msgId, message = self.generateEmail(
            inviteState, calendar, orgEmail,
            orgCN, attendees, formattedFrom, addressWithToken, recipient,
            language=self.language)

        try:
            success = (yield self.smtpSender.sendMessage(
                fromAddr, toAddr, msgId, message))
            returnValue(success)
        except Exception, e:
            self.log.error("Failed to send IMIP message (%s)" % (str(e),))
            returnValue(False)
Пример #12
0
    def doImplicitAttendeeCancel(self):
        """
        An iTIP CANCEL message has been sent to an attendee. If there is no existing resource, we will simply
        ignore the message. If there is an existing resource we need to reconcile the changes between it and the
        iTIP message.

        @return: C{tuple} of (processed, auto-processed, store inbox item, changes)
        """

        # If there is no existing copy, then ignore
        if self.recipient_calendar is None:
            log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:CANCEL, UID: '%s' - attendee has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
            result = (True, True, True, None)
        else:
            # Need to check for auto-respond attendees. These need to suppress the inbox message
            # if the cancel is processed. However, if the principal is a user we always force the
            # inbox item on them even if auto-schedule is true so that they get a notification
            # of the cancel.
            organizer = normalizeCUAddr(self.message.getOrganizer())
            autoprocessed = yield self.recipient.record.canAutoSchedule(organizer=organizer)
            store_inbox = not autoprocessed or self.recipient.record.getCUType() == "INDIVIDUAL"

            # Check to see if this is a cancel of the entire event
            processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar, autoprocessing=autoprocessed)
            if processed_message:
                if autoprocessed and accountingEnabled("AutoScheduling", self.recipient.record):
                    accounting = {
                        "action": "cancel",
                        "when": DateTime.getNowUTC().getText(),
                        "deleting": delete_original,
                    }
                    emitAccounting(
                        "AutoScheduling",
                        self.recipient.record,
                        json.dumps(accounting) + "\r\n",
                        filename=self.uid.encode("base64")[:-1] + ".txt"
                    )

                if delete_original:

                    # Delete the attendee's copy of the event
                    log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                    yield self.deleteCalendarResource(self.recipient_calendar_resource)

                    # Build the schedule-changes XML element
                    changes = customxml.ScheduleChanges(
                        customxml.DTStamp(),
                        customxml.Action(
                            customxml.Cancel(),
                        ),
                    )
                    result = (True, autoprocessed, store_inbox, changes,)

                else:

                    # Update the attendee's copy of the event
                    log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                    yield self.writeCalendarResource(None, self.recipient_calendar_resource, self.recipient_calendar)

                    # Build the schedule-changes XML element
                    if rids:
                        action = customxml.Cancel(
                            *[customxml.Recurrence(customxml.RecurrenceID.fromString(rid.getText())) for rid in sorted(rids)]
                        )
                    else:
                        action = customxml.Cancel()
                    changes = customxml.ScheduleChanges(
                        customxml.DTStamp(),
                        customxml.Action(action),
                    )
                    result = (True, autoprocessed, store_inbox, changes)
            else:
                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                result = (True, True, False, None)

        returnValue(result)
Пример #13
0
    def attendeeMerge(self, attendee):
        """
        Merge the ATTENDEE specific changes with the organizer's view of the attendee's event.
        This will remove any attempt by the attendee to change things like the time or location.

        @param attendee: the value of the ATTENDEE property corresponding to the attendee making the change
        @type attendee: C{str}

        @return: C{tuple} of:
            C{bool} - change is allowed
            C{bool} - iTIP reply needs to be sent
            C{list} - list of RECURRENCE-IDs changed
            L{Component} - new calendar object to store
        """

        self.attendee = normalizeCUAddr(attendee)

        returnCalendar = self.oldcalendar.duplicate()
        returnMaster = returnCalendar.masterComponent()

        changeCausesReply = False
        changedRids = []

        # First get uid/rid map of components
        def mapComponents(calendar):
            map = {}
            cancelledRids = set()
            master = None
            for component in calendar.subcomponents():
                if component.name() == "VTIMEZONE":
                    continue
                name = component.name()
                uid = component.propertyValue("UID")
                rid = component.getRecurrenceIDUTC()
                map[(name, uid, rid,)] = component
                if component.propertyValue("STATUS") == "CANCELLED" and rid is not None:
                    cancelledRids.add(rid)
                if rid is None:
                    master = component

            # Normalize each master by adding any STATUS:CANCELLED components as EXDATEs
            exdates = None
            if master:
                # Get all EXDATEs in UTC
                exdates = set()
                for exdate in master.properties("EXDATE"):
                    exdates.update([value.getValue().duplicate().adjustToUTC() for value in exdate.value()])

            return exdates, map, master

        exdatesold, mapold, masterold = mapComponents(self.oldcalendar)
        setold = set(mapold.keys())
        exdatesnew, mapnew, masternew = mapComponents(self.newcalendar)
        setnew = set(mapnew.keys())

        # Handle case where iCal breaks events without a master component
        if masternew is not None and masterold is None:
            masternewStart = masternew.getStartDateUTC()
            keynew = (masternew.name(), masternew.propertyValue("UID"), masternewStart)
            if keynew not in setold:
                # The DTSTART in the fake master does not match a RECURRENCE-ID in the real data.
                # We have to do a brute force search for the component that matches based on DTSTART
                for componentold in self.oldcalendar.subcomponents():
                    if componentold.name() == "VTIMEZONE":
                        continue
                    if masternewStart == componentold.getStartDateUTC():
                        break
                else:
                    # Nothing matches - this has to be treated as an error
                    self._logDiffError("attendeeMerge: Unable to match fake master component: %s" % (keynew,))
                    return False, False, (), None
            else:
                componentold = self.oldcalendar.overriddenComponent(masternewStart)

            # Take the recurrence ID from component1 and fix map2/set2
            keynew = (masternew.name(), masternew.propertyValue("UID"), None)
            componentnew = mapnew[keynew]
            del mapnew[keynew]

            ridold = componentold.getRecurrenceIDUTC()
            newkeynew = (masternew.name(), masternew.propertyValue("UID"), ridold)
            mapnew[newkeynew] = componentnew
            setnew.remove(keynew)
            setnew.add(newkeynew)

        # All the components in oldcalendar must be in newcalendar unless they are CANCELLED
        for key in setold - setnew:
            _ignore_name, _ignore_uid, rid = key
            component = mapold[key]
            if component.propertyValue("STATUS") != "CANCELLED":
                # Attendee may decline by EXDATE'ing an instance - we need to handle that
                if exdatesnew is None or rid in exdatesnew:
                    # Mark Attendee as DECLINED in the server instance
                    overridden = returnCalendar.overriddenComponent(rid)
                    if self._attendeeDecline(overridden):
                        changeCausesReply = True
                        changedRids.append(rid)

                    # When a master component is present we keep the missing override in place but mark it as hidden.
                    # When no master is present we now do the same so we can track updates to the override correctly.
                    overridden.replaceProperty(Property(Component.HIDDEN_INSTANCE_PROPERTY, "T"))

                else:
                    # We used to generate a 403 here - but instead we now ignore this error and let the server data
                    # override the client.

                    # If smart_merge is happening, then derive an instance in the new data as the change in the old
                    # data is valid and likely due to some other attendee changing their status.
                    if  self.smart_merge:
                        newOverride = self.newcalendar.deriveInstance(rid, allowCancelled=True)
                        if newOverride is None:
                            self._logDiffError("attendeeMerge: Could not derive instance for uncancelled component: %s" % (key,))
                        else:
                            self.newcalendar.addComponent(newOverride)
                            setnew.add(key)
                            mapnew[key] = newOverride
                    else:
                        self._logDiffError("attendeeMerge: Missing uncancelled component from first calendar: %s" % (key,))
            else:
                if exdatesnew is not None and rid not in exdatesnew:
                    # We used to generate a 403 here - but instead we now ignore this error and let the server data
                    # override the client
                    self._logDiffError("attendeeMerge: Missing EXDATE for cancelled components from first calendar: %s" % (key,))
                else:
                    # Remove the CANCELLED component from the new calendar and add an EXDATE
                    overridden = returnCalendar.overriddenComponent(rid)
                    returnCalendar.removeComponent(overridden)
                    if returnMaster:
                        # Use the original R-ID value so we preserve the timezone
                        original_rid = component.propertyValue("RECURRENCE-ID")
                        returnMaster.addProperty(Property("EXDATE", [original_rid, ]))

        # Derive a new component in the new calendar for each new one in setnew
        for key in setnew - setold:

            # First check if the attendee's copy is cancelled and properly EXDATE'd
            # and skip it if so.
            _ignore_name, _ignore_uid, rid = key
            componentnew = mapnew[key]
            if componentnew.propertyValue("STATUS") == "CANCELLED":
                if exdatesold is None or rid not in exdatesold:
                    # We used to generate a 403 here - but instead we now ignore this error and let the server data
                    # override the client
                    self._logDiffError("attendeeMerge: Cancelled component not found in first calendar (or no EXDATE): %s" % (key,))
                    setnew.remove(key)
                else:
                    # Derive new component with STATUS:CANCELLED and remove EXDATE
                    newOverride = returnCalendar.deriveInstance(rid, allowCancelled=True)
                    if newOverride is None:
                        # We used to generate a 403 here - but instead we now ignore this error and let the server data
                        # override the client
                        self._logDiffError("attendeeMerge: Could not derive instance for cancelled component: %s" % (key,))
                        setnew.remove(key)
                    else:
                        returnCalendar.addComponent(newOverride)
            else:
                # Derive new component
                newOverride = returnCalendar.deriveInstance(rid)
                if newOverride is None:
                    # We used to generate a 403 here - but instead we now ignore this error and let the server data
                    # override the client
                    self._logDiffError("attendeeMerge: Could not derive instance for uncancelled component: %s" % (key,))
                    setnew.remove(key)
                else:
                    returnCalendar.addComponent(newOverride)

        # So now returnCalendar has all the same components as set2. Check changes and do transfers.

        # Make sure the same VCALENDAR properties match
        if not self._checkVCALENDARProperties(returnCalendar, self.newcalendar):
            # We used to generate a 403 here - but instead we now ignore this error and let the server data
            # override the client
            self._logDiffError("attendeeMerge: VCALENDAR properties do not match")

        # Now we transfer per-Attendee
        # data from newcalendar into returnCalendar to sync up changes, whilst verifying that other
        # key properties are unchanged
        declines = []
        for key in setnew:
            _ignore_name, _ignore_uid, rid = key
            serverData = returnCalendar.overriddenComponent(rid)
            clientData = mapnew[key]

            allowed, reply = self._transferAttendeeData(serverData, clientData, declines)
            if not allowed:
                # We used to generate a 403 here - but instead we now ignore this error and let the server data
                # override the client
                self._logDiffError("attendeeMerge: Mismatched calendar objects")
                #return False, False, (), None
            changeCausesReply |= reply
            if reply:
                changedRids.append(rid)

        # We need to derive instances for any declined using an EXDATE
        for decline in sorted(declines):
            overridden = returnCalendar.overriddenComponent(decline)
            if not overridden:
                overridden = returnCalendar.deriveInstance(decline)
                if overridden is not None:
                    if self._attendeeDecline(overridden):
                        changeCausesReply = True
                        changedRids.append(decline)

                    # When a master component is present we keep the missing override in place but mark it as hidden.
                    # When no master is present we remove the override,
                    if exdatesnew is not None:
                        overridden.replaceProperty(Property(Component.HIDDEN_INSTANCE_PROPERTY, "T"))
                        returnCalendar.addComponent(overridden)
                else:
                    self._logDiffError("attendeeMerge: Unable to override an instance to mark as DECLINED: %s" % (decline,))
                    return False, False, (), None

        return True, changeCausesReply, changedRids, returnCalendar
Пример #14
0
    def attendeeMerge(self, attendee):
        """
        Merge the ATTENDEE specific changes with the organizer's view of the attendee's event.
        This will remove any attempt by the attendee to change things like the time or location.

        @param attendee: the value of the ATTENDEE property corresponding to the attendee making the change
        @type attendee: C{str}

        @return: C{tuple} of:
            C{bool} - change is allowed
            C{bool} - iTIP reply needs to be sent
            C{list} - list of RECURRENCE-IDs changed
            L{Component} - new calendar object to store
        """

        self.attendee = normalizeCUAddr(attendee)

        returnCalendar = self.oldcalendar.duplicate()
        returnMaster = returnCalendar.masterComponent()

        changeCausesReply = False
        changedRids = []

        # First get uid/rid map of components
        def mapComponents(calendar):
            map = {}
            cancelledRids = set()
            master = None
            for component in calendar.subcomponents():
                if component.name() == "VTIMEZONE":
                    continue
                name = component.name()
                uid = component.propertyValue("UID")
                rid = component.getRecurrenceIDUTC()
                map[(
                    name,
                    uid,
                    rid,
                )] = component
                if component.propertyValue(
                        "STATUS") == "CANCELLED" and rid is not None:
                    cancelledRids.add(rid)
                if rid is None:
                    master = component

            # Normalize each master by adding any STATUS:CANCELLED components as EXDATEs
            exdates = None
            if master:
                # Get all EXDATEs in UTC
                exdates = set()
                for exdate in master.properties("EXDATE"):
                    exdates.update([
                        value.getValue().duplicate().adjustToUTC()
                        for value in exdate.value()
                    ])

            return exdates, map, master

        exdatesold, mapold, masterold = mapComponents(self.oldcalendar)
        setold = set(mapold.keys())
        exdatesnew, mapnew, masternew = mapComponents(self.newcalendar)
        setnew = set(mapnew.keys())

        # Handle case where iCal breaks events without a master component
        if masternew is not None and masterold is None:
            masternewStart = masternew.getStartDateUTC()
            keynew = (masternew.name(), masternew.propertyValue("UID"),
                      masternewStart)
            if keynew not in setold:
                # The DTSTART in the fake master does not match a RECURRENCE-ID in the real data.
                # We have to do a brute force search for the component that matches based on DTSTART
                for componentold in self.oldcalendar.subcomponents():
                    if componentold.name() == "VTIMEZONE":
                        continue
                    if masternewStart == componentold.getStartDateUTC():
                        break
                else:
                    # Nothing matches - this has to be treated as an error
                    self._logDiffError(
                        "attendeeMerge: Unable to match fake master component: %s"
                        % (keynew, ))
                    return False, False, (), None
            else:
                componentold = self.oldcalendar.overriddenComponent(
                    masternewStart)

            # Take the recurrence ID from component1 and fix map2/set2
            keynew = (masternew.name(), masternew.propertyValue("UID"), None)
            componentnew = mapnew[keynew]
            del mapnew[keynew]

            ridold = componentold.getRecurrenceIDUTC()
            newkeynew = (masternew.name(), masternew.propertyValue("UID"),
                         ridold)
            mapnew[newkeynew] = componentnew
            setnew.remove(keynew)
            setnew.add(newkeynew)

        # All the components in oldcalendar must be in newcalendar unless they are CANCELLED
        for key in setold - setnew:
            _ignore_name, _ignore_uid, rid = key
            component = mapold[key]
            if component.propertyValue("STATUS") != "CANCELLED":
                # Attendee may decline by EXDATE'ing an instance - we need to handle that
                if exdatesnew is None or rid in exdatesnew:
                    # Mark Attendee as DECLINED in the server instance
                    overridden = returnCalendar.overriddenComponent(rid)
                    if self._attendeeDecline(overridden):
                        changeCausesReply = True
                        changedRids.append(rid)

                    # When a master component is present we keep the missing override in place but mark it as hidden.
                    # When no master is present we now do the same so we can track updates to the override correctly.
                    overridden.replaceProperty(
                        Property(Component.HIDDEN_INSTANCE_PROPERTY, "T"))

                else:
                    # We used to generate a 403 here - but instead we now ignore this error and let the server data
                    # override the client.

                    # If smart_merge is happening, then derive an instance in the new data as the change in the old
                    # data is valid and likely due to some other attendee changing their status.
                    if self.smart_merge:
                        newOverride = self.newcalendar.deriveInstance(
                            rid, allowCancelled=True)
                        if newOverride is None:
                            self._logDiffError(
                                "attendeeMerge: Could not derive instance for uncancelled component: %s"
                                % (key, ))
                        else:
                            self.newcalendar.addComponent(newOverride)
                            setnew.add(key)
                            mapnew[key] = newOverride
                    else:
                        self._logDiffError(
                            "attendeeMerge: Missing uncancelled component from first calendar: %s"
                            % (key, ))
            else:
                if exdatesnew is not None and rid not in exdatesnew:
                    # We used to generate a 403 here - but instead we now ignore this error and let the server data
                    # override the client
                    self._logDiffError(
                        "attendeeMerge: Missing EXDATE for cancelled components from first calendar: %s"
                        % (key, ))
                else:
                    # Remove the CANCELLED component from the new calendar and add an EXDATE
                    overridden = returnCalendar.overriddenComponent(rid)
                    returnCalendar.removeComponent(overridden)
                    if returnMaster:
                        # Use the original R-ID value so we preserve the timezone
                        original_rid = component.propertyValue("RECURRENCE-ID")
                        returnMaster.addProperty(
                            Property("EXDATE", [
                                original_rid,
                            ]))

        # Derive a new component in the new calendar for each new one in setnew
        for key in setnew - setold:

            # First check if the attendee's copy is cancelled and properly EXDATE'd
            # and skip it if so.
            _ignore_name, _ignore_uid, rid = key
            componentnew = mapnew[key]
            if componentnew.propertyValue("STATUS") == "CANCELLED":
                if exdatesold is None or rid not in exdatesold:
                    # We used to generate a 403 here - but instead we now ignore this error and let the server data
                    # override the client
                    self._logDiffError(
                        "attendeeMerge: Cancelled component not found in first calendar (or no EXDATE): %s"
                        % (key, ))
                    setnew.remove(key)
                else:
                    # Derive new component with STATUS:CANCELLED and remove EXDATE
                    newOverride = returnCalendar.deriveInstance(
                        rid, allowCancelled=True)
                    if newOverride is None:
                        # We used to generate a 403 here - but instead we now ignore this error and let the server data
                        # override the client
                        self._logDiffError(
                            "attendeeMerge: Could not derive instance for cancelled component: %s"
                            % (key, ))
                        setnew.remove(key)
                    else:
                        returnCalendar.addComponent(newOverride)
            else:
                # Derive new component
                newOverride = returnCalendar.deriveInstance(rid)
                if newOverride is None:
                    # We used to generate a 403 here - but instead we now ignore this error and let the server data
                    # override the client
                    self._logDiffError(
                        "attendeeMerge: Could not derive instance for uncancelled component: %s"
                        % (key, ))
                    setnew.remove(key)
                else:
                    returnCalendar.addComponent(newOverride)

        # So now returnCalendar has all the same components as set2. Check changes and do transfers.

        # Make sure the same VCALENDAR properties match
        if not self._checkVCALENDARProperties(returnCalendar,
                                              self.newcalendar):
            # We used to generate a 403 here - but instead we now ignore this error and let the server data
            # override the client
            self._logDiffError(
                "attendeeMerge: VCALENDAR properties do not match")

        # Now we transfer per-Attendee
        # data from newcalendar into returnCalendar to sync up changes, whilst verifying that other
        # key properties are unchanged
        declines = []
        for key in setnew:
            _ignore_name, _ignore_uid, rid = key
            serverData = returnCalendar.overriddenComponent(rid)
            clientData = mapnew[key]

            allowed, reply = self._transferAttendeeData(
                serverData, clientData, declines)
            if not allowed:
                # We used to generate a 403 here - but instead we now ignore this error and let the server data
                # override the client
                self._logDiffError(
                    "attendeeMerge: Mismatched calendar objects")
                # return False, False, (), None
            changeCausesReply |= reply
            if reply:
                changedRids.append(rid)

        # We need to derive instances for any declined using an EXDATE
        for decline in sorted(declines):
            overridden = returnCalendar.overriddenComponent(decline)
            if not overridden:
                overridden = returnCalendar.deriveInstance(decline)
                if overridden is not None:
                    if self._attendeeDecline(overridden):
                        changeCausesReply = True
                        changedRids.append(decline)

                    # When a master component is present we keep the missing override in place but mark it as hidden.
                    # When no master is present we remove the override,
                    if exdatesnew is not None:
                        overridden.replaceProperty(
                            Property(Component.HIDDEN_INSTANCE_PROPERTY, "T"))
                        returnCalendar.addComponent(overridden)
                else:
                    self._logDiffError(
                        "attendeeMerge: Unable to override an instance to mark as DECLINED: %s"
                        % (decline, ))
                    return False, False, (), None

        return True, changeCausesReply, changedRids, returnCalendar
Пример #15
0
 def _organizerMerge(self):
     """
     Merge changes to ATTENDEE properties in oldcalendar into newcalendar.
     """
     organizer = normalizeCUAddr(self.newcalendar.masterComponent().propertyValue("ORGANIZER"))
     self._doSmartMerge(organizer, True)
Пример #16
0
    def doImplicitAttendeeRequest(self):
        """
        An iTIP REQUEST message has been sent to an attendee. If there is no existing resource, we will simply
        create a new one. If there is an existing resource we need to reconcile the changes between it and the
        iTIP message.

        @return: C{tuple} of (processed, auto-processed, store inbox item, changes)
        """

        # If there is no existing copy, then look for default calendar and copy it here
        if self.new_resource:

            # Check if the incoming data has the recipient declined in all instances. In that case we will not create
            # a new resource as chances are the recipient previously deleted the resource and we want to keep it deleted.
            attendees = self.message.getAttendeeProperties((self.recipient.cuaddr,))
            if all([attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED" for attendee in attendees]):
                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring all declined" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                returnValue((True, False, False, None,))

            # Check for default calendar
            default = (yield self.recipient.inbox.viewerHome().defaultCalendar(self.message.mainType()))
            if default is None:
                log.error("No default calendar for recipient: '%s'." % (self.recipient.cuaddr,))
                raise ImplicitProcessorException(iTIPRequestStatus.NO_USER_SUPPORT)

            log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - new processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
            new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr, creating=True)

            # Handle auto-reply behavior
            organizer = normalizeCUAddr(self.message.getOrganizer())
            if (yield self.recipient.record.canAutoSchedule(organizer=organizer)):
                # auto schedule mode can depend on who the organizer is
                mode = yield self.recipient.record.getAutoScheduleMode(organizer=organizer)
                send_reply, store_inbox, partstat, accounting = (yield self.checkAttendeeAutoReply(new_calendar, mode))
                if accounting is not None:
                    accounting["action"] = "create"
                    emitAccounting(
                        "AutoScheduling",
                        self.recipient.record,
                        json.dumps(accounting) + "\r\n",
                        filename=self.uid.encode("base64")[:-1] + ".txt"
                    )

                # Only store inbox item when reply is not sent or always for users
                store_inbox = store_inbox or self.recipient.record.getCUType() == "INDIVIDUAL"
            else:
                send_reply = False
                store_inbox = True

            new_resource = (yield self.writeCalendarResource(default, None, new_calendar))

            if send_reply:
                # Track outstanding auto-reply processing
                log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - auto-reply queued" % (self.recipient.cuaddr, self.uid,))
                ScheduleAutoReplyWork.autoReply(self.txn, new_resource, partstat)

            # Build the schedule-changes XML element
            changes = customxml.ScheduleChanges(
                customxml.DTStamp(),
                customxml.Action(
                    customxml.Create(),
                ),
            )
            result = (True, send_reply, store_inbox, changes,)
        else:
            # Processing update to existing event
            new_calendar, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr)
            if new_calendar:

                # Handle auto-reply behavior
                organizer = normalizeCUAddr(self.message.getOrganizer())
                if (yield self.recipient.record.canAutoSchedule(organizer=organizer)) and not hasattr(self.txn, "doing_attendee_refresh"):
                    # auto schedule mode can depend on who the organizer is
                    mode = yield self.recipient.record.getAutoScheduleMode(organizer=organizer)
                    send_reply, store_inbox, partstat, accounting = (yield self.checkAttendeeAutoReply(new_calendar, mode))
                    if accounting is not None:
                        accounting["action"] = "modify"
                        emitAccounting(
                            "AutoScheduling",
                            self.recipient.record,
                            json.dumps(accounting) + "\r\n",
                            filename=self.uid.encode("base64")[:-1] + ".txt"
                        )

                    # Only store inbox item when reply is not sent or always for users
                    store_inbox = store_inbox or self.recipient.record.getCUType() == "INDIVIDUAL"
                else:
                    send_reply = False
                    store_inbox = True

                # Let the store know that no time-range info has changed for a refresh (assuming that
                # no auto-accept changes were made)
                if hasattr(self.txn, "doing_attendee_refresh"):
                    new_calendar.noInstanceIndexing = not send_reply

                # Update the attendee's copy of the event
                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                new_resource = (yield self.writeCalendarResource(None, self.recipient_calendar_resource, new_calendar))

                if send_reply:
                    # Track outstanding auto-reply processing
                    log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - auto-reply queued" % (self.recipient.cuaddr, self.uid,))
                    ScheduleAutoReplyWork.autoReply(self.txn, new_resource, partstat)

                # Build the schedule-changes XML element
                update_details = []
                for rid, props_changed in sorted(rids.iteritems(), key=lambda x: x[0]):
                    recurrence = []
                    if rid is None:
                        recurrence.append(customxml.Master())
                    else:
                        recurrence.append(customxml.RecurrenceID.fromString(rid.getText()))
                    changes = []
                    for propName, paramNames in sorted(props_changed.iteritems(), key=lambda x: x[0]):
                        params = tuple([customxml.ChangedParameter(name=param) for param in paramNames])
                        changes.append(customxml.ChangedProperty(*params, **{"name": propName}))
                    recurrence.append(customxml.Changes(*changes))
                    update_details += (customxml.Recurrence(*recurrence),)

                changes = customxml.ScheduleChanges(
                    customxml.DTStamp(),
                    customxml.Action(
                        customxml.Update(*update_details),
                    ),
                )

                # Refresh from another Attendee should not have Inbox item
                if hasattr(self.txn, "doing_attendee_refresh"):
                    store_inbox = False

                result = (True, send_reply, store_inbox, changes,)

            else:
                # Request needs to be ignored
                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                result = (True, True, False, None,)

        returnValue(result)
Пример #17
0
    def outbound(self, txn, originator, recipient, calendar, onlyAfter=None):
        """
        Generates and sends an outbound IMIP message.

        @param txn: the transaction to use for looking up/creating tokens
        @type txn: L{CommonStoreTransaction}
        """

        if onlyAfter is None:
            duration = Duration(days=self.suppressionDays)
            onlyAfter = DateTime.getNowUTC() - duration

        icaluid = calendar.resourceUID()
        method = calendar.propertyValue("METHOD")

        # Clean up the attendee list which is purely used within the human
        # readable email message (not modifying the calendar body)
        attendees = []
        for attendeeProp in calendar.getAllAttendeeProperties():
            cutype = attendeeProp.parameterValue("CUTYPE", "INDIVIDUAL")
            if cutype == "INDIVIDUAL":
                cn = attendeeProp.parameterValue("CN", None)
                if cn is not None:
                    cn = cn.decode("utf-8")
                cuaddr = normalizeCUAddr(attendeeProp.value())
                if cuaddr.startswith("mailto:"):
                    mailto = cuaddr[7:]
                    if not cn:
                        cn = mailto
                else:
                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
                    if emailAddress:
                        mailto = emailAddress
                    else:
                        mailto = None

                if cn or mailto:
                    attendees.append((cn, mailto))

        toAddr = recipient
        if not recipient.lower().startswith("mailto:"):
            raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP "
                             "operation." % (recipient, ))
        recipient = recipient[7:]

        if method != "REPLY":
            # Invites and cancellations:

            # Reuse or generate a token based on originator, toAddr, and
            # event uid
            token = (yield txn.imipGetToken(originator, toAddr.lower(),
                                            icaluid))
            if token is None:

                # Because in the past the originator was sometimes in mailto:
                # form, lookup an existing token by mailto: as well
                organizerProperty = calendar.getOrganizerProperty()
                organizerEmailAddress = organizerProperty.parameterValue(
                    "EMAIL", None)
                if organizerEmailAddress is not None:
                    token = (yield txn.imipGetToken(
                        "mailto:%s" % (organizerEmailAddress.lower(), ),
                        toAddr.lower(), icaluid))

            if token is None:
                token = (yield txn.imipCreateToken(originator, toAddr.lower(),
                                                   icaluid))
                self.log.debug(
                    "Mail gateway created token %s for %s "
                    "(originator), %s (recipient) and %s (icaluid)" %
                    (token, originator, toAddr, icaluid))
                inviteState = "new"

            else:
                self.log.debug(
                    "Mail gateway reusing token %s for %s "
                    "(originator), %s (recipient) and %s (icaluid)" %
                    (token, originator, toAddr, icaluid))
                inviteState = "update"

            fullServerAddress = self.address
            _ignore_name, serverAddress = email.utils.parseaddr(
                fullServerAddress)
            pre, post = serverAddress.split('@')
            addressWithToken = "%s+%s@%s" % (pre, token, post)

            organizerProperty = calendar.getOrganizerProperty()
            organizerEmailAddress = organizerProperty.parameterValue(
                "EMAIL", None)
            organizerValue = organizerProperty.value()
            organizerProperty.setValue("mailto:%s" % (addressWithToken, ))

            # If the organizer is also an attendee, update that attendee value
            # to match
            organizerAttendeeProperty = calendar.getAttendeeProperty(
                [organizerValue])
            if organizerAttendeeProperty is not None:
                organizerAttendeeProperty.setValue("mailto:%s" %
                                                   (addressWithToken, ))

            # The email's From will include the originator's real name email
            # address if available.  Otherwise it will be the server's email
            # address (without # + addressing)
            if organizerEmailAddress:
                orgEmail = fromAddr = organizerEmailAddress
            else:
                fromAddr = serverAddress
                orgEmail = None
            cn = calendar.getOrganizerProperty().parameterValue('CN', None)
            if cn is None:
                cn = u'Calendar Server'
                orgCN = orgEmail
            else:
                orgCN = cn = cn.decode("utf-8")

            # a unicode cn (rather than an encode string value) means the
            # from address will get properly encoded per rfc2047 within the
            # MIMEMultipart in generateEmail
            formattedFrom = "%s <%s>" % (cn, fromAddr)

            # Reply-to address will be the server+token address

        else:  # REPLY
            inviteState = "reply"

            # Look up the attendee property corresponding to the originator
            # of this reply
            originatorAttendeeProperty = calendar.getAttendeeProperty(
                [originator])
            formattedFrom = fromAddr = originator = ""
            if originatorAttendeeProperty:
                originatorAttendeeEmailAddress = (
                    originatorAttendeeProperty.parameterValue("EMAIL", None))
                if originatorAttendeeEmailAddress:
                    formattedFrom = fromAddr = originator = (
                        originatorAttendeeEmailAddress)

            organizerMailto = str(calendar.getOrganizer())
            if not organizerMailto.lower().startswith("mailto:"):
                raise ValueError("ORGANIZER address '%s' must be mailto: "
                                 "for REPLY." % (organizerMailto, ))
            orgEmail = organizerMailto[7:]

            orgCN = calendar.getOrganizerProperty().parameterValue('CN', None)
            addressWithToken = formattedFrom

        # At the point we've created the token in the db, which we always
        # want to do, but if this message is for an event completely in
        # the past we don't want to actually send an email.
        if not calendar.hasInstancesAfter(onlyAfter):
            self.log.debug("Skipping IMIP message for old event")
            returnValue(True)

        # Now prevent any "internal" CUAs from being exposed by converting
        # to mailto: if we have one
        for attendeeProp in calendar.getAllAttendeeProperties():
            cutype = attendeeProp.parameterValue('CUTYPE', None)
            if cutype == "INDIVIDUAL":
                cuaddr = normalizeCUAddr(attendeeProp.value())
                if not cuaddr.startswith("mailto:"):
                    emailAddress = attendeeProp.parameterValue("EMAIL", None)
                    if emailAddress:
                        attendeeProp.setValue("mailto:%s" % (emailAddress, ))

        msgId, message = self.generateEmail(inviteState,
                                            calendar,
                                            orgEmail,
                                            orgCN,
                                            attendees,
                                            formattedFrom,
                                            addressWithToken,
                                            recipient,
                                            language=self.language)

        try:
            success = (yield
                       self.smtpSender.sendMessage(fromAddr, toAddr, msgId,
                                                   message))
            returnValue(success)
        except Exception, e:
            self.log.error("Failed to send IMIP message (%s)" % (str(e), ))
            returnValue(False)