예제 #1
0
/**
* Actually write the resource to the database.  All checking of whether this is reasonable
* should be done before this is called.
* 
* @param DAVResource $resource The resource being written
* @param string $caldav_data The actual data to be written
* @param DAVResource $collection The collection containing the resource being written
* @param int $author The user_no who wants to put this resource on the server
* @param string $etag An etag unique for this event
* @param string $put_action_type INSERT or UPDATE depending on what we are to do
* @param boolean $caldav_context True, if we are responding via CalDAV, false for other ways of calling this
* @param string Either 'INSERT' or 'UPDATE': the type of action we are doing
* @param boolean $log_action Whether to log the fact that we are writing this into an action log (if configured)
* @param string $weak_etag An etag that is NOT modified on ATTENDEE changes for this event
* 
* @return boolean True for success, false for failure.
*/
function write_resource(DAVResource $resource, $caldav_data, DAVResource $collection, $author, &$etag, $put_action_type, $caldav_context, $log_action = true, $weak_etag = null)
{
    global $tz_regex, $session;
    $path = $resource->bound_from();
    $user_no = $collection->user_no();
    $vcal = new vCalendar($caldav_data);
    $resources = $vcal->GetComponents('VTIMEZONE', false);
    // Not matching VTIMEZONE
    if (!isset($resources[0])) {
        $resource_type = 'Unknown';
        /** @todo Handle writing non-calendar resources, like address book entries or random file data */
        rollback_on_error($caldav_context, $user_no, $path, translate('No calendar content'), 412);
        return false;
    } else {
        $first = $resources[0];
        if (!$first instanceof vComponent) {
            print $vcal->Render();
            fatal('This is not a vComponent!');
        }
        $resource_type = $first->GetType();
    }
    $collection_id = $collection->collection_id();
    $qry = new AwlQuery();
    $qry->Begin();
    $dav_params = array(':etag' => $etag, ':dav_data' => $caldav_data, ':caldav_type' => $resource_type, ':session_user' => $author, ':weak_etag' => $weak_etag);
    $calitem_params = array(':etag' => $etag);
    if ($put_action_type == 'INSERT') {
        $qry->QDo('SELECT nextval(\'dav_id_seq\') AS dav_id, null AS caldav_data');
    } else {
        $qry->QDo('SELECT dav_id, caldav_data FROM caldav_data WHERE dav_name = :dav_name ', array(':dav_name' => $path));
    }
    if ($qry->rows() != 1 || !($row = $qry->Fetch())) {
        // No dav_id?  => We're toast!
        trace_bug('No dav_id for "%s" on %s!!!', $path, $create_resource ? 'create' : 'update');
        rollback_on_error($caldav_context, $user_no, $path);
        return false;
    }
    $dav_id = $row->dav_id;
    $old_dav_data = $row->caldav_data;
    $dav_params[':dav_id'] = $dav_id;
    $calitem_params[':dav_id'] = $dav_id;
    $due = null;
    if ($first->GetType() == 'VTODO') {
        $due = $first->GetPValue('DUE');
    }
    $calitem_params[':due'] = $due;
    $dtstart = $first->GetPValue('DTSTART');
    if (empty($dtstart)) {
        $dtstart = $due;
    }
    $calitem_params[':dtstart'] = $dtstart;
    $dtend = $first->GetPValue('DTEND');
    if (isset($dtend) && $dtend != '') {
        dbg_error_log('PUT', ' DTEND: "%s", DTSTART: "%s", DURATION: "%s"', $dtend, $dtstart, $first->GetPValue('DURATION'));
        $calitem_params[':dtend'] = $dtend;
        $dtend = ':dtend';
    } else {
        // In this case we'll construct the SQL directly as a calculation relative to :dtstart
        $dtend = 'NULL';
        if ($first->GetPValue('DURATION') != '' and $dtstart != '') {
            $duration = trim(preg_replace('#[PT]#', ' ', $first->GetPValue('DURATION')));
            if ($duration == '') {
                $duration = '0 seconds';
            }
            $dtend = '(:dtstart::timestamp with time zone + :duration::interval)';
            $calitem_params[':duration'] = $duration;
        } elseif ($first->GetType() == 'VEVENT') {
            /**
             * From RFC2445 4.6.1:
             * For cases where a "VEVENT" calendar component specifies a "DTSTART"
             * property with a DATE data type but no "DTEND" property, the events
             * non-inclusive end is the end of the calendar date specified by the
             * "DTSTART" property. For cases where a "VEVENT" calendar component specifies
             * a "DTSTART" property with a DATE-TIME data type but no "DTEND" property,
             * the event ends on the same calendar date and time of day specified by the
             * "DTSTART" property.
             *
             * So we're looking for 'VALUE=DATE', to identify the duration, effectively.
             *
             */
            $dtstart_prop = $first->GetProperty('DTSTART');
            $value_type = $dtstart_prop->GetParameterValue('VALUE');
            dbg_error_log('PUT', 'DTSTART without DTEND. DTSTART value type is %s', $value_type);
            if (isset($value_type) && $value_type == 'DATE') {
                $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)';
            } else {
                $dtend = ':dtstart';
            }
        }
    }
    $dtstamp = $first->GetPValue('DTSTAMP');
    if (!isset($dtstamp) || $dtstamp == '') {
        // Strictly, we're dealing with an out of spec component here, but we'll try and survive
        $dtstamp = gmdate('Ymd\\THis\\Z');
    }
    $calitem_params[':dtstamp'] = $dtstamp;
    $last_modified = $first->GetPValue('LAST-MODIFIED');
    if (!isset($last_modified) || $last_modified == '') {
        $last_modified = $dtstamp;
    }
    $dav_params[':modified'] = $last_modified;
    $calitem_params[':modified'] = $last_modified;
    $created = $first->GetPValue('CREATED');
    if ($created == '00001231T000000Z') {
        $created = '20001231T000000Z';
    }
    $class = $first->GetPValue('CLASS');
    /* Check and see if we should over ride the class. */
    /** @todo is there some way we can move this out of this function? Or at least get rid of the need for the SQL query here. */
    if (public_events_only($user_no, $path)) {
        $class = 'PUBLIC';
    }
    /*
     * It seems that some calendar clients don't set a class...
     * RFC2445, 4.8.1.3:
     * Default is PUBLIC
     */
    if (!isset($class) || $class == '') {
        $class = 'PUBLIC';
    }
    $calitem_params[':class'] = $class;
    /** Calculate what timezone to set, first, if possible */
    $last_olson = 'Turkmenikikamukau';
    // I really hope this location doesn't exist!
    $tzid = GetTZID($first);
    if (!empty($tzid)) {
        $timezones = $vcal->GetComponents('VTIMEZONE');
        foreach ($timezones as $k => $tz) {
            if ($tz->GetPValue('TZID') != $tzid) {
                /**
                 * We'll skip any tz definitions that are for a TZID other than the DTSTART/DUE on the first VEVENT/VTODO 
                 */
                dbg_error_log('ERROR', ' Event uses TZID[%s], skipping included TZID[%s]!', $tz->GetPValue('TZID'), $tzid);
                continue;
            }
            $olson = olson_from_tzstring($tzid);
            if (empty($olson)) {
                $olson = $tz->GetPValue('X-LIC-LOCATION');
                if (!empty($olson)) {
                    $olson = olson_from_tzstring($olson);
                }
            }
        }
        dbg_error_log('PUT', ' Using TZID[%s] and location of [%s]', $tzid, isset($olson) ? $olson : '');
        if (!empty($olson) && $olson != $last_olson && preg_match($tz_regex, $olson)) {
            dbg_error_log('PUT', ' Setting timezone to %s', $olson);
            if ($olson != '') {
                $qry->QDo('SET TIMEZONE TO \'' . $olson . "'");
            }
            $last_olson = $olson;
        }
        $params = array(':tzid' => $tzid);
        $qry = new AwlQuery('SELECT 1 FROM timezones WHERE tzid = :tzid', $params);
        if ($qry->Exec('PUT', __LINE__, __FILE__) && $qry->rows() == 0) {
            $params[':olson_name'] = $olson;
            $params[':vtimezone'] = isset($tz) ? $tz->Render() : null;
            $qry->QDo('INSERT INTO timezones (tzid, olson_name, active, vtimezone) VALUES(:tzid,:olson_name,false,:vtimezone)', $params);
        }
        if (!isset($olson) || $olson == '') {
            $olson = $tzid;
        }
    }
    $qry->QDo('SELECT new_sync_token(0,' . $collection_id . ')');
    $calitem_params[':tzid'] = $tzid;
    $calitem_params[':uid'] = $first->GetPValue('UID');
    $calitem_params[':summary'] = $first->GetPValue('SUMMARY');
    $calitem_params[':location'] = $first->GetPValue('LOCATION');
    $calitem_params[':transp'] = $first->GetPValue('TRANSP');
    $calitem_params[':description'] = $first->GetPValue('DESCRIPTION');
    $calitem_params[':rrule'] = $first->GetPValue('RRULE');
    $calitem_params[':url'] = $first->GetPValue('URL');
    $calitem_params[':priority'] = $first->GetPValue('PRIORITY');
    $calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE');
    $calitem_params[':status'] = $first->GetPValue('STATUS');
    if (!$collection->IsSchedulingCollection()) {
        if (do_scheduling_requests($vcal, $put_action_type == 'INSERT', $old_dav_data, true)) {
            $dav_params[':dav_data'] = $vcal->Render(null, true);
            $etag = null;
        }
    }
    if (!isset($dav_params[':modified'])) {
        $dav_params[':modified'] = 'now';
    }
    if ($put_action_type == 'INSERT') {
        $sql = 'INSERT INTO caldav_data ( dav_id, user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id, weak_etag )
            VALUES( :dav_id, :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, :created, :modified, :collection_id, :weak_etag )';
        $dav_params[':collection_id'] = $collection_id;
        $dav_params[':user_no'] = $user_no;
        $dav_params[':dav_name'] = $path;
        $dav_params[':created'] = isset($created) && $created != '' ? $created : $dtstamp;
    } else {
        $sql = 'UPDATE caldav_data SET caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user,
            modified=:modified, weak_etag=:weak_etag WHERE dav_id=:dav_id';
    }
    $qry = new AwlQuery($sql, $dav_params);
    if (!$qry->Exec('PUT', __LINE__, __FILE__)) {
        fatal('Insert into calendar_item failed...');
        rollback_on_error($caldav_context, $user_no, $path);
        return false;
    }
    if ($put_action_type == 'INSERT') {
        $sql = <<<EOSQL
INSERT INTO calendar_item (user_no, dav_name, dav_id, dav_etag, uid, dtstamp,
                dtstart, dtend, summary, location, class, transp,
                description, rrule, tz_id, last_modified, url, priority,
                created, due, percent_complete, status, collection_id )
   VALUES ( :user_no, :dav_name, :dav_id, :etag, :uid, :dtstamp,
                :dtstart, {$dtend}, :summary, :location, :class, :transp,
                :description, :rrule, :tzid, :modified, :url, :priority,
                :created, :due, :percent_complete, :status, :collection_id )
EOSQL;
        $sync_change = 201;
        $calitem_params[':collection_id'] = $collection_id;
        $calitem_params[':user_no'] = $user_no;
        $calitem_params[':dav_name'] = $path;
        $calitem_params[':created'] = $dav_params[':created'];
    } else {
        $sql = <<<EOSQL
UPDATE calendar_item SET dav_etag=:etag, uid=:uid, dtstamp=:dtstamp,
                dtstart=:dtstart, dtend={$dtend}, summary=:summary, location=:location,
                class=:class, transp=:transp, description=:description, rrule=:rrule,
                tz_id=:tzid, last_modified=:modified, url=:url, priority=:priority,
                due=:due, percent_complete=:percent_complete, status=:status
       WHERE dav_id=:dav_id
EOSQL;
        $sync_change = 200;
    }
    write_alarms($dav_id, $first);
    write_attendees($dav_id, $vcal);
    if ($log_action && function_exists('log_caldav_action')) {
        log_caldav_action($put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path);
    } else {
        if ($log_action) {
            dbg_error_log('PUT', 'No log_caldav_action( %s, %s, %s, %s, %s) can be called.', $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path);
        }
    }
    $qry = new AwlQuery($sql, $calitem_params);
    if (!$qry->Exec('PUT', __LINE__, __FILE__)) {
        rollback_on_error($caldav_context, $user_no, $path);
        return false;
    }
    $qry->QDo("SELECT write_sync_change( {$collection_id}, {$sync_change}, :dav_name)", array(':dav_name' => $path));
    $qry->Commit();
    if (function_exists('post_commit_action')) {
        post_commit_action($put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path);
    }
    // Uncache anything to do with the collection
    $cache = getCacheInstance();
    $cache_ns = 'collection-' . preg_replace('{/[^/]*$}', '/', $path);
    $cache->delete($cache_ns, null);
    dbg_error_log('PUT', 'User: %d, ETag: %s, Path: %s', $author, $etag, $path);
    return true;
    // Success!
}
예제 #2
0
    $request->XMLResponse(200, $response);
}
function handle_cancel_request($ic)
{
    global $c, $session, $request;
    $request->NeedPrivilege('CALDAV:schedule-send-reply');
    $reply = new XMLDocument(array("DAV:" => "", "urn:ietf:params:xml:ns:caldav" => "C"));
    $response = $reply->NewXMLElement("response", false, false, 'urn:ietf:params:xml:ns:caldav');
    $reply->CalDAVElement($response, "request-status", "2.0;Success");
    // Cargo-cult setting
    $response = $reply->NewXMLElement("schedule-response", $response, $reply->GetXmlNsArray());
    $request->XMLResponse(200, $response);
}
$ical = new vCalendar($request->raw_post);
$method = $ical->GetPValue('METHOD');
$resources = $ical->GetComponents('VTIMEZONE', false);
$first = $resources[0];
switch ($method) {
    case 'REQUEST':
        dbg_error_log('POST', 'Handling iTIP "REQUEST" method with "%s" component.', $method, $first->GetType());
        if ($first->GetType() == 'VFREEBUSY') {
            handle_freebusy_request($first);
        } elseif ($first->GetType() == 'VEVENT') {
            $request->NeedPrivilege('CALDAV:schedule-send-invite');
            handle_schedule_request($ical);
        } else {
            dbg_error_log('POST', 'Ignoring iTIP "REQUEST" with "%s" component.', $first->GetType());
        }
        break;
    case 'REPLY':
        dbg_error_log('POST', 'Handling iTIP "REPLY" with "%s" component.', $first->GetType());
예제 #3
0
         $transparency = preg_replace('{^.*:}', '', $transparency[0]->GetNSTag());
         $qry->QDo('UPDATE collection SET schedule_transp = :transparency WHERE dav_name = :dav_name', array(':dav_name' => $dav_resource->dav_name(), ':transparency' => $transparency));
         $success[$tag] = 1;
     } else {
         add_failure('set', $tag, 'HTTP/1.1 409 Conflict', translate("The CalDAV:schedule-calendar-transp property may only be set on calendars."));
     }
     break;
 case 'urn:ietf:params:xml:ns:caldav:calendar-free-busy-set':
     add_failure('set', $tag, 'HTTP/1.1 409 Conflict', translate("The calendar-free-busy-set is superseded by the  schedule-calendar-transp property of a calendar collection."));
     break;
 case 'urn:ietf:params:xml:ns:caldav:calendar-timezone':
     if ($dav_resource->IsCollection() && $dav_resource->IsCalendar() && !$dav_resource->IsBinding()) {
         $tzcomponent = $setting->GetPath('urn:ietf:params:xml:ns:caldav:calendar-timezone');
         $tzstring = $tzcomponent[0]->GetContent();
         $calendar = new vCalendar($tzstring);
         $timezones = $calendar->GetComponents('VTIMEZONE');
         if (count($timezones) == 0) {
             break;
         }
         $tz = $timezones[0];
         // Backward compatibility
         $tzid = $tz->GetPValue('TZID');
         $params = array(':tzid' => $tzid);
         $qry = new AwlQuery('SELECT 1 FROM timezones WHERE tzid = :tzid', $params);
         if ($qry->Exec('PUT', __LINE__, __FILE__) && $qry->rows() == 0) {
             $params[':olson_name'] = $calendar->GetOlsonName($tz);
             $params[':vtimezone'] = isset($tz) ? $tz->Render() : null;
             $qry->QDo('INSERT INTO timezones (tzid, olson_name, active, vtimezone) VALUES(:tzid,:olson_name,false,:vtimezone)', $params);
         }
         $qry->QDo('UPDATE collection SET timezone = :tzid WHERE dav_name = :dav_name', array(':tzid' => $tzid, ':dav_name' => $dav_resource->dav_name()));
     } else {
예제 #4
0
/**
* Expand the instances for a STANDARD or DAYLIGHT component of a VTIMEZONE 
*
* @param object $vResource is a VCALENDAR with a VTIMEZONE containing components needing expansion
* @param object $range_start A RepeatRuleDateTime which is the beginning of the range for events.
* @param object $range_end A RepeatRuleDateTime which is the end of the range for events.
* @param int $offset_from The offset from UTC in seconds at the onset time.
*
* @return array of onset datetimes with UTC from/to offsets
*/
function expand_timezone_onsets(vCalendar $vResource, RepeatRuleDateTime $range_start, RepeatRuleDateTime $range_end)
{
    global $c;
    $vtimezones = $vResource->GetComponents();
    $vtz = $vtimezones[0];
    $components = $vtz->GetComponents();
    $instances = array();
    $dtstart = null;
    $is_date = false;
    $has_repeats = false;
    $zone_tz = $vtz->GetPValue('TZID');
    foreach ($components as $k => $comp) {
        if (DEBUG_EXPAND) {
            printf("Starting TZ expansion for component '%s' in timezone '%s'\n", $comp->GetType(), $zone_tz);
            foreach ($instances as $k => $v) {
                print ' : ' . $k;
            }
            print "\n";
        }
        $dtstart_prop = $comp->GetProperty('DTSTART');
        if (!isset($dtstart_prop)) {
            continue;
        }
        $dtstart = new RepeatRuleDateTime($dtstart_prop);
        $dtstart->setTimeZone('UTC');
        $offset_from = $comp->GetPValue('TZOFFSETFROM');
        $offset_from = $offset_from / 100 * 3600 + abs($offset_from) % 100 * 60 * ($offset_from < 0 ? -1 : 0);
        $offset_from *= -1;
        $offset_from = "{$offset_from} seconds";
        dbg_error_log('tz/update', "%s of offset\n", $offset_from);
        $dtstart->modify($offset_from);
        $is_date = $dtstart->isDate();
        $instances[$dtstart->UTC('Y-m-d\\TH:i:s\\Z')] = $comp;
        $rrule = $comp->GetProperty('RRULE');
        $has_repeats = isset($rrule);
        if (!$has_repeats) {
            continue;
        }
        $recur = $comp->GetProperty('RRULE');
        if (isset($recur)) {
            $recur = $recur->Value();
            $this_start = clone $dtstart;
            $rule = new RepeatRule($this_start, $recur, $is_date);
            $i = 0;
            $result_limit = 1000;
            while ($date = $rule->next()) {
                $instances[$date->UTC('Y-m-d\\TH:i:s\\Z')] = $comp;
                if ($i++ >= $result_limit || $date > $range_end) {
                    break;
                }
            }
            if (DEBUG_EXPAND) {
                print "After rrule_expand";
                foreach ($instances as $k => $v) {
                    print ' : ' . $k;
                }
                print "\n";
            }
        }
        $properties = $comp->GetProperties('RDATE');
        if (count($properties)) {
            foreach ($properties as $p) {
                $timezone = $p->GetParameterValue('TZID');
                $rdate = $p->Value();
                $rdates = explode(',', $rdate);
                foreach ($rdates as $k => $v) {
                    $rdate = new RepeatRuleDateTime($v, $timezone, $is_date);
                    if ($return_floating_times) {
                        $rdate->setAsFloat();
                    }
                    $instances[$rdate->UTC('Y-m-d\\TH:i:s\\Z')] = $comp;
                    if ($rdate > $range_end) {
                        break;
                    }
                }
            }
            if (DEBUG_EXPAND) {
                print "After rdate_expand";
                foreach ($instances as $k => $v) {
                    print ' : ' . $k;
                }
                print "\n";
            }
        }
    }
    ksort($instances);
    $onsets = array();
    $start_utc = $range_start->UTC('Y-m-d\\TH:i:s\\Z');
    $end_utc = $range_end->UTC('Y-m-d\\TH:i:s\\Z');
    foreach ($instances as $utc => $comp) {
        if ($utc > $end_utc) {
            if (DEBUG_EXPAND) {
                printf("We're done: {$utc} is out of the range.\n");
            }
            break;
        }
        if ($utc < $start_utc) {
            continue;
        }
        $onsets[$utc] = array('from' => $comp->GetPValue('TZOFFSETFROM'), 'to' => $comp->GetPValue('TZOFFSETTO'), 'name' => $comp->GetPValue('TZNAME'), 'type' => $comp->GetType());
    }
    return $onsets;
}
예제 #5
0
 /**
  * Given a dav_id and an original vCalendar, pull out each of the VALARMs
  * and write the values into the calendar_alarm table.
  *
  * @return null
  */
 function WriteCalendarAlarms($dav_id, vCalendar $vcal)
 {
     $qry = new AwlQuery('DELETE FROM calendar_alarm WHERE dav_id = ' . $dav_id);
     $qry->Exec('PUT', __LINE__, __FILE__);
     $components = $vcal->GetComponents();
     $qry->SetSql('INSERT INTO calendar_alarm ( dav_id, action, trigger, summary, description, component, next_trigger )
         VALUES( ' . $dav_id . ', :action, :trigger, :summary, :description, :component,
                                     :related::timestamp with time zone + :related_trigger::interval )');
     $qry->Prepare();
     foreach ($components as $component) {
         if ($component->GetType() == 'VTIMEZONE') {
             continue;
         }
         $alarms = $component->GetComponents('VALARM');
         if (count($alarms) < 1) {
             return;
         }
         foreach ($alarms as $v) {
             $trigger = array_merge($v->GetProperties('TRIGGER'));
             if ($trigger == null) {
                 continue;
             }
             // Bogus data.
             $trigger = $trigger[0];
             $related = null;
             $related_trigger = '0M';
             $trigger_type = $trigger->GetParameterValue('VALUE');
             if (!isset($trigger_type) || $trigger_type == 'DURATION') {
                 switch ($trigger->GetParameterValue('RELATED')) {
                     case 'DTEND':
                         $related = $component->GetPValue('DTEND');
                         break;
                     case 'DUE':
                         $related = $component->GetPValue('DUE');
                         break;
                     default:
                         $related = $component->GetPValue('DTSTART');
                 }
                 $duration = $trigger->Value();
                 if (!preg_match('{^-?P(:?\\d+W)?(:?\\d+D)?(:?T(:?\\d+H)?(:?\\d+M)?(:?\\d+S)?)?$}', $duration)) {
                     continue;
                 }
                 $minus = substr($duration, 0, 1) == '-';
                 $related_trigger = trim(preg_replace('#[PT-]#', ' ', $duration));
                 if ($minus) {
                     $related_trigger = preg_replace('{(\\d+[WDHMS])}', '-$1 ', $related_trigger);
                 } else {
                     $related_trigger = preg_replace('{(\\d+[WDHMS])}', '$1 ', $related_trigger);
                 }
             } else {
                 if (false === strtotime($trigger->Value())) {
                     continue;
                 }
                 // Invalid date.
             }
             $qry->Bind(':action', $v->GetPValue('ACTION'));
             $qry->Bind(':trigger', $trigger->Render());
             $qry->Bind(':summary', $v->GetPValue('SUMMARY'));
             $qry->Bind(':description', $v->GetPValue('DESCRIPTION'));
             $qry->Bind(':component', $v->Render());
             $qry->Bind(':related', $related);
             $qry->Bind(':related_trigger', $related_trigger);
             $qry->Exec('PUT', __LINE__, __FILE__);
         }
     }
 }
예제 #6
0
/**
 * Send an iMIP message since they look like a non-local user.
 *  
 * @param string $method The METHOD parameter from the iTIP
 * @param string $to_email The e-mail address we're going to send to
 * @param vCalendar $vcal The iTIP part of the message.
 */
function doImipMessage($method, $to_email, vCalendar $itip)
{
    global $c, $request;
    header('Debug: Sending iMIP ' . $method . ' message to ' . $to_email);
    $mime = new MultiPart();
    $mime->addPart($itip->Render(), 'text/calendar; charset=UTF-8; method=' . $method);
    $friendly_part = isset($c->iMIP->template[$method]) ? $c->iMIP->template[$method] : <<<EOTEMPLATE
This is a meeting ##METHOD## which your e-mail program should be able to
import into your calendar.  Alternatively you could save the attachment
and load that into your calendar instead.
EOTEMPLATE;
    $components = $itip->GetComponents('VTIMEZONE', false);
    $replaceable = array('METHOD', 'DTSTART', 'DTEND', 'SUMMARY', 'DESCRIPTION', 'URL');
    foreach ($replaceable as $pname) {
        $search = '##' . $pname . '##';
        if (strstr($friendly_part, $search) !== false) {
            $property = $itip->GetProperty($pname);
            if (empty($property)) {
                $property = $components[0]->GetProperty($pname);
            }
            if (empty($property)) {
                $replace = '';
            } else {
                switch ($pname) {
                    case 'DTSTART':
                    case 'DTEND':
                        $when = new RepeatRuleDateTime($property);
                        $replace = $when->format('c');
                        break;
                    default:
                        $replace = $property->GetValue();
                }
            }
            $friendly_part = str_replace($search, $replace, $friendly_part);
        }
    }
    $mime->addPart($friendly_part, 'text/plain');
    $email = new EMail();
    $email->SetFrom($request->principal->email());
    $email->AddTo($to_email);
    $email->SetSubject($components[0]->GetPValue('SUMMARY'));
    $email->SetBody($mime->getMimeParts());
    if (isset($c->iMIP->pretend_email)) {
        $email->Pretend($mime->getMimeHeaders());
    } else {
        if (!isset($c->iMIP->send_email) || !$c->iMIP->send_email) {
            $email->PretendLog($mime->getMimeHeaders());
        } else {
            $email->Send($mime->getMimeHeaders());
        }
    }
}