class.meetingrequest.php 127 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081
  1. <?php
  2. /*
  3. * Copyright 2005 - 2016 Zarafa and its licensors
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License, version 3,
  7. * as published by the Free Software Foundation.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU Affero General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. *
  17. */
  18. ?>
  19. <?php
  20. class Meetingrequest {
  21. /*
  22. * NOTE
  23. *
  24. * This class is designed to modify and update meeting request properties
  25. * and to search for linked appointments in the calendar. It does not
  26. * - set standard properties like subject or location
  27. * - commit property changes through savechanges() (except in accept() and decline())
  28. *
  29. * To set all the other properties, just handle the item as any other appointment
  30. * item. You aren't even required to set those properties before or after using
  31. * this class. If you update properties before REsending a meeting request (ie with
  32. * a time change) you MUST first call updateMeetingRequest() so the internal counters
  33. * can be updated. You can then submit the message any way you like.
  34. *
  35. */
  36. /*
  37. * How to use
  38. * ----------
  39. *
  40. * Sending a meeting request:
  41. * - Create appointment item as normal, but as 'tentative'
  42. * (this is the state of the item when the receiving user has received but
  43. * not accepted the item)
  44. * - Set recipients as normally in e-mails
  45. * - Create Meetingrequest class instance
  46. * - Call setMeetingRequest(), this turns on all the meeting request properties in the
  47. * calendar item
  48. * - Call sendMeetingRequest(), this sends a copy of the item with some extra properties
  49. *
  50. * Updating a meeting request:
  51. * - Create Meetingrequest class instance
  52. * - Call updateMeetingRequest(), this updates the counters
  53. * - Call sendMeetingRequest()
  54. *
  55. * Clicking on a an e-mail:
  56. * - Create Meetingrequest class instance
  57. * - Check isMeetingRequest(), if true:
  58. * - Check isLocalOrganiser(), if true then ignore the message
  59. * - Check isInCalendar(), if not call doAccept(true, false, false). This adds the item in your
  60. * calendar as tentative without sending a response
  61. * - Show Accept, Tentative, Decline buttons
  62. * - When the user presses Accept, Tentative or Decline, call doAccept(false, true, true),
  63. * doAccept(true, true, true) or doDecline(true) respectively to really accept or decline and
  64. * send the response. This will remove the request from your inbox.
  65. * - Check isMeetingRequestResponse, if true:
  66. * - Check isLocalOrganiser(), if not true then ignore the message
  67. * - Call processMeetingRequestResponse()
  68. * This will update the trackstatus of all recipients, and set the item to 'busy'
  69. * when all the recipients have accepted.
  70. * - Check isMeetingCancellation(), if true:
  71. * - Check isLocalOrganiser(), if true then ignore the message
  72. * - Check isInCalendar(), if not, then ignore
  73. * Call processMeetingCancellation()
  74. * - Show 'Remove item' button to user
  75. * - When userpresses button, call doCancel(), which removes the item from your
  76. * calendar and deletes the message
  77. */
  78. // All properties for a recipient that are interesting
  79. var $recipprops = Array(
  80. PR_ENTRYID,
  81. PR_DISPLAY_NAME,
  82. PR_EMAIL_ADDRESS,
  83. PR_RECIPIENT_ENTRYID,
  84. PR_RECIPIENT_TYPE,
  85. PR_SEND_INTERNET_ENCODING,
  86. PR_SEND_RICH_INFO,
  87. PR_RECIPIENT_DISPLAY_NAME,
  88. PR_ADDRTYPE,
  89. PR_DISPLAY_TYPE,
  90. PR_RECIPIENT_TRACKSTATUS,
  91. PR_RECIPIENT_TRACKSTATUS_TIME,
  92. PR_RECIPIENT_FLAGS,
  93. PR_ROWID,
  94. PR_OBJECT_TYPE,
  95. PR_SEARCH_KEY
  96. );
  97. /**
  98. * Indication whether the setting of resources in a Meeting Request is success (false) or if it
  99. * has failed (integer).
  100. */
  101. var $errorSetResource;
  102. /**
  103. * Takes a store and a message. The message is an appointment item
  104. * that should be converted into a meeting request or an incoming
  105. * e-mail message that is a meeting request.
  106. *
  107. * The $session variable is optional, but required if the following features
  108. * are to be used:
  109. *
  110. * - Sending meeting requests for meetings that are not in your own store
  111. * - Sending meeting requests to resources, resource availability checking and resource freebusy updates
  112. */
  113. function __construct($store, $message, $session = false, $enableDirectBooking = true)
  114. {
  115. $this->store = $store;
  116. $this->message = $message;
  117. $this->session = $session;
  118. // This variable string saves time information for the MR.
  119. $this->meetingTimeInfo = false;
  120. $this->enableDirectBooking = $enableDirectBooking;
  121. $properties["goid"] = "PT_BINARY:PSETID_Meeting:0x3";
  122. $properties["goid2"] = "PT_BINARY:PSETID_Meeting:0x23";
  123. $properties["type"] = "PT_STRING8:PSETID_Meeting:0x24";
  124. $properties["meetingrecurring"] = "PT_BOOLEAN:PSETID_Meeting:0x5";
  125. $properties["unknown2"] = "PT_BOOLEAN:PSETID_Meeting:0xa";
  126. $properties["attendee_critical_change"] = "PT_SYSTIME:PSETID_Meeting:0x1";
  127. $properties["owner_critical_change"] = "PT_SYSTIME:PSETID_Meeting:0x1a";
  128. $properties["meetingstatus"] = "PT_LONG:PSETID_Appointment:0x8217";
  129. $properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218";
  130. $properties["unknown6"] = "PT_LONG:PSETID_Meeting:0x4";
  131. $properties["replytime"] = "PT_SYSTIME:PSETID_Appointment:0x8220";
  132. $properties["usetnef"] = "PT_BOOLEAN:PSETID_Common:0x8582";
  133. $properties["recurrence_data"] = "PT_BINARY:PSETID_Appointment:0x8216";
  134. $properties["reminderminutes"] = "PT_LONG:PSETID_Common:0x8501";
  135. $properties["reminderset"] = "PT_BOOLEAN:PSETID_Common:0x8503";
  136. $properties["sendasical"] = "PT_BOOLEAN:PSETID_Appointment:0x8200";
  137. $properties["updatecounter"] = "PT_LONG:PSETID_Appointment:0x8201"; // AppointmentSequenceNumber
  138. $properties["last_updatecounter"] = "PT_LONG:PSETID_Appointment:0x8203"; // AppointmentLastSequence
  139. $properties["unknown7"] = "PT_LONG:PSETID_Appointment:0x8202";
  140. $properties["busystatus"] = "PT_LONG:PSETID_Appointment:0x8205";
  141. $properties["intendedbusystatus"] = "PT_LONG:PSETID_Appointment:0x8224";
  142. $properties["start"] = "PT_SYSTIME:PSETID_Appointment:0x820d";
  143. $properties["responselocation"] = "PT_STRING8:PSETID_Meeting:0x2";
  144. $properties["location"] = "PT_STRING8:PSETID_Appointment:0x8208";
  145. $properties["requestsent"] = "PT_BOOLEAN:PSETID_Appointment:0x8229"; // PidLidFInvited, MeetingRequestWasSent
  146. $properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:0x820d";
  147. $properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:0x820e";
  148. $properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
  149. $properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
  150. $properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:0x8223";
  151. $properties["clipstart"] = "PT_SYSTIME:PSETID_Appointment:0x8235";
  152. $properties["clipend"] = "PT_SYSTIME:PSETID_Appointment:0x8236";
  153. $properties["start_recur_date"] = "PT_LONG:PSETID_Meeting:0xD"; // StartRecurTime
  154. $properties["start_recur_time"] = "PT_LONG:PSETID_Meeting:0xE"; // StartRecurTime
  155. $properties["end_recur_date"] = "PT_LONG:PSETID_Meeting:0xF"; // EndRecurDate
  156. $properties["end_recur_time"] = "PT_LONG:PSETID_Meeting:0x10"; // EndRecurTime
  157. $properties["is_exception"] = "PT_BOOLEAN:PSETID_Meeting:0xA"; // LID_IS_EXCEPTION
  158. $properties["apptreplyname"] = "PT_STRING8:PSETID_Appointment:0x8230";
  159. // Propose new time properties
  160. $properties["proposed_start_whole"] = "PT_SYSTIME:PSETID_Appointment:0x8250";
  161. $properties["proposed_end_whole"] = "PT_SYSTIME:PSETID_Appointment:0x8251";
  162. $properties["proposed_duration"] = "PT_LONG:PSETID_Appointment:0x8256";
  163. $properties["counter_proposal"] = "PT_BOOLEAN:PSETID_Appointment:0x8257";
  164. $properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment:0x8232";
  165. $properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:0x8228";
  166. $properties["meetingtype"] = "PT_LONG:PSETID_Meeting:0x26";
  167. $properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:0x8233";
  168. $properties["timezone"] = "PT_STRING8:PSETID_Appointment:0x8234";
  169. $properties["toattendeesstring"] = "PT_STRING8:PSETID_Appointment:0x823B";
  170. $properties["ccattendeesstring"] = "PT_STRING8:PSETID_Appointment:0x823C";
  171. $this->proptags = getPropIdsFromStrings($store, $properties);
  172. }
  173. /**
  174. * Sets the direct booking property. This is an alternative to the setting of the direct booking
  175. * property through the constructor. However, setting it in the constructor is prefered.
  176. * @param Boolean $directBookingSetting
  177. *
  178. */
  179. function setDirectBooking($directBookingSetting)
  180. {
  181. $this->enableDirectBooking = $directBookingSetting;
  182. }
  183. /**
  184. * Returns TRUE if the message pointed to is an incoming meeting request and should
  185. * therefore be replied to with doAccept or doDecline()
  186. */
  187. function isMeetingRequest()
  188. {
  189. $props = mapi_getprops($this->message, Array(PR_MESSAGE_CLASS));
  190. if(isset($props[PR_MESSAGE_CLASS]) && $props[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Request")
  191. return true;
  192. }
  193. /**
  194. * Returns TRUE if the message pointed to is a returning meeting request response
  195. */
  196. function isMeetingRequestResponse()
  197. {
  198. $props = mapi_getprops($this->message, Array(PR_MESSAGE_CLASS));
  199. if(isset($props[PR_MESSAGE_CLASS]) && strpos($props[PR_MESSAGE_CLASS], "IPM.Schedule.Meeting.Resp") === 0)
  200. return true;
  201. }
  202. /**
  203. * Returns TRUE if the message pointed to is a cancellation request
  204. */
  205. function isMeetingCancellation()
  206. {
  207. $props = mapi_getprops($this->message, Array(PR_MESSAGE_CLASS));
  208. if(isset($props[PR_MESSAGE_CLASS]) && $props[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Canceled")
  209. return true;
  210. }
  211. /**
  212. * Process an incoming meeting request response as Delegate. This will updates the appointment
  213. * in Organiser's calendar.
  214. * @returns the entryids(storeid, parententryid, entryid, also basedate if response is occurrence)
  215. * of corresponding meeting in Calendar
  216. */
  217. function processMeetingRequestResponseAsDelegate()
  218. {
  219. if(!$this->isMeetingRequestResponse())
  220. return;
  221. $messageprops = mapi_getprops($this->message);
  222. $goid2 = $messageprops[$this->proptags['goid2']];
  223. if(!isset($goid2) || !isset($messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS]))
  224. return;
  225. // Find basedate in GlobalID(0x3), this can be a response for an occurrence
  226. $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
  227. if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])) {
  228. $delegatorStore = $this->getDelegatorStore($messageprops);
  229. $userStore = $delegatorStore['store'];
  230. $calFolder = $delegatorStore['calFolder'];
  231. if($calFolder){
  232. $calendaritems = $this->findCalendarItems($goid2, $calFolder);
  233. // $calendaritems now contains the ENTRYIDs of all the calendar items to which
  234. // this meeting request points.
  235. // Open the calendar items, and update all the recipients of the calendar item that match
  236. // the email address of the response.
  237. if (!empty($calendaritems))
  238. return $this->processResponse($userStore, $calendaritems[0], $basedate, $messageprops);
  239. else
  240. return false;
  241. }
  242. }
  243. }
  244. /**
  245. * Process an incoming meeting request response. This updates the appointment
  246. * in your calendar to show whether the user has accepted or declined.
  247. * @returns the entryids(storeid, parententryid, entryid, also basedate if response is occurrence)
  248. * of corresponding meeting in Calendar
  249. */
  250. function processMeetingRequestResponse()
  251. {
  252. if(!$this->isLocalOrganiser())
  253. return;
  254. if(!$this->isMeetingRequestResponse())
  255. return;
  256. // Get information we need from the response message
  257. $messageprops = mapi_getprops($this->message, Array(
  258. $this->proptags['goid'],
  259. $this->proptags['goid2'],
  260. PR_OWNER_APPT_ID,
  261. PR_SENT_REPRESENTING_EMAIL_ADDRESS,
  262. PR_SENT_REPRESENTING_NAME,
  263. PR_SENT_REPRESENTING_ADDRTYPE,
  264. PR_SENT_REPRESENTING_ENTRYID,
  265. PR_MESSAGE_DELIVERY_TIME,
  266. PR_MESSAGE_CLASS,
  267. PR_PROCESSED,
  268. $this->proptags['proposed_start_whole'],
  269. $this->proptags['proposed_end_whole'],
  270. $this->proptags['proposed_duration'],
  271. $this->proptags['counter_proposal'],
  272. $this->proptags['attendee_critical_change']));
  273. $goid2 = $messageprops[$this->proptags['goid2']];
  274. if(!isset($goid2) || !isset($messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS]))
  275. return;
  276. // Find basedate in GlobalID(0x3), this can be a response for an occurrence
  277. $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
  278. $calendaritems = $this->findCalendarItems($goid2);
  279. // $calendaritems now contains the ENTRYIDs of all the calendar items to which
  280. // this meeting request points.
  281. // Open the calendar items, and update all the recipients of the calendar item that match
  282. // the email address of the response.
  283. if (!empty($calendaritems))
  284. return $this->processResponse($this->store, $calendaritems[0], $basedate, $messageprops);
  285. else
  286. return false;
  287. }
  288. /**
  289. * Process every incoming MeetingRequest response.This updates the appointment
  290. * in your calendar to show whether the user has accepted or declined.
  291. *@param resource $store contains the userStore in which the meeting is created
  292. *@param $entryid contains the ENTRYID of the calendar items to which this meeting request points.
  293. *@param boolean $basedate if present the create an exception
  294. *@param array $messageprops contains m3/17/2010essage properties.
  295. *@return entryids(storeid, parententryid, entryid, also basedate if response is occurrence) of corresponding meeting in Calendar
  296. */
  297. function processResponse($store, $entryid, $basedate, $messageprops)
  298. {
  299. $data = array();
  300. $senderentryid = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
  301. $messageclass = $messageprops[PR_MESSAGE_CLASS];
  302. $deliverytime = $messageprops[PR_MESSAGE_DELIVERY_TIME];
  303. // Open the calendar item, find the sender in the recipient table and update all the recipients of the calendar item that match
  304. // the email address of the response.
  305. $calendaritem = mapi_msgstore_openentry($store, $entryid);
  306. $calendaritemProps = mapi_getprops($calendaritem, array($this->proptags['recurring'], PR_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_ENTRYID, $this->proptags['updatecounter']));
  307. $data["storeid"] = bin2hex($calendaritemProps[PR_STORE_ENTRYID]);
  308. $data["parententryid"] = bin2hex($calendaritemProps[PR_PARENT_ENTRYID]);
  309. $data["entryid"] = bin2hex($calendaritemProps[PR_ENTRYID]);
  310. $data["basedate"] = $basedate;
  311. $data["updatecounter"] = isset($calendaritemProps[$this->proptags['updatecounter']]) ? $calendaritemProps[$this->proptags['updatecounter']] : 0;
  312. /**
  313. * Check if meeting is updated or not in organizer's calendar
  314. */
  315. $data["meeting_updated"] = $this->isMeetingUpdated();
  316. if(isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
  317. // meeting is already processed
  318. return $data;
  319. } else {
  320. mapi_setprops($this->message, Array(PR_PROCESSED => true));
  321. mapi_savechanges($this->message);
  322. }
  323. // if meeting is updated in organizer's calendar then we don't need to process
  324. // old response
  325. if ($data['meeting_updated'] === true)
  326. return $data;
  327. // If basedate is found, then create/modify exception msg and do processing
  328. if ($basedate && $calendaritemProps[$this->proptags['recurring']]) {
  329. $recurr = new Recurrence($store, $calendaritem);
  330. // Copy properties from meeting request
  331. $exception_props = mapi_getprops($this->message, array(PR_OWNER_APPT_ID,
  332. $this->proptags['proposed_start_whole'],
  333. $this->proptags['proposed_end_whole'],
  334. $this->proptags['proposed_duration'],
  335. $this->proptags['counter_proposal']
  336. ));
  337. // Create/modify exception
  338. if($recurr->isException($basedate)) {
  339. $recurr->modifyException($exception_props, $basedate);
  340. } else {
  341. // When we are creating an exception we need copy recipients from main recurring item
  342. $recipTable = mapi_message_getrecipienttable($calendaritem);
  343. $recips = mapi_table_queryallrows($recipTable, $this->recipprops);
  344. // Retrieve actual start/due dates from calendar item.
  345. $exception_props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
  346. $exception_props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
  347. $recurr->createException($exception_props, $basedate, false, $recips);
  348. }
  349. mapi_message_savechanges($calendaritem);
  350. $attach = $recurr->getExceptionAttachment($basedate);
  351. if (!$attach)
  352. return $false;
  353. $recurringItem = $calendaritem;
  354. $calendaritem = mapi_attach_openobj($attach, MAPI_MODIFY);
  355. }
  356. // Get the recipients of the calendar item
  357. $reciptable = mapi_message_getrecipienttable($calendaritem);
  358. $recipients = mapi_table_queryallrows($reciptable, $this->recipprops);
  359. // FIXME we should look at the updatecounter property and compare it
  360. // to the counter in the recipient to see if this update is actually
  361. // newer than the status in the calendar item
  362. $found = false;
  363. $totalrecips = 0;
  364. $acceptedrecips = 0;
  365. foreach($recipients as $recipient) {
  366. $totalrecips++;
  367. if(isset($recipient[PR_ENTRYID]) && $this->compareABEntryIDs($recipient[PR_ENTRYID],$senderentryid)) {
  368. $found = true;
  369. /**
  370. * If value of attendee_critical_change on meeting response mail is less than PR_RECIPIENT_TRACKSTATUS_TIME
  371. * on the corresponding recipientRow of meeting then we ignore this response mail.
  372. */
  373. if (isset($recipient[PR_RECIPIENT_TRACKSTATUS_TIME]) && ($messageprops[$this->proptags['attendee_critical_change']] < $recipient[PR_RECIPIENT_TRACKSTATUS_TIME]))
  374. continue;
  375. // The email address matches, update the row
  376. $recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
  377. $recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $messageprops[$this->proptags['attendee_critical_change']];
  378. // If this is a counter proposal, set the proposal properties in the recipient row
  379. if(isset($messageprops[$this->proptags['counter_proposal']]) && $messageprops[$this->proptags['counter_proposal']]){
  380. $recipient[PR_PROPOSENEWTIME_START] = $messageprops[$this->proptags['proposed_start_whole']];
  381. $recipient[PR_PROPOSENEWTIME_END] = $messageprops[$this->proptags['proposed_end_whole']];
  382. $recipient[PR_PROPOSEDNEWTIME] = $messageprops[$this->proptags['counter_proposal']];
  383. }
  384. mapi_message_modifyrecipients($calendaritem, MODRECIP_MODIFY, Array($recipient));
  385. }
  386. if(isset($recipient[PR_RECIPIENT_TRACKSTATUS]) && $recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted)
  387. $acceptedrecips++;
  388. }
  389. // If the recipient was not found in the original calendar item,
  390. // then add the recpient as a new optional recipient
  391. if(!$found) {
  392. $recipient = Array();
  393. $recipient[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
  394. $recipient[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
  395. $recipient[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
  396. $recipient[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
  397. $recipient[PR_RECIPIENT_TYPE] = MAPI_CC;
  398. $recipient[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
  399. $recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
  400. $recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $deliverytime;
  401. // If this is a counter proposal, set the proposal properties in the recipient row
  402. if(isset($messageprops[$this->proptags['counter_proposal']])){
  403. $recipient[PR_PROPOSENEWTIME_START] = $messageprops[$this->proptags['proposed_start_whole']];
  404. $recipient[PR_PROPOSENEWTIME_END] = $messageprops[$this->proptags['proposed_end_whole']];
  405. $recipient[PR_PROPOSEDNEWTIME] = $messageprops[$this->proptags['counter_proposal']];
  406. }
  407. mapi_message_modifyrecipients($calendaritem, MODRECIP_ADD, Array($recipient));
  408. $totalrecips++;
  409. if($recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted)
  410. $acceptedrecips++;
  411. }
  412. //TODO: Upate counter proposal number property on message
  413. /*
  414. If it is the first time this attendee has proposed a new date/time, increment the value of the PidLidAppointmentProposalNumber property on the organizer’s meeting object, by 0x00000001. If this property did not previously exist on the organizer’s meeting object, it MUST be set with a value of 0x00000001.
  415. */
  416. // If this is a counter proposal, set the counter proposal indicator boolean
  417. if(isset($messageprops[$this->proptags['counter_proposal']])){
  418. $props = Array();
  419. if ($messageprops[$this->proptags['counter_proposal']])
  420. $props[$this->proptags['counter_proposal']] = true;
  421. else
  422. $props[$this->proptags['counter_proposal']] = false;
  423. mapi_message_setprops($calendaritem, $props);
  424. }
  425. mapi_message_savechanges($calendaritem);
  426. if (isset($attach)) {
  427. mapi_message_savechanges($attach);
  428. mapi_message_savechanges($recurringItem);
  429. }
  430. return $data;
  431. }
  432. /**
  433. * Process an incoming meeting request cancellation. This updates the
  434. * appointment in your calendar to show that the meeting has been cancelled.
  435. */
  436. function processMeetingCancellation()
  437. {
  438. if($this->isLocalOrganiser())
  439. return;
  440. if(!$this->isMeetingCancellation())
  441. return;
  442. if(!$this->isInCalendar())
  443. return;
  444. $listProperties = $this->proptags;
  445. $listProperties['subject'] = PR_SUBJECT;
  446. $listProperties['sent_representing_name'] = PR_SENT_REPRESENTING_NAME;
  447. $listProperties['sent_representing_address_type'] = PR_SENT_REPRESENTING_ADDRTYPE;
  448. $listProperties['sent_representing_email_address'] = PR_SENT_REPRESENTING_EMAIL_ADDRESS;
  449. $listProperties['sent_representing_entryid'] = PR_SENT_REPRESENTING_ENTRYID;
  450. $listProperties['sent_representing_search_key'] = PR_SENT_REPRESENTING_SEARCH_KEY;
  451. $listProperties['rcvd_representing_name'] = PR_RCVD_REPRESENTING_NAME;
  452. $messageprops = mapi_getprops($this->message, $listProperties);
  453. $store = $this->store;
  454. $goid = $messageprops[$this->proptags['goid']]; //GlobalID (0x3)
  455. if(!isset($goid))
  456. return;
  457. if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])){
  458. $delegatorStore = $this->getDelegatorStore($messageprops);
  459. $store = $delegatorStore['store'];
  460. $calFolder = $delegatorStore['calFolder'];
  461. } else {
  462. $calFolder = $this->openDefaultCalendar();
  463. }
  464. // First, find the items in the calendar by GOID
  465. $calendaritems = $this->findCalendarItems($goid, $calFolder);
  466. $basedate = $this->getBasedateFromGlobalID($goid);
  467. if ($basedate) {
  468. // Calendaritems with GlobalID were not found, so find main recurring item using CleanGlobalID(0x23)
  469. if (empty($calendaritems)) {
  470. // This meeting req is of an occurrance
  471. $goid2 = $messageprops[$this->proptags['goid2']];
  472. // First, find the items in the calendar by GOID
  473. $calendaritems = $this->findCalendarItems($goid2);
  474. foreach($calendaritems as $entryid) {
  475. // Open each calendar item and set the properties of the cancellation object
  476. $calendaritem = mapi_msgstore_openentry($store, $entryid);
  477. if ($calendaritem){
  478. $calendaritemProps = mapi_getprops($calendaritem, array($this->proptags['recurring']));
  479. if ($calendaritemProps[$this->proptags['recurring']]){
  480. $recurr = new Recurrence($store, $calendaritem);
  481. // Set message class
  482. $messageprops[PR_MESSAGE_CLASS] = 'IPM.Appointment';
  483. if($recurr->isException($basedate))
  484. $recurr->modifyException($messageprops, $basedate);
  485. else
  486. $recurr->createException($messageprops, $basedate);
  487. }
  488. mapi_savechanges($calendaritem);
  489. }
  490. }
  491. }
  492. }
  493. if (!isset($calendaritem)) {
  494. foreach($calendaritems as $entryid) {
  495. // Open each calendar item and set the properties of the cancellation object
  496. $calendaritem = mapi_msgstore_openentry($store, $entryid);
  497. mapi_message_setprops($calendaritem, $messageprops);
  498. mapi_savechanges($calendaritem);
  499. }
  500. }
  501. }
  502. /**
  503. * Returns true if the item is already in the calendar
  504. */
  505. function isInCalendar() {
  506. $messageprops = mapi_getprops($this->message, Array($this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_NAME));
  507. $goid = $messageprops[$this->proptags['goid']];
  508. $goid2 = $messageprops[$this->proptags['goid2']];
  509. $basedate = $this->getBasedateFromGlobalID($goid);
  510. if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])){
  511. $delegatorStore = $this->getDelegatorStore($messageprops);
  512. $calFolder = $delegatorStore['calFolder'];
  513. } else {
  514. $calFolder = $this->openDefaultCalendar();
  515. }
  516. /**
  517. * If basedate is found in globalID, then there are two possibilities.
  518. * case 1) User has only this occurrence OR
  519. * case 2) User has recurring item and has received an update for an occurrence
  520. */
  521. if ($basedate) {
  522. // First try with GlobalID(0x3) (case 1)
  523. $entryid = $this->findCalendarItems($goid, $calFolder);
  524. // If not found then try with CleanGlobalID(0x23) (case 2)
  525. if (!is_array($entryid))
  526. $entryid = $this->findCalendarItems($goid2, $calFolder);
  527. } else {
  528. $entryid = $this->findCalendarItems($goid2, $calFolder);
  529. }
  530. return is_array($entryid);
  531. }
  532. /**
  533. * Accepts the meeting request by moving the item to the calendar
  534. * and sending a confirmation message back to the sender. If $tentative
  535. * is TRUE, then the item is accepted tentatively. After accepting, you
  536. * can't use this class instance any more. The message is closed. If you
  537. * specify TRUE for 'move', then the item is actually moved (from your
  538. * inbox probably) to the calendar. If you don't, it is copied into
  539. * your calendar.
  540. *@param boolean $tentative true if user as tentative accepted the meeting
  541. *@param boolean $sendresponse true if a response has to be send to organizer
  542. *@param boolean $move true if the meeting request should be moved to the deleted items after processing
  543. *@param string $newProposedStartTime contains starttime if user has proposed other time
  544. *@param string $newProposedEndTime contains endtime if user has proposed other time
  545. *@param string $basedate start of day of occurrence for which user has accepted the recurrent meeting
  546. *@return string $entryid entryid of item which created/updated in calendar
  547. */
  548. function doAccept($tentative, $sendresponse, $move, $newProposedStartTime=false, $newProposedEndTime=false, $body=false, $userAction = false, $store=false, $basedate = false)
  549. {
  550. if($this->isLocalOrganiser())
  551. return false;
  552. // Remove any previous calendar items with this goid and appt id
  553. $messageprops = mapi_getprops($this->message, Array(PR_ENTRYID, PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['goid2'], PR_OWNER_APPT_ID, $this->proptags['updatecounter'], PR_PROCESSED, $this->proptags['recurring'], $this->proptags['intendedbusystatus'], PR_RCVD_REPRESENTING_NAME));
  554. /**
  555. * if this function is called automatically with meeting request object then there will be
  556. * two possibilitites
  557. * 1) meeting request is opened first time, in this case make a tentative appointment in
  558. recipient's calendar
  559. * 2) after this every subsequest request to open meeting request will not do any processing
  560. */
  561. if($messageprops[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Request" && $userAction == false) {
  562. if(isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
  563. // if meeting request is already processed then don't do anything
  564. return false;
  565. }
  566. mapi_setprops($this->message, Array(PR_PROCESSED => true));
  567. mapi_message_savechanges($this->message);
  568. }
  569. // If this meeting request is received by a delegate then open delegator's store.
  570. if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])) {
  571. $delegatorStore = $this->getDelegatorStore($messageprops);
  572. $store = $delegatorStore['store'];
  573. $calFolder = $delegatorStore['calFolder'];
  574. } else {
  575. $calFolder = $this->openDefaultCalendar();
  576. $store = $this->store;
  577. }
  578. return $this->accept($tentative, $sendresponse, $move, $newProposedStartTime, $newProposedEndTime, $body, $userAction, $store, $calFolder, $basedate);
  579. }
  580. function accept($tentative, $sendresponse, $move, $newProposedStartTime=false, $newProposedEndTime=false, $body=false, $userAction = false, $store, $calFolder, $basedate = false)
  581. {
  582. $messageprops = mapi_getprops($this->message);
  583. $isDelegate = false;
  584. if (isset($messageprops[PR_DELEGATED_BY_RULE]))
  585. $isDelegate = true;
  586. $goid = $messageprops[$this->proptags['goid2']];
  587. // Retrieve basedate from globalID, if it is not recieved as argument
  588. if (!$basedate)
  589. $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
  590. if ($sendresponse)
  591. $this->createResponse($tentative ? olResponseTentative : olResponseAccepted, $newProposedStartTime, $newProposedEndTime, $body, $store, $basedate, $calFolder);
  592. $entryids = $this->findCalendarItems($goid, $calFolder);
  593. if(is_array($entryids)) {
  594. // Only check the first, there should only be one anyway...
  595. $previtem = mapi_msgstore_openentry($store, $entryids[0]);
  596. $prevcounterprops = mapi_getprops($previtem, array($this->proptags['updatecounter']));
  597. // Check if the existing item has an updatecounter that is lower than the request we are processing. If not, then we ignore this call, since the
  598. // meeting request is out of date.
  599. /*
  600. if(message_counter < appointment_counter) do_nothing
  601. if(message_counter == appointment_counter) do_something_if_the_user_tells_us (userAction == true)
  602. if(message_counter > appointment_counter) do_something_even_automatically
  603. */
  604. if(isset($prevcounterprops[$this->proptags['updatecounter']]) && $messageprops[$this->proptags['updatecounter']] < $prevcounterprops[$this->proptags['updatecounter']]) {
  605. return false;
  606. } else if(isset($prevcounterprops[$this->proptags['updatecounter']]) && $messageprops[$this->proptags['updatecounter']] == $prevcounterprops[$this->proptags['updatecounter']]) {
  607. if ($userAction == false && !$basedate)
  608. return false;
  609. }
  610. }
  611. // set counter proposal properties in calendar item when proposing new time
  612. // @FIXME this can be moved before call to createResponse function so that function doesn't need to recalculate duration
  613. $proposeNewTimeProps = array();
  614. if($newProposedStartTime && $newProposedEndTime) {
  615. $proposeNewTimeProps[$this->proptags['proposed_start_whole']] = $newProposedStartTime;
  616. $proposeNewTimeProps[$this->proptags['proposed_end_whole']] = $newProposedEndTime;
  617. $proposeNewTimeProps[$this->proptags['proposed_duration']] = round($newProposedEndTime - $newProposedStartTime) / 60;
  618. $proposeNewTimeProps[$this->proptags['counter_proposal']] = true;
  619. }
  620. /**
  621. * Further processing depends on what user is receiving. User can receive recurring item, a single occurrence or a normal meeting.
  622. * 1) If meeting req is of recurrence then we find all the occurrence in calendar because in past user might have recivied one or few occurrences.
  623. * 2) If single occurrence then find occurrence itself using globalID and if item is not found then user cleanGlobalID to find main recurring item
  624. * 3) Normal meeting req are handled normally has they were handled previously.
  625. *
  626. * Also user can respond(accept/decline) to item either from previewpane or from calendar by opening the item. If user is responding the meeting from previewpane
  627. * and that item is not found in calendar then item is move else item is opened and all properties, attachments and recipient are copied from meeting request.
  628. * If user is responding from calendar then item is opened and properties are set such as meetingstatus, responsestatus, busystatus etc.
  629. */
  630. if ($messageprops[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Request") {
  631. // While processing the item mark it as read.
  632. mapi_message_setreadflag($this->message, SUPPRESS_RECEIPT);
  633. // This meeting request item is recurring, so find all occurrences and saves them all as exceptions to this meeting request item.
  634. if ($messageprops[$this->proptags['recurring']] == true) {
  635. $calendarItem = false;
  636. // Find main recurring item based on GlobalID (0x3)
  637. $items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
  638. if (is_array($items))
  639. foreach($items as $key => $entryid)
  640. $calendarItem = mapi_msgstore_openentry($store, $entryid);
  641. // Recurring item not found, so create new meeting in Calendar
  642. if (!$calendarItem)
  643. $calendarItem = mapi_folder_createmessage($calFolder);
  644. // Copy properties
  645. $props = mapi_getprops($this->message);
  646. $props[PR_MESSAGE_CLASS] = 'IPM.Appointment';
  647. $props[$this->proptags['meetingstatus']] = olMeetingReceived;
  648. // when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
  649. $props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
  650. if (isset($props[$this->proptags['intendedbusystatus']])) {
  651. if ($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree)
  652. $props[$this->proptags['busystatus']] = $tentative;
  653. else
  654. $props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']];
  655. // we already have intendedbusystatus value in $props so no need to copy it
  656. } else {
  657. $props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
  658. }
  659. if ($userAction)
  660. // if user has responded then set replytime
  661. $props[$this->proptags['replytime']] = time();
  662. mapi_setprops($calendarItem, $props);
  663. // Copy attachments too
  664. $this->replaceAttachments($this->message, $calendarItem);
  665. // Copy recipients too
  666. $this->replaceRecipients($this->message, $calendarItem, $isDelegate);
  667. // Find all occurrences based on CleanGlobalID (0x23)
  668. $items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
  669. if (is_array($items)) {
  670. // Save all existing occurrence as exceptions
  671. foreach($items as $entryid) {
  672. // Open occurrence
  673. $occurrenceItem = mapi_msgstore_openentry($store, $entryid);
  674. // Save occurrence into main recurring item as exception
  675. if ($occurrenceItem) {
  676. $occurrenceItemProps = mapi_getprops($occurrenceItem, array($this->proptags['goid'], $this->proptags['recurring']));
  677. // Find basedate of occurrence item
  678. $basedate = $this->getBasedateFromGlobalID($occurrenceItemProps[$this->proptags['goid']]);
  679. if ($basedate && $occurrenceItemProps[$this->proptags['recurring']] != true)
  680. $this->acceptException($calendarItem, $occurrenceItem, $basedate, true, $tentative, $userAction, $store, $isDelegate);
  681. }
  682. }
  683. }
  684. mapi_savechanges($calendarItem);
  685. if ($move) {
  686. $wastebasket = $this->openDefaultWastebasket();
  687. mapi_folder_copymessages($calFolder, Array($props[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE);
  688. }
  689. $entryid = $props[PR_ENTRYID];
  690. } else {
  691. /**
  692. * This meeting request is not recurring, so can be an exception or normal meeting.
  693. * If exception then find main recurring item and update exception
  694. * If main recurring item is not found then put exception into Calendar as normal meeting.
  695. */
  696. $calendarItem = false;
  697. // We found basedate in GlobalID of this meeting request, so this meeting request if for an occurrence.
  698. if ($basedate) {
  699. // Find main recurring item from CleanGlobalID of this meeting request
  700. $items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
  701. if (is_array($items))
  702. foreach($items as $key => $entryid)
  703. $calendarItem = mapi_msgstore_openentry($store, $entryid);
  704. // Main recurring item is found, so now update exception
  705. if ($calendarItem) {
  706. $this->acceptException($calendarItem, $this->message, $basedate, $move, $tentative, $userAction, $store, $isDelegate);
  707. $calendarItemProps = mapi_getprops($calendarItem, array(PR_ENTRYID));
  708. $entryid = $calendarItemProps[PR_ENTRYID];
  709. }
  710. }
  711. if (!$calendarItem) {
  712. $items = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
  713. if (is_array($items))
  714. mapi_folder_deletemessages($calFolder, $items);
  715. if ($move) {
  716. // All we have to do is open the default calendar,
  717. // set the message class correctly to be an appointment item
  718. // and move it to the calendar folder
  719. $sourcefolder = $this->openParentFolder();
  720. /* create a new calendar message, and copy the message to there,
  721. since we want to delete (move to wastebasket) the original message */
  722. $old_entryid = mapi_getprops($this->message, Array(PR_ENTRYID));
  723. $calmsg = mapi_folder_createmessage($calFolder);
  724. mapi_copyto($this->message, array(), array(), $calmsg); /* includes attachments and recipients */
  725. /* release old message */
  726. $message = null;
  727. $calItemProps = Array();
  728. $calItemProps[PR_MESSAGE_CLASS] = "IPM.Appointment";
  729. if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
  730. if ($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree)
  731. $calItemProps[$this->proptags['busystatus']] = $tentative;
  732. else
  733. $calItemProps[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
  734. $calItemProps[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
  735. } else {
  736. $calItemProps[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
  737. }
  738. // when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
  739. $calItemProps[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
  740. if ($userAction)
  741. // if user has responded then set replytime
  742. $calItemProps[$this->proptags['replytime']] = time();
  743. mapi_setprops($calmsg, $proposeNewTimeProps + $calItemProps);
  744. // get properties which stores owner information in meeting request mails
  745. $props = mapi_getprops($calmsg, array(PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY));
  746. // add owner to recipient table
  747. $recips = array();
  748. $this->addOrganizer($props, $recips);
  749. if($isDelegate) {
  750. /**
  751. * If user is delegate then remove that user from recipienttable of the MR.
  752. * and delegate MR mail doesn't contain any of the attendees in recipient table.
  753. * So, other required and optional attendees are added from
  754. * toattendeesstring and ccattendeesstring properties.
  755. */
  756. $this->setRecipsFromString($recips, $messageprops[$this->proptags['toattendeesstring']], MAPI_TO);
  757. $this->setRecipsFromString($recips, $messageprops[$this->proptags['ccattendeesstring']], MAPI_CC);
  758. mapi_message_modifyrecipients($calmsg, 0, $recips);
  759. } else {
  760. mapi_message_modifyrecipients($calmsg, MODRECIP_ADD, $recips);
  761. }
  762. mapi_message_savechanges($calmsg);
  763. // Move the message to the wastebasket
  764. $wastebasket = $this->openDefaultWastebasket();
  765. mapi_folder_copymessages($sourcefolder, array($old_entryid[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE);
  766. $messageprops = mapi_getprops($calmsg, array(PR_ENTRYID));
  767. $entryid = $messageprops[PR_ENTRYID];
  768. } else {
  769. // Create a new appointment with duplicate properties and recipient, but as an IPM.Appointment
  770. $new = mapi_folder_createmessage($calFolder);
  771. $props = mapi_getprops($this->message);
  772. $props[PR_MESSAGE_CLASS] = "IPM.Appointment";
  773. // when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
  774. $props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
  775. if (isset($props[$this->proptags['intendedbusystatus']])) {
  776. if ($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree)
  777. $props[$this->proptags['busystatus']] = $tentative;
  778. else
  779. $props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']];
  780. // we already have intendedbusystatus value in $props so no need to copy it
  781. } else {
  782. $props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
  783. }
  784. if ($userAction)
  785. // if user has responded then set replytime
  786. $props[$this->proptags['replytime']] = time();
  787. mapi_setprops($new, $proposeNewTimeProps + $props);
  788. $reciptable = mapi_message_getrecipienttable($this->message);
  789. $recips = array();
  790. if(!$isDelegate)
  791. $recips = mapi_table_queryallrows($reciptable, $this->recipprops);
  792. $this->addOrganizer($props, $recips);
  793. if($isDelegate) {
  794. /**
  795. * If user is delegate then remove that user from recipienttable of the MR.
  796. * and delegate MR mail doesn't contain any of the attendees in recipient table.
  797. * So, other required and optional attendees are added from
  798. * toattendeesstring and ccattendeesstring properties.
  799. */
  800. $this->setRecipsFromString($recips, $messageprops[$this->proptags['toattendeesstring']], MAPI_TO);
  801. $this->setRecipsFromString($recips, $messageprops[$this->proptags['ccattendeesstring']], MAPI_CC);
  802. mapi_message_modifyrecipients($new, 0, $recips);
  803. } else {
  804. mapi_message_modifyrecipients($new, MODRECIP_ADD, $recips);
  805. }
  806. // Copy attachments too
  807. $this->replaceAttachments($this->message, $new);
  808. mapi_message_savechanges($new);
  809. $props = mapi_getprops($new, array(PR_ENTRYID));
  810. $entryid = $props[PR_ENTRYID];
  811. }
  812. }
  813. }
  814. } else {
  815. // Here only properties are set on calendaritem, because user is responding from calendar.
  816. $props = array();
  817. $props[$this->proptags['responsestatus']] = $tentative ? olResponseTentative : olResponseAccepted;
  818. if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
  819. if ($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree)
  820. $props[$this->proptags['busystatus']] = $tentative;
  821. else
  822. $props[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
  823. $props[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
  824. } else {
  825. $props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
  826. }
  827. $props[$this->proptags['meetingstatus']] = olMeetingReceived;
  828. $props[$this->proptags['replytime']] = time();
  829. if ($basedate) {
  830. $recurr = new Recurrence($store, $this->message);
  831. // Copy recipients list
  832. $reciptable = mapi_message_getrecipienttable($this->message);
  833. $recips = mapi_table_queryallrows($reciptable, $this->recipprops);
  834. if($recurr->isException($basedate)) {
  835. $recurr->modifyException($proposeNewTimeProps + $props, $basedate, $recips);
  836. } else {
  837. $props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
  838. $props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
  839. $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
  840. $props[PR_SENT_REPRESENTING_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
  841. $props[PR_SENT_REPRESENTING_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
  842. $props[PR_SENT_REPRESENTING_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
  843. $props[PR_SENT_REPRESENTING_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
  844. $recurr->createException($proposeNewTimeProps + $props, $basedate, false, $recips);
  845. }
  846. } else {
  847. mapi_setprops($this->message, $proposeNewTimeProps + $props);
  848. }
  849. mapi_savechanges($this->message);
  850. $entryid = $messageprops[PR_ENTRYID];
  851. }
  852. return $entryid;
  853. }
  854. /**
  855. * Declines the meeting request by moving the item to the deleted
  856. * items folder and sending a decline message. After declining, you
  857. * can't use this class instance any more. The message is closed.
  858. * When an occurrence is decline then false is returned because that
  859. * occurrence is deleted not the recurring item.
  860. *
  861. *@param boolean $sendresponse true if a response has to be sent to organizer
  862. *@param resource $store MAPI_store of user
  863. *@param string $basedate if specified contains starttime of day of an occurrence
  864. *@return boolean true if item is deleted from Calendar else false
  865. */
  866. function doDecline($sendresponse, $store=false, $basedate = false, $body = false)
  867. {
  868. $result = true;
  869. $calendaritem = false;
  870. if($this->isLocalOrganiser())
  871. return;
  872. // Remove any previous calendar items with this goid and appt id
  873. $messageprops = mapi_getprops($this->message, Array($this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_NAME));
  874. // If this meeting request is received by a delegate then open delegator's store.
  875. if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])) {
  876. $delegatorStore = $this->getDelegatorStore($messageprops);
  877. $store = $delegatorStore['store'];
  878. $calFolder = $delegatorStore['calFolder'];
  879. } else {
  880. $calFolder = $this->openDefaultCalendar();
  881. $store = $this->store;
  882. }
  883. $goid = $messageprops[$this->proptags['goid']];
  884. // First, find the items in the calendar by GlobalObjid (0x3)
  885. $entryids = $this->findCalendarItems($goid, $calFolder);
  886. if (!$basedate)
  887. $basedate = $this->getBasedateFromGlobalID($goid);
  888. if($sendresponse)
  889. $this->createResponse(olResponseDeclined, false, false, $body, $store, $basedate, $calFolder);
  890. if ($basedate) {
  891. // use CleanGlobalObjid (0x23)
  892. $calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
  893. foreach($calendaritems as $entryid) {
  894. // Open each calendar item and set the properties of the cancellation object
  895. $calendaritem = mapi_msgstore_openentry($store, $entryid);
  896. // Recurring item is found, now delete exception
  897. if ($calendaritem)
  898. $this->doRemoveExceptionFromCalendar($basedate, $calendaritem, $store);
  899. }
  900. if ($this->isMeetingRequest())
  901. $calendaritem = false;
  902. else
  903. $result = false;
  904. }
  905. if (!$calendaritem) {
  906. $calendar = $this->openDefaultCalendar();
  907. if (!empty($entryids))
  908. mapi_folder_deletemessages($calendar, $entryids);
  909. // All we have to do to decline, is to move the item to the waste basket
  910. $wastebasket = $this->openDefaultWastebasket();
  911. $sourcefolder = $this->openParentFolder();
  912. $messageprops = mapi_getprops($this->message, Array(PR_ENTRYID));
  913. // Release the message
  914. $this->message = null;
  915. // Move the message to the waste basket
  916. mapi_folder_copymessages($sourcefolder, Array($messageprops[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE);
  917. }
  918. return $result;
  919. }
  920. /**
  921. * Removes a meeting request from the calendar when the user presses the
  922. * 'remove from calendar' button in response to a meeting cancellation.
  923. * @param string $basedate if specified contains starttime of day of an occurrence
  924. */
  925. function doRemoveFromCalendar($basedate)
  926. {
  927. if($this->isLocalOrganiser())
  928. return false;
  929. $store = $this->store;
  930. $messageprops = mapi_getprops($this->message, Array(PR_ENTRYID, $this->proptags['goid'], PR_RCVD_REPRESENTING_NAME, PR_MESSAGE_CLASS));
  931. $goid = $messageprops[$this->proptags['goid']];
  932. if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])) {
  933. $delegatorStore = $this->getDelegatorStore($messageprops);
  934. $store = $delegatorStore['store'];
  935. $calFolder = $delegatorStore['calFolder'];
  936. } else {
  937. $calFolder = $this->openDefaultCalendar();
  938. }
  939. $wastebasket = $this->openDefaultWastebasket();
  940. $sourcefolder = $this->openParentFolder();
  941. // Check if the message is a meeting request in the inbox or a calendaritem by checking the message class
  942. if (strpos($messageprops[PR_MESSAGE_CLASS], 'IPM.Schedule.Meeting') === 0) {
  943. /**
  944. * 'Remove from calendar' option from previewpane then we have to check GlobalID of this meeting request.
  945. * If basedate found then open meeting from calendar and delete that occurence.
  946. */
  947. $basedate = false;
  948. if ($goid) {
  949. // Retrieve GlobalID and find basedate in it.
  950. $basedate = $this->getBasedateFromGlobalID($goid);
  951. // Basedate found, Now find item.
  952. if ($basedate) {
  953. $guid = $this->setBasedateInGlobalID($goid);
  954. // First, find the items in the calendar by GOID
  955. $calendaritems = $this->findCalendarItems($guid, $calFolder);
  956. if(is_array($calendaritems)) {
  957. foreach($calendaritems as $entryid) {
  958. // Open each calendar item and set the properties of the cancellation object
  959. $calendaritem = mapi_msgstore_openentry($store, $entryid);
  960. if ($calendaritem)
  961. $this->doRemoveExceptionFromCalendar($basedate, $calendaritem, $store);
  962. }
  963. }
  964. }
  965. }
  966. // It is normal/recurring meeting item.
  967. if (!$basedate) {
  968. if (!isset($calFolder)) $calFolder = $this->openDefaultCalendar();
  969. $entryids = $this->findCalendarItems($goid, $calFolder);
  970. if (is_array($entryids))
  971. // Move the calendaritem to the waste basket
  972. mapi_folder_copymessages($sourcefolder, $entryids, $wastebasket, MESSAGE_MOVE);
  973. }
  974. // Release the message
  975. $this->message = null;
  976. // Move the message to the waste basket
  977. mapi_folder_copymessages($sourcefolder, Array($messageprops[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE);
  978. } else {
  979. // Here only properties are set on calendaritem, because user is responding from calendar.
  980. if ($basedate) //remove the occurence
  981. $this->doRemoveExceptionFromCalendar($basedate, $this->message, $store);
  982. else //remove normal/recurring meeting item.
  983. // Move the message to the waste basket
  984. mapi_folder_copymessages($sourcefolder, Array($messageprops[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE);
  985. }
  986. }
  987. /**
  988. * Removes the meeting request by moving the item to the deleted
  989. * items folder. After canceling, youcan't use this class instance
  990. * any more. The message is closed.
  991. */
  992. function doCancel()
  993. {
  994. if($this->isLocalOrganiser())
  995. return;
  996. if(!$this->isMeetingCancellation())
  997. return;
  998. // Remove any previous calendar items with this goid and appt id
  999. $messageprops = mapi_getprops($this->message, Array($this->proptags['goid']));
  1000. $goid = $messageprops[$this->proptags['goid']];
  1001. $entryids = $this->findCalendarItems($goid);
  1002. $calendar = $this->openDefaultCalendar();
  1003. mapi_folder_deletemessages($calendar, $entryids);
  1004. // All we have to do to decline, is to move the item to the waste basket
  1005. $wastebasket = $this->openDefaultWastebasket();
  1006. $sourcefolder = $this->openParentFolder();
  1007. $messageprops = mapi_getprops($this->message, Array(PR_ENTRYID));
  1008. // Release the message
  1009. $this->message = null;
  1010. // Move the message to the waste basket
  1011. mapi_folder_copymessages($sourcefolder, Array($messageprops[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE);
  1012. }
  1013. /**
  1014. * Sets the properties in the message so that is can be sent
  1015. * as a meeting request. The caller has to submit the message. This
  1016. * is only used for new MeetingRequests. Pass the appointment item as $message
  1017. * in the constructor to do this.
  1018. */
  1019. function setMeetingRequest($basedate = false)
  1020. {
  1021. $props = mapi_getprops($this->message, Array($this->proptags['updatecounter']));
  1022. // Create a new global id for this item
  1023. $goid = pack("H*", "040000008200E00074C5B7101A82E00800000000");
  1024. for ($i=0; $i<36; $i++)
  1025. $goid .= chr(rand(0, 255));
  1026. // Create a new appointment id for this item
  1027. $apptid = rand();
  1028. $props[PR_OWNER_APPT_ID] = $apptid;
  1029. $props[PR_ICON_INDEX] = 1026;
  1030. $props[$this->proptags['goid']] = $goid;
  1031. $props[$this->proptags['goid2']] = $goid;
  1032. if (!isset($props[$this->proptags['updatecounter']])) {
  1033. $props[$this->proptags['updatecounter']] = 0; // OL also starts sequence no with zero.
  1034. $props[$this->proptags['last_updatecounter']] = 0;
  1035. }
  1036. mapi_setprops($this->message, $props);
  1037. }
  1038. /**
  1039. * Sends a meeting request by copying it to the outbox, converting
  1040. * the message class, adding some properties that are required only
  1041. * for sending the message and submitting the message. Set cancel to
  1042. * true if you wish to completely cancel the meeting request. You can
  1043. * specify an optional 'prefix' to prefix the sent message, which is normally
  1044. * 'Canceled: '
  1045. */
  1046. function sendMeetingRequest($cancel, $prefix = false, $basedate = false, $deletedRecips = false)
  1047. {
  1048. $this->includesResources = false;
  1049. $this->nonAcceptingResources = Array();
  1050. // Get the properties of the message
  1051. $messageprops = mapi_getprops($this->message, Array($this->proptags['recurring']));
  1052. /*****************************************************************************************
  1053. * Submit message to non-resource recipients
  1054. */
  1055. // Set BusyStatus to olTentative (1)
  1056. // Set MeetingStatus to olMeetingReceived
  1057. // Set ResponseStatus to olResponseNotResponded
  1058. /**
  1059. * While sending recurrence meeting exceptions are not send as attachments
  1060. * because first all exceptions are send and then recurrence meeting is sent.
  1061. */
  1062. if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']] && !$basedate) {
  1063. // Book resource
  1064. $resourceRecipData = $this->bookResources($this->message, $cancel, $prefix);
  1065. if (!$this->errorSetResource) {
  1066. $recurr = new Recurrence($this->openDefaultStore(), $this->message);
  1067. // First send meetingrequest for recurring item
  1068. $this->submitMeetingRequest($this->message, $cancel, $prefix, false, $recurr, false, $deletedRecips);
  1069. // Then send all meeting request for all exceptions
  1070. $exceptions = $recurr->getAllExceptions();
  1071. if ($exceptions) {
  1072. foreach($exceptions as $exceptionBasedate) {
  1073. $attach = $recurr->getExceptionAttachment($exceptionBasedate);
  1074. if (!$attach)
  1075. continue;
  1076. $occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
  1077. $this->submitMeetingRequest($occurrenceItem, $cancel, false, $exceptionBasedate, $recurr, false, $deletedRecips);
  1078. mapi_savechanges($attach);
  1079. }
  1080. }
  1081. }
  1082. } else {
  1083. // Basedate found, an exception is to be send
  1084. if ($basedate) {
  1085. $recurr = new Recurrence($this->openDefaultStore(), $this->message);
  1086. if ($cancel) {
  1087. //@TODO: remove occurrence from Resource's Calendar if resource was booked for whole series
  1088. $this->submitMeetingRequest($this->message, $cancel, $prefix, $basedate, $recurr, false);
  1089. } else {
  1090. $attach = $recurr->getExceptionAttachment($basedate);
  1091. if ($attach) {
  1092. $occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
  1093. // Book resource for this occurrence
  1094. $resourceRecipData = $this->bookResources($occurrenceItem, $cancel, $prefix, $basedate);
  1095. if (!$this->errorSetResource) {
  1096. // Save all previous changes
  1097. mapi_savechanges($this->message);
  1098. $this->submitMeetingRequest($occurrenceItem, $cancel, $prefix, $basedate, $recurr, true, $deletedRecips);
  1099. mapi_savechanges($occurrenceItem);
  1100. mapi_savechanges($attach);
  1101. }
  1102. }
  1103. }
  1104. } else {
  1105. // This is normal meeting
  1106. $resourceRecipData = $this->bookResources($this->message, $cancel, $prefix);
  1107. if (!$this->errorSetResource)
  1108. $this->submitMeetingRequest($this->message, $cancel, $prefix, false, false, false, $deletedRecips);
  1109. }
  1110. }
  1111. if (isset($this->errorSetResource) && $this->errorSetResource)
  1112. return Array(
  1113. 'error' => $this->errorSetResource,
  1114. 'displayname' => $this->recipientDisplayname
  1115. );
  1116. else
  1117. return true;
  1118. }
  1119. function getFreeBusyInfo($entryID,$start,$end)
  1120. {
  1121. $result = array();
  1122. $fbsupport = mapi_freebusysupport_open($this->session);
  1123. if (mapi_last_hresult() != NOERROR)
  1124. return $result;
  1125. $fbDataArray = mapi_freebusysupport_loaddata($fbsupport, array($entryID));
  1126. if($fbDataArray[0] != NULL){
  1127. foreach($fbDataArray as $fbDataUser){
  1128. $rangeuser1 = mapi_freebusydata_getpublishrange($fbDataUser);
  1129. if ($rangeuser1 == NULL)
  1130. return $result;
  1131. $enumblock = mapi_freebusydata_enumblocks($fbDataUser, $start, $end);
  1132. mapi_freebusyenumblock_reset($enumblock);
  1133. while(true){
  1134. $blocks = mapi_freebusyenumblock_next($enumblock, 100);
  1135. if (!$blocks)
  1136. break;
  1137. foreach ($blocks as $blockItem)
  1138. $result[] = $blockItem;
  1139. }
  1140. }
  1141. }
  1142. mapi_freebusysupport_close($fbsupport);
  1143. return $result;
  1144. }
  1145. /**
  1146. * Updates the message after an update has been performed (for example,
  1147. * changing the time of the meeting). This must be called before re-sending
  1148. * the meeting request. You can also call this function instead of 'setMeetingRequest()'
  1149. * as it will automatically call setMeetingRequest on this object if it is the first
  1150. * call to this function.
  1151. */
  1152. function updateMeetingRequest($basedate = false)
  1153. {
  1154. $messageprops = mapi_getprops($this->message, Array($this->proptags['last_updatecounter'], $this->proptags['goid']));
  1155. if(!isset($messageprops[$this->proptags['last_updatecounter']]) || !isset($messageprops[$this->proptags['goid']])) {
  1156. $this->setMeetingRequest($basedate);
  1157. return;
  1158. }
  1159. $counter = $messageprops[$this->proptags['last_updatecounter']] + 1;
  1160. // increment value of last_updatecounter, last_updatecounter will be common for recurring series
  1161. // so even if you sending an exception only you need to update the last_updatecounter in the recurring series message
  1162. // this way we can make sure that everytime we will be using a uniwue number for every operation
  1163. mapi_setprops($this->message, Array($this->proptags['last_updatecounter'] => $counter));
  1164. }
  1165. /**
  1166. * Returns TRUE if we are the organiser of the meeting.
  1167. */
  1168. function isLocalOrganiser()
  1169. {
  1170. if($this->isMeetingRequest() || $this->isMeetingRequestResponse()) {
  1171. $messageid = $this->getAppointmentEntryID();
  1172. if(!isset($messageid))
  1173. return false;
  1174. $message = mapi_msgstore_openentry($this->store, $messageid);
  1175. $messageprops = mapi_getprops($this->message, Array($this->proptags['goid']));
  1176. $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
  1177. if ($basedate) {
  1178. $recurr = new Recurrence($this->store, $message);
  1179. $attach = $recurr->getExceptionAttachment($basedate);
  1180. if ($attach) {
  1181. $occurItem = mapi_attach_openobj($attach);
  1182. $occurItemProps = mapi_getprops($occurItem, Array($this->proptags['responsestatus']));
  1183. }
  1184. }
  1185. $messageprops = mapi_getprops($message, Array($this->proptags['responsestatus']));
  1186. }
  1187. /**
  1188. * User can send recurring meeting or any occurrences from a recurring appointment so
  1189. * to be organizer 'responseStatus' property should be 'olResponseOrganized' on either
  1190. * of the recurring item or occurrence item.
  1191. */
  1192. if ((isset($messageprops[$this->proptags['responsestatus']]) && $messageprops[$this->proptags['responsestatus']] == olResponseOrganized)
  1193. || (isset($occurItemProps[$this->proptags['responsestatus']]) && $occurItemProps[$this->proptags['responsestatus']] == olResponseOrganized))
  1194. return true;
  1195. else
  1196. return false;
  1197. }
  1198. /**
  1199. * Returns the entryid of the appointment that this message points at. This is
  1200. * only used on messages that are not in the calendar.
  1201. */
  1202. function getAppointmentEntryID()
  1203. {
  1204. $messageprops = mapi_getprops($this->message, Array($this->proptags['goid2']));
  1205. $goid2 = $messageprops[$this->proptags['goid2']];
  1206. $items = $this->findCalendarItems($goid2);
  1207. if(empty($items))
  1208. return;
  1209. // There should be just one item. If there are more, we just take the first one
  1210. return $items[0];
  1211. }
  1212. /***************************************************************************************************
  1213. * Support functions - INTERNAL ONLY
  1214. ***************************************************************************************************
  1215. */
  1216. /**
  1217. * Return the tracking status of a recipient based on the IPM class (passed)
  1218. */
  1219. function getTrackStatus($class) {
  1220. $status = olRecipientTrackStatusNone;
  1221. switch($class)
  1222. {
  1223. case "IPM.Schedule.Meeting.Resp.Pos":
  1224. $status = olRecipientTrackStatusAccepted;
  1225. break;
  1226. case "IPM.Schedule.Meeting.Resp.Tent":
  1227. $status = olRecipientTrackStatusTentative;
  1228. break;
  1229. case "IPM.Schedule.Meeting.Resp.Neg":
  1230. $status = olRecipientTrackStatusDeclined;
  1231. break;
  1232. }
  1233. return $status;
  1234. }
  1235. function openParentFolder() {
  1236. $messageprops = mapi_getprops($this->message, Array(PR_PARENT_ENTRYID));
  1237. $parentfolder = mapi_msgstore_openentry($this->store, $messageprops[PR_PARENT_ENTRYID]);
  1238. return $parentfolder;
  1239. }
  1240. function openDefaultCalendar() {
  1241. return $this->openDefaultFolder(PR_IPM_APPOINTMENT_ENTRYID);
  1242. }
  1243. function openDefaultOutbox($store=false) {
  1244. return $this->openBaseFolder(PR_IPM_OUTBOX_ENTRYID, $store);
  1245. }
  1246. function openDefaultWastebasket() {
  1247. return $this->openBaseFolder(PR_IPM_WASTEBASKET_ENTRYID);
  1248. }
  1249. function getDefaultWastebasketEntryID() {
  1250. return $this->getBaseEntryID(PR_IPM_WASTEBASKET_ENTRYID);
  1251. }
  1252. function getDefaultSentmailEntryID($store=false) {
  1253. return $this->getBaseEntryID(PR_IPM_SENTMAIL_ENTRYID, $store);
  1254. }
  1255. function getDefaultFolderEntryID($prop) {
  1256. try {
  1257. $inbox = mapi_msgstore_getreceivefolder($this->store);
  1258. } catch (MAPIException $e) {
  1259. // public store doesn't support this method
  1260. if($e->getCode() == MAPI_E_NO_SUPPORT) {
  1261. // don't propogate this error to parent handlers, if store doesn't support it
  1262. $e->setHandled();
  1263. return;
  1264. }
  1265. }
  1266. $inboxprops = mapi_getprops($inbox, Array($prop));
  1267. if(!isset($inboxprops[$prop]))
  1268. return;
  1269. return $inboxprops[$prop];
  1270. }
  1271. function openDefaultFolder($prop) {
  1272. $entryid = $this->getDefaultFolderEntryID($prop);
  1273. $folder = mapi_msgstore_openentry($this->store, $entryid);
  1274. return $folder;
  1275. }
  1276. function getBaseEntryID($prop, $store=false) {
  1277. $storeprops = mapi_getprops( (($store)?$store:$this->store) , Array($prop));
  1278. if(!isset($storeprops[$prop]))
  1279. return;
  1280. return $storeprops[$prop];
  1281. }
  1282. function openBaseFolder($prop, $store=false) {
  1283. $entryid = $this->getBaseEntryID($prop, $store);
  1284. $folder = mapi_msgstore_openentry( (($store)?$store:$this->store) , $entryid);
  1285. return $folder;
  1286. }
  1287. /**
  1288. * Function which sends response to organizer when attendee accepts, declines or proposes new time to a received meeting request.
  1289. *@param integer $status response status of attendee
  1290. *@param integer $proposalStartTime proposed starttime by attendee
  1291. *@param integer $proposalEndTime proposed endtime by attendee
  1292. *@param integer $basedate date of occurrence which attendee has responded
  1293. */
  1294. function createResponse($status, $proposalStartTime=false, $proposalEndTime=false, $body=false, $store, $basedate = false, $calFolder) {
  1295. $messageprops = mapi_getprops($this->message, Array(PR_SENT_REPRESENTING_ENTRYID,
  1296. PR_SENT_REPRESENTING_EMAIL_ADDRESS,
  1297. PR_SENT_REPRESENTING_ADDRTYPE,
  1298. PR_SENT_REPRESENTING_NAME,
  1299. PR_SENT_REPRESENTING_SEARCH_KEY,
  1300. $this->proptags['goid'],
  1301. $this->proptags['goid2'],
  1302. $this->proptags['location'],
  1303. $this->proptags['startdate'],
  1304. $this->proptags['duedate'],
  1305. $this->proptags['recurring'],
  1306. $this->proptags['recurring_pattern'],
  1307. $this->proptags['recurrence_data'],
  1308. $this->proptags['timezone_data'],
  1309. $this->proptags['timezone'],
  1310. $this->proptags['updatecounter'],
  1311. PR_SUBJECT,
  1312. PR_MESSAGE_CLASS,
  1313. PR_OWNER_APPT_ID,
  1314. $this->proptags['is_exception']
  1315. ));
  1316. if ($basedate && $messageprops[PR_MESSAGE_CLASS] != "IPM.Schedule.Meeting.Request" ){
  1317. // we are creating response from a recurring calendar item object
  1318. // We found basedate,so opened occurrence and get properties.
  1319. $recurr = new Recurrence($store, $this->message);
  1320. $exception = $recurr->getExceptionAttachment($basedate);
  1321. if ($exception) {
  1322. // Exception found, Now retrieve properties
  1323. $imessage = mapi_attach_openobj($exception, 0);
  1324. $imsgprops = mapi_getprops($imessage);
  1325. // If location is provided, copy it to the response
  1326. if (isset($imsgprops[$this->proptags['location']]))
  1327. $messageprops[$this->proptags['location']] = $imsgprops[$this->proptags['location']];
  1328. // Update $messageprops with timings of occurrence
  1329. $messageprops[$this->proptags['startdate']] = $imsgprops[$this->proptags['startdate']];
  1330. $messageprops[$this->proptags['duedate']] = $imsgprops[$this->proptags['duedate']];
  1331. // Meeting related properties
  1332. $props[$this->proptags['meetingstatus']] = $imsgprops[$this->proptags['meetingstatus']];
  1333. $props[$this->proptags['responsestatus']] = $imsgprops[$this->proptags['responsestatus']];
  1334. $props[PR_SUBJECT] = $imsgprops[PR_SUBJECT];
  1335. } else {
  1336. // Exceptions is deleted.
  1337. // Update $messageprops with timings of occurrence
  1338. $messageprops[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
  1339. $messageprops[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
  1340. $props[$this->proptags['meetingstatus']] = olNonMeeting;
  1341. $props[$this->proptags['responsestatus']] = olResponseNone;
  1342. }
  1343. $props[$this->proptags['recurring']] = false;
  1344. $props[$this->proptags['is_exception']] = true;
  1345. } else {
  1346. // we are creating a response from meeting request mail (it could be recurring or non-recurring)
  1347. // Send all recurrence info in response, if this is a recurrence meeting.
  1348. $isRecurring = isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']];
  1349. $isException = isset($messageprops[$this->proptags['is_exception']]) && $messageprops[$this->proptags['is_exception']];
  1350. if ($isRecurring || $isException) {
  1351. if ($isRecurring)
  1352. $props[$this->proptags['recurring']] = $messageprops[$this->proptags['recurring']];
  1353. if ($isException)
  1354. $props[$this->proptags['is_exception']] = $messageprops[$this->proptags['is_exception']];
  1355. $calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
  1356. $calendaritem = mapi_msgstore_openentry($this->store, $calendaritems[0]);
  1357. $recurr = new Recurrence($store, $calendaritem);
  1358. }
  1359. }
  1360. // we are sending a response for recurring meeting request (or exception), so set some required properties
  1361. if(isset($recurr) && $recurr) {
  1362. if (!empty($messageprops[$this->proptags['recurring_pattern']]))
  1363. $props[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
  1364. if (!empty($messageprops[$this->proptags['recurrence_data']]))
  1365. $props[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
  1366. $props[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
  1367. $props[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
  1368. $this->generateRecurDates($recurr, $messageprops, $props);
  1369. }
  1370. // Create a response message
  1371. $recip = Array();
  1372. $recip[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
  1373. $recip[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
  1374. $recip[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
  1375. $recip[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
  1376. $recip[PR_RECIPIENT_TYPE] = MAPI_TO;
  1377. $recip[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
  1378. switch($status) {
  1379. case olResponseAccepted:
  1380. $classpostfix = "Pos";
  1381. $subjectprefix = dgettext("kopano","Accepted");
  1382. break;
  1383. case olResponseDeclined:
  1384. $classpostfix = "Neg";
  1385. $subjectprefix = dgettext("kopano","Declined");
  1386. break;
  1387. case olResponseTentative:
  1388. $classpostfix = "Tent";
  1389. $subjectprefix = dgettext("kopano","Tentatively accepted");
  1390. break;
  1391. }
  1392. if ($proposalStartTime && $proposalEndTime)
  1393. // if attendee has proposed new time then change subject prefix
  1394. $subjectprefix = dgettext("kopano","New Time Proposed");
  1395. $props[PR_SUBJECT] = $subjectprefix . ": " . $messageprops[PR_SUBJECT];
  1396. $props[PR_MESSAGE_CLASS] = "IPM.Schedule.Meeting.Resp." . $classpostfix;
  1397. if (isset($messageprops[PR_OWNER_APPT_ID]))
  1398. $props[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
  1399. // Set GLOBALID AND CLEANGLOBALID, if exception then also set basedate into GLOBALID(0x3).
  1400. $props[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate);
  1401. $props[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
  1402. $props[$this->proptags['updatecounter']] = $messageprops[$this->proptags['updatecounter']];
  1403. // get the default store, in which we have to store the accepted email by delegate or normal user.
  1404. $defaultStore = $this->openDefaultStore();
  1405. $props[PR_SENTMAIL_ENTRYID] = $this->getDefaultSentmailEntryID($defaultStore);
  1406. if($proposalStartTime && $proposalEndTime){
  1407. $props[$this->proptags['proposed_start_whole']] = $proposalStartTime;
  1408. $props[$this->proptags['proposed_end_whole']] = $proposalEndTime;
  1409. $props[$this->proptags['proposed_duration']] = round($proposalEndTime - $proposalStartTime)/60;
  1410. $props[$this->proptags['counter_proposal']] = true;
  1411. }
  1412. //Set body message in Appointment
  1413. if (isset($body))
  1414. $props[PR_BODY] = $this->getMeetingTimeInfo() ? $this->getMeetingTimeInfo() : $body;
  1415. // PR_START_DATE/PR_END_DATE is used in the UI in Outlook on the response message
  1416. $props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
  1417. $props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
  1418. // Set startdate and duedate in response mail.
  1419. $props[$this->proptags['startdate']] = $messageprops[$this->proptags['startdate']];
  1420. $props[$this->proptags['duedate']] = $messageprops[$this->proptags['duedate']];
  1421. // responselocation is used in the UI in Outlook on the response message
  1422. if (isset($messageprops[$this->proptags['location']])) {
  1423. $props[$this->proptags['responselocation']] = $messageprops[$this->proptags['location']];
  1424. $props[$this->proptags['location']] = $messageprops[$this->proptags['location']];
  1425. }
  1426. // check if $store is set and it is not equal to $defaultStore (means its the delegation case)
  1427. if(isset($store) && isset($defaultStore)) {
  1428. $storeProps = mapi_getprops($store, array(PR_ENTRYID));
  1429. $defaultStoreProps = mapi_getprops($defaultStore, array(PR_ENTRYID));
  1430. // @FIXME use entryid comparison functions here
  1431. if($storeProps[PR_ENTRYID] !== $defaultStoreProps[PR_ENTRYID]){
  1432. // get the properties of the other user (for which the logged in user is a delegate).
  1433. $storeProps = mapi_getprops($store, array(PR_MAILBOX_OWNER_ENTRYID));
  1434. $addrbook = mapi_openaddressbook($this->session);
  1435. $addrbookitem = mapi_ab_openentry($addrbook, $storeProps[PR_MAILBOX_OWNER_ENTRYID]);
  1436. $addrbookitemprops = mapi_getprops($addrbookitem, array(PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY));
  1437. // setting the following properties will ensure that the delegation part of message.
  1438. $props[PR_SENT_REPRESENTING_ENTRYID] = $storeProps[PR_MAILBOX_OWNER_ENTRYID];
  1439. $props[PR_SENT_REPRESENTING_NAME] = $addrbookitemprops[PR_DISPLAY_NAME];
  1440. $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $addrbookitemprops[PR_EMAIL_ADDRESS];
  1441. $props[PR_SENT_REPRESENTING_ADDRTYPE] = "ZARAFA";
  1442. $props[PR_SENT_REPRESENTING_SEARCH_KEY] = $addrbookitemprops[PR_SEARCH_KEY];
  1443. // set the following properties will ensure the sender's details, which will be the default user in this case.
  1444. //the function returns array($name, $emailaddr, $addrtype, $entryid, $searchkey);
  1445. $defaultUserDetails = $this->getOwnerAddress($defaultStore);
  1446. $props[PR_SENDER_ENTRYID] = $defaultUserDetails[3];
  1447. $props[PR_SENDER_EMAIL_ADDRESS] = $defaultUserDetails[1];
  1448. $props[PR_SENDER_NAME] = $defaultUserDetails[0];
  1449. $props[PR_SENDER_ADDRTYPE] = $defaultUserDetails[2];
  1450. $props[PR_SENDER_SEARCH_KEY] = $defaultUserDetails[4];
  1451. }
  1452. }
  1453. // pass the default store to get the required store.
  1454. $outbox = $this->openDefaultOutbox($defaultStore);
  1455. $message = mapi_folder_createmessage($outbox);
  1456. mapi_setprops($message, $props);
  1457. mapi_message_modifyrecipients($message, MODRECIP_ADD, Array($recip));
  1458. mapi_message_savechanges($message);
  1459. mapi_message_submitmessage($message);
  1460. }
  1461. /**
  1462. * Function which finds items in calendar based on specified parameters.
  1463. *@param binary $goid GlobalID(0x3) of item
  1464. *@param resource $calendar MAPI_folder of user
  1465. *@param boolean $use_cleanGlobalID if true then search should be performed on cleanGlobalID(0x23) else globalID(0x3)
  1466. */
  1467. function findCalendarItems($goid, $calendar = false, $use_cleanGlobalID = false) {
  1468. if (!$calendar)
  1469. // Open the Calendar
  1470. $calendar = $this->openDefaultCalendar();
  1471. // Find the item by restricting all items to the correct ID
  1472. $restrict = Array(RES_AND, Array());
  1473. array_push($restrict[1], Array(RES_PROPERTY,
  1474. Array(RELOP => RELOP_EQ,
  1475. ULPROPTAG => ($use_cleanGlobalID ? $this->proptags['goid2'] : $this->proptags['goid']),
  1476. VALUE => $goid
  1477. )
  1478. ));
  1479. $calendarcontents = mapi_folder_getcontentstable($calendar);
  1480. $rows = mapi_table_queryallrows($calendarcontents, Array(PR_ENTRYID), $restrict);
  1481. if(empty($rows))
  1482. return;
  1483. $calendaritems = Array();
  1484. // In principle, there should only be one row, but we'll handle them all just in case
  1485. foreach ($rows as $row)
  1486. $calendaritems[] = $row[PR_ENTRYID];
  1487. return $calendaritems;
  1488. }
  1489. // Returns TRUE if both entryids are equal. Equality is defined by both entryids pointing at the
  1490. // same SMTP address when converted to SMTP
  1491. function compareABEntryIDs($entryid1, $entryid2) {
  1492. // If the session was not passed, just do a 'normal' compare.
  1493. if (!$this->session)
  1494. return $entryid1 == $entryid2;
  1495. $smtp1 = $this->getSMTPAddress($entryid1);
  1496. $smtp2 = $this->getSMTPAddress($entryid2);
  1497. if ($smtp1 == $smtp2)
  1498. return true;
  1499. else
  1500. return false;
  1501. }
  1502. // Gets the SMTP address of the passed addressbook entryid
  1503. function getSMTPAddress($entryid) {
  1504. if (!$this->session)
  1505. return false;
  1506. $ab = mapi_openaddressbook($this->session);
  1507. $abitem = mapi_ab_openentry($ab, $entryid);
  1508. if (!$abitem)
  1509. return "";
  1510. $props = mapi_getprops($abitem, array(PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS));
  1511. if ($props[PR_ADDRTYPE] == "SMTP")
  1512. return $props[PR_EMAIL_ADDRESS];
  1513. else
  1514. return $props[PR_SMTP_ADDRESS];
  1515. }
  1516. /**
  1517. * Gets the properties associated with the owner of the passed store:
  1518. * PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_ADDRTYPE, PR_ENTRYID, PR_SEARCH_KEY
  1519. *
  1520. * @param $store message store
  1521. * @param $fallbackToLoggedInUser if true then return properties of logged in user instead of mailbox owner
  1522. * not used when passed store is public store. for public store we are always returning logged in user's info.
  1523. * @return properties of logged in user in an array in sequence of display_name, email address, address tyep,
  1524. * entryid and search key.
  1525. */
  1526. function getOwnerAddress($store, $fallbackToLoggedInUser = true)
  1527. {
  1528. if(!$this->session)
  1529. return false;
  1530. $storeProps = mapi_getprops($store, array(PR_MAILBOX_OWNER_ENTRYID, PR_USER_ENTRYID));
  1531. $ownerEntryId = false;
  1532. if (isset($storeProps[PR_USER_ENTRYID]) && $storeProps[PR_USER_ENTRYID])
  1533. $ownerEntryId = $storeProps[PR_USER_ENTRYID];
  1534. if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID]) && $storeProps[PR_MAILBOX_OWNER_ENTRYID] && !$fallbackToLoggedInUser)
  1535. $ownerEntryId = $storeProps[PR_MAILBOX_OWNER_ENTRYID];
  1536. if($ownerEntryId) {
  1537. $ab = mapi_openaddressbook($this->session);
  1538. $zuser = mapi_ab_openentry($ab, $ownerEntryId);
  1539. if(!$zuser)
  1540. return false;
  1541. $ownerProps = mapi_getprops($zuser, array(PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY));
  1542. $addrType = $ownerProps[PR_ADDRTYPE];
  1543. $name = $ownerProps[PR_DISPLAY_NAME];
  1544. $emailAddr = $ownerProps[PR_EMAIL_ADDRESS];
  1545. $searchKey = $ownerProps[PR_SEARCH_KEY];
  1546. $entryId = $ownerEntryId;
  1547. return array($name, $emailAddr, $addrType, $entryId, $searchKey);
  1548. }
  1549. return false;
  1550. }
  1551. // Opens this session's default message store
  1552. function openDefaultStore()
  1553. {
  1554. $storestable = mapi_getmsgstorestable($this->session);
  1555. $rows = mapi_table_queryallrows($storestable, array(PR_ENTRYID, PR_DEFAULT_STORE));
  1556. $entry = false;
  1557. foreach($rows as $row) {
  1558. if(isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) {
  1559. $entryid = $row[PR_ENTRYID];
  1560. break;
  1561. }
  1562. }
  1563. if(!$entryid)
  1564. return false;
  1565. return mapi_openmsgstore($this->session, $entryid);
  1566. }
  1567. /**
  1568. * Function which adds organizer to recipient list which is passed.
  1569. * This function also checks if it has organizer.
  1570. *
  1571. * @param array $messageProps message properties
  1572. * @param array $recipients recipients list of message.
  1573. * @param boolean $isException true if we are processing recipient of exception
  1574. */
  1575. function addOrganizer($messageProps, &$recipients, $isException = false){
  1576. $hasOrganizer = false;
  1577. // Check if meeting already has an organizer.
  1578. foreach ($recipients as $key => $recipient){
  1579. if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer))
  1580. $hasOrganizer = true;
  1581. else if ($isException && !isset($recipient[PR_RECIPIENT_FLAGS]))
  1582. // Recipients for an occurrence
  1583. $recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
  1584. }
  1585. if (!$hasOrganizer){
  1586. // Create organizer.
  1587. $organizer = array();
  1588. $organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
  1589. $organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
  1590. $organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
  1591. $organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
  1592. $organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
  1593. $organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
  1594. $organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
  1595. $organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
  1596. $organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
  1597. // Add organizer to recipients list.
  1598. array_unshift($recipients, $organizer);
  1599. }
  1600. }
  1601. /**
  1602. * Function adds recipients in recips array from the string.
  1603. *
  1604. * @param array $recips recipient array.
  1605. * @param string $recipString recipient string attendees.
  1606. * @param int $type type of the recipient, MAPI_TO/MAPI_CC.
  1607. */
  1608. function setRecipsFromString(&$recips, $recipString, $recipType = MAPI_TO)
  1609. {
  1610. $extraRecipient = array();
  1611. $recipArray = explode(";", $recipString);
  1612. foreach($recipArray as $recip) {
  1613. $recip = trim($recip);
  1614. if (!empty($recip)) {
  1615. $extraRecipient[PR_RECIPIENT_TYPE] = $recipType;
  1616. $extraRecipient[PR_DISPLAY_NAME] = $recip;
  1617. array_push($recips, $extraRecipient);
  1618. }
  1619. }
  1620. }
  1621. /**
  1622. * Function which removes an exception/occurrence from recurrencing meeting
  1623. * when a meeting cancellation of an occurrence is processed.
  1624. *@param string $basedate basedate of an occurrence
  1625. *@param resource $message recurring item from which occurrence has to be deleted
  1626. *@param resource $store MAPI_MSG_Store which contains the item
  1627. */
  1628. function doRemoveExceptionFromCalendar($basedate, $message, $store)
  1629. {
  1630. $recurr = new Recurrence($store, $message);
  1631. $recurr->createException(array(), $basedate, true);
  1632. mapi_savechanges($message);
  1633. }
  1634. /**
  1635. * Function which returns basedate of a changed occurrance from globalID of meeting request.
  1636. *@param binary $goid globalID
  1637. *@return boolean true if basedate is found else false it not found
  1638. */
  1639. function getBasedateFromGlobalID($goid)
  1640. {
  1641. $hexguid = bin2hex($goid);
  1642. $hexbase = substr($hexguid, 32, 8);
  1643. $day = hexdec(substr($hexbase, 6, 2));
  1644. $month = hexdec(substr($hexbase, 4, 2));
  1645. $year = hexdec(substr($hexbase, 0, 4));
  1646. if ($day && $month && $year)
  1647. return gmmktime(0, 0, 0, $month, $day, $year);
  1648. else
  1649. return false;
  1650. }
  1651. /**
  1652. * Function which sets basedate in globalID of changed occurrance which is to be send.
  1653. *@param binary $goid globalID
  1654. *@param string basedate of changed occurrance
  1655. *@return binary globalID with basedate in it
  1656. */
  1657. function setBasedateInGlobalID($goid, $basedate = false)
  1658. {
  1659. $hexguid = bin2hex($goid);
  1660. $year = $basedate ? sprintf('%04s', dechex(date('Y', $basedate))) : '0000';
  1661. $month = $basedate ? sprintf('%02s', dechex(date('m', $basedate))) : '00';
  1662. $day = $basedate ? sprintf('%02s', dechex(date('d', $basedate))) : '00';
  1663. return hex2bin(strtoupper(substr($hexguid, 0, 32) . $year . $month . $day . substr($hexguid, 40)));
  1664. }
  1665. /**
  1666. * Function which replaces attachments with copy_from in copy_to.
  1667. *@param resource $copy_from MAPI_message from which attachments are to be copied.
  1668. *@param resource $copy_to MAPI_message to which attachment are to be copied.
  1669. *@param boolean $copyExceptions if true then all exceptions should also be sent as attachments
  1670. */
  1671. function replaceAttachments($copy_from, $copy_to, $copyExceptions = true)
  1672. {
  1673. /* remove all old attachments */
  1674. $attachmentTable = mapi_message_getattachmenttable($copy_to);
  1675. if($attachmentTable) {
  1676. $attachments = mapi_table_queryallrows($attachmentTable, array(PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME));
  1677. foreach($attachments as $attach_props){
  1678. /* remove exceptions too? */
  1679. if (!$copyExceptions && $attach_props[PR_ATTACH_METHOD] == 5 && isset($attach_props[PR_EXCEPTION_STARTTIME]))
  1680. continue;
  1681. mapi_message_deleteattach($copy_to, $attach_props[PR_ATTACH_NUM]);
  1682. }
  1683. }
  1684. $attachmentTable = false;
  1685. /* copy new attachments */
  1686. $attachmentTable = mapi_message_getattachmenttable($copy_from);
  1687. if($attachmentTable) {
  1688. $attachments = mapi_table_queryallrows($attachmentTable, array(PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME));
  1689. foreach($attachments as $attach_props){
  1690. if (!$copyExceptions && $attach_props[PR_ATTACH_METHOD] == 5 && isset($attach_props[PR_EXCEPTION_STARTTIME]))
  1691. continue;
  1692. $attach_old = mapi_message_openattach($copy_from, (int) $attach_props[PR_ATTACH_NUM]);
  1693. $attach_newResourceMsg = mapi_message_createattach($copy_to);
  1694. mapi_copyto($attach_old, array(), array(), $attach_newResourceMsg, 0);
  1695. mapi_savechanges($attach_newResourceMsg);
  1696. }
  1697. }
  1698. }
  1699. /**
  1700. * Function which replaces recipients in copy_to with recipients from copy_from.
  1701. *@param resource $copy_from MAPI_message from which recipients are to be copied.
  1702. *@param resource $copy_to MAPI_message to which recipients are to be copied.
  1703. */
  1704. function replaceRecipients($copy_from, $copy_to, $isDelegate = false)
  1705. {
  1706. $recipienttable = mapi_message_getrecipienttable($copy_from);
  1707. // If delegate, then do not add the delegate in recipients
  1708. if ($isDelegate) {
  1709. $delegate = mapi_getprops($copy_from, array(PR_RECEIVED_BY_EMAIL_ADDRESS));
  1710. $res = array(RES_PROPERTY, array(RELOP => RELOP_NE, ULPROPTAG => PR_EMAIL_ADDRESS, VALUE => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]));
  1711. $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $res);
  1712. } else {
  1713. $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops);
  1714. }
  1715. $copy_to_recipientTable = mapi_message_getrecipienttable($copy_to);
  1716. $copy_to_recipientRows = mapi_table_queryallrows($copy_to_recipientTable, array(PR_ROWID));
  1717. foreach ($copy_to_recipientRows as $recipient)
  1718. mapi_message_modifyrecipients($copy_to, MODRECIP_REMOVE, array($recipient));
  1719. mapi_message_modifyrecipients($copy_to, MODRECIP_ADD, $recipients);
  1720. }
  1721. /**
  1722. * Function creates meeting item in resource's calendar.
  1723. *@param resource $message MAPI_message which is to create in resource's calendar
  1724. *@param boolean $cancel cancel meeting
  1725. *@param string $prefix prefix for subject of meeting
  1726. */
  1727. function bookResources($message, $cancel, $prefix, $basedate = false)
  1728. {
  1729. if(!$this->enableDirectBooking)
  1730. return array();
  1731. // Get the properties of the message
  1732. $messageprops = mapi_getprops($message);
  1733. if ($basedate) {
  1734. $recurrItemProps = mapi_getprops($this->message, array($this->proptags['goid'], $this->proptags['goid2'], $this->proptags['timezone_data'], $this->proptags['timezone'], PR_OWNER_APPT_ID));
  1735. $messageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid']], $basedate);
  1736. $messageprops[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
  1737. // Delete properties which are not needed.
  1738. $deleteProps = array($this->proptags['basedate'], PR_DISPLAY_NAME, PR_ATTACHMENT_FLAGS, PR_ATTACHMENT_HIDDEN, PR_ATTACHMENT_LINKID, PR_ATTACH_FLAGS, PR_ATTACH_METHOD);
  1739. foreach ($deleteProps as $propID)
  1740. if (isset($messageprops[$propID]))
  1741. unset($messageprops[$propID]);
  1742. if (isset($messageprops[$this->proptags['recurring']])) $messageprops[$this->proptags['recurring']] = false;
  1743. // Set Outlook properties
  1744. $messageprops[$this->proptags['clipstart']] = $messageprops[$this->proptags['startdate']];
  1745. $messageprops[$this->proptags['clipend']] = $messageprops[$this->proptags['duedate']];
  1746. $messageprops[$this->proptags['timezone_data']] = $recurrItemProps[$this->proptags['timezone_data']];
  1747. $messageprops[$this->proptags['timezone']] = $recurrItemProps[$this->proptags['timezone']];
  1748. $messageprops[$this->proptags['attendee_critical_change']] = time();
  1749. $messageprops[$this->proptags['owner_critical_change']] = time();
  1750. }
  1751. // Get resource recipients
  1752. $getResourcesRestriction = Array(RES_AND,
  1753. Array(Array(RES_PROPERTY,
  1754. Array(RELOP => RELOP_EQ, // Equals recipient type 3: Resource
  1755. ULPROPTAG => PR_RECIPIENT_TYPE,
  1756. VALUE => array(PR_RECIPIENT_TYPE =>MAPI_BCC)
  1757. )
  1758. ))
  1759. );
  1760. $recipienttable = mapi_message_getrecipienttable($message);
  1761. $resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
  1762. $this->errorSetResource = false;
  1763. $resourceRecipData = Array();
  1764. // Put appointment into store resource users
  1765. $i = 0;
  1766. $len = count($resourceRecipients);
  1767. while(!$this->errorSetResource && $i < $len){
  1768. $request = array(array(PR_DISPLAY_NAME => $resourceRecipients[$i][PR_DISPLAY_NAME]));
  1769. $ab = mapi_openaddressbook($this->session);
  1770. $ret = mapi_ab_resolvename($ab, $request, EMS_AB_ADDRESS_LOOKUP);
  1771. $result = mapi_last_hresult();
  1772. if ($result == NOERROR)
  1773. $result = $ret[0][PR_ENTRYID];
  1774. $resourceUsername = $ret[0][PR_EMAIL_ADDRESS];
  1775. $resourceABEntryID = $ret[0][PR_ENTRYID];
  1776. // Get StoreEntryID by username
  1777. $user_entryid = mapi_msgstore_createentryid($this->store, $resourceUsername);
  1778. // Open store of the user
  1779. $userStore = mapi_openmsgstore($this->session, $user_entryid);
  1780. // Open root folder
  1781. $userRoot = mapi_msgstore_openentry($userStore, null);
  1782. // Get calendar entryID
  1783. $userRootProps = mapi_getprops($userRoot, array(PR_STORE_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_FREEBUSY_ENTRYIDS));
  1784. // Open Calendar folder [check hresult==0]
  1785. $accessToFolder = false;
  1786. try {
  1787. $calFolder = mapi_msgstore_openentry($userStore, $userRootProps[PR_IPM_APPOINTMENT_ENTRYID]);
  1788. if($calFolder){
  1789. $calFolderProps = mapi_getProps($calFolder, Array(PR_ACCESS));
  1790. if (($calFolderProps[PR_ACCESS] & MAPI_ACCESS_CREATE_CONTENTS) !== 0)
  1791. $accessToFolder = true;
  1792. }
  1793. } catch (MAPIException $e) {
  1794. $e->setHandled();
  1795. $this->errorSetResource = 1; // No access
  1796. }
  1797. if($accessToFolder) {
  1798. /**
  1799. * Get the LocalFreebusy message that contains the properties that
  1800. * are set to accept or decline resource meeting requests
  1801. */
  1802. // Use PR_FREEBUSY_ENTRYIDS[1] to open folder the LocalFreeBusy msg
  1803. $localFreebusyMsg = mapi_msgstore_openentry($userStore, $userRootProps[PR_FREEBUSY_ENTRYIDS][1]);
  1804. if($localFreebusyMsg){
  1805. $props = mapi_getprops($localFreebusyMsg, array(PR_PROCESS_MEETING_REQUESTS, PR_DECLINE_RECURRING_MEETING_REQUESTS, PR_DECLINE_CONFLICTING_MEETING_REQUESTS));
  1806. $acceptMeetingRequests = ($props[PR_PROCESS_MEETING_REQUESTS])?1:0;
  1807. $declineRecurringMeetingRequests = ($props[PR_DECLINE_RECURRING_MEETING_REQUESTS])?1:0;
  1808. $declineConflictingMeetingRequests = ($props[PR_DECLINE_CONFLICTING_MEETING_REQUESTS])?1:0;
  1809. if(!$acceptMeetingRequests){
  1810. /**
  1811. * When a resource has not been set to automatically accept meeting requests,
  1812. * the meeting request has to be sent to him rather than being put directly into
  1813. * his calendar. No error should be returned.
  1814. */
  1815. //$errorSetResource = 2;
  1816. $this->nonAcceptingResources[] = $resourceRecipients[$i];
  1817. }else{
  1818. if($declineRecurringMeetingRequests && !$cancel){
  1819. // Check if appointment is recurring
  1820. if ($messageprops[ $this->proptags['recurring']])
  1821. $this->errorSetResource = 3;
  1822. }
  1823. if($declineConflictingMeetingRequests && !$cancel){
  1824. // Check for conflicting items
  1825. $conflicting = false;
  1826. // Open the calendar
  1827. $calFolder = mapi_msgstore_openentry($userStore, $userRootProps[PR_IPM_APPOINTMENT_ENTRYID]);
  1828. if($calFolder) {
  1829. if ($this->isMeetingConflicting($message, $userStore, $calFolder, $messageprops))
  1830. $conflicting = true;
  1831. } else {
  1832. $this->errorSetResource = 1; // No access
  1833. }
  1834. if ($conflicting)
  1835. $this->errorSetResource = 4; // Conflict
  1836. }
  1837. }
  1838. }
  1839. }
  1840. if(!$this->errorSetResource && $accessToFolder){
  1841. /**
  1842. * First search on GlobalID(0x3)
  1843. * If (recurring and occurrence) If Resource was booked for only this occurrence then Resource should have only this occurrence in Calendar and not whole series.
  1844. * If (normal meeting) then GlobalID(0x3) and CleanGlobalID(0x23) are same, so doesnt matter if search is based on GlobalID.
  1845. */
  1846. $rows = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
  1847. /**
  1848. * If no entry is found then
  1849. * 1) Resource doesnt have meeting in Calendar. Seriously!!
  1850. * OR
  1851. * 2) We were looking for occurrence item but Resource has whole series
  1852. */
  1853. if(empty($rows)){
  1854. /**
  1855. * Now search on CleanGlobalID(0x23) WHY???
  1856. * Because we are looking recurring item
  1857. *
  1858. * Possible results of this search
  1859. * 1) If Resource was booked for more than one occurrences then this search will return all those occurrence because search is perform on CleanGlobalID
  1860. * 2) If Resource was booked for whole series then it should return series.
  1861. */
  1862. $rows = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
  1863. $newResourceMsg = false;
  1864. if (!empty($rows)) {
  1865. // Since we are looking for recurring item, open every result and check for 'recurring' property.
  1866. foreach($rows as $row) {
  1867. $ResourceMsg = mapi_msgstore_openentry($userStore, $row);
  1868. $ResourceMsgProps = mapi_getprops($ResourceMsg, array($this->proptags['recurring']));
  1869. if (isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
  1870. $newResourceMsg = $ResourceMsg;
  1871. break;
  1872. }
  1873. }
  1874. }
  1875. // Still no results found. I giveup, create new message.
  1876. if (!$newResourceMsg)
  1877. $newResourceMsg = mapi_folder_createmessage($calFolder);
  1878. }else{
  1879. $newResourceMsg = mapi_msgstore_openentry($userStore, $rows[0]);
  1880. }
  1881. // Prefix the subject if needed
  1882. if ($prefix && isset($messageprops[PR_SUBJECT]))
  1883. $messageprops[PR_SUBJECT] = $prefix . $messageprops[PR_SUBJECT];
  1884. // Set status to cancelled if needed
  1885. $messageprops[$this->proptags['busystatus']] = fbBusy; // The default status (Busy)
  1886. if($cancel) {
  1887. $messageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // The meeting has been canceled
  1888. $messageprops[$this->proptags['busystatus']] = fbFree; // Free
  1889. } else {
  1890. $messageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
  1891. }
  1892. $messageprops[$this->proptags['responsestatus']] = olResponseAccepted; // The resource autmatically accepts the appointment
  1893. $messageprops[PR_MESSAGE_CLASS] = "IPM.Appointment";
  1894. // Remove the PR_ICON_INDEX as it is not needed in the sent message
  1895. // and it also confuses webaccess
  1896. $messageprops[PR_ICON_INDEX] = null;
  1897. $messageprops[PR_RESPONSE_REQUESTED] = true;
  1898. $addrinfo = $this->getOwnerAddress($this->store);
  1899. if($addrinfo) {
  1900. list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrinfo;
  1901. $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
  1902. $messageprops[PR_SENT_REPRESENTING_NAME] = $ownername;
  1903. $messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
  1904. $messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
  1905. $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
  1906. $messageprops[$this->proptags['apptreplyname']] = $ownername;
  1907. $messageprops[$this->proptags['replytime']] = time();
  1908. }
  1909. if ($basedate && isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
  1910. $recurr = new Recurrence($userStore, $newResourceMsg);
  1911. // Copy recipients list
  1912. $reciptable = mapi_message_getrecipienttable($message);
  1913. $recips = mapi_table_queryallrows($reciptable, $this->recipprops);
  1914. // add owner to recipient table
  1915. $this->addOrganizer($messageprops, $recips, true);
  1916. // Update occurrence
  1917. if($recurr->isException($basedate))
  1918. $recurr->modifyException($messageprops, $basedate, $recips);
  1919. else
  1920. $recurr->createException($messageprops, $basedate, false, $recips);
  1921. } else {
  1922. mapi_setprops($newResourceMsg, $messageprops);
  1923. // Copy attachments
  1924. $this->replaceAttachments($message, $newResourceMsg);
  1925. // Copy all recipients too
  1926. $this->replaceRecipients($message, $newResourceMsg);
  1927. // Now add organizer also to recipient table
  1928. $recips = Array();
  1929. $this->addOrganizer($messageprops, $recips);
  1930. mapi_message_modifyrecipients($newResourceMsg, MODRECIP_ADD, $recips);
  1931. }
  1932. mapi_savechanges($newResourceMsg);
  1933. $resourceRecipData[] = Array(
  1934. 'store' => $userStore,
  1935. 'folder' => $calFolder,
  1936. 'msg' => $newResourceMsg,
  1937. );
  1938. $this->includesResources = true;
  1939. }else{
  1940. /**
  1941. * If no other errors occurred and you have no access to the
  1942. * folder of the resource, throw an error=1.
  1943. */
  1944. if (!$this->errorSetResource)
  1945. $this->errorSetResource = 1;
  1946. for($j = 0, $len = count($resourceRecipData); $j < $len; $j++){
  1947. // Get the EntryID
  1948. $props = mapi_message_getprops($resourceRecipData[$j]['msg']);
  1949. mapi_folder_deletemessages($resourceRecipData[$j]['folder'], Array($props[PR_ENTRYID]), DELETE_HARD_DELETE);
  1950. }
  1951. $this->recipientDisplayname = $resourceRecipients[$i][PR_DISPLAY_NAME];
  1952. }
  1953. $i++;
  1954. }
  1955. /**************************************************************
  1956. * Set the BCC-recipients (resources) tackstatus to accepted.
  1957. */
  1958. // Get resource recipients
  1959. $getResourcesRestriction = Array(RES_AND,
  1960. Array(Array(RES_PROPERTY,
  1961. Array(RELOP => RELOP_EQ, // Equals recipient type 3: Resource
  1962. ULPROPTAG => PR_RECIPIENT_TYPE,
  1963. VALUE => array(PR_RECIPIENT_TYPE =>MAPI_BCC)
  1964. )
  1965. ))
  1966. );
  1967. $recipienttable = mapi_message_getrecipienttable($message);
  1968. $resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
  1969. if(!empty($resourceRecipients)){
  1970. // Set Tracking status of resource recipients to olResponseAccepted (3)
  1971. for($i = 0, $len = count($resourceRecipients); $i < $len; $i++){
  1972. $resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusAccepted;
  1973. $resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS_TIME] = time();
  1974. }
  1975. mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $resourceRecipients);
  1976. }
  1977. // Publish updated free/busy information
  1978. if(!$this->errorSetResource){
  1979. for($i = 0, $len = count($resourceRecipData); $i < $len; $i++){
  1980. $storeProps = mapi_msgstore_getprops($resourceRecipData[$i]['store'], array(PR_MAILBOX_OWNER_ENTRYID));
  1981. if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])){
  1982. $pub = new FreeBusyPublish($this->session, $resourceRecipData[$i]['store'], $resourceRecipData[$i]['folder'], $storeProps[PR_MAILBOX_OWNER_ENTRYID]);
  1983. $pub->publishFB(time() - (7 * 24 * 60 * 60), 6 * 30 * 24 * 60 * 60); // publish from one week ago, 6 months ahead
  1984. }
  1985. }
  1986. }
  1987. return $resourceRecipData;
  1988. }
  1989. /**
  1990. * Function which save an exception into recurring item
  1991. *
  1992. * @param resource $recurringItem reference to MAPI_message of recurring item
  1993. * @param resource $occurrenceItem reference to MAPI_message of occurrence
  1994. * @param string $basedate basedate of occurrence
  1995. * @param boolean $move if true then occurrence item is deleted
  1996. * @param boolean $tentative true if user has tentatively accepted it or false if user has accepted it.
  1997. * @param boolean $userAction true if user has manually responded to meeting request
  1998. * @param resource $store user store
  1999. * @param boolean $isDelegate true if delegate is processing this meeting request
  2000. */
  2001. function acceptException(&$recurringItem, &$occurrenceItem, $basedate, $move = false, $tentative, $userAction = false, $store, $isDelegate = false)
  2002. {
  2003. $recurr = new Recurrence($store, $recurringItem);
  2004. // Copy properties from meeting request
  2005. $exception_props = mapi_getprops($occurrenceItem);
  2006. // Copy recipients list
  2007. $reciptable = mapi_message_getrecipienttable($occurrenceItem);
  2008. // If delegate, then do not add the delegate in recipients
  2009. if ($isDelegate) {
  2010. $delegate = mapi_getprops($this->message, array(PR_RECEIVED_BY_EMAIL_ADDRESS));
  2011. $res = array(RES_PROPERTY, array(RELOP => RELOP_NE, ULPROPTAG => PR_EMAIL_ADDRESS, VALUE => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]));
  2012. $recips = mapi_table_queryallrows($reciptable, $this->recipprops, $res);
  2013. } else {
  2014. $recips = mapi_table_queryallrows($reciptable, $this->recipprops);
  2015. }
  2016. // add owner to recipient table
  2017. $this->addOrganizer($exception_props, $recips, true);
  2018. // add delegator to meetings
  2019. if ($isDelegate) $this->addDelegator($exception_props, $recips);
  2020. $exception_props[$this->proptags['meetingstatus']] = olMeetingReceived;
  2021. $exception_props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
  2022. // Set basedate property (ExceptionReplaceTime)
  2023. if (isset($exception_props[$this->proptags['intendedbusystatus']])) {
  2024. if ($tentative && $exception_props[$this->proptags['intendedbusystatus']] !== fbFree)
  2025. $exception_props[$this->proptags['busystatus']] = $tentative;
  2026. else
  2027. $exception_props[$this->proptags['busystatus']] = $exception_props[$this->proptags['intendedbusystatus']];
  2028. // we already have intendedbusystatus value in $exception_props so no need to copy it
  2029. } else {
  2030. $exception_props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
  2031. }
  2032. if ($userAction)
  2033. // if user has responded then set replytime
  2034. $exception_props[$this->proptags['replytime']] = time();
  2035. if($recurr->isException($basedate))
  2036. $recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
  2037. else
  2038. $recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
  2039. // Move the occurrenceItem to the waste basket
  2040. if ($move) {
  2041. $wastebasket = $this->openDefaultWastebasket();
  2042. $sourcefolder = mapi_msgstore_openentry($this->store, $exception_props[PR_PARENT_ENTRYID]);
  2043. mapi_folder_copymessages($sourcefolder, Array($exception_props[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE);
  2044. }
  2045. mapi_savechanges($recurringItem);
  2046. }
  2047. /**
  2048. * Function which submits meeting request based on arguments passed to it.
  2049. *@param resource $message MAPI_message whose meeting request is to be send
  2050. *@param boolean $cancel if true send request, else send cancellation
  2051. *@param string $prefix subject prefix
  2052. *@param integer $basedate basedate for an occurrence
  2053. *@param Object $recurObject recurrence object of mr
  2054. *@param boolean $copyExceptions When sending update mail for recurring item then we dont send exceptions in attachments
  2055. */
  2056. function submitMeetingRequest($message, $cancel, $prefix, $basedate = false, $recurObject = false, $copyExceptions = true, $deletedRecips = false)
  2057. {
  2058. $newmessageprops = $messageprops = mapi_getprops($this->message);
  2059. $new = $this->createOutgoingMessage();
  2060. // Copy the entire message into the new meeting request message
  2061. if ($basedate) {
  2062. // messageprops contains properties of whole recurring series
  2063. // and newmessageprops contains properties of exception item
  2064. $newmessageprops = mapi_getprops($message);
  2065. // Ensure that the correct basedate is set in the new message
  2066. $newmessageprops[$this->proptags['basedate']] = $basedate;
  2067. // Set isRecurring to false, because this is an exception
  2068. $newmessageprops[$this->proptags['recurring']] = false;
  2069. // set LID_IS_EXCEPTION to true
  2070. $newmessageprops[$this->proptags['is_exception']] = true;
  2071. // Set to high importance
  2072. if($cancel) $newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;
  2073. // Set startdate and enddate of exception
  2074. if ($cancel && $recurObject) {
  2075. $newmessageprops[$this->proptags['startdate']] = $recurObject->getOccurrenceStart($basedate);
  2076. $newmessageprops[$this->proptags['duedate']] = $recurObject->getOccurrenceEnd($basedate);
  2077. }
  2078. // Set basedate in guid (0x3)
  2079. $newmessageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate);
  2080. $newmessageprops[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
  2081. $newmessageprops[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
  2082. // Get deleted recipiets from exception msg
  2083. $restriction = Array(RES_AND,
  2084. Array(
  2085. Array(RES_BITMASK,
  2086. Array( ULTYPE => BMR_NEZ,
  2087. ULPROPTAG => PR_RECIPIENT_FLAGS,
  2088. ULMASK => recipExceptionalDeleted
  2089. )
  2090. ),
  2091. Array(RES_BITMASK,
  2092. Array( ULTYPE => BMR_EQZ,
  2093. ULPROPTAG => PR_RECIPIENT_FLAGS,
  2094. ULMASK => recipOrganizer
  2095. )
  2096. ),
  2097. )
  2098. );
  2099. // In direct-booking mode, we don't need to send cancellations to resources
  2100. if ($this->enableDirectBooking)
  2101. $restriction[1][] = Array(RES_PROPERTY,
  2102. Array(RELOP => RELOP_NE, // Does not equal recipient type: MAPI_BCC (Resource)
  2103. ULPROPTAG => PR_RECIPIENT_TYPE,
  2104. VALUE => array(PR_RECIPIENT_TYPE => MAPI_BCC)
  2105. )
  2106. );
  2107. $recipienttable = mapi_message_getrecipienttable($message);
  2108. $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $restriction);
  2109. if (!$deletedRecips)
  2110. $deletedRecips = array_merge(array(), $recipients);
  2111. else
  2112. $deletedRecips = array_merge($deletedRecips, $recipients);
  2113. }
  2114. // Remove the PR_ICON_INDEX as it is not needed in the sent message and it also
  2115. // confuses webaccess
  2116. $newmessageprops[PR_ICON_INDEX] = null;
  2117. $newmessageprops[PR_RESPONSE_REQUESTED] = true;
  2118. // PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
  2119. $newmessageprops[PR_START_DATE] = $newmessageprops[$this->proptags['startdate']];
  2120. $newmessageprops[PR_END_DATE] = $newmessageprops[$this->proptags['duedate']];
  2121. // Set updatecounter/AppointmentSequenceNumber
  2122. // get the value of latest updatecounter for the whole series and use it
  2123. $newmessageprops[$this->proptags['updatecounter']] = $messageprops[$this->proptags['last_updatecounter']];
  2124. $meetingTimeInfo = $this->getMeetingTimeInfo();
  2125. if($meetingTimeInfo)
  2126. $newmessageprops[PR_BODY] = $meetingTimeInfo;
  2127. // Send all recurrence info in mail, if this is a recurrence meeting.
  2128. if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']]) {
  2129. if (!empty($messageprops[$this->proptags['recurring_pattern']]))
  2130. $newmessageprops[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
  2131. $newmessageprops[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
  2132. $newmessageprops[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
  2133. $newmessageprops[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
  2134. if ($recurObject)
  2135. $this->generateRecurDates($recurObject, $messageprops, $newmessageprops);
  2136. }
  2137. if (isset($newmessageprops[$this->proptags['counter_proposal']]))
  2138. unset($newmessageprops[$this->proptags['counter_proposal']]);
  2139. // Prefix the subject if needed
  2140. if ($prefix && isset($newmessageprops[PR_SUBJECT]))
  2141. $newmessageprops[PR_SUBJECT] = $prefix . $newmessageprops[PR_SUBJECT];
  2142. mapi_setprops($new, $newmessageprops);
  2143. // Copy attachments
  2144. $this->replaceAttachments($message, $new, $copyExceptions);
  2145. // Retrieve only those recipient who should receive this meeting request.
  2146. $stripResourcesRestriction = Array(RES_AND,
  2147. Array(
  2148. Array(RES_BITMASK,
  2149. Array( ULTYPE => BMR_EQZ,
  2150. ULPROPTAG => PR_RECIPIENT_FLAGS,
  2151. ULMASK => recipExceptionalDeleted
  2152. )
  2153. ),
  2154. Array(RES_BITMASK,
  2155. Array( ULTYPE => BMR_EQZ,
  2156. ULPROPTAG => PR_RECIPIENT_FLAGS,
  2157. ULMASK => recipOrganizer
  2158. )
  2159. ),
  2160. )
  2161. );
  2162. // In direct-booking mode, resources do not receive a meeting request
  2163. if ($this->enableDirectBooking)
  2164. $stripResourcesRestriction[1][] =
  2165. Array(RES_PROPERTY,
  2166. Array(RELOP => RELOP_NE, // Does not equal recipient type: MAPI_BCC (Resource)
  2167. ULPROPTAG => PR_RECIPIENT_TYPE,
  2168. VALUE => array(PR_RECIPIENT_TYPE => MAPI_BCC)
  2169. )
  2170. );
  2171. $recipienttable = mapi_message_getrecipienttable($message);
  2172. $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
  2173. if ($basedate && empty($recipients)) {
  2174. // Retrieve full list
  2175. $recipienttable = mapi_message_getrecipienttable($this->message);
  2176. $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops);
  2177. // Save recipients in exceptions
  2178. mapi_message_modifyrecipients($message, MODRECIP_ADD, $recipients);
  2179. // Now retrieve only those recipient who should receive this meeting request.
  2180. $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
  2181. }
  2182. //@TODO: handle nonAcceptingResources
  2183. /**
  2184. * Add resource recipients that did not automatically accept the meeting request.
  2185. * (note: meaning that they did not decline the meeting request)
  2186. *//*
  2187. for($i=0;$i<count($this->nonAcceptingResources);$i++){
  2188. $recipients[] = $this->nonAcceptingResources[$i];
  2189. }*/
  2190. if(!empty($recipients)) {
  2191. // Strip out the sender/"owner" recipient
  2192. mapi_message_modifyrecipients($new, MODRECIP_ADD, $recipients);
  2193. // Set some properties that are different in the sent request than
  2194. // in the item in our calendar
  2195. // we should store busystatus value to intendedbusystatus property, because busystatus for outgoing meeting request
  2196. // should always be fbTentative
  2197. $newmessageprops[$this->proptags['intendedbusystatus']] = isset($newmessageprops[$this->proptags['busystatus']]) ? $newmessageprops[$this->proptags['busystatus']] : $messageprops[$this->proptags['busystatus']];
  2198. $newmessageprops[$this->proptags['busystatus']] = fbTentative; // The default status when not accepted
  2199. $newmessageprops[$this->proptags['responsestatus']] = olResponseNotResponded; // The recipient has not responded yet
  2200. $newmessageprops[$this->proptags['attendee_critical_change']] = time();
  2201. $newmessageprops[$this->proptags['owner_critical_change']] = time();
  2202. $newmessageprops[$this->proptags['meetingtype']] = mtgRequest;
  2203. if ($cancel) {
  2204. $newmessageprops[PR_MESSAGE_CLASS] = "IPM.Schedule.Meeting.Canceled";
  2205. $newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
  2206. $newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
  2207. } else {
  2208. $newmessageprops[PR_MESSAGE_CLASS] = "IPM.Schedule.Meeting.Request";
  2209. $newmessageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
  2210. }
  2211. mapi_setprops($new, $newmessageprops);
  2212. mapi_message_savechanges($new);
  2213. // Submit message to non-resource recipients
  2214. mapi_message_submitmessage($new);
  2215. }
  2216. // Send cancellation to deleted attendees
  2217. if ($deletedRecips && !empty($deletedRecips)) {
  2218. $new = $this->createOutgoingMessage();
  2219. mapi_message_modifyrecipients($new, MODRECIP_ADD, $deletedRecips);
  2220. $newmessageprops[PR_MESSAGE_CLASS] = "IPM.Schedule.Meeting.Canceled";
  2221. $newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
  2222. $newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
  2223. $newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH; // HIGH Importance
  2224. if (isset($newmessageprops[PR_SUBJECT]))
  2225. $newmessageprops[PR_SUBJECT] = dgettext("kopano","Canceled").": " . $newmessageprops[PR_SUBJECT];
  2226. mapi_setprops($new, $newmessageprops);
  2227. mapi_message_savechanges($new);
  2228. // Submit message to non-resource recipients
  2229. mapi_message_submitmessage($new);
  2230. }
  2231. // Set properties on meeting object in calendar
  2232. // Set requestsent to 'true' (turns on 'tracking', etc)
  2233. $props = array();
  2234. $props[$this->proptags['meetingstatus']] = olMeeting;
  2235. $props[$this->proptags['responsestatus']] = olResponseOrganized;
  2236. $props[$this->proptags['requestsent']] = (!empty($recipients)) || ($this->includesResources && !$this->errorSetResource);
  2237. $props[$this->proptags['attendee_critical_change']] = time();
  2238. $props[$this->proptags['owner_critical_change']] = time();
  2239. $props[$this->proptags['meetingtype']] = mtgRequest;
  2240. // save the new updatecounter to exception/recurring series/normal meeting
  2241. $props[$this->proptags['updatecounter']] = $newmessageprops[$this->proptags['updatecounter']];
  2242. // PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
  2243. $props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
  2244. $props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
  2245. mapi_setprops($message, $props);
  2246. // saving of these properties on calendar item should be handled by caller function
  2247. // based on sending meeting request was successful or not.
  2248. }
  2249. /**
  2250. * OL2007 uses these 4 properties to specify occurence that should be updated.
  2251. * ical generates RECURRENCE-ID property based on exception's basedate (PidLidExceptionReplaceTime),
  2252. * but OL07 doesn't send this property, so ical will generate RECURRENCE-ID property based on date
  2253. * from GlobalObjId and time from StartRecurTime property, so we are sending basedate property and
  2254. * also additionally we are sending these properties.
  2255. * Ref: MS-OXCICAL 2.2.1.20.20 Property: RECURRENCE-ID
  2256. * @param Object $recurObject instance of recurrence class for this message
  2257. * @param Array $messageprops properties of meeting object that is going to be send
  2258. * @param Array $newmessageprops properties of meeting request/response that is going to be send
  2259. */
  2260. function generateRecurDates($recurObject, $messageprops, &$newmessageprops)
  2261. {
  2262. if($messageprops[$this->proptags['startdate']] && $messageprops[$this->proptags['duedate']]) {
  2263. $startDate = date("Y:n:j:G:i:s", $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['startdate']]));
  2264. $endDate = date("Y:n:j:G:i:s", $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['duedate']]));
  2265. $startDate = explode(":", $startDate);
  2266. $endDate = explode(":", $endDate);
  2267. // [0] => year, [1] => month, [2] => day, [3] => hour, [4] => minutes, [5] => seconds
  2268. // RecurStartDate = year * 512 + month_number * 32 + day_number
  2269. $newmessageprops[$this->proptags["start_recur_date"]] = (((int) $startDate[0]) * 512) + (((int) $startDate[1]) * 32) + ((int) $startDate[2]);
  2270. // RecurStartTime = hour * 4096 + minutes * 64 + seconds
  2271. $newmessageprops[$this->proptags["start_recur_time"]] = (((int) $startDate[3]) * 4096) + (((int) $startDate[4]) * 64) + ((int) $startDate[5]);
  2272. $newmessageprops[$this->proptags["end_recur_date"]] = (((int) $endDate[0]) * 512) + (((int) $endDate[1]) * 32) + ((int) $endDate[2]);
  2273. $newmessageprops[$this->proptags["end_recur_time"]] = (((int) $endDate[3]) * 4096) + (((int) $endDate[4]) * 64) + ((int) $endDate[5]);
  2274. }
  2275. }
  2276. function createOutgoingMessage()
  2277. {
  2278. $sentprops = array();
  2279. $outbox = $this->openDefaultOutbox($this->openDefaultStore());
  2280. $outgoing = mapi_folder_createmessage($outbox);
  2281. if(!$outgoing) return false;
  2282. $addrinfo = $this->getOwnerAddress($this->store);
  2283. if($addrinfo) {
  2284. list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrinfo;
  2285. $sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
  2286. $sentprops[PR_SENT_REPRESENTING_NAME] = $ownername;
  2287. $sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
  2288. $sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
  2289. $sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
  2290. }
  2291. $sentprops[PR_SENTMAIL_ENTRYID] = $this->getDefaultSentmailEntryID($this->openDefaultStore());
  2292. mapi_setprops($outgoing, $sentprops);
  2293. return $outgoing;
  2294. }
  2295. /**
  2296. * Function which checks received meeting request is either old(outofdate) or new.
  2297. * @return boolean true if meeting request is outofdate else false if it is new
  2298. */
  2299. function isMeetingOutOfDate()
  2300. {
  2301. $result = false;
  2302. $store = $this->store;
  2303. $props = mapi_getprops($this->message, array($this->proptags['goid'], $this->proptags['goid2'], $this->proptags['updatecounter'], $this->proptags['meetingtype'], $this->proptags['owner_critical_change']));
  2304. if (isset($props[$this->proptags['meetingtype']]) && ($props[$this->proptags['meetingtype']] & mtgOutOfDate) == mtgOutOfDate)
  2305. return true;
  2306. // get the basedate to check for exception
  2307. $basedate = $this->getBasedateFromGlobalID($props[$this->proptags['goid']]);
  2308. $calendarItems = $this->getCorrespondedCalendarItems();
  2309. foreach($calendarItems as $calendarItem) {
  2310. if ($calendarItem) {
  2311. $calendarItemProps = mapi_getprops($calendarItem, array(
  2312. $this->proptags['owner_critical_change'],
  2313. $this->proptags['updatecounter'],
  2314. $this->proptags['recurring']
  2315. ));
  2316. // If these items is recurring and basedate is found then open exception to compare it with meeting request
  2317. if (isset($calendarItemProps[$this->proptags['recurring']]) && $calendarItemProps[$this->proptags['recurring']] && $basedate) {
  2318. $recurr = new Recurrence($store, $calendarItem);
  2319. if ($recurr->isException($basedate)) {
  2320. $attach = $recurr->getExceptionAttachment($basedate);
  2321. $exception = mapi_attach_openobj($attach, 0);
  2322. $occurrenceItemProps = mapi_getprops($exception, array(
  2323. $this->proptags['owner_critical_change'],
  2324. $this->proptags['updatecounter']
  2325. ));
  2326. }
  2327. // we found the exception, compare with it
  2328. if(isset($occurrenceItemProps)) {
  2329. if ((isset($occurrenceItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $occurrenceItemProps[$this->proptags['updatecounter']])
  2330. || (isset($occurrenceItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $occurrenceItemProps[$this->proptags['owner_critical_change']])) {
  2331. mapi_setprops($this->message, array($this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033));
  2332. mapi_savechanges($this->message);
  2333. $result = true;
  2334. }
  2335. } else if ((isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]) ||
  2336. (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']])) {
  2337. // we are not able to find exception, could mean that a significant change has occurred on series
  2338. // and it deleted all exceptions, so compare with series
  2339. mapi_setprops($this->message, array($this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033));
  2340. mapi_savechanges($this->message);
  2341. $result = true;
  2342. }
  2343. } else if ((isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]) ||
  2344. (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']])) {
  2345. // normal / recurring series
  2346. mapi_setprops($this->message, array($this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033));
  2347. mapi_savechanges($this->message);
  2348. $result = true;
  2349. }
  2350. }
  2351. }
  2352. return $result;
  2353. }
  2354. /**
  2355. * Function which checks received meeting request is updated later or not.
  2356. * @return boolean true if meeting request is updated later.
  2357. * @TODO: Implement handling for recurrings and exceptions.
  2358. */
  2359. function isMeetingUpdated()
  2360. {
  2361. $result = false;
  2362. $store = $this->store;
  2363. $props = mapi_getprops($this->message, array($this->proptags['goid'], $this->proptags['goid2'], $this->proptags['updatecounter'], $this->proptags['owner_critical_change'], $this->proptags['updatecounter']));
  2364. $calendarItems = $this->getCorrespondedCalendarItems();
  2365. foreach($calendarItems as $calendarItem) {
  2366. if ($calendarItem) {
  2367. $calendarItemProps = mapi_getprops($calendarItem, array(
  2368. $this->proptags['updatecounter'],
  2369. $this->proptags['recurring']
  2370. ));
  2371. if (isset($calendarItemProps[$this->proptags['updatecounter']]) && isset($props[$this->proptags['updatecounter']]) && $calendarItemProps[$this->proptags['updatecounter']] > $props[$this->proptags['updatecounter']])
  2372. $result = true;
  2373. }
  2374. }
  2375. return $result;
  2376. }
  2377. /**
  2378. * Checks if there has been any significant changes on appointment/meeting item.
  2379. * Significant changes be:
  2380. * 1) startdate has been changed
  2381. * 2) duedate has been changed OR
  2382. * 3) recurrence pattern has been created, modified or removed
  2383. *
  2384. * @param Array oldProps old props before an update
  2385. * @param Number basedate basedate
  2386. * @param Boolean isRecurrenceChanged for change in recurrence pattern.
  2387. * isRecurrenceChanged true means Recurrence pattern has been changed, so clear all attendees response
  2388. */
  2389. function checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged = false)
  2390. {
  2391. $message = null;
  2392. $attach = null;
  2393. // If basedate is specified then we need to open exception message to clear recipient responses
  2394. if($basedate) {
  2395. $recurrence = new Recurrence($this->store, $this->message);
  2396. if($recurrence->isException($basedate)){
  2397. $attach = $recurrence->getExceptionAttachment($basedate);
  2398. if ($attach)
  2399. $message = mapi_attach_openobj($attach, MAPI_MODIFY);
  2400. }
  2401. } else {
  2402. // use normal message or recurring series message
  2403. $message = $this->message;
  2404. }
  2405. if (!$message)
  2406. return;
  2407. $newProps = mapi_getprops($message, array($this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['updatecounter']));
  2408. // Check whether message is updated or not.
  2409. if (isset($newProps[$this->proptags['updatecounter']]) && $newProps[$this->proptags['updatecounter']] == 0)
  2410. return;
  2411. if (($newProps[$this->proptags['startdate']] != $oldProps[$this->proptags['startdate']])
  2412. || ($newProps[$this->proptags['duedate']] != $oldProps[$this->proptags['duedate']])
  2413. || $isRecurrenceChanged) {
  2414. $this->clearRecipientResponse($message);
  2415. mapi_setprops($message, array($this->proptags['owner_critical_change'] => time()));
  2416. mapi_savechanges($message);
  2417. if ($attach) // Also save attachment Object.
  2418. mapi_savechanges($attach);
  2419. }
  2420. }
  2421. /**
  2422. * Clear responses of all attendees who have replied in past.
  2423. * @param MAPI_MESSAGE $message on which responses should be cleared
  2424. */
  2425. function clearRecipientResponse($message)
  2426. {
  2427. $recipTable = mapi_message_getrecipienttable($message);
  2428. $recipsRows = mapi_table_queryallrows($recipTable, $this->recipprops);
  2429. foreach($recipsRows as $recipient) {
  2430. if (($recipient[PR_RECIPIENT_FLAGS] & recipOrganizer) != recipOrganizer)
  2431. // Recipient is attendee, set the trackstatus to "Not Responded"
  2432. $recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
  2433. else
  2434. // Recipient is organizer, this is not possible, but for safety
  2435. // it is best to clear the trackstatus for him as well by setting
  2436. // the trackstatus to "Organized".
  2437. $recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
  2438. mapi_message_modifyrecipients($message, MODRECIP_MODIFY, array($recipient));
  2439. }
  2440. }
  2441. /**
  2442. * Function returns corresponded calendar items attached with
  2443. * the meeting request.
  2444. * @return Array array of correlated calendar items.
  2445. */
  2446. function getCorrespondedCalendarItems()
  2447. {
  2448. $store = $this->store;
  2449. $props = mapi_getprops($this->message, array($this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_NAME));
  2450. $basedate = $this->getBasedateFromGlobalID($props[$this->proptags['goid']]);
  2451. // If Delegate is processing mr for Delegator then retrieve Delegator's store and calendar.
  2452. if (isset($props[PR_RCVD_REPRESENTING_NAME])) {
  2453. $delegatorStore = $this->getDelegatorStore($props);
  2454. $store = $delegatorStore['store'];
  2455. $calFolder = $delegatorStore['calFolder'];
  2456. } else {
  2457. $calFolder = $this->openDefaultCalendar();
  2458. }
  2459. // Finding item in calendar with GlobalID(0x3), not necessary that attendee is having recurring item, he/she can also have only a occurrence
  2460. $entryids = $this->findCalendarItems($props[$this->proptags['goid']], $calFolder);
  2461. // Basedate found, so this meeting request is an update of an occurrence.
  2462. if ($basedate) {
  2463. if (!$entryids)
  2464. // Find main recurring item in calendar with GlobalID(0x23)
  2465. $entryids = $this->findCalendarItems($props[$this->proptags['goid2']], $calFolder);
  2466. }
  2467. $calendarItems = array();
  2468. if ($entryids)
  2469. foreach ($entryids as $entryid)
  2470. $calendarItems[] = mapi_msgstore_openentry($store, $entryid);
  2471. return $calendarItems;
  2472. }
  2473. /**
  2474. * Function which checks whether received meeting request is either conflicting with other appointments or not.
  2475. *@return mixed(boolean/integer) true if normal meeting is conflicting or an integer which specifies no of instances
  2476. * conflict of recurring meeting and false if meeting is not conflicting.
  2477. */
  2478. function isMeetingConflicting($message = false, $userStore = false, $calFolder = false, $msgprops = false)
  2479. {
  2480. $returnValue = false;
  2481. $conflicting = false;
  2482. $noOfInstances = 0;
  2483. if (!$message) $message = $this->message;
  2484. if (!$userStore) $userStore = $this->store;
  2485. if (!$calFolder) {
  2486. $root = mapi_msgstore_openentry($userStore);
  2487. $rootprops = mapi_getprops($root, array(PR_STORE_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_FREEBUSY_ENTRYIDS));
  2488. if (!isset($rootprops[PR_IPM_APPOINTMENT_ENTRYID]))
  2489. return;
  2490. $calFolder = mapi_msgstore_openentry($userStore, $rootprops[PR_IPM_APPOINTMENT_ENTRYID]);
  2491. }
  2492. if (!$msgprops) $msgprops = mapi_getprops($message, array($this->proptags['goid'], $this->proptags['goid2'], $this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['recurring'], $this->proptags['clipstart'], $this->proptags['clipend']));
  2493. if ($calFolder) {
  2494. // Meeting request is recurring, so get all occurrence and check for each occurrence whether it conflicts with other appointments in Calendar.
  2495. if (isset($msgprops[$this->proptags['recurring']]) && $msgprops[$this->proptags['recurring']]) {
  2496. // Apply recurrence class and retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
  2497. $recurr = new Recurrence($userStore, $message);
  2498. $items = $recurr->getItems($msgprops[$this->proptags['clipstart']], $msgprops[$this->proptags['clipend']] * (24*24*60), 30);
  2499. foreach ($items as $item) {
  2500. // Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
  2501. $calendarItems = $recurr->getCalendarItems($userStore, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], array($this->proptags['goid'], $this->proptags['busystatus'], PR_OWNER_APPT_ID));
  2502. foreach ($calendarItems as $calendarItem) {
  2503. if ($calendarItem[$this->proptags['busystatus']] == fbFree)
  2504. continue;
  2505. /**
  2506. * Only meeting requests have globalID, normal appointments do not have globalID
  2507. * so if any normal appointment if found then it is assumed to be conflict.
  2508. */
  2509. if (!isset($calendarItem[$this->proptags['goid']])) {
  2510. $noOfInstances++;
  2511. break;
  2512. }
  2513. if ($calendarItem[$this->proptags['goid']] !== $msgprops[$this->proptags['goid']]) {
  2514. $noOfInstances++;
  2515. break;
  2516. }
  2517. }
  2518. }
  2519. $returnValue = $noOfInstances;
  2520. } else {
  2521. // Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
  2522. $items = getCalendarItems($userStore, $calFolder, $msgprops[$this->proptags['startdate']], $msgprops[$this->proptags['duedate']], array($this->proptags['goid'], $this->proptags['busystatus'], PR_OWNER_APPT_ID));
  2523. foreach($items as $item) {
  2524. if ($item[$this->proptags['busystatus']] == fbFree)
  2525. continue;
  2526. if (!isset($item[$this->proptags['goid']])) {
  2527. $conflicting = true;
  2528. break;
  2529. }
  2530. if (($item[$this->proptags['goid']] !== $msgprops[$this->proptags['goid']])
  2531. && ($item[$this->proptags['goid']] !== $msgprops[$this->proptags['goid2']])) {
  2532. $conflicting = true;
  2533. break;
  2534. }
  2535. }
  2536. if ($conflicting) $returnValue = true;
  2537. }
  2538. }
  2539. return $returnValue;
  2540. }
  2541. /**
  2542. * Function which adds organizer to recipient list which is passed.
  2543. * This function also checks if it has organizer.
  2544. *
  2545. * @param array $messageProps message properties
  2546. * @param array $recipients recipients list of message.
  2547. * @param boolean $isException true if we are processing recipient of exception
  2548. */
  2549. function addDelegator($messageProps, &$recipients)
  2550. {
  2551. $hasDelegator = false;
  2552. // Check if meeting already has an organizer.
  2553. foreach ($recipients as $key => $recipient)
  2554. if (isset($messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) && $recipient[PR_EMAIL_ADDRESS] == $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS])
  2555. $hasDelegator = true;
  2556. if (!$hasDelegator){
  2557. // Create delegator.
  2558. $delegator = array();
  2559. $delegator[PR_ENTRYID] = $messageProps[PR_RCVD_REPRESENTING_ENTRYID];
  2560. $delegator[PR_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
  2561. $delegator[PR_EMAIL_ADDRESS] = $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS];
  2562. $delegator[PR_RECIPIENT_TYPE] = MAPI_TO;
  2563. $delegator[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
  2564. $delegator[PR_ADDRTYPE] = empty($messageProps[PR_RCVD_REPRESENTING_ADDRTYPE]) ? 'SMTP':$messageProps[PR_RCVD_REPRESENTING_ADDRTYPE];
  2565. $delegator[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
  2566. $delegator[PR_RECIPIENT_FLAGS] = recipSendable;
  2567. $delegator[PR_SEARCH_KEY] = $messageProps[PR_RCVD_REPRESENTING_SEARCH_KEY];
  2568. // Add organizer to recipients list.
  2569. array_unshift($recipients, $delegator);
  2570. }
  2571. }
  2572. function getDelegatorStore($messageprops)
  2573. {
  2574. // Find the organiser of appointment in addressbook
  2575. $delegatorName = array(array(PR_DISPLAY_NAME => $messageprops[PR_RCVD_REPRESENTING_NAME]));
  2576. $ab = mapi_openaddressbook($this->session);
  2577. $user = mapi_ab_resolvename($ab, $delegatorName, EMS_AB_ADDRESS_LOOKUP);
  2578. // Get StoreEntryID by username
  2579. $delegatorEntryid = mapi_msgstore_createentryid($this->store, $user[0][PR_EMAIL_ADDRESS]);
  2580. // Open store of the delegator
  2581. $delegatorStore = mapi_openmsgstore($this->session, $delegatorEntryid);
  2582. // Open root folder
  2583. $delegatorRoot = mapi_msgstore_openentry($delegatorStore, null);
  2584. // Get calendar entryID
  2585. $delegatorRootProps = mapi_getprops($delegatorRoot, array(PR_IPM_APPOINTMENT_ENTRYID));
  2586. // Open the calendar Folder
  2587. $calFolder = mapi_msgstore_openentry($delegatorStore, $delegatorRootProps[PR_IPM_APPOINTMENT_ENTRYID]);
  2588. return Array('store' => $delegatorStore, 'calFolder' => $calFolder);
  2589. }
  2590. /**
  2591. * Function returns extra info about meeting timing along with message body
  2592. * which will be included in body while sending meeting request/response.
  2593. *
  2594. * @return string $meetingTimeInfo info about meeting timing along with message body
  2595. */
  2596. function getMeetingTimeInfo()
  2597. {
  2598. return $this->meetingTimeInfo;
  2599. }
  2600. /**
  2601. * Function sets extra info about meeting timing along with message body
  2602. * which will be included in body while sending meeting request/response.
  2603. *
  2604. * @param string $meetingTimeInfo info about meeting timing along with message body
  2605. */
  2606. function setMeetingTimeInfo($meetingTimeInfo)
  2607. {
  2608. $this->meetingTimeInfo = $meetingTimeInfo;
  2609. }
  2610. }
  2611. ?>