123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499 |
- #!/usr/bin/env php
- <?php
- # -*- Mode: php -*-
- /*
- * Copyright 2005 - 2014 Zarafa B.V.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- include('mapi/mapi.util.php');
- include('mapi/mapidefs.php');
- include('mapi/mapicode.php');
- include('mapi/mapitags.php');
- include('mapi/mapiguid.php');
- include('mapi/class.meetingrequest.php');
- include('mapi/class.recurrence.php');
- include('mapi/class.freebusypublish.php');
- define('POLICY_PROCESS_MEETING_REQUESTS', 0x0001);
- define('POLICY_DECLINE_RECURRING_MEETING_REQUESTS', 0x0002);
- define('POLICY_DECLINE_CONFLICTING_MEETING_REQUESTS', 0x0004);
- define('RECURRENCE_AVAILABILITY_RANGE', 60 * 60 * 24 * 180); // 180 days
- $DEBUG = 1;
- function parseConfig($configfile)
- {
- $fp = fopen($configfile, "rt");
- if(!$fp)
- return false;
-
- $settings = array();
-
- while($line = fgets($fp)) {
- if($line[0] == '#')
- continue;
-
- $pos = strpos($line, "=");
- if($pos) {
- $key = trim(substr($line, 0, $pos));
- $value = trim(substr($line, $pos+1));
-
- $settings[$key] = $value;
- }
- }
- return $settings;
- }
- function u2w($s)
- {
- return $s;
- }
- if(!function_exists('hex2bin')){
- function hex2bin($data)
- {
- return pack("H*", $data);
- }
- }
- /**
- * Sorts by timestamp, if equal, then end before start. Used by getOverlapDepth()
- */
- function cmp($a, $b)
- {
- if ($a["time"] == $b["time"]) {
- if($a["type"] < $b["type"])
- return 1;
- if($a["type"] > $b["type"])
- return -1;
- return 0;
- }
- return ($a["time"] > $b["time"] ? 1 : -1);
- }
- /**
- * Get the overlap depth of the passed items.
- *
- * This function calculates the maximum number of overlapping appointments at any one time
- * for all the passed appointments, disregarding 'free' appointments.
- */
- function getOverlapDepth($items, $proptags, $goid2)
- {
- $timestamps = Array();
- $cbusy = Array();
- $level = 0;
- $maxlevel = 0;
- foreach($items as $item)
- {
- // Disregard 'free' items and the item that we are updating
- if($item[$proptags['busystatus']] > 0 && $item[$proptags['goid2']] != $goid2) {
- $ts["type"] = 0;
- $ts["time"] = $item[$proptags["startdate"]];
- $timestamps[] = $ts;
- $ts["type"] = 1;
- $ts["time"] = $item[$proptags["duedate"]];
- $timestamps[] = $ts;
- }
- }
- usort($timestamps, "cmp");
- foreach($timestamps as $ts)
- {
- switch ($ts["type"])
- {
- case 0: // Start
- $level++;
- $maxlevel = max($level, $maxlevel);
- break;
- case 1: // End
- $level--;
- break;
- }
- }
- return $maxlevel;
- }
- /**
- * Get unresponded items from the specified folder
- *
- * Looks for messages which have PR_RESPONSE_REQUESTED = TRUE, but no PR_PROCESSED = TRUE and have
- * class IPM.Meeting.Req.*
- */
- function getUnresponded($folder)
- {
- $contents = mapi_folder_getcontentstable($folder);
-
- $restriction = Array(RES_OR,
- Array(
- Array(RES_AND,
- Array(
- Array(RES_PROPERTY, Array(RELOP => RELOP_EQ, ULPROPTAG => PR_MESSAGE_CLASS, VALUE => 'IPM.Schedule.Meeting.Request') ),
- Array(RES_PROPERTY, Array(RELOP => RELOP_EQ, ULPROPTAG => PR_RESPONSE_REQUESTED, VALUE => true ) ),
- Array(RES_PROPERTY, Array(RELOP => RELOP_NE, ULPROPTAG => PR_PROCESSED, VALUE => true ) )
- )
- ),
- Array(RES_AND,
- Array(
- Array(RES_PROPERTY, Array(RELOP => RELOP_EQ, ULPROPTAG => PR_MESSAGE_CLASS, VALUE => 'IPM.Schedule.Meeting.Canceled') ),
- Array(RES_PROPERTY, Array(RELOP => RELOP_NE, ULPROPTAG => PR_PROCESSED, VALUE => true ) )
- )
- )
- )
- );
-
- $rows = mapi_table_queryallrows($contents, Array(PR_ENTRYID), $restriction);
-
- $entryids = array();
-
- foreach ($rows as $row) {
- $entryids[] = $row[PR_ENTRYID];
- }
-
- return $entryids;
- }
- /**
- * Get the capacity of the resource store
- *
- * For rooms, the capacity is always 1. For equipment the capacity is 1 if
- * PR_EMS_AB_ROOM_CAPACITY does not exist, otherwise it is equal to
- * PR_EMS_AB_ROOM_CAPACITY. Other objects always have a capacity of 1.
- */
- function getCapacity($session, $store)
- {
- $storeprops = mapi_getprops($store, array(PR_MAILBOX_OWNER_ENTRYID));
- $ab = mapi_openaddressbook($session);
-
- $mailuser = mapi_ab_openentry($ab, $storeprops[PR_MAILBOX_OWNER_ENTRYID]);
-
- $props = mapi_getprops($mailuser, array(PR_EMS_AB_ROOM_CAPACITY, PR_DISPLAY_TYPE_EX));
-
- if(!isset($props[PR_EMS_AB_ROOM_CAPACITY]) || $props[PR_EMS_AB_ROOM_CAPACITY] <= 0 || !isset($props[PR_DISPLAY_TYPE_EX]) || $props[PR_DISPLAY_TYPE_EX] != DT_EQUIPMENT) {
- $capacity = 1;
- } else {
- $capacity = $props[PR_EMS_AB_ROOM_CAPACITY];
- }
-
- return $capacity;
- }
- /**
- * Get local freebusy message for this store
- */
- function getLocalFBMessage($store)
- {
- $root = mapi_msgstore_openentry($store);
-
- $rootprops = mapi_getprops($root, array(PR_FREEBUSY_ENTRYIDS));
-
- if(!isset($rootprops[PR_FREEBUSY_ENTRYIDS]) || count($rootprops[PR_FREEBUSY_ENTRYIDS]) < 2)
- return false;
-
- $message = mapi_msgstore_openentry($store, $rootprops[PR_FREEBUSY_ENTRYIDS][1]);
-
- return $message;
- }
- /**
- * Get auto-respond policy for a store
- *
- * Can return POLICY_PROCESS_MEETING_REQUESTS, POLICY_DECLINE_RECURRING_MEETING_REQUESTS and POLICY_DECLINE_CONFLICTING_MEETING_REQUESTS
- * in any combination
- *
- */
- function getPolicy($store)
- {
- $localfbmessage = getLocalFBMessage($store);
-
- if(!$localfbmessage) {
- return 0;
- }
-
- $props = mapi_getprops($localfbmessage, array(PR_PROCESS_MEETING_REQUESTS, PR_DECLINE_CONFLICTING_MEETING_REQUESTS, PR_DECLINE_RECURRING_MEETING_REQUESTS));
-
- $flags = 0;
-
- if(isset($props[PR_PROCESS_MEETING_REQUESTS]) && $props[PR_PROCESS_MEETING_REQUESTS])
- $flags |= POLICY_PROCESS_MEETING_REQUESTS;
- if(isset($props[PR_DECLINE_CONFLICTING_MEETING_REQUESTS]) && $props[PR_DECLINE_CONFLICTING_MEETING_REQUESTS])
- $flags |= POLICY_DECLINE_CONFLICTING_MEETING_REQUESTS;
- if(isset($props[PR_DECLINE_RECURRING_MEETING_REQUESTS]) && $props[PR_DECLINE_RECURRING_MEETING_REQUESTS])
- $flags |= POLICY_DECLINE_RECURRING_MEETING_REQUESTS;
-
- return $flags;
- }
- function debugLog($message)
- {
- global $DEBUG;
-
- if($DEBUG) {
- print($message);
- }
- }
- /**
- * Return TRUE if two appointments overlap
- *
- */
- function apptOverlap($appt1, $appt2, $proptags)
- {
- // If appt1 starts after appt2 has ended, no overlap
- if($appt1[$proptags['startdate']] >= $appt2[$proptags['duedate']]) {
- return false;
- }
-
- // If appt2 starts after appt1 has ended, no overlap
- if($appt2[$proptags['startdate']] >= $appt1[$proptags['duedate']]) {
- return false;
- }
- return true;
- }
- /**
- * Return the intersection of a list of appointments with one appointment
- *
- * This means that only the appointments in $list that overlap with $appointment
- * are returned in a list
- */
- function intersectAppointmentWithList($appointment, $list, $proptags)
- {
- $intersect = array();
-
- foreach($list as $item) {
- if(apptOverlap($item, $appointment, $proptags))
- $intersect[] = $item;
- }
-
- return $intersect;
- }
- /**
- * Auto-respond to a meeting request
- *
- * Looks at the incoming meeting request, checks availability for the resource, and responds accordingly
- */
- function autoRespond($session, $store, $entryid, $capacity, $policy)
- {
- debugLog("Processing item with entryid " . bin2hex($entryid) . "\n");
- $calendar = getCalendar($store);
-
- if(!$calendar) {
- debugLog("Unable to open calendar.\n");
- return false;
- }
-
- $proptags = getPropIdsFromStrings($store, array(
- 'startdate' => "PT_SYSTIME:PSETID_Appointment:0x820d",
- 'duedate' => "PT_SYSTIME:PSETID_Appointment:0x820e",
- 'busystatus' => "PT_LONG:PSETID_Appointment:0x8205",
- 'recurring' => "PT_BOOLEAN:PSETID_Appointment:0x8223",
- 'goid2' => "PT_BINARY:PSETID_Meeting:0x23",
- 'subject' => PR_SUBJECT
- ));
-
- $request = mapi_msgstore_openentry($store, $entryid);
-
- if(!$request) {
- debugLog("Unable to open item with entryid " . bin2hex($entryid) . "\n");
- return false;
- }
-
- $mr = new Meetingrequest($store, $request, $session);
-
- if($mr->isMeetingRequest()) {
- $props = mapi_getprops($request, $proptags);
-
- // Check general policy settings
-
- if(isset($props[$proptags['recurring']]) && $props[$proptags['recurring']] && ($policy & POLICY_DECLINE_RECURRING_MEETING_REQUESTS)) {
- $mr->doDecline(true, false, false, _("Recurring meetings are not allowed"));
- debugLog("Declined due to recurrence against non-recurring policy.\n");
- return true;
- }
-
- if($policy & POLICY_DECLINE_CONFLICTING_MEETING_REQUESTS) {
- if(isset($props[$proptags['recurring']]) && $props[$proptags['recurring']]) {
- $rec = new Recurrence($store, $request);
-
- // Only check for conflicts in the first X months, otherwise processing would become too
- // complicated.
- $reqitems = $rec->GetItems($props[$proptags['startdate']], $props[$proptags['startdate']] + RECURRENCE_AVAILABILITY_RANGE);
-
- // Get all the possible conflicts in the coming X months
- debugLog('Getting conflicts from ' . strftime('%x %X', $props[$proptags['startdate']]) . '\n');
- debugLog('Getting conflicts to ' . strftime('%x %X', $props[$proptags['startdate']] + RECURRENCE_AVAILABILITY_RANGE) . '\n');
-
- $possibleconflicts = getCalendarItems($store, $calendar, $props[$proptags['startdate']], $props[$proptags['startdate']] + RECURRENCE_AVAILABILITY_RANGE, $proptags);
- } else {
- $reqitems = array($props);
-
- // Only look at possible conflicts during the duration of the item
- $possibleconflicts = getCalendarItems($store, $calendar, $props[$proptags['startdate']], $props[$proptags['duedate']], $proptags);
- }
-
- $conflicts = array();
-
- foreach($reqitems as $reqitem) {
- // Check for conflicting appointments
- $start = $reqitem[$proptags['startdate']];
- $end = $reqitem[$proptags['duedate']];
-
- debugLog("Checking availability from " . strftime("%x %X", $start) . " to " . strftime("%x %X", $end) . "\n");
-
- $items = intersectAppointmentWithList($reqitem, $possibleconflicts, $proptags);
-
- debugLog("Found " . count($items) . " overlapping records\n");
-
- $currentdepth = getOverlapDepth($items, $proptags, $props[$proptags['goid2']]);
-
- debugLog("Overlap depth is " . $currentdepth . "\n");
-
- if($currentdepth >= $capacity) {
- $conflicts[] = $reqitem;
- }
- }
-
- if(count($conflicts) > 0) {
- // At least one conflict
- if(count($conflicts) == count($reqitems)) {
- $body = _("The requested time slot is unavailable");
- } else {
- $body = _("The requested time slots are unavailble on the following dates:") . "\n\n";
-
- foreach($conflicts as $conflict) {
- $body .= strftime(_("%x %X"), $conflict[$proptags["startdate"]]) . " - " . strftime(_("%x %X"), $conflict[$proptags["duedate"]]) . "\n";
- }
- }
-
- $mr->doDecline(true, false, false, $body);
- debugLog("Declined due to capacity reached.\n");
- return true;
- }
- }
-
- // Checks passed, book the meeting
- $ceid = $mr->doAccept(false, true, true);
- if ($ceid === false) {
- debugLog("Failed to accept: " . sprintf("0x%X", mapi_last_hresult()) . "\n");
- return false;
- }
- debugLog("Accepted.\n");
- // reopen entry to add self as BCC recipient for ZCP-9901
- $calitem = mapi_msgstore_openentry($store, $ceid);
- if ($calitem) {
- $storeprops = mapi_getprops($store, array(PR_MAILBOX_OWNER_ENTRYID));
- $ab = mapi_openaddressbook($session);
- $mailuser = mapi_ab_openentry($ab, $storeprops[PR_MAILBOX_OWNER_ENTRYID]);
- $recip = mapi_getprops($mailuser, array(PR_ACCOUNT, PR_ADDRTYPE, PR_DISPLAY_NAME, PR_DISPLAY_TYPE, PR_DISPLAY_TYPE_EX,
- PR_EMAIL_ADDRESS, PR_ENTRYID, PR_OBJECT_TYPE, PR_SEARCH_KEY, PR_SMTP_ADDRESS));
- $recip[PR_RECIPIENT_ENTRYID] = $recip[PR_ENTRYID];
- $recip[PR_RECIPIENT_FLAGS] = 256 | 1;
- $recip[PR_RECIPIENT_TRACKSTATUS] = 0;
- $recip[PR_RECIPIENT_TYPE] = MAPI_BCC;
- // not setting PidLidAllAttendees, not important
- mapi_message_modifyrecipients($calitem, MODRECIP_ADD, array($recip));
- mapi_message_savechanges($calitem);
- debugLog("Accept updated.\n");
- } else {
- debugLog("Unable to update accepted item.\n");
- }
- return true;
- } else if($mr->isMeetingCancellation()) {
- $mr->processMeetingCancellation();
- $mr->doRemoveFromCalendar();
- debugLog("Removed canceled meeting\n");
- return true;
- }
- }
- // Since the username we are getting from the commandline is always in utf8, we have
- // to force LC_CTYPE to an UTF-8 language. This makes sure that opening the user's store
- // will always open the correct user's store.
- forceUTF8(LC_CTYPE);
- forceUTF8(LC_MESSAGES);
- forceUTF8(LC_TIME);
- textdomain("kopano");
- if(count($argv) != 3 && count($argv) != 4) {
- print "Usage: " . $argv[0] . " <username> <path/to/dagent.cfg> [<entryid>]\n";
- print
- print "If <entryid> is not specified, all unresponded MR's in the inbox are processed\n";
- exit(1);
- }
- $username = $argv[1];
- $config = $argv[2];
- if(isset($argv[3]))
- $entryid = $argv[3];
- $settings = parseConfig($config);
- if(!$settings || !isset($settings["server_socket"])) {
- $settings["server_socket"] = "default:";
- }
- if(isset($settings["sslkey_file"]) && isset($settings["sslkey_pass"]))
- $session = mapi_logon_zarafa($username, "", $settings["server_socket"], $settings["sslkey_file"], $settings["sslkey_pass"]);
- else
- $session = mapi_logon_zarafa($username, "", $settings["server_socket"]);
-
- $store = GetDefaultStore($session);
- $capacity = getCapacity($session, $store);
- $policy = getPolicy($store);
- debugLog("Policy is " . $policy . "\n");
- if(($policy & POLICY_PROCESS_MEETING_REQUESTS) == 0) {
- debugLog("Policy auto-respond not set.\n");
- }
- debugLog("Resource capacity is $capacity\n");
- $inbox = mapi_msgstore_getreceivefolder($store);
- if(isset($entryid)) {
- $items = array (hex2bin($entryid));
- } else {
- $items = getUnresponded($inbox);
- }
- debugLog("Found " . count($items) . " items to process\n");
- foreach ($items as $item) {
- autoRespond($session, $store, $item, $capacity, $policy);
- }
- $storeprops = mapi_getprops($store, array(PR_MAILBOX_OWNER_ENTRYID));
- $fb = new FreeBusyPublish($session, $store, getCalendar($store), $storeprops[PR_MAILBOX_OWNER_ENTRYID]);
- $fb->PublishFB(time() - (7 * 24 * 60 * 60), 6 * 30 * 24 * 60 * 60); // publish from one week ago, 6 months ahead
- exit(0);
- ?>
|