1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only |
5
|
|
|
* SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH |
6
|
|
|
* SPDX-FileCopyrightText: Copyright 2020-2025 grommunio GmbH |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
class MAPIProvider { |
10
|
|
|
private $session; |
11
|
|
|
private $store; |
12
|
|
|
private $zRFC822; |
13
|
|
|
private $addressbook; |
14
|
|
|
private $storeProps; |
15
|
|
|
private $inboxProps; |
16
|
|
|
private $rootProps; |
17
|
|
|
private $specialFoldersData; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Constructor of the MAPI Provider |
21
|
|
|
* Almost all methods of this class require a MAPI session and/or store. |
22
|
|
|
* |
23
|
|
|
* @param resource $session |
24
|
|
|
* @param resource $store |
25
|
|
|
*/ |
26
|
|
|
public function __construct($session, $store) { |
27
|
|
|
$this->session = $session; |
28
|
|
|
$this->store = $store; |
29
|
|
|
} |
30
|
|
|
|
31
|
|
|
/*---------------------------------------------------------------------------------------------------------- |
32
|
|
|
* GETTER |
33
|
|
|
*/ |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Reads a message from MAPI |
37
|
|
|
* Depending on the message class, a contact, appointment, task or email is read. |
38
|
|
|
* |
39
|
|
|
* @param mixed $mapimessage |
40
|
|
|
* @param ContentParameters $contentparameters |
41
|
|
|
* |
42
|
|
|
* @return SyncObject |
43
|
|
|
*/ |
44
|
|
|
public function GetMessage($mapimessage, $contentparameters) { |
45
|
|
|
// Gets the Sync object from a MAPI object according to its message class |
46
|
|
|
|
47
|
|
|
$props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]); |
|
|
|
|
48
|
|
|
if (isset($props[PR_MESSAGE_CLASS])) { |
49
|
|
|
$messageclass = $props[PR_MESSAGE_CLASS]; |
50
|
|
|
} |
51
|
|
|
else { |
52
|
|
|
$messageclass = "IPM"; |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
if (strpos($messageclass, "IPM.Contact") === 0) { |
56
|
|
|
return $this->getContact($mapimessage, $contentparameters); |
57
|
|
|
} |
58
|
|
|
if (strpos($messageclass, "IPM.Appointment") === 0) { |
59
|
|
|
return $this->getAppointment($mapimessage, $contentparameters); |
60
|
|
|
} |
61
|
|
|
if (strpos($messageclass, "IPM.Task") === 0 && strpos($messageclass, "IPM.TaskRequest") === false) { |
62
|
|
|
return $this->getTask($mapimessage, $contentparameters); |
63
|
|
|
} |
64
|
|
|
if (strpos($messageclass, "IPM.StickyNote") === 0) { |
65
|
|
|
return $this->getNote($mapimessage, $contentparameters); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
return $this->getEmail($mapimessage, $contentparameters); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Reads a contact object from MAPI. |
73
|
|
|
* |
74
|
|
|
* @param mixed $mapimessage |
75
|
|
|
* @param ContentParameters $contentparameters |
76
|
|
|
* |
77
|
|
|
* @return SyncContact |
78
|
|
|
*/ |
79
|
|
|
private function getContact($mapimessage, $contentparameters) { |
80
|
|
|
$message = new SyncContact(); |
81
|
|
|
|
82
|
|
|
// Standard one-to-one mappings first |
83
|
|
|
$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetContactMapping()); |
84
|
|
|
|
85
|
|
|
// Contact specific props |
86
|
|
|
$contactproperties = MAPIMapping::GetContactProperties(); |
87
|
|
|
$messageprops = $this->getProps($mapimessage, $contactproperties); |
88
|
|
|
|
89
|
|
|
// set the body according to contentparameters and supported AS version |
90
|
|
|
$this->setMessageBody($mapimessage, $contentparameters, $message); |
91
|
|
|
|
92
|
|
|
// check the picture |
93
|
|
|
if (isset($messageprops[$contactproperties["haspic"]]) && $messageprops[$contactproperties["haspic"]]) { |
94
|
|
|
// Add attachments |
95
|
|
|
$attachtable = mapi_message_getattachmenttable($mapimessage); |
96
|
|
|
mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction()); |
97
|
|
|
$rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM, PR_ATTACH_SIZE]); |
|
|
|
|
98
|
|
|
|
99
|
|
|
foreach ($rows as $row) { |
100
|
|
|
if (isset($row[PR_ATTACH_NUM])) { |
101
|
|
|
$mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); |
102
|
|
|
$message->picture = base64_encode(mapi_attach_openbin($mapiattach, PR_ATTACH_DATA_BIN)); |
|
|
|
|
103
|
|
|
} |
104
|
|
|
} |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
return $message; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Reads a task object from MAPI. |
112
|
|
|
* |
113
|
|
|
* @param mixed $mapimessage |
114
|
|
|
* @param ContentParameters $contentparameters |
115
|
|
|
* |
116
|
|
|
* @return SyncTask |
117
|
|
|
*/ |
118
|
|
|
private function getTask($mapimessage, $contentparameters) { |
119
|
|
|
$message = new SyncTask(); |
120
|
|
|
|
121
|
|
|
// Standard one-to-one mappings first |
122
|
|
|
$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetTaskMapping()); |
123
|
|
|
|
124
|
|
|
// Task specific props |
125
|
|
|
$taskproperties = MAPIMapping::GetTaskProperties(); |
126
|
|
|
$messageprops = $this->getProps($mapimessage, $taskproperties); |
127
|
|
|
|
128
|
|
|
// set the body according to contentparameters and supported AS version |
129
|
|
|
$this->setMessageBody($mapimessage, $contentparameters, $message); |
130
|
|
|
|
131
|
|
|
// task with deadoccur is an occurrence of a recurring task and does not need to be handled as recurring |
132
|
|
|
// webaccess does not set deadoccur for the initial recurring task |
133
|
|
|
if (isset($messageprops[$taskproperties["isrecurringtag"]]) && |
134
|
|
|
$messageprops[$taskproperties["isrecurringtag"]] && |
135
|
|
|
(!isset($messageprops[$taskproperties["deadoccur"]]) || |
136
|
|
|
(isset($messageprops[$taskproperties["deadoccur"]]) && |
137
|
|
|
!$messageprops[$taskproperties["deadoccur"]]))) { |
138
|
|
|
// Process recurrence |
139
|
|
|
$message->recurrence = new SyncTaskRecurrence(); |
140
|
|
|
$this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, false, $taskproperties); |
|
|
|
|
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
// when set the task to complete using the WebAccess, the dateComplete property is not set correctly |
144
|
|
|
if ($message->complete == 1 && !isset($message->datecompleted)) { |
145
|
|
|
$message->datecompleted = time(); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
// if no reminder is set, announce that to the mobile |
149
|
|
|
if (!isset($message->reminderset)) { |
150
|
|
|
$message->reminderset = 0; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
return $message; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Reads an appointment object from MAPI. |
158
|
|
|
* |
159
|
|
|
* @param mixed $mapimessage |
160
|
|
|
* @param ContentParameters $contentparameters |
161
|
|
|
* |
162
|
|
|
* @return SyncAppointment |
163
|
|
|
*/ |
164
|
|
|
private function getAppointment($mapimessage, $contentparameters) { |
165
|
|
|
$message = new SyncAppointment(); |
166
|
|
|
|
167
|
|
|
// Standard one-to-one mappings first |
168
|
|
|
$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetAppointmentMapping()); |
169
|
|
|
|
170
|
|
|
// Appointment specific props |
171
|
|
|
$appointmentprops = MAPIMapping::GetAppointmentProperties(); |
172
|
|
|
$messageprops = $this->getProps($mapimessage, $appointmentprops); |
173
|
|
|
|
174
|
|
|
// set the body according to contentparameters and supported AS version |
175
|
|
|
$this->setMessageBody($mapimessage, $contentparameters, $message); |
176
|
|
|
|
177
|
|
|
// Set reminder time if reminderset is true |
178
|
|
|
if (isset($messageprops[$appointmentprops["reminderset"]]) && $messageprops[$appointmentprops["reminderset"]] == true) { |
179
|
|
|
if (!isset($messageprops[$appointmentprops["remindertime"]]) || $messageprops[$appointmentprops["remindertime"]] == 0x5AE980E1) { |
180
|
|
|
$message->reminder = 15; |
181
|
|
|
} |
182
|
|
|
else { |
183
|
|
|
$message->reminder = $messageprops[$appointmentprops["remindertime"]]; |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
if (!isset($message->uid)) { |
188
|
|
|
$message->uid = bin2hex($messageprops[$appointmentprops["sourcekey"]]); |
189
|
|
|
} |
190
|
|
|
else { |
191
|
|
|
// if no embedded vCal-Uid is found use hexed GOID |
192
|
|
|
$message->uid = getUidFromGoid($message->uid) ?? strtoupper(bin2hex($message->uid)); |
|
|
|
|
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
// Always set organizer information because some devices do not work properly without it |
196
|
|
|
if (isset($messageprops[$appointmentprops["representingentryid"]], $messageprops[$appointmentprops["representingname"]])) { |
197
|
|
|
$message->organizeremail = $this->getSMTPAddressFromEntryID($messageprops[$appointmentprops["representingentryid"]]); |
198
|
|
|
// if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY |
199
|
|
|
if ($message->organizeremail == "" && isset($messageprops[$appointmentprops["sentrepresentinsrchk"]])) { |
200
|
|
|
$message->organizeremail = $this->getEmailAddressFromSearchKey($messageprops[$appointmentprops["sentrepresentinsrchk"]]); |
201
|
|
|
} |
202
|
|
|
$message->organizername = $messageprops[$appointmentprops["representingname"]]; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
if (!empty($messageprops[$appointmentprops["timezonetag"]])) { |
206
|
|
|
$tz = $this->getTZFromMAPIBlob($messageprops[$appointmentprops["timezonetag"]]); |
207
|
|
|
$message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
208
|
|
|
} |
209
|
|
|
elseif (!empty($messageprops[$appointmentprops["tzdefstart"]])) { |
210
|
|
|
$tzDefStart = TimezoneUtil::CreateTimezoneDefinitionObject($messageprops[$appointmentprops["tzdefstart"]]); |
211
|
|
|
$tz = TimezoneUtil::GetTzFromTimezoneDef($tzDefStart); |
212
|
|
|
$message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
213
|
|
|
} |
214
|
|
|
elseif (!empty($messageprops[$appointmentprops["timezonedesc"]])) { |
215
|
|
|
// Windows uses UTC in timezone description in opposite to mstzones in TimezoneUtil which uses GMT |
216
|
|
|
$wintz = str_replace("UTC", "GMT", $messageprops[$appointmentprops["timezonedesc"]]); |
217
|
|
|
$tz = TimezoneUtil::GetFullTZFromTZName(TimezoneUtil::GetTZNameFromWinTZ($wintz)); |
218
|
|
|
$message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
219
|
|
|
} |
220
|
|
|
else { |
221
|
|
|
// set server default timezone (correct timezone should be configured!) |
222
|
|
|
$tz = TimezoneUtil::GetFullTZ(); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
if (isset($messageprops[$appointmentprops["isrecurring"]]) && $messageprops[$appointmentprops["isrecurring"]]) { |
226
|
|
|
// Process recurrence |
227
|
|
|
$message->recurrence = new SyncRecurrence(); |
228
|
|
|
$this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, $tz, $appointmentprops); |
229
|
|
|
|
230
|
|
|
if (empty($message->alldayevent)) { |
231
|
|
|
$message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
232
|
|
|
} |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
// Do attendees |
236
|
|
|
$reciptable = mapi_message_getrecipienttable($mapimessage); |
237
|
|
|
// Only get first 256 recipients, to prevent possible load issues. |
238
|
|
|
$rows = mapi_table_queryrows($reciptable, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ADDRTYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TYPE, PR_SEARCH_KEY], 0, 256); |
|
|
|
|
239
|
|
|
|
240
|
|
|
// Exception: we do not synchronize appointments with more than 250 attendees |
241
|
|
|
if (count($rows) > 250) { |
242
|
|
|
$message->id = bin2hex($messageprops[$appointmentprops["sourcekey"]]); |
243
|
|
|
$mbe = new SyncObjectBrokenException("Appointment has too many attendees"); |
244
|
|
|
$mbe->SetSyncObject($message); |
245
|
|
|
|
246
|
|
|
throw $mbe; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
if (count($rows) > 0) { |
250
|
|
|
$message->attendees = []; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
foreach ($rows as $row) { |
254
|
|
|
$attendee = new SyncAttendee(); |
255
|
|
|
|
256
|
|
|
$attendee->name = $row[PR_DISPLAY_NAME]; |
257
|
|
|
// smtp address is always a proper email address |
258
|
|
|
if (isset($row[PR_SMTP_ADDRESS])) { |
259
|
|
|
$attendee->email = $row[PR_SMTP_ADDRESS]; |
260
|
|
|
} |
261
|
|
|
elseif (isset($row[PR_ADDRTYPE], $row[PR_EMAIL_ADDRESS])) { |
262
|
|
|
// if address type is SMTP, it's also a proper email address |
263
|
|
|
if ($row[PR_ADDRTYPE] == "SMTP") { |
264
|
|
|
$attendee->email = $row[PR_EMAIL_ADDRESS]; |
265
|
|
|
} |
266
|
|
|
// if address type is ZARAFA, the PR_EMAIL_ADDRESS contains username |
267
|
|
|
elseif ($row[PR_ADDRTYPE] == "ZARAFA") { |
268
|
|
|
$userinfo = @nsp_getuserinfo($row[PR_EMAIL_ADDRESS]); |
269
|
|
|
if (is_array($userinfo) && isset($userinfo["primary_email"])) { |
270
|
|
|
$attendee->email = $userinfo["primary_email"]; |
271
|
|
|
} |
272
|
|
|
// if the user was not found, do a fallback to PR_SEARCH_KEY |
273
|
|
|
elseif (isset($row[PR_SEARCH_KEY])) { |
274
|
|
|
$attendee->email = $this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY]); |
275
|
|
|
} |
276
|
|
|
else { |
277
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->getAppointment: The attendee '%s' of type ZARAFA can not be resolved. Code: 0x%X", $row[PR_EMAIL_ADDRESS], mapi_last_hresult())); |
278
|
|
|
} |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
// set attendee's status and type if they're available and if we are the organizer |
283
|
|
|
$storeprops = $this->GetStoreProps(); |
284
|
|
|
if (isset($row[PR_RECIPIENT_TRACKSTATUS], $messageprops[$appointmentprops["representingentryid"]], $storeprops[PR_MAILBOX_OWNER_ENTRYID]) && |
|
|
|
|
285
|
|
|
$messageprops[$appointmentprops["representingentryid"]] == $storeprops[PR_MAILBOX_OWNER_ENTRYID]) { |
286
|
|
|
$attendee->attendeestatus = $row[PR_RECIPIENT_TRACKSTATUS]; |
287
|
|
|
} |
288
|
|
|
if (isset($row[PR_RECIPIENT_TYPE])) { |
289
|
|
|
$attendee->attendeetype = $row[PR_RECIPIENT_TYPE]; |
290
|
|
|
} |
291
|
|
|
// Some attendees have no email or name (eg resources), and if you |
292
|
|
|
// don't send one of those fields, the phone will give an error ... so |
293
|
|
|
// we don't send it in that case. |
294
|
|
|
// also ignore the "attendee" if the email is equal to the organizers' email |
295
|
|
|
if (isset($attendee->name, $attendee->email) && $attendee->email != "" && (!isset($message->organizeremail) || (isset($message->organizeremail) && $attendee->email != $message->organizeremail))) { |
296
|
|
|
array_push($message->attendees, $attendee); |
297
|
|
|
} |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
// Status 0 = no meeting, status 1 = organizer, status 2/3/4/5 = tentative/accepted/declined/notresponded |
301
|
|
|
if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] > 1) { |
302
|
|
|
if (!isset($message->attendees) || !is_array($message->attendees)) { |
303
|
|
|
$message->attendees = []; |
304
|
|
|
} |
305
|
|
|
// Work around iOS6 cancellation issue when there are no attendees for this meeting. Just add ourselves as the sole attendee. |
306
|
|
|
if (count($message->attendees) == 0) { |
307
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->getAppointment: adding ourself as an attendee for iOS6 workaround")); |
308
|
|
|
$attendee = new SyncAttendee(); |
309
|
|
|
|
310
|
|
|
$meinfo = nsp_getuserinfo(Request::GetUserIdentifier()); |
311
|
|
|
|
312
|
|
|
if (is_array($meinfo)) { |
313
|
|
|
$attendee->email = $meinfo["primary_email"]; |
314
|
|
|
$attendee->name = $meinfo["fullname"]; |
315
|
|
|
$attendee->attendeetype = MAPI_TO; |
|
|
|
|
316
|
|
|
|
317
|
|
|
array_push($message->attendees, $attendee); |
318
|
|
|
} |
319
|
|
|
} |
320
|
|
|
$message->responsetype = $messageprops[$appointmentprops["responsestatus"]]; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
// If it's an appointment which doesn't have any attendees, we have to make sure that |
324
|
|
|
// the user is the owner or it will not work properly with android devices |
325
|
|
|
if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] == olNonMeeting && empty($message->attendees)) { |
|
|
|
|
326
|
|
|
$meinfo = nsp_getuserinfo(Request::GetUserIdentifier()); |
327
|
|
|
|
328
|
|
|
if (is_array($meinfo)) { |
329
|
|
|
$message->organizeremail = $meinfo["primary_email"]; |
330
|
|
|
$message->organizername = $meinfo["fullname"]; |
331
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): setting ourself as the organizer for an appointment without attendees."); |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
if (!isset($message->nativebodytype)) { |
336
|
|
|
$message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops); |
337
|
|
|
} |
338
|
|
|
elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
339
|
|
|
$nbt = MAPIUtils::GetNativeBodyType($messageprops); |
340
|
|
|
SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getAppointment(): native body type is undefined. Set it to %d.", $nbt)); |
341
|
|
|
$message->nativebodytype = $nbt; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
// If the user is working from a location other than the office the busystatus should be interpreted as free. |
345
|
|
|
if (isset($message->busystatus) && $message->busystatus == fbWorkingElsewhere) { |
|
|
|
|
346
|
|
|
$message->busystatus = fbFree; |
|
|
|
|
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
// If the busystatus has the value of -1, we should be interpreted as tentative (1) |
350
|
|
|
if (isset($message->busystatus) && $message->busystatus == -1) { |
351
|
|
|
$message->busystatus = fbTentative; |
|
|
|
|
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
// All-day events might appear as 24h (or multiple of it) long when they start not exactly at midnight (+/- bias of the timezone) |
355
|
|
|
if (isset($message->alldayevent) && $message->alldayevent) { |
356
|
|
|
// Adjust all day events for the appointments timezone |
357
|
|
|
$duration = $message->endtime - $message->starttime; |
358
|
|
|
// AS pre 16: time in local timezone - convert if it isn't on midnight |
359
|
|
|
if (Request::GetProtocolVersion() < 16.0) { |
360
|
|
|
$localStartTime = localtime($message->starttime, 1); |
361
|
|
|
if ($localStartTime['tm_hour'] || $localStartTime['tm_min']) { |
362
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): all-day event starting not midnight - convert to local time"); |
363
|
|
|
$serverTz = TimezoneUtil::GetFullTZ(); |
364
|
|
|
$message->starttime = $this->getGMTTimeByTZ($this->getLocaltimeByTZ($message->starttime, $tz), $serverTz); |
365
|
|
|
if (!$message->timezone) { |
366
|
|
|
$message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
else { |
371
|
|
|
// AS 16: apply timezone as this MUST result in midnight (to be sent to the client) |
372
|
|
|
// Adjust for TZ only if a timezone was saved with the message. |
373
|
|
|
// If the starttime is not at midnight and if there is no saved timezone with the message, try applying the server TZ as well. |
374
|
|
|
if ($message->timezone || boolval(intval(gmdate("H", $message->starttime))) || boolval(intval(gmdate("i", $message->starttime)))) { |
375
|
|
|
$message->starttime = $this->getLocaltimeByTZ($message->starttime, $tz); |
376
|
|
|
} |
377
|
|
|
} |
378
|
|
|
$message->endtime = $message->starttime + $duration; |
379
|
|
|
if (Request::GetProtocolVersion() >= 16.0) { |
380
|
|
|
// no timezone information should be sent |
381
|
|
|
unset($message->timezone); |
382
|
|
|
} |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
// Add attachments to message for AS 16.0 and higher |
386
|
|
|
if (Request::GetProtocolVersion() >= 16.0) { |
387
|
|
|
// add attachments |
388
|
|
|
$entryid = bin2hex($messageprops[$appointmentprops["entryid"]]); |
389
|
|
|
$parentSourcekey = bin2hex($messageprops[$appointmentprops["parentsourcekey"]]); |
390
|
|
|
$this->setAttachment($mapimessage, $message, $entryid, $parentSourcekey); |
391
|
|
|
// add location |
392
|
|
|
$message->location2 = new SyncLocation(); |
393
|
|
|
$this->getASlocation($mapimessage, $message->location2, $appointmentprops); |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
return $message; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* Reads recurrence information from MAPI. |
401
|
|
|
* |
402
|
|
|
* @param mixed $mapimessage |
403
|
|
|
* @param array $recurprops |
404
|
|
|
* @param SyncObject &$syncMessage the message |
405
|
|
|
* @param SyncObject &$syncRecurrence the recurrence message |
406
|
|
|
* @param array $tz timezone information |
407
|
|
|
* @param array $appointmentprops property definitions |
408
|
|
|
*/ |
409
|
|
|
private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz, $appointmentprops) { |
410
|
|
|
if ($syncRecurrence instanceof SyncTaskRecurrence) { |
411
|
|
|
$recurrence = new TaskRecurrence($this->store, $mapimessage); |
|
|
|
|
412
|
|
|
} |
413
|
|
|
else { |
414
|
|
|
$recurrence = new Recurrence($this->store, $mapimessage); |
|
|
|
|
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
switch ($recurrence->recur["type"]) { |
418
|
|
|
case 10: // daily |
419
|
|
|
switch ($recurrence->recur["subtype"]) { |
420
|
|
|
default: |
421
|
|
|
$syncRecurrence->type = 0; |
422
|
|
|
break; |
423
|
|
|
|
424
|
|
|
case 1: |
425
|
|
|
$syncRecurrence->type = 0; |
426
|
|
|
$syncRecurrence->dayofweek = 62; // mon-fri |
427
|
|
|
$syncRecurrence->interval = 1; |
428
|
|
|
break; |
429
|
|
|
} |
430
|
|
|
break; |
431
|
|
|
|
432
|
|
|
case 11: // weekly |
433
|
|
|
$syncRecurrence->type = 1; |
434
|
|
|
break; |
435
|
|
|
|
436
|
|
|
case 12: // monthly |
437
|
|
|
switch ($recurrence->recur["subtype"]) { |
438
|
|
|
default: |
439
|
|
|
$syncRecurrence->type = 2; |
440
|
|
|
break; |
441
|
|
|
|
442
|
|
|
case 3: |
443
|
|
|
$syncRecurrence->type = 3; |
444
|
|
|
break; |
445
|
|
|
} |
446
|
|
|
break; |
447
|
|
|
|
448
|
|
|
case 13: // yearly |
449
|
|
|
switch ($recurrence->recur["subtype"]) { |
450
|
|
|
default: |
451
|
|
|
$syncRecurrence->type = 4; |
452
|
|
|
break; |
453
|
|
|
|
454
|
|
|
case 2: |
455
|
|
|
$syncRecurrence->type = 5; |
456
|
|
|
break; |
457
|
|
|
|
458
|
|
|
case 3: |
459
|
|
|
$syncRecurrence->type = 6; |
460
|
|
|
break; |
461
|
|
|
} |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
// Termination |
465
|
|
|
switch ($recurrence->recur["term"]) { |
466
|
|
|
case 0x21: |
467
|
|
|
$syncRecurrence->until = $recurrence->recur["end"]; |
468
|
|
|
// fixes Mantis #350 : recur-end does not consider timezones - use ClipEnd if available |
469
|
|
|
if (isset($recurprops[$recurrence->proptags["enddate_recurring"]])) { |
470
|
|
|
$syncRecurrence->until = $recurprops[$recurrence->proptags["enddate_recurring"]]; |
471
|
|
|
} |
472
|
|
|
// add one day (minus 1 sec) to the end time to make sure the last occurrence is covered |
473
|
|
|
$syncRecurrence->until += 86399; |
474
|
|
|
break; |
475
|
|
|
|
476
|
|
|
case 0x22: |
477
|
|
|
$syncRecurrence->occurrences = $recurrence->recur["numoccur"]; |
478
|
|
|
break; |
479
|
|
|
|
480
|
|
|
case 0x23: |
481
|
|
|
// never ends |
482
|
|
|
break; |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
// Correct 'alldayevent' because outlook fails to set it on recurring items of 24 hours or longer |
486
|
|
|
if (isset($recurrence->recur["endocc"], $recurrence->recur["startocc"]) && ($recurrence->recur["endocc"] - $recurrence->recur["startocc"] >= 1440)) { |
487
|
|
|
$syncMessage->alldayevent = true; |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
// Interval is different according to the type/subtype |
491
|
|
|
switch ($recurrence->recur["type"]) { |
492
|
|
|
case 10: |
493
|
|
|
if ($recurrence->recur["subtype"] == 0) { |
494
|
|
|
$syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 1440); |
495
|
|
|
} // minutes |
496
|
|
|
break; |
497
|
|
|
|
498
|
|
|
case 11: |
499
|
|
|
case 12: |
500
|
|
|
$syncRecurrence->interval = $recurrence->recur["everyn"]; |
501
|
|
|
break; // months / weeks |
502
|
|
|
|
503
|
|
|
case 13: |
504
|
|
|
$syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 12); |
505
|
|
|
break; // months |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
if (isset($recurrence->recur["weekdays"])) { |
509
|
|
|
$syncRecurrence->dayofweek = $recurrence->recur["weekdays"]; |
510
|
|
|
} // bitmask of days (1 == sunday, 128 == saturday |
511
|
|
|
if (isset($recurrence->recur["nday"])) { |
512
|
|
|
$syncRecurrence->weekofmonth = $recurrence->recur["nday"]; |
513
|
|
|
} // N'th {DAY} of {X} (0-5) |
514
|
|
|
if (isset($recurrence->recur["month"])) { |
515
|
|
|
$syncRecurrence->monthofyear = (int) ($recurrence->recur["month"] / (60 * 24 * 29)) + 1; |
516
|
|
|
} // works ok due to rounding. see also $monthminutes below (1-12) |
517
|
|
|
if (isset($recurrence->recur["monthday"])) { |
518
|
|
|
$syncRecurrence->dayofmonth = $recurrence->recur["monthday"]; |
519
|
|
|
} // day of month (1-31) |
520
|
|
|
|
521
|
|
|
// All changed exceptions are appointments within the 'exceptions' array. They contain the same items as a normal appointment |
522
|
|
|
foreach ($recurrence->recur["changed_occurrences"] as $change) { |
523
|
|
|
$exception = new SyncAppointmentException(); |
524
|
|
|
|
525
|
|
|
// start, end, basedate, subject, remind_before, reminderset, location, busystatus, alldayevent, label |
526
|
|
|
if (isset($change["start"])) { |
527
|
|
|
$exception->starttime = $this->getGMTTimeByTZ($change["start"], $tz); |
528
|
|
|
} |
529
|
|
|
if (isset($change["end"])) { |
530
|
|
|
$exception->endtime = $this->getGMTTimeByTZ($change["end"], $tz); |
531
|
|
|
} |
532
|
|
|
if (isset($change["basedate"])) { |
533
|
|
|
// depending on the AS version the streamer is going to send the correct value |
534
|
|
|
$exception->exceptionstarttime = $exception->instanceid = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($change["basedate"]) + $recurrence->recur["startocc"] * 60, $tz); |
|
|
|
|
535
|
|
|
|
536
|
|
|
// open body because getting only property might not work because of memory limit |
537
|
|
|
$exceptionatt = $recurrence->getExceptionAttachment($change["basedate"]); |
538
|
|
|
if ($exceptionatt) { |
539
|
|
|
$exceptionobj = mapi_attach_openobj($exceptionatt, 0); |
540
|
|
|
$this->setMessageBodyForType($exceptionobj, SYNC_BODYPREFERENCE_PLAIN, $exception); |
541
|
|
|
if (Request::GetProtocolVersion() >= 16.0) { |
542
|
|
|
// add attachment |
543
|
|
|
$data = mapi_message_getprops($mapimessage, [PR_ENTRYID, PR_PARENT_SOURCE_KEY]); |
|
|
|
|
544
|
|
|
$this->setAttachment($exceptionobj, $exception, bin2hex($data[PR_ENTRYID]), bin2hex($data[PR_PARENT_SOURCE_KEY]), bin2hex($change["basedate"])); |
545
|
|
|
// add location |
546
|
|
|
$exception->location2 = new SyncLocation(); |
547
|
|
|
$this->getASlocation($exceptionobj, $exception->location2, $appointmentprops); |
548
|
|
|
} |
549
|
|
|
} |
550
|
|
|
} |
551
|
|
|
if (isset($change["subject"])) { |
552
|
|
|
$exception->subject = $change["subject"]; |
553
|
|
|
} |
554
|
|
|
if (isset($change["reminder_before"]) && $change["reminder_before"]) { |
555
|
|
|
$exception->reminder = $change["remind_before"]; |
556
|
|
|
} |
557
|
|
|
if (isset($change["location"])) { |
558
|
|
|
$exception->location = $change["location"]; |
559
|
|
|
} |
560
|
|
|
if (isset($change["busystatus"])) { |
561
|
|
|
$exception->busystatus = $change["busystatus"]; |
562
|
|
|
} |
563
|
|
|
if (isset($change["alldayevent"])) { |
564
|
|
|
$exception->alldayevent = $change["alldayevent"]; |
565
|
|
|
} |
566
|
|
|
|
567
|
|
|
// set some data from the original appointment |
568
|
|
|
if (isset($syncMessage->uid)) { |
569
|
|
|
$exception->uid = $syncMessage->uid; |
570
|
|
|
} |
571
|
|
|
if (isset($syncMessage->organizername)) { |
572
|
|
|
$exception->organizername = $syncMessage->organizername; |
573
|
|
|
} |
574
|
|
|
if (isset($syncMessage->organizeremail)) { |
575
|
|
|
$exception->organizeremail = $syncMessage->organizeremail; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
if (!isset($syncMessage->exceptions)) { |
579
|
|
|
$syncMessage->exceptions = []; |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
// If the user is working from a location other than the office the busystatus should be interpreted as free. |
583
|
|
|
if (isset($exception->busystatus) && $exception->busystatus == fbWorkingElsewhere) { |
|
|
|
|
584
|
|
|
$exception->busystatus = fbFree; |
|
|
|
|
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
// If the busystatus has the value of -1, we should be interpreted as tentative (1) |
588
|
|
|
if (isset($exception->busystatus) && $exception->busystatus == -1) { |
589
|
|
|
$exception->busystatus = fbTentative; |
|
|
|
|
590
|
|
|
} |
591
|
|
|
|
592
|
|
|
// if an exception lasts 24 hours and the series are an allday events, set also the exception to allday event, |
593
|
|
|
// otherwise it will be a 24 hour long event on some mobiles. |
594
|
|
|
if (isset($exception->starttime, $exception->endtime) && ($exception->endtime - $exception->starttime == 86400) && $syncMessage->alldayevent) { |
595
|
|
|
$exception->alldayevent = 1; |
596
|
|
|
} |
597
|
|
|
array_push($syncMessage->exceptions, $exception); |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
// Deleted appointments contain only the original date (basedate) and a 'deleted' tag |
601
|
|
|
foreach ($recurrence->recur["deleted_occurrences"] as $deleted) { |
602
|
|
|
$exception = new SyncAppointmentException(); |
603
|
|
|
|
604
|
|
|
// depending on the AS version the streamer is going to send the correct value |
605
|
|
|
$exception->exceptionstarttime = $exception->instanceid = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($deleted) + $recurrence->recur["startocc"] * 60, $tz); |
606
|
|
|
$exception->deleted = "1"; |
607
|
|
|
|
608
|
|
|
if (!isset($syncMessage->exceptions)) { |
609
|
|
|
$syncMessage->exceptions = []; |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
array_push($syncMessage->exceptions, $exception); |
613
|
|
|
} |
614
|
|
|
|
615
|
|
|
if (isset($syncMessage->complete) && $syncMessage->complete) { |
616
|
|
|
$syncRecurrence->complete = $syncMessage->complete; |
617
|
|
|
} |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
/** |
621
|
|
|
* Reads an email object from MAPI. |
622
|
|
|
* |
623
|
|
|
* @param mixed $mapimessage |
624
|
|
|
* @param ContentParameters $contentparameters |
625
|
|
|
* |
626
|
|
|
* @return SyncEmail |
627
|
|
|
*/ |
628
|
|
|
private function getEmail($mapimessage, $contentparameters) { |
629
|
|
|
// FIXME: It should be properly fixed when refactoring. |
630
|
|
|
$bpReturnType = Utils::GetBodyPreferenceBestMatch($contentparameters->GetBodyPreference()); |
|
|
|
|
631
|
|
|
if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) || |
|
|
|
|
632
|
|
|
($key = array_search(SYNC_BODYPREFERENCE_MIME, $contentparameters->GetBodyPreference()) === false) || |
|
|
|
|
633
|
|
|
$bpReturnType != SYNC_BODYPREFERENCE_MIME) { |
634
|
|
|
MAPIUtils::ParseSmime($this->session, $this->store, $this->getAddressbook(), $mapimessage); |
|
|
|
|
635
|
|
|
} |
636
|
|
|
|
637
|
|
|
$message = new SyncMail(); |
638
|
|
|
|
639
|
|
|
$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetEmailMapping()); |
640
|
|
|
|
641
|
|
|
$emailproperties = MAPIMapping::GetEmailProperties(); |
642
|
|
|
$messageprops = $this->getProps($mapimessage, $emailproperties); |
643
|
|
|
|
644
|
|
|
if (isset($messageprops[PR_SOURCE_KEY])) { |
|
|
|
|
645
|
|
|
$sourcekey = $messageprops[PR_SOURCE_KEY]; |
|
|
|
|
646
|
|
|
} |
647
|
|
|
else { |
648
|
|
|
$mbe = new SyncObjectBrokenException("The message doesn't have a sourcekey"); |
649
|
|
|
$mbe->SetSyncObject($message); |
650
|
|
|
|
651
|
|
|
throw $mbe; |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
// set the body according to contentparameters and supported AS version |
655
|
|
|
$this->setMessageBody($mapimessage, $contentparameters, $message); |
656
|
|
|
|
657
|
|
|
$fromname = $fromaddr = ""; |
658
|
|
|
|
659
|
|
|
if (isset($messageprops[$emailproperties["representingname"]])) { |
660
|
|
|
// remove encapsulating double quotes from the representingname |
661
|
|
|
$fromname = preg_replace('/^\"(.*)\"$/', "\${1}", $messageprops[$emailproperties["representingname"]]); |
662
|
|
|
} |
663
|
|
|
if (isset($messageprops[$emailproperties["representingsendersmtpaddress"]])) { |
664
|
|
|
$fromaddr = $messageprops[$emailproperties["representingsendersmtpaddress"]]; |
665
|
|
|
} |
666
|
|
|
if ($fromaddr == "" && isset($messageprops[$emailproperties["representingentryid"]])) { |
667
|
|
|
$fromaddr = $this->getSMTPAddressFromEntryID($messageprops[$emailproperties["representingentryid"]]); |
668
|
|
|
} |
669
|
|
|
|
670
|
|
|
// if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY |
671
|
|
|
if ($fromaddr == "" && isset($messageprops[$emailproperties["representingsearchkey"]])) { |
672
|
|
|
$fromaddr = $this->getEmailAddressFromSearchKey($messageprops[$emailproperties["representingsearchkey"]]); |
673
|
|
|
} |
674
|
|
|
|
675
|
|
|
// if we couldn't still not get any $fromaddr, fall back to PR_SENDER_EMAIL_ADDRESS |
676
|
|
|
if ($fromaddr == "" && isset($messageprops[$emailproperties["senderemailaddress"]])) { |
677
|
|
|
$fromaddr = $messageprops[$emailproperties["senderemailaddress"]]; |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
// there is some name, but no email address (e.g. mails from System Administrator) - use a generic invalid address |
681
|
|
|
if ($fromname != "" && $fromaddr == "") { |
682
|
|
|
$fromaddr = "invalid@invalid"; |
683
|
|
|
} |
684
|
|
|
|
685
|
|
|
if ($fromname == $fromaddr) { |
686
|
|
|
$fromname = ""; |
687
|
|
|
} |
688
|
|
|
|
689
|
|
|
if ($fromname) { |
690
|
|
|
$from = "\"" . $fromname . "\" <" . $fromaddr . ">"; |
691
|
|
|
} |
692
|
|
|
else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown |
693
|
|
|
$from = "\"" . $fromaddr . "\" <" . $fromaddr . ">"; |
694
|
|
|
} |
695
|
|
|
// END CHANGED dw2412 HTC shows "error" if sender name is unknown |
696
|
|
|
|
697
|
|
|
$message->from = $from; |
698
|
|
|
|
699
|
|
|
// process Meeting Requests |
700
|
|
|
if (isset($message->messageclass) && strpos($message->messageclass, "IPM.Schedule.Meeting") === 0) { |
701
|
|
|
$message->meetingrequest = new SyncMeetingRequest(); |
702
|
|
|
$this->getPropsFromMAPI($message->meetingrequest, $mapimessage, MAPIMapping::GetMeetingRequestMapping()); |
703
|
|
|
|
704
|
|
|
$meetingrequestproperties = MAPIMapping::GetMeetingRequestProperties(); |
705
|
|
|
$props = $this->getProps($mapimessage, $meetingrequestproperties); |
706
|
|
|
|
707
|
|
|
// Get the GOID |
708
|
|
|
if (isset($props[$meetingrequestproperties["goidtag"]])) { |
709
|
|
|
$message->meetingrequest->globalobjid = base64_encode($props[$meetingrequestproperties["goidtag"]]); |
710
|
|
|
} |
711
|
|
|
|
712
|
|
|
// Set Timezone |
713
|
|
|
if (isset($props[$meetingrequestproperties["timezonetag"]])) { |
714
|
|
|
$tz = $this->getTZFromMAPIBlob($props[$meetingrequestproperties["timezonetag"]]); |
715
|
|
|
} |
716
|
|
|
else { |
717
|
|
|
$tz = TimezoneUtil::GetFullTZ(); |
718
|
|
|
} |
719
|
|
|
|
720
|
|
|
$message->meetingrequest->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
721
|
|
|
|
722
|
|
|
// send basedate if exception |
723
|
|
|
if (isset($props[$meetingrequestproperties["recReplTime"]]) || |
724
|
|
|
(isset($props[$meetingrequestproperties["lidIsException"]]) && $props[$meetingrequestproperties["lidIsException"]] == true)) { |
725
|
|
|
if (isset($props[$meetingrequestproperties["recReplTime"]])) { |
726
|
|
|
$basedate = $props[$meetingrequestproperties["recReplTime"]]; |
727
|
|
|
$message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, TimezoneUtil::GetGMTTz()); |
728
|
|
|
} |
729
|
|
|
else { |
730
|
|
|
if (!isset($props[$meetingrequestproperties["goidtag"]]) || !isset($props[$meetingrequestproperties["recurStartTime"]]) || !isset($props[$meetingrequestproperties["timezonetag"]])) { |
731
|
|
|
SLog::Write(LOGLEVEL_WARN, "Missing property to set correct basedate for exception"); |
732
|
|
|
} |
733
|
|
|
else { |
734
|
|
|
$basedate = Utils::ExtractBaseDate($props[$meetingrequestproperties["goidtag"]], $props[$meetingrequestproperties["recurStartTime"]]); |
735
|
|
|
$message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $tz); |
736
|
|
|
} |
737
|
|
|
} |
738
|
|
|
} |
739
|
|
|
|
740
|
|
|
// Organizer is the sender |
741
|
|
|
if (strpos($message->messageclass, "IPM.Schedule.Meeting.Resp") === 0) { |
742
|
|
|
$message->meetingrequest->organizer = $message->to; |
743
|
|
|
} |
744
|
|
|
else { |
745
|
|
|
$message->meetingrequest->organizer = $message->from; |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
// Process recurrence |
749
|
|
|
if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]]) { |
750
|
|
|
$myrec = new SyncMeetingRequestRecurrence(); |
751
|
|
|
// get recurrence -> put $message->meetingrequest as message so the 'alldayevent' is set correctly |
752
|
|
|
$this->getRecurrence($mapimessage, $props, $message->meetingrequest, $myrec, $tz, $meetingrequestproperties); |
753
|
|
|
$message->meetingrequest->recurrences = [$myrec]; |
754
|
|
|
} |
755
|
|
|
|
756
|
|
|
// Force the 'alldayevent' in the object at all times. (non-existent == 0) |
757
|
|
|
if (!isset($message->meetingrequest->alldayevent) || $message->meetingrequest->alldayevent == "") { |
758
|
|
|
$message->meetingrequest->alldayevent = 0; |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
// Instancetype |
762
|
|
|
// 0 = single appointment |
763
|
|
|
// 1 = master recurring appointment |
764
|
|
|
// 2 = single instance of recurring appointment |
765
|
|
|
// 3 = exception of recurring appointment |
766
|
|
|
$message->meetingrequest->instancetype = 0; |
767
|
|
|
if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]] == 1) { |
768
|
|
|
$message->meetingrequest->instancetype = 1; |
769
|
|
|
} |
770
|
|
|
elseif ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) { |
771
|
|
|
if (isset($props[$meetingrequestproperties["appSeqNr"]]) && $props[$meetingrequestproperties["appSeqNr"]] == 0) { |
772
|
|
|
$message->meetingrequest->instancetype = 2; |
773
|
|
|
} |
774
|
|
|
else { |
775
|
|
|
$message->meetingrequest->instancetype = 3; |
776
|
|
|
} |
777
|
|
|
} |
778
|
|
|
|
779
|
|
|
// Disable reminder if it is off |
780
|
|
|
if (!isset($props[$meetingrequestproperties["reminderset"]]) || $props[$meetingrequestproperties["reminderset"]] == false) { |
781
|
|
|
$message->meetingrequest->reminder = ""; |
782
|
|
|
} |
783
|
|
|
// the property saves reminder in minutes, but we need it in secs |
784
|
|
|
else { |
785
|
|
|
// /set the default reminder time to seconds |
786
|
|
|
if ($props[$meetingrequestproperties["remindertime"]] == 0x5AE980E1) { |
787
|
|
|
$message->meetingrequest->reminder = 900; |
788
|
|
|
} |
789
|
|
|
else { |
790
|
|
|
$message->meetingrequest->reminder = $props[$meetingrequestproperties["remindertime"]] * 60; |
791
|
|
|
} |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
// Set sensitivity to 0 if missing |
795
|
|
|
if (!isset($message->meetingrequest->sensitivity)) { |
796
|
|
|
$message->meetingrequest->sensitivity = 0; |
797
|
|
|
} |
798
|
|
|
|
799
|
|
|
// If the user is working from a location other than the office the busystatus should be interpreted as free. |
800
|
|
|
if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == fbWorkingElsewhere) { |
|
|
|
|
801
|
|
|
$message->meetingrequest->busystatus = fbFree; |
|
|
|
|
802
|
|
|
} |
803
|
|
|
|
804
|
|
|
// If the busystatus has the value of -1, we should be interpreted as tentative (1) |
805
|
|
|
if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == -1) { |
806
|
|
|
$message->meetingrequest->busystatus = fbTentative; |
|
|
|
|
807
|
|
|
} |
808
|
|
|
|
809
|
|
|
// if a meeting request response hasn't been processed yet, |
810
|
|
|
// do it so that the attendee status is updated on the mobile |
811
|
|
|
if (!isset($messageprops[$emailproperties["processed"]])) { |
812
|
|
|
// check if we are not sending the MR so we can process it |
813
|
|
|
$cuser = GSync::GetBackend()->GetUserDetails(Request::GetUserIdentifier()); |
814
|
|
|
if (isset($cuser["emailaddress"]) && $cuser["emailaddress"] != $fromaddr) { |
815
|
|
|
if (!isset($req)) { |
|
|
|
|
816
|
|
|
$req = new Meetingrequest($this->store, $mapimessage, $this->session); |
|
|
|
|
817
|
|
|
} |
818
|
|
|
if ($req->isMeetingRequest() && !$req->isLocalOrganiser() && !$req->isMeetingOutOfDate()) { |
819
|
|
|
$req->doAccept(true, false, false); |
820
|
|
|
} |
821
|
|
|
if ($req->isMeetingRequestResponse()) { |
822
|
|
|
$req->processMeetingRequestResponse(); |
823
|
|
|
} |
824
|
|
|
if ($req->isMeetingCancellation()) { |
825
|
|
|
$req->processMeetingCancellation(); |
826
|
|
|
} |
827
|
|
|
} |
828
|
|
|
} |
829
|
|
|
$message->contentclass = DEFAULT_CALENDAR_CONTENTCLASS; |
830
|
|
|
|
831
|
|
|
// MeetingMessageType values |
832
|
|
|
// 0 = A silent update was performed, or the message type is unspecified. |
833
|
|
|
// 1 = Initial meeting request. |
834
|
|
|
// 2 = Full update. |
835
|
|
|
// 3 = Informational update. |
836
|
|
|
// 4 = Outdated. A newer meeting request or meeting update was received after this message. |
837
|
|
|
// 5 = Identifies the delegator's copy of the meeting request. |
838
|
|
|
// 6 = Identifies that the meeting request has been delegated and the meeting request cannot be responded to. |
839
|
|
|
$message->meetingrequest->meetingmessagetype = mtgEmpty; |
|
|
|
|
840
|
|
|
|
841
|
|
|
if (isset($props[$meetingrequestproperties["meetingType"]])) { |
842
|
|
|
switch ($props[$meetingrequestproperties["meetingType"]]) { |
843
|
|
|
case mtgRequest: |
|
|
|
|
844
|
|
|
$message->meetingrequest->meetingmessagetype = 1; |
845
|
|
|
break; |
846
|
|
|
|
847
|
|
|
case mtgFull: |
|
|
|
|
848
|
|
|
$message->meetingrequest->meetingmessagetype = 2; |
849
|
|
|
break; |
850
|
|
|
|
851
|
|
|
case mtgInfo: |
|
|
|
|
852
|
|
|
$message->meetingrequest->meetingmessagetype = 3; |
853
|
|
|
break; |
854
|
|
|
|
855
|
|
|
case mtgOutOfDate: |
|
|
|
|
856
|
|
|
$message->meetingrequest->meetingmessagetype = 4; |
857
|
|
|
break; |
858
|
|
|
|
859
|
|
|
case mtgDelegatorCopy: |
|
|
|
|
860
|
|
|
$message->meetingrequest->meetingmessagetype = 5; |
861
|
|
|
break; |
862
|
|
|
} |
863
|
|
|
} |
864
|
|
|
} |
865
|
|
|
|
866
|
|
|
// Add attachments to message |
867
|
|
|
$entryid = bin2hex($messageprops[$emailproperties["entryid"]]); |
868
|
|
|
$parentSourcekey = bin2hex($messageprops[$emailproperties["parentsourcekey"]]); |
869
|
|
|
$this->setAttachment($mapimessage, $message, $entryid, $parentSourcekey); |
870
|
|
|
|
871
|
|
|
// Get To/Cc as SMTP addresses (this is different from displayto and displaycc because we are putting |
872
|
|
|
// in the SMTP addresses as well, while displayto and displaycc could just contain the display names |
873
|
|
|
$message->to = []; |
874
|
|
|
$message->cc = []; |
875
|
|
|
$message->bcc = []; |
876
|
|
|
|
877
|
|
|
$reciptable = mapi_message_getrecipienttable($mapimessage); |
878
|
|
|
$rows = mapi_table_queryallrows($reciptable, [PR_RECIPIENT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ENTRYID, PR_SEARCH_KEY]); |
|
|
|
|
879
|
|
|
|
880
|
|
|
foreach ($rows as $row) { |
881
|
|
|
$address = ""; |
882
|
|
|
$fulladdr = ""; |
883
|
|
|
|
884
|
|
|
$addrtype = isset($row[PR_ADDRTYPE]) ? $row[PR_ADDRTYPE] : ""; |
885
|
|
|
|
886
|
|
|
if (isset($row[PR_SMTP_ADDRESS])) { |
887
|
|
|
$address = $row[PR_SMTP_ADDRESS]; |
888
|
|
|
} |
889
|
|
|
elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) { |
890
|
|
|
$address = $row[PR_EMAIL_ADDRESS]; |
891
|
|
|
} |
892
|
|
|
elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) { |
893
|
|
|
$address = $this->getSMTPAddressFromEntryID($row[PR_ENTRYID]); |
894
|
|
|
} |
895
|
|
|
|
896
|
|
|
// if the user was not found, do a fallback to PR_SEARCH_KEY |
897
|
|
|
if (empty($address) && isset($row[PR_SEARCH_KEY])) { |
898
|
|
|
$address = $this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY]); |
899
|
|
|
} |
900
|
|
|
|
901
|
|
|
$name = isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : ""; |
902
|
|
|
|
903
|
|
|
if ($name == "" || $name == $address) { |
904
|
|
|
$fulladdr = $address; |
905
|
|
|
} |
906
|
|
|
else { |
907
|
|
|
if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { |
908
|
|
|
$fulladdr = "\"" . $name . "\" <" . $address . ">"; |
909
|
|
|
} |
910
|
|
|
else { |
911
|
|
|
$fulladdr = $name . "<" . $address . ">"; |
912
|
|
|
} |
913
|
|
|
} |
914
|
|
|
|
915
|
|
|
if ($row[PR_RECIPIENT_TYPE] == MAPI_TO) { |
|
|
|
|
916
|
|
|
array_push($message->to, $fulladdr); |
917
|
|
|
} |
918
|
|
|
elseif ($row[PR_RECIPIENT_TYPE] == MAPI_CC) { |
|
|
|
|
919
|
|
|
array_push($message->cc, $fulladdr); |
920
|
|
|
} |
921
|
|
|
elseif ($row[PR_RECIPIENT_TYPE] == MAPI_BCC) { |
|
|
|
|
922
|
|
|
array_push($message->bcc, $fulladdr); |
923
|
|
|
} |
924
|
|
|
} |
925
|
|
|
|
926
|
|
|
if (is_array($message->to) && !empty($message->to)) { |
927
|
|
|
$message->to = implode(", ", $message->to); |
928
|
|
|
} |
929
|
|
|
if (is_array($message->cc) && !empty($message->cc)) { |
930
|
|
|
$message->cc = implode(", ", $message->cc); |
931
|
|
|
} |
932
|
|
|
if (is_array($message->bcc) && !empty($message->bcc)) { |
933
|
|
|
$message->bcc = implode(", ", $message->bcc); |
934
|
|
|
} |
935
|
|
|
|
936
|
|
|
// without importance some mobiles assume "0" (low) - Mantis #439 |
937
|
|
|
if (!isset($message->importance)) { |
938
|
|
|
$message->importance = IMPORTANCE_NORMAL; |
|
|
|
|
939
|
|
|
} |
940
|
|
|
|
941
|
|
|
if (!isset($message->internetcpid)) { |
942
|
|
|
$message->internetcpid = (defined('STORE_INTERNET_CPID')) ? constant('STORE_INTERNET_CPID') : INTERNET_CPID_WINDOWS1252; |
943
|
|
|
} |
944
|
|
|
$this->setFlag($mapimessage, $message); |
945
|
|
|
// TODO checkcontentclass |
946
|
|
|
if (!isset($message->contentclass)) { |
947
|
|
|
$message->contentclass = DEFAULT_EMAIL_CONTENTCLASS; |
948
|
|
|
} |
949
|
|
|
|
950
|
|
|
if (!isset($message->nativebodytype)) { |
951
|
|
|
$message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops); |
952
|
|
|
} |
953
|
|
|
elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
954
|
|
|
$nbt = MAPIUtils::GetNativeBodyType($messageprops); |
955
|
|
|
SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getEmail(): native body type is undefined. Set it to %d.", $nbt)); |
956
|
|
|
$message->nativebodytype = $nbt; |
957
|
|
|
} |
958
|
|
|
|
959
|
|
|
// reply, reply to all, forward flags |
960
|
|
|
if (isset($message->lastverbexecuted) && $message->lastverbexecuted) { |
961
|
|
|
$message->lastverbexecuted = Utils::GetLastVerbExecuted($message->lastverbexecuted); |
962
|
|
|
} |
963
|
|
|
|
964
|
|
|
if ($messageprops[$emailproperties["messageflags"]] & MSGFLAG_UNSENT) { |
|
|
|
|
965
|
|
|
$message->isdraft = true; |
966
|
|
|
} |
967
|
|
|
|
968
|
|
|
return $message; |
969
|
|
|
} |
970
|
|
|
|
971
|
|
|
/** |
972
|
|
|
* Reads a note object from MAPI. |
973
|
|
|
* |
974
|
|
|
* @param mixed $mapimessage |
975
|
|
|
* @param ContentParameters $contentparameters |
976
|
|
|
* |
977
|
|
|
* @return SyncNote |
978
|
|
|
*/ |
979
|
|
|
private function getNote($mapimessage, $contentparameters) { |
980
|
|
|
$message = new SyncNote(); |
981
|
|
|
|
982
|
|
|
// Standard one-to-one mappings first |
983
|
|
|
$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetNoteMapping()); |
984
|
|
|
|
985
|
|
|
// set the body according to contentparameters and supported AS version |
986
|
|
|
$this->setMessageBody($mapimessage, $contentparameters, $message); |
987
|
|
|
|
988
|
|
|
return $message; |
989
|
|
|
} |
990
|
|
|
|
991
|
|
|
/** |
992
|
|
|
* Creates a SyncFolder from MAPI properties. |
993
|
|
|
* |
994
|
|
|
* @param mixed $folderprops |
995
|
|
|
* |
996
|
|
|
* @return SyncFolder |
997
|
|
|
*/ |
998
|
|
|
public function GetFolder($folderprops) { |
999
|
|
|
$folder = new SyncFolder(); |
1000
|
|
|
|
1001
|
|
|
$storeprops = $this->GetStoreProps(); |
1002
|
|
|
|
1003
|
|
|
// For ZCP 7.0.x we need to retrieve more properties explicitly |
1004
|
|
|
if (isset($folderprops[PR_SOURCE_KEY]) && !isset($folderprops[PR_ENTRYID]) && !isset($folderprops[PR_CONTAINER_CLASS])) { |
|
|
|
|
1005
|
|
|
$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $folderprops[PR_SOURCE_KEY]); |
1006
|
|
|
$mapifolder = mapi_msgstore_openentry($this->store, $entryid); |
1007
|
|
|
$folderprops = mapi_getprops($mapifolder, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN, PR_EXTENDED_FOLDER_FLAGS]); |
|
|
|
|
1008
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetFolder(): received insufficient of data from ICS. Fetching required data."); |
1009
|
|
|
} |
1010
|
|
|
|
1011
|
|
|
if (!isset( |
1012
|
|
|
$folderprops[PR_DISPLAY_NAME], |
1013
|
|
|
$folderprops[PR_PARENT_ENTRYID], |
1014
|
|
|
$folderprops[PR_SOURCE_KEY], |
1015
|
|
|
$folderprops[PR_ENTRYID], |
1016
|
|
|
$folderprops[PR_PARENT_SOURCE_KEY], |
1017
|
|
|
$storeprops[PR_IPM_SUBTREE_ENTRYID] |
|
|
|
|
1018
|
|
|
)) { |
1019
|
|
|
SLog::Write(LOGLEVEL_ERROR, "MAPIProvider->GetFolder(): invalid folder. Missing properties"); |
1020
|
|
|
|
1021
|
|
|
return false; |
|
|
|
|
1022
|
|
|
} |
1023
|
|
|
|
1024
|
|
|
// ignore hidden folders |
1025
|
|
|
if (isset($folderprops[PR_ATTR_HIDDEN]) && $folderprops[PR_ATTR_HIDDEN] != false) { |
1026
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): invalid folder '%s' as it is a hidden folder (PR_ATTR_HIDDEN)", $folderprops[PR_DISPLAY_NAME])); |
1027
|
|
|
|
1028
|
|
|
return false; |
|
|
|
|
1029
|
|
|
} |
1030
|
|
|
|
1031
|
|
|
// ignore certain undesired folders, like "RSS Feeds", "Suggested contacts" and Journal |
1032
|
|
|
if ((isset($folderprops[PR_CONTAINER_CLASS]) && ( |
|
|
|
|
1033
|
|
|
$folderprops[PR_CONTAINER_CLASS] == "IPF.Note.OutlookHomepage" || $folderprops[PR_CONTAINER_CLASS] == "IPF.Journal" |
1034
|
|
|
)) || |
1035
|
|
|
in_array($folderprops[PR_ENTRYID], $this->getSpecialFoldersData()) |
1036
|
|
|
) { |
1037
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): folder '%s' should not be synchronized", $folderprops[PR_DISPLAY_NAME])); |
1038
|
|
|
|
1039
|
|
|
return false; |
|
|
|
|
1040
|
|
|
} |
1041
|
|
|
|
1042
|
|
|
$folder->BackendId = bin2hex($folderprops[PR_SOURCE_KEY]); |
1043
|
|
|
$folderOrigin = DeviceManager::FLD_ORIGIN_USER; |
1044
|
|
|
if (GSync::GetBackend()->GetImpersonatedUser()) { |
1045
|
|
|
$folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED; |
1046
|
|
|
} |
1047
|
|
|
$folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folderprops[PR_DISPLAY_NAME]); |
1048
|
|
|
if ($folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_SUBTREE_ENTRYID] || $folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]) { |
|
|
|
|
1049
|
|
|
$folder->parentid = "0"; |
1050
|
|
|
} |
1051
|
|
|
else { |
1052
|
|
|
$folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId(bin2hex($folderprops[PR_PARENT_SOURCE_KEY])); |
1053
|
|
|
} |
1054
|
|
|
$folder->displayname = $folderprops[PR_DISPLAY_NAME]; |
1055
|
|
|
$folder->type = $this->GetFolderType($folderprops[PR_ENTRYID], isset($folderprops[PR_CONTAINER_CLASS]) ? $folderprops[PR_CONTAINER_CLASS] : false); |
|
|
|
|
1056
|
|
|
|
1057
|
|
|
return $folder; |
1058
|
|
|
} |
1059
|
|
|
|
1060
|
|
|
/** |
1061
|
|
|
* Returns the foldertype for an entryid |
1062
|
|
|
* Gets the folder type by checking the default folders in MAPI. |
1063
|
|
|
* |
1064
|
|
|
* @param string $entryid |
1065
|
|
|
* @param string $class (opt) |
1066
|
|
|
* |
1067
|
|
|
* @return long |
|
|
|
|
1068
|
|
|
*/ |
1069
|
|
|
public function GetFolderType($entryid, $class = false) { |
1070
|
|
|
$storeprops = $this->GetStoreProps(); |
1071
|
|
|
$inboxprops = $this->GetInboxProps(); |
1072
|
|
|
|
1073
|
|
|
if ($entryid == $storeprops[PR_IPM_WASTEBASKET_ENTRYID]) { |
|
|
|
|
1074
|
|
|
return SYNC_FOLDER_TYPE_WASTEBASKET; |
|
|
|
|
1075
|
|
|
} |
1076
|
|
|
if ($entryid == $storeprops[PR_IPM_SENTMAIL_ENTRYID]) { |
|
|
|
|
1077
|
|
|
return SYNC_FOLDER_TYPE_SENTMAIL; |
|
|
|
|
1078
|
|
|
} |
1079
|
|
|
if ($entryid == $storeprops[PR_IPM_OUTBOX_ENTRYID]) { |
|
|
|
|
1080
|
|
|
return SYNC_FOLDER_TYPE_OUTBOX; |
|
|
|
|
1081
|
|
|
} |
1082
|
|
|
|
1083
|
|
|
// Public folders do not have inboxprops |
1084
|
|
|
if (!empty($inboxprops)) { |
1085
|
|
|
if ($entryid == $inboxprops[PR_ENTRYID]) { |
1086
|
|
|
return SYNC_FOLDER_TYPE_INBOX; |
|
|
|
|
1087
|
|
|
} |
1088
|
|
|
if ($entryid == $inboxprops[PR_IPM_DRAFTS_ENTRYID]) { |
|
|
|
|
1089
|
|
|
return SYNC_FOLDER_TYPE_DRAFTS; |
|
|
|
|
1090
|
|
|
} |
1091
|
|
|
if ($entryid == $inboxprops[PR_IPM_TASK_ENTRYID]) { |
|
|
|
|
1092
|
|
|
return SYNC_FOLDER_TYPE_TASK; |
|
|
|
|
1093
|
|
|
} |
1094
|
|
|
if ($entryid == $inboxprops[PR_IPM_APPOINTMENT_ENTRYID]) { |
|
|
|
|
1095
|
|
|
return SYNC_FOLDER_TYPE_APPOINTMENT; |
|
|
|
|
1096
|
|
|
} |
1097
|
|
|
if ($entryid == $inboxprops[PR_IPM_CONTACT_ENTRYID]) { |
|
|
|
|
1098
|
|
|
return SYNC_FOLDER_TYPE_CONTACT; |
|
|
|
|
1099
|
|
|
} |
1100
|
|
|
if ($entryid == $inboxprops[PR_IPM_NOTE_ENTRYID]) { |
|
|
|
|
1101
|
|
|
return SYNC_FOLDER_TYPE_NOTE; |
|
|
|
|
1102
|
|
|
} |
1103
|
|
|
if ($entryid == $inboxprops[PR_IPM_JOURNAL_ENTRYID]) { |
|
|
|
|
1104
|
|
|
return SYNC_FOLDER_TYPE_JOURNAL; |
|
|
|
|
1105
|
|
|
} |
1106
|
|
|
} |
1107
|
|
|
|
1108
|
|
|
// user created folders |
1109
|
|
|
if ($class == "IPF.Note") { |
1110
|
|
|
return SYNC_FOLDER_TYPE_USER_MAIL; |
|
|
|
|
1111
|
|
|
} |
1112
|
|
|
if ($class == "IPF.Task") { |
1113
|
|
|
return SYNC_FOLDER_TYPE_USER_TASK; |
|
|
|
|
1114
|
|
|
} |
1115
|
|
|
if ($class == "IPF.Appointment") { |
1116
|
|
|
return SYNC_FOLDER_TYPE_USER_APPOINTMENT; |
|
|
|
|
1117
|
|
|
} |
1118
|
|
|
if ($class == "IPF.Contact") { |
1119
|
|
|
return SYNC_FOLDER_TYPE_USER_CONTACT; |
|
|
|
|
1120
|
|
|
} |
1121
|
|
|
if ($class == "IPF.StickyNote") { |
1122
|
|
|
return SYNC_FOLDER_TYPE_USER_NOTE; |
|
|
|
|
1123
|
|
|
} |
1124
|
|
|
if ($class == "IPF.Journal") { |
1125
|
|
|
return SYNC_FOLDER_TYPE_USER_JOURNAL; |
|
|
|
|
1126
|
|
|
} |
1127
|
|
|
|
1128
|
|
|
return SYNC_FOLDER_TYPE_OTHER; |
|
|
|
|
1129
|
|
|
} |
1130
|
|
|
|
1131
|
|
|
/** |
1132
|
|
|
* Indicates if the entry id is a default MAPI folder. |
1133
|
|
|
* |
1134
|
|
|
* @param string $entryid |
1135
|
|
|
* |
1136
|
|
|
* @return bool |
1137
|
|
|
*/ |
1138
|
|
|
public function IsMAPIDefaultFolder($entryid) { |
1139
|
|
|
$msgstore_props = mapi_getprops($this->store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]); |
|
|
|
|
1140
|
|
|
|
1141
|
|
|
$inboxProps = []; |
1142
|
|
|
$inbox = mapi_msgstore_getreceivefolder($this->store); |
1143
|
|
|
if (!mapi_last_hresult()) { |
1144
|
|
|
$inboxProps = mapi_getprops($inbox, [PR_ENTRYID]); |
1145
|
|
|
} |
1146
|
|
|
|
1147
|
|
|
$root = mapi_msgstore_openentry($this->store, null); // TODO use getRootProps() |
1148
|
|
|
$rootProps = mapi_getprops($root, [PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID, PR_ADDITIONAL_REN_ENTRYIDS]); |
|
|
|
|
1149
|
|
|
|
1150
|
|
|
$additional_ren_entryids = []; |
1151
|
|
|
if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) { |
1152
|
|
|
$additional_ren_entryids = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS]; |
1153
|
|
|
} |
1154
|
|
|
|
1155
|
|
|
$defaultfolders = [ |
1156
|
|
|
"inbox" => ["inbox" => PR_ENTRYID], |
1157
|
|
|
"outbox" => ["store" => PR_IPM_OUTBOX_ENTRYID], |
1158
|
|
|
"sent" => ["store" => PR_IPM_SENTMAIL_ENTRYID], |
1159
|
|
|
"wastebasket" => ["store" => PR_IPM_WASTEBASKET_ENTRYID], |
1160
|
|
|
"favorites" => ["store" => PR_IPM_FAVORITES_ENTRYID], |
1161
|
|
|
"publicfolders" => ["store" => PR_IPM_PUBLIC_FOLDERS_ENTRYID], |
1162
|
|
|
"calendar" => ["root" => PR_IPM_APPOINTMENT_ENTRYID], |
1163
|
|
|
"contact" => ["root" => PR_IPM_CONTACT_ENTRYID], |
1164
|
|
|
"drafts" => ["root" => PR_IPM_DRAFTS_ENTRYID], |
1165
|
|
|
"journal" => ["root" => PR_IPM_JOURNAL_ENTRYID], |
1166
|
|
|
"note" => ["root" => PR_IPM_NOTE_ENTRYID], |
1167
|
|
|
"task" => ["root" => PR_IPM_TASK_ENTRYID], |
1168
|
|
|
"junk" => ["additional" => 4], |
1169
|
|
|
"syncissues" => ["additional" => 1], |
1170
|
|
|
"conflicts" => ["additional" => 0], |
1171
|
|
|
"localfailures" => ["additional" => 2], |
1172
|
|
|
"serverfailures" => ["additional" => 3], |
1173
|
|
|
]; |
1174
|
|
|
|
1175
|
|
|
foreach ($defaultfolders as $key => $prop) { |
1176
|
|
|
$tag = reset($prop); |
1177
|
|
|
$from = key($prop); |
1178
|
|
|
|
1179
|
|
|
switch ($from) { |
1180
|
|
|
case "inbox": |
1181
|
|
|
if (isset($inboxProps[$tag]) && $entryid == $inboxProps[$tag]) { |
1182
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Inbox found, key '%s'", $key)); |
1183
|
|
|
|
1184
|
|
|
return true; |
1185
|
|
|
} |
1186
|
|
|
break; |
1187
|
|
|
|
1188
|
|
|
case "store": |
1189
|
|
|
if (isset($msgstore_props[$tag]) && $entryid == $msgstore_props[$tag]) { |
1190
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Store folder found, key '%s'", $key)); |
1191
|
|
|
|
1192
|
|
|
return true; |
1193
|
|
|
} |
1194
|
|
|
break; |
1195
|
|
|
|
1196
|
|
|
case "root": |
1197
|
|
|
if (isset($rootProps[$tag]) && $entryid == $rootProps[$tag]) { |
1198
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Root folder found, key '%s'", $key)); |
1199
|
|
|
|
1200
|
|
|
return true; |
1201
|
|
|
} |
1202
|
|
|
break; |
1203
|
|
|
|
1204
|
|
|
case "additional": |
1205
|
|
|
if (isset($additional_ren_entryids[$tag]) && $entryid == $additional_ren_entryids[$tag]) { |
1206
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Additional folder found, key '%s'", $key)); |
1207
|
|
|
|
1208
|
|
|
return true; |
1209
|
|
|
} |
1210
|
|
|
break; |
1211
|
|
|
} |
1212
|
|
|
} |
1213
|
|
|
|
1214
|
|
|
return false; |
1215
|
|
|
} |
1216
|
|
|
|
1217
|
|
|
/*---------------------------------------------------------------------------------------------------------- |
1218
|
|
|
* PreDeleteMessage |
1219
|
|
|
*/ |
1220
|
|
|
|
1221
|
|
|
/** |
1222
|
|
|
* Performs any actions before a message is imported for deletion. |
1223
|
|
|
* |
1224
|
|
|
* @param mixed $mapimessage |
1225
|
|
|
*/ |
1226
|
|
|
public function PreDeleteMessage($mapimessage) { |
1227
|
|
|
if ($mapimessage === false) { |
1228
|
|
|
return; |
1229
|
|
|
} |
1230
|
|
|
// Currently this is relevant only for MeetingRequests so cancellation emails can be sent to attendees. |
1231
|
|
|
$props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]); |
|
|
|
|
1232
|
|
|
$messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false; |
1233
|
|
|
|
1234
|
|
|
if ($messageClass !== false && stripos($messageClass, 'ipm.appointment') === 0) { |
1235
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->PreDeleteMessage(): Appointment message"); |
1236
|
|
|
$mr = new Meetingrequest($this->store, $mapimessage, $this->session); |
1237
|
|
|
$mr->doCancelInvitation(); |
1238
|
|
|
} |
1239
|
|
|
} |
1240
|
|
|
|
1241
|
|
|
/*---------------------------------------------------------------------------------------------------------- |
1242
|
|
|
* SETTER |
1243
|
|
|
*/ |
1244
|
|
|
|
1245
|
|
|
/** |
1246
|
|
|
* Writes a SyncObject to MAPI |
1247
|
|
|
* Depending on the message class, a contact, appointment, task or email is written. |
1248
|
|
|
* |
1249
|
|
|
* @param mixed $mapimessage |
1250
|
|
|
* @param SyncObject $message |
1251
|
|
|
* |
1252
|
|
|
* @return SyncObject |
1253
|
|
|
*/ |
1254
|
|
|
public function SetMessage($mapimessage, $message) { |
1255
|
|
|
// TODO check with instanceof |
1256
|
|
|
switch (strtolower(get_class($message))) { |
1257
|
|
|
case "synccontact": |
1258
|
|
|
return $this->setContact($mapimessage, $message); |
|
|
|
|
1259
|
|
|
|
1260
|
|
|
case "syncappointment": |
1261
|
|
|
return $this->setAppointment($mapimessage, $message); |
1262
|
|
|
|
1263
|
|
|
case "synctask": |
1264
|
|
|
return $this->setTask($mapimessage, $message); |
|
|
|
|
1265
|
|
|
|
1266
|
|
|
case "syncnote": |
1267
|
|
|
return $this->setNote($mapimessage, $message); |
|
|
|
|
1268
|
|
|
|
1269
|
|
|
default: |
1270
|
|
|
// for emails only flag (read and todo) changes are possible |
1271
|
|
|
return $this->setEmail($mapimessage, $message); |
1272
|
|
|
} |
1273
|
|
|
} |
1274
|
|
|
|
1275
|
|
|
/** |
1276
|
|
|
* Writes SyncMail to MAPI (actually flags only). |
1277
|
|
|
* |
1278
|
|
|
* @param mixed $mapimessage |
1279
|
|
|
* @param SyncMail $message |
1280
|
|
|
* |
1281
|
|
|
* @return SyncObject |
1282
|
|
|
*/ |
1283
|
|
|
private function setEmail($mapimessage, $message) { |
1284
|
|
|
$response = new SyncMailResponse(); |
1285
|
|
|
// update categories |
1286
|
|
|
if (!isset($message->categories)) { |
1287
|
|
|
$message->categories = []; |
1288
|
|
|
} |
1289
|
|
|
$emailmap = MAPIMapping::GetEmailMapping(); |
1290
|
|
|
$emailprops = MAPIMapping::GetEmailProperties(); |
1291
|
|
|
$this->setPropsInMAPI($mapimessage, $message, ["categories" => $emailmap["categories"]]); |
1292
|
|
|
|
1293
|
|
|
$flagmapping = MAPIMapping::GetMailFlagsMapping(); |
1294
|
|
|
$flagprops = MAPIMapping::GetMailFlagsProperties(); |
1295
|
|
|
$flagprops = array_merge($this->getPropIdsFromStrings($flagmapping), $this->getPropIdsFromStrings($flagprops)); |
1296
|
|
|
// flag specific properties to be set |
1297
|
|
|
$props = $delprops = []; |
1298
|
|
|
|
1299
|
|
|
// save DRAFTs |
1300
|
|
|
if (isset($message->asbody) && $message->asbody instanceof SyncBaseBody) { |
1301
|
|
|
// iOS+Nine send a RFC822 message |
1302
|
|
|
if (isset($message->asbody->type) && $message->asbody->type == SYNC_BODYPREFERENCE_MIME) { |
1303
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setEmail(): Use the mapi_inetmapi_imtomapi function to save draft email"); |
1304
|
|
|
$mime = stream_get_contents($message->asbody->data); |
1305
|
|
|
$ab = mapi_openaddressbook($this->session); |
1306
|
|
|
mapi_inetmapi_imtomapi($this->session, $this->store, $ab, $mapimessage, $mime, []); |
1307
|
|
|
} |
1308
|
|
|
else { |
1309
|
|
|
$props[$emailmap["messageclass"]] = "IPM.Note"; |
1310
|
|
|
$this->setPropsInMAPI($mapimessage, $message, $emailmap); |
1311
|
|
|
} |
1312
|
|
|
$props[$emailprops["messageflags"]] = MSGFLAG_UNSENT | MSGFLAG_READ; |
|
|
|
|
1313
|
|
|
|
1314
|
|
|
if (isset($message->asbody->type) && $message->asbody->type == SYNC_BODYPREFERENCE_HTML && isset($message->asbody->data)) { |
1315
|
|
|
$props[$emailprops["html"]] = stream_get_contents($message->asbody->data); |
1316
|
|
|
} |
1317
|
|
|
|
1318
|
|
|
// Android devices send the recipients in to, cc and bcc tags |
1319
|
|
|
if (isset($message->to) || isset($message->cc) || isset($message->bcc)) { |
1320
|
|
|
$recips = []; |
1321
|
|
|
$this->addRecips($message->to, MAPI_TO, $recips); |
|
|
|
|
1322
|
|
|
$this->addRecips($message->cc, MAPI_CC, $recips); |
|
|
|
|
1323
|
|
|
$this->addRecips($message->bcc, MAPI_BCC, $recips); |
|
|
|
|
1324
|
|
|
|
1325
|
|
|
mapi_message_modifyrecipients($mapimessage, MODRECIP_MODIFY, $recips); |
|
|
|
|
1326
|
|
|
} |
1327
|
|
|
// remove PR_CLIENT_SUBMIT_TIME |
1328
|
|
|
mapi_deleteprops( |
1329
|
|
|
$mapimessage, |
1330
|
|
|
[ |
1331
|
|
|
$emailprops["clientsubmittime"], |
1332
|
|
|
] |
1333
|
|
|
); |
1334
|
|
|
} |
1335
|
|
|
|
1336
|
|
|
// save DRAFTs attachments |
1337
|
|
|
if (!empty($message->asattachments)) { |
1338
|
|
|
$this->editAttachments($mapimessage, $message->asattachments, $response); |
1339
|
|
|
} |
1340
|
|
|
|
1341
|
|
|
// unset message flags if: |
1342
|
|
|
// flag is not set |
1343
|
|
|
if (empty($message->flag) || |
1344
|
|
|
// flag status is not set |
1345
|
|
|
!isset($message->flag->flagstatus) || |
1346
|
|
|
// flag status is 0 or empty |
1347
|
|
|
(isset($message->flag->flagstatus) && ($message->flag->flagstatus == 0 || $message->flag->flagstatus == ""))) { |
1348
|
|
|
// if message flag is empty, some properties need to be deleted |
1349
|
|
|
// and some set to 0 or false |
1350
|
|
|
|
1351
|
|
|
$props[$flagprops["todoitemsflags"]] = 0; |
1352
|
|
|
$props[$flagprops["status"]] = 0; |
1353
|
|
|
$props[$flagprops["completion"]] = 0.0; |
1354
|
|
|
$props[$flagprops["flagtype"]] = ""; |
1355
|
|
|
$props[$flagprops["ordinaldate"]] = 0x7FFFFFFF; // ordinal date is 12am 1.1.4501, set it to max possible value |
1356
|
|
|
$props[$flagprops["subordinaldate"]] = ""; |
1357
|
|
|
$props[$flagprops["replyrequested"]] = false; |
1358
|
|
|
$props[$flagprops["responserequested"]] = false; |
1359
|
|
|
$props[$flagprops["reminderset"]] = false; |
1360
|
|
|
$props[$flagprops["complete"]] = false; |
1361
|
|
|
|
1362
|
|
|
$delprops[] = $flagprops["todotitle"]; |
1363
|
|
|
$delprops[] = $flagprops["duedate"]; |
1364
|
|
|
$delprops[] = $flagprops["startdate"]; |
1365
|
|
|
$delprops[] = $flagprops["datecompleted"]; |
1366
|
|
|
$delprops[] = $flagprops["utcstartdate"]; |
1367
|
|
|
$delprops[] = $flagprops["utcduedate"]; |
1368
|
|
|
$delprops[] = $flagprops["completetime"]; |
1369
|
|
|
$delprops[] = $flagprops["flagstatus"]; |
1370
|
|
|
$delprops[] = $flagprops["flagicon"]; |
1371
|
|
|
} |
1372
|
|
|
else { |
1373
|
|
|
$this->setPropsInMAPI($mapimessage, $message->flag, $flagmapping); |
1374
|
|
|
$props[$flagprops["todoitemsflags"]] = 1; |
1375
|
|
|
if (isset($message->subject) && strlen($message->subject) > 0) { |
1376
|
|
|
$props[$flagprops["todotitle"]] = $message->subject; |
1377
|
|
|
} |
1378
|
|
|
// ordinal date is utc current time |
1379
|
|
|
if (!isset($message->flag->ordinaldate) || empty($message->flag->ordinaldate)) { |
1380
|
|
|
$props[$flagprops["ordinaldate"]] = time(); |
1381
|
|
|
} |
1382
|
|
|
// the default value |
1383
|
|
|
if (!isset($message->flag->subordinaldate) || empty($message->flag->subordinaldate)) { |
1384
|
|
|
$props[$flagprops["subordinaldate"]] = "5555555"; |
1385
|
|
|
} |
1386
|
|
|
$props[$flagprops["flagicon"]] = 6; // red flag icon |
1387
|
|
|
$props[$flagprops["replyrequested"]] = true; |
1388
|
|
|
$props[$flagprops["responserequested"]] = true; |
1389
|
|
|
|
1390
|
|
|
if ($message->flag->flagstatus == SYNC_FLAGSTATUS_COMPLETE) { |
1391
|
|
|
$props[$flagprops["status"]] = olTaskComplete; |
|
|
|
|
1392
|
|
|
$props[$flagprops["completion"]] = 1.0; |
1393
|
|
|
$props[$flagprops["complete"]] = true; |
1394
|
|
|
$props[$flagprops["replyrequested"]] = false; |
1395
|
|
|
$props[$flagprops["responserequested"]] = false; |
1396
|
|
|
unset($props[$flagprops["flagicon"]]); |
1397
|
|
|
$delprops[] = $flagprops["flagicon"]; |
1398
|
|
|
} |
1399
|
|
|
} |
1400
|
|
|
|
1401
|
|
|
if (!empty($props)) { |
1402
|
|
|
mapi_setprops($mapimessage, $props); |
1403
|
|
|
} |
1404
|
|
|
if (!empty($delprops)) { |
1405
|
|
|
mapi_deleteprops($mapimessage, $delprops); |
1406
|
|
|
} |
1407
|
|
|
|
1408
|
|
|
return $response; |
1409
|
|
|
} |
1410
|
|
|
|
1411
|
|
|
/** |
1412
|
|
|
* Writes a SyncAppointment to MAPI. |
1413
|
|
|
* |
1414
|
|
|
* @param mixed $mapimessage |
1415
|
|
|
* @param mixed $appointment |
1416
|
|
|
* |
1417
|
|
|
* @return SyncObject |
1418
|
|
|
*/ |
1419
|
|
|
private function setAppointment($mapimessage, $appointment) { |
1420
|
|
|
$response = new SyncAppointmentResponse(); |
1421
|
|
|
|
1422
|
|
|
$isAllday = isset($appointment->alldayevent) && $appointment->alldayevent; |
1423
|
|
|
$isMeeting = isset($appointment->meetingstatus) && $appointment->meetingstatus > 0; |
1424
|
|
|
$isAs16 = Request::GetProtocolVersion() >= 16.0; |
1425
|
|
|
|
1426
|
|
|
// Get timezone info |
1427
|
|
|
if (isset($appointment->timezone)) { |
1428
|
|
|
$tz = $this->getTZFromSyncBlob(base64_decode($appointment->timezone)); |
1429
|
|
|
} |
1430
|
|
|
// AS 16: doesn't sent a timezone - use server TZ |
1431
|
|
|
elseif ($isAs16 && $isAllday) { |
1432
|
|
|
$tz = TimezoneUtil::GetFullTZ(); |
1433
|
|
|
} |
1434
|
|
|
else { |
1435
|
|
|
$tz = false; |
1436
|
|
|
} |
1437
|
|
|
|
1438
|
|
|
$appointmentmapping = MAPIMapping::GetAppointmentMapping(); |
1439
|
|
|
$appointmentprops = MAPIMapping::GetAppointmentProperties(); |
1440
|
|
|
$appointmentprops = array_merge($this->getPropIdsFromStrings($appointmentmapping), $this->getPropIdsFromStrings($appointmentprops)); |
1441
|
|
|
|
1442
|
|
|
// AS 16: incoming instanceid means we need to create/update an appointment exception |
1443
|
|
|
if ($isAs16 && isset($appointment->instanceid) && $appointment->instanceid) { |
1444
|
|
|
// this property wasn't decoded so use Utils->ParseDate to convert it into a timestamp and get basedate from it |
1445
|
|
|
$instanceid = Utils::ParseDate($appointment->instanceid); |
1446
|
|
|
$basedate = $this->getDayStartOfTimestamp($instanceid); |
1447
|
|
|
|
1448
|
|
|
// get compatible TZ data |
1449
|
|
|
$props = [$appointmentprops["timezonetag"], $appointmentprops["isrecurring"]]; |
1450
|
|
|
$tzprop = $this->getProps($mapimessage, $props); |
1451
|
|
|
$tz = $this->getTZFromMAPIBlob($tzprop[$appointmentprops["timezonetag"]]); |
|
|
|
|
1452
|
|
|
|
1453
|
|
|
if ($appointmentprops["isrecurring"] == false) { |
1454
|
|
|
SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->setAppointment(): Cannot modify exception instanceId '%s' as target appointment is not recurring. Ignoring.", $appointment->instanceid)); |
1455
|
|
|
|
1456
|
|
|
return false; |
|
|
|
|
1457
|
|
|
} |
1458
|
|
|
// get a recurrence object |
1459
|
|
|
$recurrence = new Recurrence($this->store, $mapimessage); |
1460
|
|
|
|
1461
|
|
|
// check if the exception is to be deleted |
1462
|
|
|
if (isset($appointment->instanceiddelete) && $appointment->instanceiddelete === true) { |
1463
|
|
|
// Delete exception |
1464
|
|
|
$recurrence->createException([], $basedate, true); |
1465
|
|
|
} |
1466
|
|
|
// create or update the exception |
1467
|
|
|
else { |
1468
|
|
|
$exceptionprops = []; |
1469
|
|
|
|
1470
|
|
|
if (isset($appointment->starttime)) { |
1471
|
|
|
$exceptionprops[$appointmentprops["starttime"]] = $appointment->starttime; |
1472
|
|
|
} |
1473
|
|
|
if (isset($appointment->endtime)) { |
1474
|
|
|
$exceptionprops[$appointmentprops["endtime"]] = $appointment->endtime; |
1475
|
|
|
} |
1476
|
|
|
if (isset($appointment->subject)) { |
1477
|
|
|
$exceptionprops[$appointmentprops["subject"]] = $appointment->subject; |
1478
|
|
|
} |
1479
|
|
|
if (isset($appointment->location)) { |
1480
|
|
|
$exceptionprops[$appointmentprops["location"]] = $appointment->location; |
1481
|
|
|
} |
1482
|
|
|
if (isset($appointment->busystatus)) { |
1483
|
|
|
$exceptionprops[$appointmentprops["busystatus"]] = $appointment->busystatus; |
1484
|
|
|
} |
1485
|
|
|
if (isset($appointment->reminder)) { |
1486
|
|
|
$exceptionprops[$appointmentprops["reminderset"]] = 1; |
1487
|
|
|
$exceptionprops[$appointmentprops["remindertime"]] = $appointment->reminder; |
1488
|
|
|
} |
1489
|
|
|
if (isset($appointment->alldayevent)) { |
1490
|
|
|
$exceptionprops[$appointmentprops["alldayevent"]] = $mapiexception["alldayevent"] = $appointment->alldayevent; |
|
|
|
|
1491
|
|
|
} |
1492
|
|
|
if (isset($appointment->body)) { |
1493
|
|
|
$exceptionprops[$appointmentprops["body"]] = $appointment->body; |
1494
|
|
|
} |
1495
|
|
|
if (isset($appointment->asbody)) { |
1496
|
|
|
$this->setASbody($appointment->asbody, $exceptionprops, $appointmentprops); |
1497
|
|
|
} |
1498
|
|
|
if (isset($appointment->location2)) { |
1499
|
|
|
$this->setASlocation($appointment->location2, $exceptionprops, $appointmentprops); |
1500
|
|
|
} |
1501
|
|
|
|
1502
|
|
|
// modify if exists else create exception |
1503
|
|
|
if ($recurrence->isException($basedate)) { |
1504
|
|
|
$recurrence->modifyException($exceptionprops, $basedate); |
1505
|
|
|
} |
1506
|
|
|
else { |
1507
|
|
|
$recurrence->createException($exceptionprops, $basedate); |
1508
|
|
|
} |
1509
|
|
|
} |
1510
|
|
|
|
1511
|
|
|
// instantiate the MR so we can send a updates to the attendees |
1512
|
|
|
$mr = new Meetingrequest($this->store, $mapimessage, $this->session); |
1513
|
|
|
$mr->updateMeetingRequest($basedate); |
1514
|
|
|
$deleteException = isset($appointment->instanceiddelete) && $appointment->instanceiddelete === true; |
1515
|
|
|
$mr->sendMeetingRequest($deleteException, false, $basedate); |
1516
|
|
|
|
1517
|
|
|
return $response; |
1518
|
|
|
} |
1519
|
|
|
|
1520
|
|
|
// Save OldProps to later check which data is being changed |
1521
|
|
|
$oldProps = $this->getProps($mapimessage, $appointmentprops); |
1522
|
|
|
|
1523
|
|
|
// start and end time may not be set - try to get them from the existing appointment for further calculation. |
1524
|
|
|
if (!isset($appointment->starttime) || !isset($appointment->endtime)) { |
1525
|
|
|
$amapping = MAPIMapping::GetAppointmentMapping(); |
1526
|
|
|
$amapping = $this->getPropIdsFromStrings($amapping); |
1527
|
|
|
$existingstartendpropsmap = [$amapping["starttime"], $amapping["endtime"]]; |
1528
|
|
|
$existingstartendprops = $this->getProps($mapimessage, $existingstartendpropsmap); |
1529
|
|
|
|
1530
|
|
|
if (isset($existingstartendprops[$amapping["starttime"]]) && !isset($appointment->starttime)) { |
1531
|
|
|
$appointment->starttime = $existingstartendprops[$amapping["starttime"]]; |
1532
|
|
|
SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'starttime' was not set, using value from MAPI %d (%s).", $appointment->starttime, Utils::FormatDate($appointment->starttime))); |
1533
|
|
|
} |
1534
|
|
|
if (isset($existingstartendprops[$amapping["endtime"]]) && !isset($appointment->endtime)) { |
1535
|
|
|
$appointment->endtime = $existingstartendprops[$amapping["endtime"]]; |
1536
|
|
|
SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'endtime' was not set, using value from MAPI %d (%s).", $appointment->endtime, Utils::FormatDate($appointment->endtime))); |
1537
|
|
|
} |
1538
|
|
|
} |
1539
|
|
|
if (!isset($appointment->starttime) || !isset($appointment->endtime)) { |
1540
|
|
|
throw new StatusException("MAPIProvider->setAppointment(): Error, start and/or end time not set and can not be retrieved from MAPI.", SYNC_STATUS_SYNCCANNOTBECOMPLETED); |
1541
|
|
|
} |
1542
|
|
|
|
1543
|
|
|
// calculate duration because without it some webaccess views are broken. duration is in min |
1544
|
|
|
$localstart = $this->getLocaltimeByTZ($appointment->starttime, $tz); |
|
|
|
|
1545
|
|
|
$localend = $this->getLocaltimeByTZ($appointment->endtime, $tz); |
1546
|
|
|
$duration = ($localend - $localstart) / 60; |
1547
|
|
|
|
1548
|
|
|
// nokia sends an yearly event with 0 mins duration but as all day event, |
1549
|
|
|
// so make it end next day |
1550
|
|
|
if ($appointment->starttime == $appointment->endtime && $isAllday) { |
1551
|
|
|
$duration = 1440; |
1552
|
|
|
$appointment->endtime = $appointment->starttime + 24 * 60 * 60; |
1553
|
|
|
$localend = $localstart + 24 * 60 * 60; |
1554
|
|
|
} |
1555
|
|
|
|
1556
|
|
|
// use clientUID if set |
1557
|
|
|
if ($appointment->clientuid && !$appointment->uid) { |
1558
|
|
|
$appointment->uid = $appointment->clientuid; |
1559
|
|
|
// Facepalm: iOS sends weird ids (without dashes and a trailing null character) |
1560
|
|
|
if (strlen($appointment->uid) == 33) { |
1561
|
|
|
$appointment->uid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split($appointment->uid, 4)); |
|
|
|
|
1562
|
|
|
} |
1563
|
|
|
} |
1564
|
|
|
// is the transmitted UID OL compatible? |
1565
|
|
|
if ($appointment->uid && substr($appointment->uid, 0, 16) != "040000008200E000") { |
1566
|
|
|
// if not, encapsulate the transmitted uid |
1567
|
|
|
$appointment->uid = getGoidFromUid($appointment->uid); |
|
|
|
|
1568
|
|
|
} |
1569
|
|
|
// if there was a clientuid transport the new UID to the response |
1570
|
|
|
if ($appointment->clientuid) { |
1571
|
|
|
$response->uid = bin2hex($appointment->uid); |
1572
|
|
|
$response->hasResponse = true; |
1573
|
|
|
} |
1574
|
|
|
|
1575
|
|
|
mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Appointment"]); |
|
|
|
|
1576
|
|
|
|
1577
|
|
|
$this->setPropsInMAPI($mapimessage, $appointment, $appointmentmapping); |
1578
|
|
|
|
1579
|
|
|
// appointment specific properties to be set |
1580
|
|
|
$props = []; |
1581
|
|
|
|
1582
|
|
|
// sensitivity is not enough to mark an appointment as private, so we use another mapi tag |
1583
|
|
|
$private = (isset($appointment->sensitivity) && $appointment->sensitivity >= SENSITIVITY_PRIVATE) ? true : false; |
|
|
|
|
1584
|
|
|
|
1585
|
|
|
// Set commonstart/commonend to start/end and remindertime to start, duration, private and cleanGlobalObjectId |
1586
|
|
|
$props[$appointmentprops["commonstart"]] = $appointment->starttime; |
1587
|
|
|
$props[$appointmentprops["commonend"]] = $appointment->endtime; |
1588
|
|
|
$props[$appointmentprops["reminderstart"]] = $appointment->starttime; |
1589
|
|
|
// Set reminder boolean to 'true' if reminder is set |
1590
|
|
|
$props[$appointmentprops["reminderset"]] = isset($appointment->reminder) ? true : false; |
1591
|
|
|
$props[$appointmentprops["duration"]] = $duration; |
1592
|
|
|
$props[$appointmentprops["private"]] = $private; |
1593
|
|
|
$props[$appointmentprops["uid"]] = $appointment->uid; |
1594
|
|
|
// Set named prop 8510, unknown property, but enables deleting a single occurrence of a recurring |
1595
|
|
|
// type in OLK2003. |
1596
|
|
|
$props[$appointmentprops["sideeffects"]] = 369; |
1597
|
|
|
|
1598
|
|
|
if (isset($appointment->reminder) && $appointment->reminder >= 0) { |
1599
|
|
|
// Set 'flagdueby' to correct value (start - reminderminutes) |
1600
|
|
|
$props[$appointmentprops["flagdueby"]] = $appointment->starttime - $appointment->reminder * 60; |
1601
|
|
|
$props[$appointmentprops["remindertime"]] = $appointment->reminder; |
1602
|
|
|
} |
1603
|
|
|
// unset the reminder |
1604
|
|
|
else { |
1605
|
|
|
$props[$appointmentprops["reminderset"]] = false; |
1606
|
|
|
} |
1607
|
|
|
|
1608
|
|
|
if (isset($appointment->asbody)) { |
1609
|
|
|
$this->setASbody($appointment->asbody, $props, $appointmentprops); |
1610
|
|
|
} |
1611
|
|
|
|
1612
|
|
|
if (isset($appointment->location2)) { |
1613
|
|
|
$this->setASlocation($appointment->location2, $props, $appointmentprops); |
1614
|
|
|
} |
1615
|
|
|
if ($tz !== false) { |
1616
|
|
|
if (!($isAs16 && $isAllday)) { |
1617
|
|
|
$props[$appointmentprops["timezonetag"]] = $this->getMAPIBlobFromTZ($tz); |
1618
|
|
|
} |
1619
|
|
|
} |
1620
|
|
|
|
1621
|
|
|
if (isset($appointment->recurrence)) { |
1622
|
|
|
// Set PR_ICON_INDEX to 1025 to show correct icon in category view |
1623
|
|
|
$props[$appointmentprops["icon"]] = 1025; |
1624
|
|
|
|
1625
|
|
|
// if there aren't any exceptions, use the 'old style' set recurrence |
1626
|
|
|
$noexceptions = true; |
1627
|
|
|
|
1628
|
|
|
$recurrence = new Recurrence($this->store, $mapimessage); |
1629
|
|
|
$recur = []; |
1630
|
|
|
$this->setRecurrence($appointment, $recur); |
1631
|
|
|
|
1632
|
|
|
// set the recurrence type to that of the MAPI |
1633
|
|
|
$props[$appointmentprops["recurrencetype"]] = $recur["recurrencetype"]; |
1634
|
|
|
|
1635
|
|
|
$starttime = $this->gmtime($localstart); |
1636
|
|
|
$endtime = $this->gmtime($localend); |
|
|
|
|
1637
|
|
|
|
1638
|
|
|
// set recurrence start here because it's calculated differently for tasks and appointments |
1639
|
|
|
$recur["start"] = $this->getDayStartOfTimestamp($this->getGMTTimeByTZ($localstart, $tz)); |
|
|
|
|
1640
|
|
|
|
1641
|
|
|
$recur["startocc"] = $starttime["tm_hour"] * 60 + $starttime["tm_min"]; |
1642
|
|
|
$recur["endocc"] = $recur["startocc"] + $duration; // Note that this may be > 24*60 if multi-day |
1643
|
|
|
|
1644
|
|
|
// only tasks can regenerate |
1645
|
|
|
$recur["regen"] = false; |
1646
|
|
|
|
1647
|
|
|
// Process exceptions. The PDA will send all exceptions for this recurring item. |
1648
|
|
|
if (isset($appointment->exceptions)) { |
1649
|
|
|
foreach ($appointment->exceptions as $exception) { |
1650
|
|
|
// we always need the base date |
1651
|
|
|
if (!isset($exception->exceptionstarttime)) { |
1652
|
|
|
continue; |
1653
|
|
|
} |
1654
|
|
|
|
1655
|
|
|
$basedate = $this->getDayStartOfTimestamp($exception->exceptionstarttime); |
1656
|
|
|
if (isset($exception->deleted) && $exception->deleted) { |
1657
|
|
|
$noexceptions = false; |
1658
|
|
|
// Delete exception |
1659
|
|
|
$recurrence->createException([], $basedate, true); |
1660
|
|
|
} |
1661
|
|
|
else { |
1662
|
|
|
// Change exception |
1663
|
|
|
$mapiexception = ["basedate" => $basedate]; |
1664
|
|
|
// other exception properties which are not handled in recurrence |
1665
|
|
|
$exceptionprops = []; |
1666
|
|
|
|
1667
|
|
|
if (isset($exception->starttime)) { |
1668
|
|
|
$mapiexception["start"] = $this->getLocaltimeByTZ($exception->starttime, $tz); |
1669
|
|
|
$exceptionprops[$appointmentprops["starttime"]] = $exception->starttime; |
1670
|
|
|
} |
1671
|
|
|
if (isset($exception->endtime)) { |
1672
|
|
|
$mapiexception["end"] = $this->getLocaltimeByTZ($exception->endtime, $tz); |
1673
|
|
|
$exceptionprops[$appointmentprops["endtime"]] = $exception->endtime; |
1674
|
|
|
} |
1675
|
|
|
if (isset($exception->subject)) { |
1676
|
|
|
$exceptionprops[$appointmentprops["subject"]] = $mapiexception["subject"] = $exception->subject; |
1677
|
|
|
} |
1678
|
|
|
if (isset($exception->location)) { |
1679
|
|
|
$exceptionprops[$appointmentprops["location"]] = $mapiexception["location"] = $exception->location; |
1680
|
|
|
} |
1681
|
|
|
if (isset($exception->busystatus)) { |
1682
|
|
|
$exceptionprops[$appointmentprops["busystatus"]] = $mapiexception["busystatus"] = $exception->busystatus; |
1683
|
|
|
} |
1684
|
|
|
if (isset($exception->reminder)) { |
1685
|
|
|
$exceptionprops[$appointmentprops["reminderset"]] = $mapiexception["reminder_set"] = 1; |
1686
|
|
|
$exceptionprops[$appointmentprops["remindertime"]] = $mapiexception["remind_before"] = $exception->reminder; |
1687
|
|
|
} |
1688
|
|
|
if (isset($exception->alldayevent)) { |
1689
|
|
|
$exceptionprops[$appointmentprops["alldayevent"]] = $mapiexception["alldayevent"] = $exception->alldayevent; |
1690
|
|
|
} |
1691
|
|
|
|
1692
|
|
|
if (!isset($recur["changed_occurrences"])) { |
1693
|
|
|
$recur["changed_occurrences"] = []; |
1694
|
|
|
} |
1695
|
|
|
|
1696
|
|
|
if (isset($exception->body)) { |
1697
|
|
|
$exceptionprops[$appointmentprops["body"]] = $exception->body; |
1698
|
|
|
} |
1699
|
|
|
|
1700
|
|
|
if (isset($exception->asbody)) { |
1701
|
|
|
$this->setASbody($exception->asbody, $exceptionprops, $appointmentprops); |
1702
|
|
|
$mapiexception["body"] = $exceptionprops[$appointmentprops["body"]] = |
1703
|
|
|
(isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] : |
1704
|
|
|
((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : ""); |
1705
|
|
|
} |
1706
|
|
|
|
1707
|
|
|
array_push($recur["changed_occurrences"], $mapiexception); |
1708
|
|
|
|
1709
|
|
|
if (!empty($exceptionprops)) { |
1710
|
|
|
$noexceptions = false; |
1711
|
|
|
if ($recurrence->isException($basedate)) { |
1712
|
|
|
$recurrence->modifyException($exceptionprops, $basedate); |
1713
|
|
|
} |
1714
|
|
|
else { |
1715
|
|
|
$recurrence->createException($exceptionprops, $basedate); |
1716
|
|
|
} |
1717
|
|
|
} |
1718
|
|
|
} |
1719
|
|
|
} |
1720
|
|
|
} |
1721
|
|
|
|
1722
|
|
|
// setRecurrence deletes the attachments from an appointment |
1723
|
|
|
if ($noexceptions) { |
1724
|
|
|
$recurrence->setRecurrence($tz, $recur); |
1725
|
|
|
} |
1726
|
|
|
} |
1727
|
|
|
else { |
1728
|
|
|
$props[$appointmentprops["isrecurring"]] = false; |
1729
|
|
|
// remove recurringstate |
1730
|
|
|
mapi_deleteprops($mapimessage, [$appointmentprops["recurringstate"]]); |
1731
|
|
|
} |
1732
|
|
|
|
1733
|
|
|
// always set the PR_SENT_REPRESENTING_* props so that the attendee status update also works with the webaccess |
1734
|
|
|
$p = [$appointmentprops["representingentryid"], $appointmentprops["representingname"], $appointmentprops["sentrepresentingaddt"], |
1735
|
|
|
$appointmentprops["sentrepresentingemail"], $appointmentprops["sentrepresentinsrchk"], $appointmentprops["responsestatus"], ]; |
1736
|
|
|
$representingprops = $this->getProps($mapimessage, $p); |
1737
|
|
|
|
1738
|
|
|
$storeProps = $this->GetStoreProps(); |
1739
|
|
|
$abEntryProps = $this->getAbPropsFromEntryID($storeProps[PR_MAILBOX_OWNER_ENTRYID]); |
|
|
|
|
1740
|
|
|
if (!isset($representingprops[$appointmentprops["representingentryid"]])) { |
1741
|
|
|
$displayname = $sentrepresentingemail = Request::GetUser(); |
1742
|
|
|
$sentrepresentingaddt = 'SMPT'; |
1743
|
|
|
if ($abEntryProps !== false) { |
1744
|
|
|
$displayname = $abEntryProps[PR_DISPLAY_NAME] ?? $displayname; |
|
|
|
|
1745
|
|
|
$sentrepresentingemail = $abEntryProps[PR_EMAIL_ADDRESS] ?? $abEntryProps[PR_SMTP_ADDRESS] ?? $sentrepresentingemail; |
|
|
|
|
1746
|
|
|
$sentrepresentingaddt = $abEntryProps[PR_ADDRTYPE] ?? $sentrepresentingaddt; |
|
|
|
|
1747
|
|
|
} |
1748
|
|
|
$props[$appointmentprops["representingentryid"]] = $storeProps[PR_MAILBOX_OWNER_ENTRYID]; |
1749
|
|
|
$props[$appointmentprops["representingname"]] = $displayname; |
1750
|
|
|
$props[$appointmentprops["sentrepresentingemail"]] = $sentrepresentingemail; |
1751
|
|
|
$props[$appointmentprops["sentrepresentingaddt"]] = $sentrepresentingaddt; |
1752
|
|
|
$props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]] . ":" . $props[$appointmentprops["sentrepresentingemail"]]; |
1753
|
|
|
|
1754
|
|
|
if (isset($appointment->attendees) && is_array($appointment->attendees) && !empty($appointment->attendees)) { |
1755
|
|
|
$props[$appointmentprops["icon"]] = 1026; |
1756
|
|
|
// the user is the organizer |
1757
|
|
|
// set these properties to show tracking tab in webapp |
1758
|
|
|
$props[$appointmentprops["responsestatus"]] = olResponseOrganized; |
|
|
|
|
1759
|
|
|
$props[$appointmentprops["meetingstatus"]] = olMeeting; |
|
|
|
|
1760
|
|
|
} |
1761
|
|
|
} |
1762
|
|
|
// we also have to set the responsestatus and not only meetingstatus, so we use another mapi tag |
1763
|
|
|
if (!isset($props[$appointmentprops["responsestatus"]])) { |
1764
|
|
|
if (isset($appointment->responsetype)) { |
1765
|
|
|
$props[$appointmentprops["responsestatus"]] = $appointment->responsetype; |
1766
|
|
|
} |
1767
|
|
|
// only set responsestatus to none if it is not set on the server |
1768
|
|
|
elseif (!isset($representingprops[$appointmentprops["responsestatus"]])) { |
1769
|
|
|
$props[$appointmentprops["responsestatus"]] = olResponseNone; |
|
|
|
|
1770
|
|
|
} |
1771
|
|
|
} |
1772
|
|
|
|
1773
|
|
|
// when updating a normal appointment to a MR we need to send MR emails |
1774
|
|
|
$forceMRUpdateSend = false; |
1775
|
|
|
|
1776
|
|
|
// Do attendees |
1777
|
|
|
// For AS-16 get a list of the current attendees (pre update) |
1778
|
|
|
if ($isAs16 && $isMeeting) { |
1779
|
|
|
$old_recipienttable = mapi_message_getrecipienttable($mapimessage); |
1780
|
|
|
$old_receipstable = mapi_table_queryallrows( |
1781
|
|
|
$old_recipienttable, |
1782
|
|
|
[ |
1783
|
|
|
PR_ENTRYID, |
1784
|
|
|
PR_DISPLAY_NAME, |
1785
|
|
|
PR_EMAIL_ADDRESS, |
1786
|
|
|
PR_RECIPIENT_ENTRYID, |
|
|
|
|
1787
|
|
|
PR_RECIPIENT_TYPE, |
|
|
|
|
1788
|
|
|
PR_SEND_INTERNET_ENCODING, |
|
|
|
|
1789
|
|
|
PR_SEND_RICH_INFO, |
|
|
|
|
1790
|
|
|
PR_RECIPIENT_DISPLAY_NAME, |
|
|
|
|
1791
|
|
|
PR_ADDRTYPE, |
1792
|
|
|
PR_DISPLAY_TYPE, |
|
|
|
|
1793
|
|
|
PR_DISPLAY_TYPE_EX, |
|
|
|
|
1794
|
|
|
PR_RECIPIENT_TRACKSTATUS, |
|
|
|
|
1795
|
|
|
PR_RECIPIENT_TRACKSTATUS_TIME, |
|
|
|
|
1796
|
|
|
PR_RECIPIENT_FLAGS, |
|
|
|
|
1797
|
|
|
PR_ROWID, |
|
|
|
|
1798
|
|
|
PR_OBJECT_TYPE, |
|
|
|
|
1799
|
|
|
PR_SEARCH_KEY, |
|
|
|
|
1800
|
|
|
] |
1801
|
|
|
); |
1802
|
|
|
$old_receips = []; |
1803
|
|
|
foreach ($old_receipstable as $oldrec) { |
1804
|
|
|
if (isset($oldrec[PR_EMAIL_ADDRESS])) { |
1805
|
|
|
$old_receips[$oldrec[PR_EMAIL_ADDRESS]] = $oldrec; |
1806
|
|
|
} |
1807
|
|
|
} |
1808
|
|
|
} |
1809
|
|
|
|
1810
|
|
|
if (isset($appointment->attendees) && is_array($appointment->attendees)) { |
1811
|
|
|
$recips = []; |
1812
|
|
|
|
1813
|
|
|
// Outlook XP requires organizer in the attendee list as well |
1814
|
|
|
// Only add organizer if it's a meeting |
1815
|
|
|
if ($isMeeting) { |
1816
|
|
|
$org = []; |
1817
|
|
|
$org[PR_ENTRYID] = isset($representingprops[$appointmentprops["representingentryid"]]) ? $representingprops[$appointmentprops["representingentryid"]] : $props[$appointmentprops["representingentryid"]]; |
1818
|
|
|
$org[PR_DISPLAY_NAME] = isset($representingprops[$appointmentprops["representingname"]]) ? $representingprops[$appointmentprops["representingname"]] : $props[$appointmentprops["representingname"]]; |
1819
|
|
|
$org[PR_ADDRTYPE] = isset($representingprops[$appointmentprops["sentrepresentingaddt"]]) ? $representingprops[$appointmentprops["sentrepresentingaddt"]] : $props[$appointmentprops["sentrepresentingaddt"]]; |
1820
|
|
|
$org[PR_SMTP_ADDRESS] = $org[PR_EMAIL_ADDRESS] = isset($representingprops[$appointmentprops["sentrepresentingemail"]]) ? $representingprops[$appointmentprops["sentrepresentingemail"]] : $props[$appointmentprops["sentrepresentingemail"]]; |
1821
|
|
|
$org[PR_SEARCH_KEY] = isset($representingprops[$appointmentprops["sentrepresentinsrchk"]]) ? $representingprops[$appointmentprops["sentrepresentinsrchk"]] : $props[$appointmentprops["sentrepresentinsrchk"]]; |
1822
|
|
|
$org[PR_RECIPIENT_FLAGS] = recipOrganizer | recipSendable; |
|
|
|
|
1823
|
|
|
$org[PR_RECIPIENT_TYPE] = MAPI_ORIG; |
|
|
|
|
1824
|
|
|
$org[PR_RECIPIENT_TRACKSTATUS] = olResponseOrganized; |
1825
|
|
|
if ($abEntryProps !== false && isset($abEntryProps[PR_SMTP_ADDRESS])) { |
1826
|
|
|
$org[PR_SMTP_ADDRESS] = $abEntryProps[PR_SMTP_ADDRESS]; |
1827
|
|
|
} |
1828
|
|
|
|
1829
|
|
|
array_push($recips, $org); |
1830
|
|
|
// remove organizer from old_receips |
1831
|
|
|
if (isset($old_receips[$org[PR_EMAIL_ADDRESS]])) { |
1832
|
|
|
unset($old_receips[$org[PR_EMAIL_ADDRESS]]); |
1833
|
|
|
} |
1834
|
|
|
} |
1835
|
|
|
|
1836
|
|
|
// Open address book for user resolve |
1837
|
|
|
$addrbook = $this->getAddressbook(); |
1838
|
|
|
foreach ($appointment->attendees as $attendee) { |
1839
|
|
|
$recip = []; |
1840
|
|
|
$recip[PR_EMAIL_ADDRESS] = $recip[PR_SMTP_ADDRESS] = $attendee->email; |
1841
|
|
|
|
1842
|
|
|
// lookup information in GAB if possible so we have up-to-date name for given address |
1843
|
|
|
$userinfo = [[PR_DISPLAY_NAME => $recip[PR_EMAIL_ADDRESS]]]; |
1844
|
|
|
$userinfo = mapi_ab_resolvename($addrbook, $userinfo, EMS_AB_ADDRESS_LOOKUP); |
|
|
|
|
1845
|
|
|
if (mapi_last_hresult() == NOERROR) { |
|
|
|
|
1846
|
|
|
$recip[PR_DISPLAY_NAME] = $userinfo[0][PR_DISPLAY_NAME]; |
1847
|
|
|
$recip[PR_EMAIL_ADDRESS] = $userinfo[0][PR_EMAIL_ADDRESS]; |
1848
|
|
|
$recip[PR_SEARCH_KEY] = $userinfo[0][PR_SEARCH_KEY]; |
1849
|
|
|
$recip[PR_ADDRTYPE] = $userinfo[0][PR_ADDRTYPE]; |
1850
|
|
|
$recip[PR_ENTRYID] = $userinfo[0][PR_ENTRYID]; |
1851
|
|
|
$recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO; |
|
|
|
|
1852
|
|
|
$recip[PR_RECIPIENT_FLAGS] = recipSendable; |
1853
|
|
|
$recip[PR_RECIPIENT_TRACKSTATUS] = isset($attendee->attendeestatus) ? $attendee->attendeestatus : olResponseNone; |
1854
|
|
|
} |
1855
|
|
|
else { |
1856
|
|
|
$recip[PR_DISPLAY_NAME] = $attendee->name; |
1857
|
|
|
$recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0"; |
1858
|
|
|
$recip[PR_ADDRTYPE] = "SMTP"; |
1859
|
|
|
$recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO; |
1860
|
|
|
$recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]); |
1861
|
|
|
} |
1862
|
|
|
|
1863
|
|
|
// remove still existing attendees from the list of pre-update attendees - remaining pre-update are considered deleted attendees |
1864
|
|
|
if (isset($old_receips[$recip[PR_EMAIL_ADDRESS]])) { |
1865
|
|
|
unset($old_receips[$recip[PR_EMAIL_ADDRESS]]); |
1866
|
|
|
} |
1867
|
|
|
// if there is a new attendee a MR update must be send -> Appointment to MR update |
1868
|
|
|
else { |
1869
|
|
|
$forceMRUpdateSend = true; |
1870
|
|
|
} |
1871
|
|
|
// the organizer is already in the recipient list, no need to add him again |
1872
|
|
|
if (isset($org[PR_EMAIL_ADDRESS]) && strcasecmp($org[PR_EMAIL_ADDRESS], $recip[PR_EMAIL_ADDRESS]) == 0) { |
1873
|
|
|
continue; |
1874
|
|
|
} |
1875
|
|
|
array_push($recips, $recip); |
1876
|
|
|
} |
1877
|
|
|
|
1878
|
|
|
mapi_message_modifyrecipients($mapimessage, ($appointment->clientuid) ? MODRECIP_ADD : MODRECIP_MODIFY, $recips); |
|
|
|
|
1879
|
|
|
} |
1880
|
|
|
mapi_setprops($mapimessage, $props); |
1881
|
|
|
|
1882
|
|
|
// Since AS 16 we have to take care of MeetingRequest updates |
1883
|
|
|
if ($isAs16 && $isMeeting) { |
1884
|
|
|
$mr = new Meetingrequest($this->store, $mapimessage, $this->session); |
1885
|
|
|
// Only send updates if this is a new MR or we are the organizer |
1886
|
|
|
if ($appointment->clientuid || $mr->isLocalOrganiser() || $forceMRUpdateSend) { |
1887
|
|
|
// initialize MR and/or update internal counters |
1888
|
|
|
$mr->updateMeetingRequest(); |
1889
|
|
|
// when updating, check for significant changes and if needed will clear the existing recipient responses |
1890
|
|
|
if (!isset($appointment->clientuid) && !$forceMRUpdateSend) { |
1891
|
|
|
$mr->checkSignificantChanges($oldProps, false, false); |
1892
|
|
|
} |
1893
|
|
|
$mr->sendMeetingRequest(false, false, false, false, array_values($old_receips)); |
|
|
|
|
1894
|
|
|
} |
1895
|
|
|
} |
1896
|
|
|
|
1897
|
|
|
// update attachments send by the mobile |
1898
|
|
|
if (!empty($appointment->asattachments)) { |
1899
|
|
|
$this->editAttachments($mapimessage, $appointment->asattachments, $response); |
1900
|
|
|
} |
1901
|
|
|
|
1902
|
|
|
// Existing allday events may have tzdef* properties set, |
1903
|
|
|
// so it's necessary to set them to UTC in order for other clients |
1904
|
|
|
// to display such events properly. |
1905
|
|
|
if ($isAllday && ( |
1906
|
|
|
isset($oldProps[$appointmentprops['tzdefstart']]) || |
1907
|
|
|
isset($oldProps[$appointmentprops['tzdefend']]) |
1908
|
|
|
)) { |
1909
|
|
|
$utc = TimezoneUtil::GetBinaryTZ('Etc/Utc'); |
1910
|
|
|
if ($utc !== false) { |
1911
|
|
|
mapi_setprops($mapimessage, [ |
1912
|
|
|
$appointmentprops['tzdefstart'] => $utc, |
1913
|
|
|
$appointmentprops['tzdefend'] => $utc, |
1914
|
|
|
]); |
1915
|
|
|
} |
1916
|
|
|
} |
1917
|
|
|
|
1918
|
|
|
return $response; |
1919
|
|
|
} |
1920
|
|
|
|
1921
|
|
|
/** |
1922
|
|
|
* Writes a SyncContact to MAPI. |
1923
|
|
|
* |
1924
|
|
|
* @param mixed $mapimessage |
1925
|
|
|
* @param SyncContact $contact |
1926
|
|
|
* |
1927
|
|
|
* @return bool |
1928
|
|
|
*/ |
1929
|
|
|
private function setContact($mapimessage, $contact) { |
1930
|
|
|
$response = new SyncContactResponse(); |
1931
|
|
|
mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Contact"]); |
|
|
|
|
1932
|
|
|
|
1933
|
|
|
// normalize email addresses |
1934
|
|
|
if (isset($contact->email1address) && (($contact->email1address = $this->extractEmailAddress($contact->email1address)) === false)) { |
|
|
|
|
1935
|
|
|
unset($contact->email1address); |
1936
|
|
|
} |
1937
|
|
|
|
1938
|
|
|
if (isset($contact->email2address) && (($contact->email2address = $this->extractEmailAddress($contact->email2address)) === false)) { |
|
|
|
|
1939
|
|
|
unset($contact->email2address); |
1940
|
|
|
} |
1941
|
|
|
|
1942
|
|
|
if (isset($contact->email3address) && (($contact->email3address = $this->extractEmailAddress($contact->email3address)) === false)) { |
|
|
|
|
1943
|
|
|
unset($contact->email3address); |
1944
|
|
|
} |
1945
|
|
|
|
1946
|
|
|
$contactmapping = MAPIMapping::GetContactMapping(); |
1947
|
|
|
$contactprops = MAPIMapping::GetContactProperties(); |
1948
|
|
|
$this->setPropsInMAPI($mapimessage, $contact, $contactmapping); |
1949
|
|
|
|
1950
|
|
|
// /set display name from contact's properties |
1951
|
|
|
$cname = $this->composeDisplayName($contact); |
1952
|
|
|
|
1953
|
|
|
// get contact specific mapi properties and merge them with the AS properties |
1954
|
|
|
$contactprops = array_merge($this->getPropIdsFromStrings($contactmapping), $this->getPropIdsFromStrings($contactprops)); |
1955
|
|
|
|
1956
|
|
|
// contact specific properties to be set |
1957
|
|
|
$props = []; |
1958
|
|
|
|
1959
|
|
|
// need to be set in order to show contacts properly in outlook and wa |
1960
|
|
|
$nremails = []; |
1961
|
|
|
$abprovidertype = 0; |
1962
|
|
|
|
1963
|
|
|
if (isset($contact->email1address)) { |
1964
|
|
|
$this->setEmailAddress($contact->email1address, $cname, 1, $props, $contactprops, $nremails, $abprovidertype); |
1965
|
|
|
} |
1966
|
|
|
if (isset($contact->email2address)) { |
1967
|
|
|
$this->setEmailAddress($contact->email2address, $cname, 2, $props, $contactprops, $nremails, $abprovidertype); |
1968
|
|
|
} |
1969
|
|
|
if (isset($contact->email3address)) { |
1970
|
|
|
$this->setEmailAddress($contact->email3address, $cname, 3, $props, $contactprops, $nremails, $abprovidertype); |
1971
|
|
|
} |
1972
|
|
|
|
1973
|
|
|
$props[$contactprops["addressbooklong"]] = $abprovidertype; |
1974
|
|
|
$props[$contactprops["displayname"]] = $props[$contactprops["subject"]] = $cname; |
1975
|
|
|
|
1976
|
|
|
// pda multiple e-mail addresses bug fix for the contact |
1977
|
|
|
if (!empty($nremails)) { |
1978
|
|
|
$props[$contactprops["addressbookmv"]] = $nremails; |
1979
|
|
|
} |
1980
|
|
|
|
1981
|
|
|
// set addresses |
1982
|
|
|
$this->setAddress("home", $contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props, $contactprops); |
1983
|
|
|
$this->setAddress("business", $contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props, $contactprops); |
1984
|
|
|
$this->setAddress("other", $contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props, $contactprops); |
1985
|
|
|
|
1986
|
|
|
// set the mailing address and its type |
1987
|
|
|
if (isset($props[$contactprops["businessaddress"]])) { |
1988
|
|
|
$props[$contactprops["mailingaddress"]] = 2; |
1989
|
|
|
$this->setMailingAddress($contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props[$contactprops["businessaddress"]], $props, $contactprops); |
1990
|
|
|
} |
1991
|
|
|
elseif (isset($props[$contactprops["homeaddress"]])) { |
1992
|
|
|
$props[$contactprops["mailingaddress"]] = 1; |
1993
|
|
|
$this->setMailingAddress($contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props[$contactprops["homeaddress"]], $props, $contactprops); |
1994
|
|
|
} |
1995
|
|
|
elseif (isset($props[$contactprops["otheraddress"]])) { |
1996
|
|
|
$props[$contactprops["mailingaddress"]] = 3; |
1997
|
|
|
$this->setMailingAddress($contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props[$contactprops["otheraddress"]], $props, $contactprops); |
1998
|
|
|
} |
1999
|
|
|
|
2000
|
|
|
if (isset($contact->picture)) { |
2001
|
|
|
$picbinary = base64_decode($contact->picture); |
2002
|
|
|
$picsize = strlen($picbinary); |
2003
|
|
|
$props[$contactprops["haspic"]] = false; |
2004
|
|
|
|
2005
|
|
|
// TODO contact picture handling |
2006
|
|
|
// check if contact has already got a picture. delete it first in that case |
2007
|
|
|
// delete it also if it was removed on a mobile |
2008
|
|
|
$picprops = mapi_getprops($mapimessage, [$contactprops["haspic"]]); |
2009
|
|
|
if (isset($picprops[$contactprops["haspic"]]) && $picprops[$contactprops["haspic"]]) { |
2010
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "Contact already has a picture. Delete it"); |
2011
|
|
|
|
2012
|
|
|
$attachtable = mapi_message_getattachmenttable($mapimessage); |
2013
|
|
|
mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction()); |
2014
|
|
|
$rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]); |
|
|
|
|
2015
|
|
|
if (isset($rows) && is_array($rows)) { |
2016
|
|
|
foreach ($rows as $row) { |
2017
|
|
|
mapi_message_deleteattach($mapimessage, $row[PR_ATTACH_NUM]); |
2018
|
|
|
} |
2019
|
|
|
} |
2020
|
|
|
} |
2021
|
|
|
|
2022
|
|
|
// only set picture if there's data in the request |
2023
|
|
|
if ($picbinary !== false && $picsize > 0) { |
2024
|
|
|
$props[$contactprops["haspic"]] = true; |
2025
|
|
|
$pic = mapi_message_createattach($mapimessage); |
2026
|
|
|
// Set properties of the attachment |
2027
|
|
|
$picprops = [ |
2028
|
|
|
PR_ATTACH_LONG_FILENAME => "ContactPicture.jpg", |
|
|
|
|
2029
|
|
|
PR_DISPLAY_NAME => "ContactPicture.jpg", |
|
|
|
|
2030
|
|
|
0x7FFF000B => true, |
2031
|
|
|
PR_ATTACHMENT_HIDDEN => false, |
|
|
|
|
2032
|
|
|
PR_ATTACHMENT_FLAGS => 1, |
|
|
|
|
2033
|
|
|
PR_ATTACH_METHOD => ATTACH_BY_VALUE, |
|
|
|
|
2034
|
|
|
PR_ATTACH_EXTENSION => ".jpg", |
|
|
|
|
2035
|
|
|
PR_ATTACH_NUM => 1, |
2036
|
|
|
PR_ATTACH_SIZE => $picsize, |
|
|
|
|
2037
|
|
|
PR_ATTACH_DATA_BIN => $picbinary, |
|
|
|
|
2038
|
|
|
]; |
2039
|
|
|
|
2040
|
|
|
mapi_setprops($pic, $picprops); |
2041
|
|
|
mapi_savechanges($pic); |
2042
|
|
|
} |
2043
|
|
|
} |
2044
|
|
|
|
2045
|
|
|
if (isset($contact->asbody)) { |
2046
|
|
|
$this->setASbody($contact->asbody, $props, $contactprops); |
2047
|
|
|
} |
2048
|
|
|
|
2049
|
|
|
// set fileas |
2050
|
|
|
if (defined('FILEAS_ORDER')) { |
2051
|
|
|
$lastname = (isset($contact->lastname)) ? $contact->lastname : ""; |
2052
|
|
|
$firstname = (isset($contact->firstname)) ? $contact->firstname : ""; |
2053
|
|
|
$middlename = (isset($contact->middlename)) ? $contact->middlename : ""; |
2054
|
|
|
$company = (isset($contact->companyname)) ? $contact->companyname : ""; |
2055
|
|
|
$props[$contactprops["fileas"]] = Utils::BuildFileAs($lastname, $firstname, $middlename, $company); |
2056
|
|
|
} |
2057
|
|
|
else { |
2058
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined"); |
2059
|
|
|
} |
2060
|
|
|
|
2061
|
|
|
mapi_setprops($mapimessage, $props); |
2062
|
|
|
|
2063
|
|
|
return $response; |
|
|
|
|
2064
|
|
|
} |
2065
|
|
|
|
2066
|
|
|
/** |
2067
|
|
|
* Writes a SyncTask to MAPI. |
2068
|
|
|
* |
2069
|
|
|
* @param mixed $mapimessage |
2070
|
|
|
* @param SyncTask $task |
2071
|
|
|
* |
2072
|
|
|
* @return bool |
2073
|
|
|
*/ |
2074
|
|
|
private function setTask($mapimessage, $task) { |
2075
|
|
|
$response = new SyncTaskResponse(); |
2076
|
|
|
mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Task"]); |
|
|
|
|
2077
|
|
|
|
2078
|
|
|
$taskmapping = MAPIMapping::GetTaskMapping(); |
2079
|
|
|
$taskprops = MAPIMapping::GetTaskProperties(); |
2080
|
|
|
$this->setPropsInMAPI($mapimessage, $task, $taskmapping); |
2081
|
|
|
$taskprops = array_merge($this->getPropIdsFromStrings($taskmapping), $this->getPropIdsFromStrings($taskprops)); |
2082
|
|
|
|
2083
|
|
|
// task specific properties to be set |
2084
|
|
|
$props = []; |
2085
|
|
|
|
2086
|
|
|
if (isset($task->asbody)) { |
2087
|
|
|
$this->setASbody($task->asbody, $props, $taskprops); |
2088
|
|
|
} |
2089
|
|
|
|
2090
|
|
|
if (isset($task->complete)) { |
2091
|
|
|
if ($task->complete) { |
2092
|
|
|
// Set completion to 100% |
2093
|
|
|
// Set status to 'complete' |
2094
|
|
|
$props[$taskprops["completion"]] = 1.0; |
2095
|
|
|
$props[$taskprops["status"]] = 2; |
2096
|
|
|
$props[$taskprops["reminderset"]] = false; |
2097
|
|
|
} |
2098
|
|
|
else { |
2099
|
|
|
// Set completion to 0% |
2100
|
|
|
// Set status to 'not started' |
2101
|
|
|
$props[$taskprops["completion"]] = 0.0; |
2102
|
|
|
$props[$taskprops["status"]] = 0; |
2103
|
|
|
} |
2104
|
|
|
} |
2105
|
|
|
if (isset($task->recurrence) && class_exists('TaskRecurrence')) { |
2106
|
|
|
$deadoccur = false; |
2107
|
|
|
if ((isset($task->recurrence->occurrences) && $task->recurrence->occurrences == 1) || |
2108
|
|
|
(isset($task->recurrence->deadoccur) && $task->recurrence->deadoccur == 1)) { // ios5 sends deadoccur inside the recurrence |
2109
|
|
|
$deadoccur = true; |
2110
|
|
|
} |
2111
|
|
|
|
2112
|
|
|
// Set PR_ICON_INDEX to 1281 to show correct icon in category view |
2113
|
|
|
$props[$taskprops["icon"]] = 1281; |
2114
|
|
|
// dead occur - false if new occurrences should be generated from the task |
2115
|
|
|
// true - if it is the last occurrence of the task |
2116
|
|
|
$props[$taskprops["deadoccur"]] = $deadoccur; |
2117
|
|
|
$props[$taskprops["isrecurringtag"]] = true; |
2118
|
|
|
|
2119
|
|
|
$recurrence = new TaskRecurrence($this->store, $mapimessage); |
2120
|
|
|
$recur = []; |
2121
|
|
|
$this->setRecurrence($task, $recur); |
2122
|
|
|
|
2123
|
|
|
// task specific recurrence properties which we need to set here |
2124
|
|
|
// "start" and "end" are in GMT when passing to class.recurrence |
2125
|
|
|
// set recurrence start here because it's calculated differently for tasks and appointments |
2126
|
|
|
$recur["start"] = $task->recurrence->start; |
2127
|
|
|
$recur["regen"] = (isset($task->recurrence->regenerate) && $task->recurrence->regenerate) ? 1 : 0; |
2128
|
|
|
// OL regenerates recurring task itself, but setting deleteOccurrence is required so that PHP-MAPI doesn't regenerate |
2129
|
|
|
// completed occurrence of a task. |
2130
|
|
|
if ($recur["regen"] == 0) { |
2131
|
|
|
$recur["deleteOccurrence"] = 0; |
2132
|
|
|
} |
2133
|
|
|
// Also add dates to $recur |
2134
|
|
|
$recur["duedate"] = $task->duedate; |
2135
|
|
|
$recur["complete"] = (isset($task->complete) && $task->complete) ? 1 : 0; |
2136
|
|
|
if (isset($task->datecompleted)) { |
2137
|
|
|
$recur["datecompleted"] = $task->datecompleted; |
2138
|
|
|
} |
2139
|
|
|
$recurrence->setRecurrence($recur); |
2140
|
|
|
} |
2141
|
|
|
|
2142
|
|
|
$props[$taskprops["private"]] = (isset($task->sensitivity) && $task->sensitivity >= SENSITIVITY_PRIVATE) ? true : false; |
|
|
|
|
2143
|
|
|
|
2144
|
|
|
// Open address book for user resolve to set the owner |
2145
|
|
|
$addrbook = $this->getAddressbook(); |
|
|
|
|
2146
|
|
|
|
2147
|
|
|
// check if there is already an owner for the task, set current user if not |
2148
|
|
|
$p = [$taskprops["owner"]]; |
2149
|
|
|
$owner = $this->getProps($mapimessage, $p); |
2150
|
|
|
if (!isset($owner[$taskprops["owner"]])) { |
2151
|
|
|
$userinfo = nsp_getuserinfo(Request::GetUserIdentifier()); |
2152
|
|
|
if (mapi_last_hresult() == NOERROR && isset($userinfo["fullname"])) { |
|
|
|
|
2153
|
|
|
$props[$taskprops["owner"]] = $userinfo["fullname"]; |
2154
|
|
|
} |
2155
|
|
|
} |
2156
|
|
|
mapi_setprops($mapimessage, $props); |
2157
|
|
|
|
2158
|
|
|
return $response; |
|
|
|
|
2159
|
|
|
} |
2160
|
|
|
|
2161
|
|
|
/** |
2162
|
|
|
* Writes a SyncNote to MAPI. |
2163
|
|
|
* |
2164
|
|
|
* @param mixed $mapimessage |
2165
|
|
|
* @param SyncNote $note |
2166
|
|
|
* |
2167
|
|
|
* @return bool |
2168
|
|
|
*/ |
2169
|
|
|
private function setNote($mapimessage, $note) { |
2170
|
|
|
$response = new SyncNoteResponse(); |
2171
|
|
|
// Touchdown does not send categories if all are unset or there is none. |
2172
|
|
|
// Setting it to an empty array will unset the property in gromox as well |
2173
|
|
|
if (!isset($note->categories)) { |
2174
|
|
|
$note->categories = []; |
2175
|
|
|
} |
2176
|
|
|
|
2177
|
|
|
// update icon index to correspond to the color |
2178
|
|
|
if (isset($note->Color) && $note->Color > -1 && $note->Color < 5) { |
2179
|
|
|
$note->Iconindex = 768 + $note->Color; |
|
|
|
|
2180
|
|
|
} |
2181
|
|
|
|
2182
|
|
|
$this->setPropsInMAPI($mapimessage, $note, MAPIMapping::GetNoteMapping()); |
2183
|
|
|
|
2184
|
|
|
$noteprops = MAPIMapping::GetNoteProperties(); |
2185
|
|
|
$noteprops = $this->getPropIdsFromStrings($noteprops); |
2186
|
|
|
|
2187
|
|
|
// note specific properties to be set |
2188
|
|
|
$props = []; |
2189
|
|
|
$props[$noteprops["messageclass"]] = "IPM.StickyNote"; |
2190
|
|
|
// set body otherwise the note will be "broken" when editing it in outlook |
2191
|
|
|
if (isset($note->asbody)) { |
2192
|
|
|
$this->setASbody($note->asbody, $props, $noteprops); |
2193
|
|
|
} |
2194
|
|
|
|
2195
|
|
|
$props[$noteprops["internetcpid"]] = INTERNET_CPID_UTF8; |
2196
|
|
|
mapi_setprops($mapimessage, $props); |
2197
|
|
|
|
2198
|
|
|
return $response; |
|
|
|
|
2199
|
|
|
} |
2200
|
|
|
|
2201
|
|
|
/*---------------------------------------------------------------------------------------------------------- |
2202
|
|
|
* HELPER |
2203
|
|
|
*/ |
2204
|
|
|
|
2205
|
|
|
/** |
2206
|
|
|
* Returns the timestamp offset. |
2207
|
|
|
* |
2208
|
|
|
* @param string $ts |
2209
|
|
|
* |
2210
|
|
|
* @return long |
2211
|
|
|
*/ |
2212
|
|
|
private function GetTZOffset($ts) { |
2213
|
|
|
$Offset = date("O", $ts); |
|
|
|
|
2214
|
|
|
|
2215
|
|
|
$Parity = $Offset < 0 ? -1 : 1; |
2216
|
|
|
$Offset *= $Parity; |
2217
|
|
|
$Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100; |
2218
|
|
|
|
2219
|
|
|
return $Parity * $Offset; |
|
|
|
|
2220
|
|
|
} |
2221
|
|
|
|
2222
|
|
|
/** |
2223
|
|
|
* UTC time of the timestamp. |
2224
|
|
|
* |
2225
|
|
|
* @param long $time |
2226
|
|
|
* |
2227
|
|
|
* @return array |
2228
|
|
|
*/ |
2229
|
|
|
private function gmtime($time) { |
2230
|
|
|
$TZOffset = $this->GetTZOffset($time); |
2231
|
|
|
|
2232
|
|
|
$t_time = $time - $TZOffset * 60; # Counter adjust for localtime() |
2233
|
|
|
|
2234
|
|
|
return localtime($t_time, 1); |
2235
|
|
|
} |
2236
|
|
|
|
2237
|
|
|
/** |
2238
|
|
|
* Sets the properties in a MAPI object according to an Sync object and a property mapping. |
2239
|
|
|
* |
2240
|
|
|
* @param mixed $mapimessage |
2241
|
|
|
* @param SyncObject $message |
2242
|
|
|
* @param array $mapping |
2243
|
|
|
*/ |
2244
|
|
|
private function setPropsInMAPI($mapimessage, $message, $mapping) { |
2245
|
|
|
$mapiprops = $this->getPropIdsFromStrings($mapping); |
2246
|
|
|
$unsetVars = $message->getUnsetVars(); |
2247
|
|
|
$propsToDelete = []; |
2248
|
|
|
$propsToSet = []; |
2249
|
|
|
|
2250
|
|
|
foreach ($mapiprops as $asprop => $mapiprop) { |
2251
|
|
|
if (isset($message->{$asprop})) { |
2252
|
|
|
$value = $message->{$asprop}; |
2253
|
|
|
|
2254
|
|
|
// Make sure the php values are the correct type |
2255
|
|
|
switch (mapi_prop_type($mapiprop)) { |
2256
|
|
|
case PT_BINARY: |
|
|
|
|
2257
|
|
|
case PT_STRING8: |
|
|
|
|
2258
|
|
|
settype($value, "string"); |
2259
|
|
|
break; |
2260
|
|
|
|
2261
|
|
|
case PT_BOOLEAN: |
|
|
|
|
2262
|
|
|
settype($value, "boolean"); |
2263
|
|
|
break; |
2264
|
|
|
|
2265
|
|
|
case PT_SYSTIME: |
|
|
|
|
2266
|
|
|
case PT_LONG: |
|
|
|
|
2267
|
|
|
settype($value, "integer"); |
2268
|
|
|
break; |
2269
|
|
|
} |
2270
|
|
|
|
2271
|
|
|
// if an "empty array" is to be saved, it the mvprop should be deleted - fixes Mantis #468 |
2272
|
|
|
if (is_array($value) && empty($value)) { |
2273
|
|
|
$propsToDelete[] = $mapiprop; |
2274
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setPropsInMAPI(): Property '%s' to be deleted as it is an empty array", $asprop)); |
2275
|
|
|
} |
2276
|
|
|
else { |
2277
|
|
|
// all properties will be set at once |
2278
|
|
|
$propsToSet[$mapiprop] = $value; |
2279
|
|
|
} |
2280
|
|
|
} |
2281
|
|
|
elseif (in_array($asprop, $unsetVars)) { |
2282
|
|
|
$propsToDelete[] = $mapiprop; |
2283
|
|
|
} |
2284
|
|
|
} |
2285
|
|
|
|
2286
|
|
|
mapi_setprops($mapimessage, $propsToSet); |
2287
|
|
|
if (mapi_last_hresult()) { |
2288
|
|
|
SLog::Write(LOGLEVEL_WARN, sprintf("Failed to set properties, trying to set them separately. Error code was:%x", mapi_last_hresult())); |
2289
|
|
|
$this->setPropsIndividually($mapimessage, $propsToSet, $mapiprops); |
2290
|
|
|
} |
2291
|
|
|
|
2292
|
|
|
mapi_deleteprops($mapimessage, $propsToDelete); |
2293
|
|
|
|
2294
|
|
|
// clean up |
2295
|
|
|
unset($unsetVars, $propsToDelete); |
2296
|
|
|
} |
2297
|
|
|
|
2298
|
|
|
/** |
2299
|
|
|
* Sets the properties one by one in a MAPI object. |
2300
|
|
|
* |
2301
|
|
|
* @param mixed &$mapimessage |
2302
|
|
|
* @param array &$propsToSet |
2303
|
|
|
* @param array &$mapiprops |
2304
|
|
|
*/ |
2305
|
|
|
private function setPropsIndividually(&$mapimessage, &$propsToSet, &$mapiprops) { |
2306
|
|
|
foreach ($propsToSet as $prop => $value) { |
2307
|
|
|
mapi_setprops($mapimessage, [$prop => $value]); |
2308
|
|
|
if (mapi_last_hresult()) { |
2309
|
|
|
SLog::Write(LOGLEVEL_ERROR, sprintf("Failed setting property [%s] with value [%s], error code was:%x", array_search($prop, $mapiprops), $value, mapi_last_hresult())); |
2310
|
|
|
} |
2311
|
|
|
} |
2312
|
|
|
} |
2313
|
|
|
|
2314
|
|
|
/** |
2315
|
|
|
* Gets the properties from a MAPI object and sets them in the Sync object according to mapping. |
2316
|
|
|
* |
2317
|
|
|
* @param SyncObject &$message |
2318
|
|
|
* @param mixed $mapimessage |
2319
|
|
|
* @param array $mapping |
2320
|
|
|
*/ |
2321
|
|
|
private function getPropsFromMAPI(&$message, $mapimessage, $mapping) { |
2322
|
|
|
$messageprops = $this->getProps($mapimessage, $mapping); |
2323
|
|
|
foreach ($mapping as $asprop => $mapiprop) { |
2324
|
|
|
// Get long strings via openproperty |
2325
|
|
|
if (isset($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))])) { |
|
|
|
|
2326
|
|
|
if ($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_32BIT || |
2327
|
|
|
$messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_64BIT) { |
2328
|
|
|
$messageprops[$mapiprop] = MAPIUtils::readPropStream($mapimessage, $mapiprop); |
2329
|
|
|
} |
2330
|
|
|
} |
2331
|
|
|
|
2332
|
|
|
if (isset($messageprops[$mapiprop])) { |
2333
|
|
|
if (mapi_prop_type($mapiprop) == PT_BOOLEAN) { |
|
|
|
|
2334
|
|
|
// Force to actual '0' or '1' |
2335
|
|
|
if ($messageprops[$mapiprop]) { |
2336
|
|
|
$message->{$asprop} = 1; |
2337
|
|
|
} |
2338
|
|
|
else { |
2339
|
|
|
$message->{$asprop} = 0; |
2340
|
|
|
} |
2341
|
|
|
} |
2342
|
|
|
else { |
2343
|
|
|
// Special handling for PR_MESSAGE_FLAGS |
2344
|
|
|
if ($mapiprop == PR_MESSAGE_FLAGS) { |
|
|
|
|
2345
|
|
|
$message->{$asprop} = $messageprops[$mapiprop] & 1; |
2346
|
|
|
} // only look at 'read' flag |
2347
|
|
|
else { |
2348
|
|
|
$message->{$asprop} = $messageprops[$mapiprop]; |
2349
|
|
|
} |
2350
|
|
|
} |
2351
|
|
|
} |
2352
|
|
|
} |
2353
|
|
|
} |
2354
|
|
|
|
2355
|
|
|
/** |
2356
|
|
|
* Wraps getPropIdsFromStrings() calls. |
2357
|
|
|
* |
2358
|
|
|
* @param mixed &$mapiprops |
2359
|
|
|
*/ |
2360
|
|
|
private function getPropIdsFromStrings(&$mapiprops) { |
2361
|
|
|
return getPropIdsFromStrings($this->store, $mapiprops); |
|
|
|
|
2362
|
|
|
} |
2363
|
|
|
|
2364
|
|
|
/** |
2365
|
|
|
* Wraps mapi_getprops() calls. |
2366
|
|
|
* |
2367
|
|
|
* @param mixed $mapimessage |
2368
|
|
|
* @param mixed $mapiproperties |
2369
|
|
|
*/ |
2370
|
|
|
protected function getProps($mapimessage, &$mapiproperties) { |
2371
|
|
|
$mapiproperties = $this->getPropIdsFromStrings($mapiproperties); |
2372
|
|
|
|
2373
|
|
|
return mapi_getprops($mapimessage, $mapiproperties); |
2374
|
|
|
} |
2375
|
|
|
|
2376
|
|
|
/** |
2377
|
|
|
* Unpack timezone info from MAPI. |
2378
|
|
|
* |
2379
|
|
|
* @param string $data |
2380
|
|
|
* |
2381
|
|
|
* @return array |
2382
|
|
|
*/ |
2383
|
|
|
private function getTZFromMAPIBlob($data) { |
2384
|
|
|
return unpack("lbias/lstdbias/ldstbias/" . |
2385
|
|
|
"vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . |
2386
|
|
|
"vconst2/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis", $data); |
2387
|
|
|
} |
2388
|
|
|
|
2389
|
|
|
/** |
2390
|
|
|
* Unpack timezone info from Sync. |
2391
|
|
|
* |
2392
|
|
|
* @param string $data |
2393
|
|
|
* |
2394
|
|
|
* @return array |
2395
|
|
|
*/ |
2396
|
|
|
private function getTZFromSyncBlob($data) { |
2397
|
|
|
$tz = unpack("lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . |
2398
|
|
|
"lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/" . |
2399
|
|
|
"ldstbias", $data); |
2400
|
|
|
|
2401
|
|
|
// Make the structure compatible with class.recurrence.php |
2402
|
|
|
$tz["timezone"] = $tz["bias"]; |
2403
|
|
|
$tz["timezonedst"] = $tz["dstbias"]; |
2404
|
|
|
|
2405
|
|
|
return $tz; |
2406
|
|
|
} |
2407
|
|
|
|
2408
|
|
|
/** |
2409
|
|
|
* Pack timezone info for MAPI. |
2410
|
|
|
* |
2411
|
|
|
* @param array $tz |
2412
|
|
|
* |
2413
|
|
|
* @return string |
2414
|
|
|
*/ |
2415
|
|
|
private function getMAPIBlobFromTZ($tz) { |
2416
|
|
|
return pack( |
2417
|
|
|
"lllvvvvvvvvvvvvvvvvvv", |
2418
|
|
|
$tz["bias"], |
2419
|
|
|
$tz["stdbias"], |
2420
|
|
|
$tz["dstbias"], |
2421
|
|
|
0, |
2422
|
|
|
0, |
2423
|
|
|
$tz["dstendmonth"], |
2424
|
|
|
$tz["dstendday"], |
2425
|
|
|
$tz["dstendweek"], |
2426
|
|
|
$tz["dstendhour"], |
2427
|
|
|
$tz["dstendminute"], |
2428
|
|
|
$tz["dstendsecond"], |
2429
|
|
|
$tz["dstendmillis"], |
2430
|
|
|
0, |
2431
|
|
|
0, |
2432
|
|
|
$tz["dststartmonth"], |
2433
|
|
|
$tz["dststartday"], |
2434
|
|
|
$tz["dststartweek"], |
2435
|
|
|
$tz["dststarthour"], |
2436
|
|
|
$tz["dststartminute"], |
2437
|
|
|
$tz["dststartsecond"], |
2438
|
|
|
$tz["dststartmillis"] |
2439
|
|
|
); |
2440
|
|
|
} |
2441
|
|
|
|
2442
|
|
|
/** |
2443
|
|
|
* Checks the date to see if it is in DST, and returns correct GMT date accordingly. |
2444
|
|
|
* |
2445
|
|
|
* @param long $localtime |
2446
|
|
|
* @param array $tz |
2447
|
|
|
* |
2448
|
|
|
* @return long |
2449
|
|
|
*/ |
2450
|
|
|
private function getGMTTimeByTZ($localtime, $tz) { |
2451
|
|
|
if (!isset($tz) || !is_array($tz)) { |
2452
|
|
|
return $localtime; |
2453
|
|
|
} |
2454
|
|
|
|
2455
|
|
|
if ($this->isDST($localtime, $tz)) { |
2456
|
|
|
return $localtime + $tz["bias"] * 60 + $tz["dstbias"] * 60; |
|
|
|
|
2457
|
|
|
} |
2458
|
|
|
|
2459
|
|
|
return $localtime + $tz["bias"] * 60; |
|
|
|
|
2460
|
|
|
} |
2461
|
|
|
|
2462
|
|
|
/** |
2463
|
|
|
* Returns the local time for the given GMT time, taking account of the given timezone. |
2464
|
|
|
* |
2465
|
|
|
* @param long $gmttime |
2466
|
|
|
* @param array $tz |
2467
|
|
|
* |
2468
|
|
|
* @return long |
2469
|
|
|
*/ |
2470
|
|
|
private function getLocaltimeByTZ($gmttime, $tz) { |
2471
|
|
|
if (!isset($tz) || !is_array($tz)) { |
2472
|
|
|
return $gmttime; |
2473
|
|
|
} |
2474
|
|
|
|
2475
|
|
|
if ($this->isDST($gmttime - $tz["bias"] * 60, $tz)) { // may bug around the switch time because it may have to be 'gmttime - bias - dstbias' |
|
|
|
|
2476
|
|
|
return $gmttime - $tz["bias"] * 60 - $tz["dstbias"] * 60; |
|
|
|
|
2477
|
|
|
} |
2478
|
|
|
|
2479
|
|
|
return $gmttime - $tz["bias"] * 60; |
|
|
|
|
2480
|
|
|
} |
2481
|
|
|
|
2482
|
|
|
/** |
2483
|
|
|
* Returns TRUE if it is the summer and therefore DST is in effect. |
2484
|
|
|
* |
2485
|
|
|
* @param long $localtime |
2486
|
|
|
* @param array $tz |
2487
|
|
|
* |
2488
|
|
|
* @return bool |
2489
|
|
|
*/ |
2490
|
|
|
private function isDST($localtime, $tz) { |
2491
|
|
|
if (!isset($tz) || !is_array($tz) || |
2492
|
|
|
!isset($tz["dstbias"]) || $tz["dstbias"] == 0 || |
2493
|
|
|
!isset($tz["dststartmonth"]) || $tz["dststartmonth"] == 0 || |
2494
|
|
|
!isset($tz["dstendmonth"]) || $tz["dstendmonth"] == 0) { |
2495
|
|
|
return false; |
2496
|
|
|
} |
2497
|
|
|
|
2498
|
|
|
$year = gmdate("Y", $localtime); |
|
|
|
|
2499
|
|
|
$start = $this->getTimestampOfWeek($year, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststartday"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"]); |
|
|
|
|
2500
|
|
|
$end = $this->getTimestampOfWeek($year, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendday"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"]); |
2501
|
|
|
|
2502
|
|
|
if ($start < $end) { |
2503
|
|
|
// northern hemisphere (july = dst) |
2504
|
|
|
if ($localtime >= $start && $localtime < $end) { |
2505
|
|
|
$dst = true; |
2506
|
|
|
} |
2507
|
|
|
else { |
2508
|
|
|
$dst = false; |
2509
|
|
|
} |
2510
|
|
|
} |
2511
|
|
|
else { |
2512
|
|
|
// southern hemisphere (january = dst) |
2513
|
|
|
if ($localtime >= $end && $localtime < $start) { |
2514
|
|
|
$dst = false; |
2515
|
|
|
} |
2516
|
|
|
else { |
2517
|
|
|
$dst = true; |
2518
|
|
|
} |
2519
|
|
|
} |
2520
|
|
|
|
2521
|
|
|
return $dst; |
2522
|
|
|
} |
2523
|
|
|
|
2524
|
|
|
/** |
2525
|
|
|
* Returns the local timestamp for the $week'th $wday of $month in $year at $hour:$minute:$second. |
2526
|
|
|
* |
2527
|
|
|
* @param int $year |
2528
|
|
|
* @param int $month |
2529
|
|
|
* @param int $week |
2530
|
|
|
* @param int $wday |
2531
|
|
|
* @param int $hour |
2532
|
|
|
* @param int $minute |
2533
|
|
|
* @param int $second |
2534
|
|
|
* |
2535
|
|
|
* @return long |
2536
|
|
|
*/ |
2537
|
|
|
private function getTimestampOfWeek($year, $month, $week, $wday, $hour, $minute, $second) { |
2538
|
|
|
if ($month == 0) { |
2539
|
|
|
return; |
2540
|
|
|
} |
2541
|
|
|
|
2542
|
|
|
$date = gmmktime($hour, $minute, $second, $month, 1, $year); |
2543
|
|
|
|
2544
|
|
|
// Find first day in month which matches day of the week |
2545
|
|
|
while (1) { |
2546
|
|
|
$wdaynow = gmdate("w", $date); |
2547
|
|
|
if ($wdaynow == $wday) { |
2548
|
|
|
break; |
2549
|
|
|
} |
2550
|
|
|
$date += 24 * 60 * 60; |
2551
|
|
|
} |
2552
|
|
|
|
2553
|
|
|
// Forward $week weeks (may 'overflow' into the next month) |
2554
|
|
|
$date = $date + $week * (24 * 60 * 60 * 7); |
2555
|
|
|
|
2556
|
|
|
// Reverse 'overflow'. Eg week '10' will always be the last week of the month in which the |
2557
|
|
|
// specified weekday exists |
2558
|
|
|
while (1) { |
2559
|
|
|
$monthnow = gmdate("n", $date); // gmdate returns 1-12 |
2560
|
|
|
if ($monthnow > $month) { |
2561
|
|
|
$date -= (24 * 7 * 60 * 60); |
2562
|
|
|
} |
2563
|
|
|
else { |
2564
|
|
|
break; |
2565
|
|
|
} |
2566
|
|
|
} |
2567
|
|
|
|
2568
|
|
|
return $date; |
|
|
|
|
2569
|
|
|
} |
2570
|
|
|
|
2571
|
|
|
/** |
2572
|
|
|
* Normalize the given timestamp to the start of the day. |
2573
|
|
|
* |
2574
|
|
|
* @param long $timestamp |
2575
|
|
|
* |
2576
|
|
|
* @return long |
2577
|
|
|
*/ |
2578
|
|
|
private function getDayStartOfTimestamp($timestamp) { |
2579
|
|
|
return $timestamp - ($timestamp % (60 * 60 * 24)); |
|
|
|
|
2580
|
|
|
} |
2581
|
|
|
|
2582
|
|
|
/** |
2583
|
|
|
* Returns an SMTP address from an entry id. |
2584
|
|
|
* |
2585
|
|
|
* @param string $entryid |
2586
|
|
|
* |
2587
|
|
|
* @return string |
2588
|
|
|
*/ |
2589
|
|
|
private function getSMTPAddressFromEntryID($entryid) { |
2590
|
|
|
$addrbook = $this->getAddressbook(); |
2591
|
|
|
|
2592
|
|
|
$mailuser = mapi_ab_openentry($addrbook, $entryid); |
2593
|
|
|
if (!$mailuser) { |
2594
|
|
|
return ""; |
2595
|
|
|
} |
2596
|
|
|
|
2597
|
|
|
$props = mapi_getprops($mailuser, [PR_ADDRTYPE, PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]); |
|
|
|
|
2598
|
|
|
|
2599
|
|
|
$addrtype = isset($props[PR_ADDRTYPE]) ? $props[PR_ADDRTYPE] : ""; |
2600
|
|
|
|
2601
|
|
|
if (isset($props[PR_SMTP_ADDRESS])) { |
2602
|
|
|
return $props[PR_SMTP_ADDRESS]; |
2603
|
|
|
} |
2604
|
|
|
|
2605
|
|
|
if ($addrtype == "SMTP" && isset($props[PR_EMAIL_ADDRESS])) { |
2606
|
|
|
return $props[PR_EMAIL_ADDRESS]; |
2607
|
|
|
} |
2608
|
|
|
|
2609
|
|
|
return ""; |
2610
|
|
|
} |
2611
|
|
|
|
2612
|
|
|
/** |
2613
|
|
|
* Returns AB data from an entryid. |
2614
|
|
|
* |
2615
|
|
|
* @param string $entryid |
2616
|
|
|
* |
2617
|
|
|
* @return mixed |
2618
|
|
|
*/ |
2619
|
|
|
private function getAbPropsFromEntryID($entryid) { |
2620
|
|
|
$addrbook = $this->getAddressbook(); |
2621
|
|
|
$mailuser = mapi_ab_openentry($addrbook, $entryid); |
2622
|
|
|
if ($mailuser) { |
2623
|
|
|
return mapi_getprops($mailuser, [PR_DISPLAY_NAME, PR_ADDRTYPE, PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]); |
|
|
|
|
2624
|
|
|
} |
2625
|
|
|
|
2626
|
|
|
SLog::Write(LOGLEVEL_ERROR, sprintf("MAPIProvider->getAbPropsFromEntryID(): Unable to get mailuser (0x%X)", mapi_last_hresult())); |
2627
|
|
|
|
2628
|
|
|
return false; |
2629
|
|
|
} |
2630
|
|
|
|
2631
|
|
|
/** |
2632
|
|
|
* Builds a displayname from several separated values. |
2633
|
|
|
* |
2634
|
|
|
* @param SyncContact $contact |
2635
|
|
|
* |
2636
|
|
|
* @return string |
2637
|
|
|
*/ |
2638
|
|
|
private function composeDisplayName(&$contact) { |
2639
|
|
|
// Set display name and subject to a combined value of firstname and lastname |
2640
|
|
|
$cname = (isset($contact->prefix)) ? $contact->prefix . " " : ""; |
2641
|
|
|
$cname .= $contact->firstname; |
2642
|
|
|
$cname .= (isset($contact->middlename)) ? " " . $contact->middlename : ""; |
2643
|
|
|
$cname .= " " . $contact->lastname; |
2644
|
|
|
$cname .= (isset($contact->suffix)) ? " " . $contact->suffix : ""; |
2645
|
|
|
|
2646
|
|
|
return trim($cname); |
2647
|
|
|
} |
2648
|
|
|
|
2649
|
|
|
/** |
2650
|
|
|
* Sets all dependent properties for an email address. |
2651
|
|
|
* |
2652
|
|
|
* @param string $emailAddress |
2653
|
|
|
* @param string $displayName |
2654
|
|
|
* @param int $cnt |
2655
|
|
|
* @param array &$props |
2656
|
|
|
* @param array &$properties |
2657
|
|
|
* @param array &$nremails |
2658
|
|
|
* @param int &$abprovidertype |
2659
|
|
|
*/ |
2660
|
|
|
private function setEmailAddress($emailAddress, $displayName, $cnt, &$props, &$properties, &$nremails, &$abprovidertype) { |
2661
|
|
|
if (isset($emailAddress)) { |
2662
|
|
|
$name = (isset($displayName)) ? $displayName : $emailAddress; |
2663
|
|
|
|
2664
|
|
|
$props[$properties["emailaddress{$cnt}"]] = $emailAddress; |
2665
|
|
|
$props[$properties["emailaddressdemail{$cnt}"]] = $emailAddress; |
2666
|
|
|
$props[$properties["emailaddressdname{$cnt}"]] = $name; |
2667
|
|
|
$props[$properties["emailaddresstype{$cnt}"]] = "SMTP"; |
2668
|
|
|
$props[$properties["emailaddressentryid{$cnt}"]] = mapi_createoneoff($name, "SMTP", $emailAddress); |
2669
|
|
|
$nremails[] = $cnt - 1; |
2670
|
|
|
$abprovidertype |= 2 ^ ($cnt - 1); |
2671
|
|
|
} |
2672
|
|
|
} |
2673
|
|
|
|
2674
|
|
|
/** |
2675
|
|
|
* Sets the properties for an address string. |
2676
|
|
|
* |
2677
|
|
|
* @param string $type which address is being set |
2678
|
|
|
* @param string $city |
2679
|
|
|
* @param string $country |
2680
|
|
|
* @param string $postalcode |
2681
|
|
|
* @param string $state |
2682
|
|
|
* @param string $street |
2683
|
|
|
* @param array &$props |
2684
|
|
|
* @param array &$properties |
2685
|
|
|
*/ |
2686
|
|
|
private function setAddress($type, &$city, &$country, &$postalcode, &$state, &$street, &$props, &$properties) { |
2687
|
|
|
if (isset($city)) { |
2688
|
|
|
$props[$properties[$type . "city"]] = $city; |
2689
|
|
|
} |
2690
|
|
|
|
2691
|
|
|
if (isset($country)) { |
2692
|
|
|
$props[$properties[$type . "country"]] = $country; |
2693
|
|
|
} |
2694
|
|
|
|
2695
|
|
|
if (isset($postalcode)) { |
2696
|
|
|
$props[$properties[$type . "postalcode"]] = $postalcode; |
2697
|
|
|
} |
2698
|
|
|
|
2699
|
|
|
if (isset($state)) { |
2700
|
|
|
$props[$properties[$type . "state"]] = $state; |
2701
|
|
|
} |
2702
|
|
|
|
2703
|
|
|
if (isset($street)) { |
2704
|
|
|
$props[$properties[$type . "street"]] = $street; |
2705
|
|
|
} |
2706
|
|
|
|
2707
|
|
|
// set composed address |
2708
|
|
|
$address = Utils::BuildAddressString($street, $postalcode, $city, $state, $country); |
2709
|
|
|
if ($address) { |
2710
|
|
|
$props[$properties[$type . "address"]] = $address; |
2711
|
|
|
} |
2712
|
|
|
} |
2713
|
|
|
|
2714
|
|
|
/** |
2715
|
|
|
* Sets the properties for a mailing address. |
2716
|
|
|
* |
2717
|
|
|
* @param string $city |
2718
|
|
|
* @param string $country |
2719
|
|
|
* @param string $postalcode |
2720
|
|
|
* @param string $state |
2721
|
|
|
* @param string $street |
2722
|
|
|
* @param string $address |
2723
|
|
|
* @param array &$props |
2724
|
|
|
* @param array &$properties |
2725
|
|
|
*/ |
2726
|
|
|
private function setMailingAddress($city, $country, $postalcode, $state, $street, $address, &$props, &$properties) { |
2727
|
|
|
if (isset($city)) { |
2728
|
|
|
$props[$properties["city"]] = $city; |
2729
|
|
|
} |
2730
|
|
|
if (isset($country)) { |
2731
|
|
|
$props[$properties["country"]] = $country; |
2732
|
|
|
} |
2733
|
|
|
if (isset($postalcode)) { |
2734
|
|
|
$props[$properties["postalcode"]] = $postalcode; |
2735
|
|
|
} |
2736
|
|
|
if (isset($state)) { |
2737
|
|
|
$props[$properties["state"]] = $state; |
2738
|
|
|
} |
2739
|
|
|
if (isset($street)) { |
2740
|
|
|
$props[$properties["street"]] = $street; |
2741
|
|
|
} |
2742
|
|
|
if (isset($address)) { |
2743
|
|
|
$props[$properties["postaladdress"]] = $address; |
2744
|
|
|
} |
2745
|
|
|
} |
2746
|
|
|
|
2747
|
|
|
/** |
2748
|
|
|
* Sets data in a recurrence array. |
2749
|
|
|
* |
2750
|
|
|
* @param SyncObject $message |
2751
|
|
|
* @param array &$recur |
2752
|
|
|
*/ |
2753
|
|
|
private function setRecurrence($message, &$recur) { |
2754
|
|
|
if (isset($message->complete)) { |
2755
|
|
|
$recur["complete"] = $message->complete; |
2756
|
|
|
} |
2757
|
|
|
|
2758
|
|
|
if (!isset($message->recurrence->interval)) { |
2759
|
|
|
$message->recurrence->interval = 1; |
2760
|
|
|
} |
2761
|
|
|
|
2762
|
|
|
// set the default value of numoccur |
2763
|
|
|
$recur["numoccur"] = 0; |
2764
|
|
|
// a place holder for recurrencetype property |
2765
|
|
|
$recur["recurrencetype"] = 0; |
2766
|
|
|
|
2767
|
|
|
switch ($message->recurrence->type) { |
2768
|
|
|
case 0: |
2769
|
|
|
$recur["type"] = 10; |
2770
|
|
|
if (isset($message->recurrence->dayofweek)) { |
2771
|
|
|
$recur["subtype"] = 1; |
2772
|
|
|
} |
2773
|
|
|
else { |
2774
|
|
|
$recur["subtype"] = 0; |
2775
|
|
|
} |
2776
|
|
|
|
2777
|
|
|
$recur["everyn"] = $message->recurrence->interval * (60 * 24); |
2778
|
|
|
$recur["recurrencetype"] = 1; |
2779
|
|
|
break; |
2780
|
|
|
|
2781
|
|
|
case 1: |
2782
|
|
|
$recur["type"] = 11; |
2783
|
|
|
$recur["subtype"] = 1; |
2784
|
|
|
$recur["everyn"] = $message->recurrence->interval; |
2785
|
|
|
$recur["recurrencetype"] = 2; |
2786
|
|
|
break; |
2787
|
|
|
|
2788
|
|
|
case 2: |
2789
|
|
|
$recur["type"] = 12; |
2790
|
|
|
$recur["subtype"] = 2; |
2791
|
|
|
$recur["everyn"] = $message->recurrence->interval; |
2792
|
|
|
$recur["recurrencetype"] = 3; |
2793
|
|
|
break; |
2794
|
|
|
|
2795
|
|
|
case 3: |
2796
|
|
|
$recur["type"] = 12; |
2797
|
|
|
$recur["subtype"] = 3; |
2798
|
|
|
$recur["everyn"] = $message->recurrence->interval; |
2799
|
|
|
$recur["recurrencetype"] = 3; |
2800
|
|
|
break; |
2801
|
|
|
|
2802
|
|
|
case 4: |
2803
|
|
|
$recur["type"] = 13; |
2804
|
|
|
$recur["subtype"] = 1; |
2805
|
|
|
$recur["everyn"] = $message->recurrence->interval * 12; |
2806
|
|
|
$recur["recurrencetype"] = 4; |
2807
|
|
|
break; |
2808
|
|
|
|
2809
|
|
|
case 5: |
2810
|
|
|
$recur["type"] = 13; |
2811
|
|
|
$recur["subtype"] = 2; |
2812
|
|
|
$recur["everyn"] = $message->recurrence->interval * 12; |
2813
|
|
|
$recur["recurrencetype"] = 4; |
2814
|
|
|
break; |
2815
|
|
|
|
2816
|
|
|
case 6: |
2817
|
|
|
$recur["type"] = 13; |
2818
|
|
|
$recur["subtype"] = 3; |
2819
|
|
|
$recur["everyn"] = $message->recurrence->interval * 12; |
2820
|
|
|
$recur["recurrencetype"] = 4; |
2821
|
|
|
break; |
2822
|
|
|
} |
2823
|
|
|
|
2824
|
|
|
// "start" and "end" are in GMT when passing to class.recurrence |
2825
|
|
|
$recur["end"] = $this->getDayStartOfTimestamp(0x7FFFFFFF); // Maximum GMT value for end by default |
|
|
|
|
2826
|
|
|
|
2827
|
|
|
if (isset($message->recurrence->until)) { |
2828
|
|
|
$recur["term"] = 0x21; |
2829
|
|
|
$recur["end"] = $message->recurrence->until; |
2830
|
|
|
} |
2831
|
|
|
elseif (isset($message->recurrence->occurrences)) { |
2832
|
|
|
$recur["term"] = 0x22; |
2833
|
|
|
$recur["numoccur"] = $message->recurrence->occurrences; |
2834
|
|
|
} |
2835
|
|
|
else { |
2836
|
|
|
$recur["term"] = 0x23; |
2837
|
|
|
} |
2838
|
|
|
|
2839
|
|
|
if (isset($message->recurrence->dayofweek)) { |
2840
|
|
|
$recur["weekdays"] = $message->recurrence->dayofweek; |
2841
|
|
|
} |
2842
|
|
|
if (isset($message->recurrence->weekofmonth)) { |
2843
|
|
|
$recur["nday"] = $message->recurrence->weekofmonth; |
2844
|
|
|
} |
2845
|
|
|
if (isset($message->recurrence->monthofyear)) { |
2846
|
|
|
// MAPI stores months as the amount of minutes until the beginning of the month in a |
2847
|
|
|
// non-leapyear. Why this is, is totally unclear. |
2848
|
|
|
$monthminutes = [0, 44640, 84960, 129600, 172800, 217440, 260640, 305280, 348480, 393120, 437760, 480960]; |
2849
|
|
|
$recur["month"] = $monthminutes[$message->recurrence->monthofyear - 1]; |
2850
|
|
|
} |
2851
|
|
|
if (isset($message->recurrence->dayofmonth)) { |
2852
|
|
|
$recur["monthday"] = $message->recurrence->dayofmonth; |
2853
|
|
|
} |
2854
|
|
|
} |
2855
|
|
|
|
2856
|
|
|
/** |
2857
|
|
|
* Extracts the email address (mailbox@host) from an email address because |
2858
|
|
|
* some devices send email address as "Firstname Lastname" <[email protected]>. |
2859
|
|
|
* |
2860
|
|
|
* @see http://developer.berlios.de/mantis/view.php?id=486 |
2861
|
|
|
* |
2862
|
|
|
* @param string $email |
2863
|
|
|
* |
2864
|
|
|
* @return string or false on error |
2865
|
|
|
*/ |
2866
|
|
|
private function extractEmailAddress($email) { |
2867
|
|
|
if (!isset($this->zRFC822)) { |
2868
|
|
|
$this->zRFC822 = new Mail_RFC822(); |
2869
|
|
|
} |
2870
|
|
|
$parsedAddress = $this->zRFC822->parseAddressList($email); |
2871
|
|
|
if (!isset($parsedAddress[0]->mailbox) || !isset($parsedAddress[0]->host)) { |
2872
|
|
|
return false; |
|
|
|
|
2873
|
|
|
} |
2874
|
|
|
|
2875
|
|
|
return $parsedAddress[0]->mailbox . '@' . $parsedAddress[0]->host; |
2876
|
|
|
} |
2877
|
|
|
|
2878
|
|
|
/** |
2879
|
|
|
* Returns the message body for a required format. |
2880
|
|
|
* |
2881
|
|
|
* @param MAPIMessage $mapimessage |
|
|
|
|
2882
|
|
|
* @param int $bpReturnType |
2883
|
|
|
* @param SyncObject $message |
2884
|
|
|
* |
2885
|
|
|
* @return bool |
2886
|
|
|
*/ |
2887
|
|
|
private function setMessageBodyForType($mapimessage, $bpReturnType, &$message) { |
2888
|
|
|
$truncateHtmlSafe = false; |
2889
|
|
|
// default value is PR_BODY |
2890
|
|
|
$property = PR_BODY; |
|
|
|
|
2891
|
|
|
|
2892
|
|
|
switch ($bpReturnType) { |
2893
|
|
|
case SYNC_BODYPREFERENCE_HTML: |
2894
|
|
|
$property = PR_HTML; |
|
|
|
|
2895
|
|
|
$truncateHtmlSafe = true; |
2896
|
|
|
break; |
2897
|
|
|
|
2898
|
|
|
case SYNC_BODYPREFERENCE_MIME: |
2899
|
|
|
$stat = $this->imtoinet($mapimessage, $message); |
2900
|
|
|
if (isset($message->asbody)) { |
2901
|
|
|
$message->asbody->type = $bpReturnType; |
2902
|
|
|
} |
2903
|
|
|
|
2904
|
|
|
return $stat; |
2905
|
|
|
} |
2906
|
|
|
|
2907
|
|
|
$stream = mapi_openproperty($mapimessage, $property, IID_IStream, 0, 0); |
|
|
|
|
2908
|
|
|
if ($stream) { |
2909
|
|
|
$stat = mapi_stream_stat($stream); |
2910
|
|
|
$streamsize = $stat['cb']; |
2911
|
|
|
} |
2912
|
|
|
else { |
2913
|
|
|
$streamsize = 0; |
2914
|
|
|
} |
2915
|
|
|
|
2916
|
|
|
// set the properties according to supported AS version |
2917
|
|
|
if (Request::GetProtocolVersion() >= 12.0) { |
2918
|
|
|
$message->asbody = new SyncBaseBody(); |
2919
|
|
|
$message->asbody->type = $bpReturnType; |
2920
|
|
|
if (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) { |
2921
|
|
|
// if PR_HTML is UTF-8 we can stream it directly, else we have to convert to UTF-8 & wrap it |
2922
|
|
|
if ($message->internetcpid == INTERNET_CPID_UTF8) { |
2923
|
|
|
$message->asbody->data = MAPIStreamWrapper::Open($stream, $truncateHtmlSafe); |
2924
|
|
|
} |
2925
|
|
|
else { |
2926
|
|
|
$body = $this->mapiReadStream($stream, $streamsize); |
2927
|
|
|
$message->asbody->data = StringStreamWrapper::Open(Utils::ConvertCodepageStringToUtf8($message->internetcpid, $body), $truncateHtmlSafe); |
2928
|
|
|
$message->internetcpid = INTERNET_CPID_UTF8; |
2929
|
|
|
} |
2930
|
|
|
} |
2931
|
|
|
else { |
2932
|
|
|
$message->asbody->data = MAPIStreamWrapper::Open($stream); |
2933
|
|
|
} |
2934
|
|
|
$message->asbody->estimatedDataSize = $streamsize; |
2935
|
|
|
} |
2936
|
|
|
else { |
2937
|
|
|
$body = $this->mapiReadStream($stream, $streamsize); |
2938
|
|
|
$message->body = str_replace("\n", "\r\n", str_replace("\r", "", $body)); |
2939
|
|
|
$message->bodysize = $streamsize; |
2940
|
|
|
$message->bodytruncated = 0; |
2941
|
|
|
} |
2942
|
|
|
|
2943
|
|
|
return true; |
2944
|
|
|
} |
2945
|
|
|
|
2946
|
|
|
/** |
2947
|
|
|
* Reads from a mapi stream, if it's set. If not, returns an empty string. |
2948
|
|
|
* |
2949
|
|
|
* @param resource $stream |
2950
|
|
|
* @param int $size |
2951
|
|
|
* |
2952
|
|
|
* @return string |
2953
|
|
|
*/ |
2954
|
|
|
private function mapiReadStream($stream, $size) { |
2955
|
|
|
if (!$stream || $size == 0) { |
|
|
|
|
2956
|
|
|
return ""; |
2957
|
|
|
} |
2958
|
|
|
|
2959
|
|
|
return mapi_stream_read($stream, $size); |
2960
|
|
|
} |
2961
|
|
|
|
2962
|
|
|
/** |
2963
|
|
|
* Build a filereference key by the clientid. |
2964
|
|
|
* |
2965
|
|
|
* @param MAPIMessage $mapimessage |
2966
|
|
|
* @param mixed $clientid |
2967
|
|
|
* @param mixed $entryid |
2968
|
|
|
* @param mixed $parentSourcekey |
2969
|
|
|
* @param mixed $exceptionBasedate |
2970
|
|
|
* |
2971
|
|
|
* @return string/bool |
|
|
|
|
2972
|
|
|
*/ |
2973
|
|
|
private function getFileReferenceForClientId($mapimessage, $clientid, $entryid = 0, $parentSourcekey = 0, $exceptionBasedate = 0) { |
2974
|
|
|
if (!$entryid || !$parentSourcekey) { |
2975
|
|
|
$props = mapi_getprops($mapimessage, [PR_ENTRYID, PR_PARENT_SOURCE_KEY]); |
|
|
|
|
2976
|
|
|
if (!$entryid && isset($props[PR_ENTRYID])) { |
2977
|
|
|
$entryid = bin2hex($props[PR_ENTRYID]); |
2978
|
|
|
} |
2979
|
|
|
if (!$parentSourcekey && isset($props[PR_PARENT_SOURCE_KEY])) { |
2980
|
|
|
$parentSourcekey = bin2hex($props[PR_PARENT_SOURCE_KEY]); |
2981
|
|
|
} |
2982
|
|
|
} |
2983
|
|
|
|
2984
|
|
|
$attachtable = mapi_message_getattachmenttable($mapimessage); |
2985
|
|
|
$rows = mapi_table_queryallrows($attachtable, [PR_EC_WA_ATTACHMENT_ID, PR_ATTACH_NUM]); |
|
|
|
|
2986
|
|
|
foreach ($rows as $row) { |
2987
|
|
|
if ($row[PR_EC_WA_ATTACHMENT_ID] == $clientid) { |
2988
|
|
|
return sprintf("%s:%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey, $exceptionBasedate); |
2989
|
|
|
} |
2990
|
|
|
} |
2991
|
|
|
|
2992
|
|
|
return false; |
2993
|
|
|
} |
2994
|
|
|
|
2995
|
|
|
/** |
2996
|
|
|
* A wrapper for mapi_inetmapi_imtoinet function. |
2997
|
|
|
* |
2998
|
|
|
* @param MAPIMessage $mapimessage |
2999
|
|
|
* @param SyncObject $message |
3000
|
|
|
* |
3001
|
|
|
* @return bool |
3002
|
|
|
*/ |
3003
|
|
|
private function imtoinet($mapimessage, &$message) { |
3004
|
|
|
$addrbook = $this->getAddressbook(); |
3005
|
|
|
$stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $mapimessage, ['use_tnef' => -1, 'ignore_missing_attachments' => 1]); |
3006
|
|
|
// is_resource($stream) returns false in PHP8 |
3007
|
|
|
if ($stream !== null && mapi_last_hresult() === ecSuccess) { |
|
|
|
|
3008
|
|
|
$mstreamstat = mapi_stream_stat($stream); |
3009
|
|
|
$streamsize = $mstreamstat["cb"]; |
3010
|
|
|
if (isset($streamsize)) { |
3011
|
|
|
if (Request::GetProtocolVersion() >= 12.0) { |
3012
|
|
|
if (!isset($message->asbody)) { |
3013
|
|
|
$message->asbody = new SyncBaseBody(); |
3014
|
|
|
} |
3015
|
|
|
$message->asbody->data = MAPIStreamWrapper::Open($stream); |
3016
|
|
|
$message->asbody->estimatedDataSize = $streamsize; |
3017
|
|
|
$message->asbody->truncated = 0; |
3018
|
|
|
} |
3019
|
|
|
else { |
3020
|
|
|
$message->mimedata = MAPIStreamWrapper::Open($stream); |
3021
|
|
|
$message->mimesize = $streamsize; |
3022
|
|
|
$message->mimetruncated = 0; |
3023
|
|
|
} |
3024
|
|
|
unset($message->body, $message->bodytruncated); |
3025
|
|
|
|
3026
|
|
|
return true; |
3027
|
|
|
} |
3028
|
|
|
} |
3029
|
|
|
SLog::Write(LOGLEVEL_ERROR, sprintf("MAPIProvider->imtoinet(): got no stream or content from mapi_inetmapi_imtoinet(): 0x%08X", mapi_last_hresult())); |
3030
|
|
|
|
3031
|
|
|
return false; |
3032
|
|
|
} |
3033
|
|
|
|
3034
|
|
|
/** |
3035
|
|
|
* Sets the message body. |
3036
|
|
|
* |
3037
|
|
|
* @param MAPIMessage $mapimessage |
3038
|
|
|
* @param ContentParameters $contentparameters |
3039
|
|
|
* @param SyncObject $message |
3040
|
|
|
*/ |
3041
|
|
|
private function setMessageBody($mapimessage, $contentparameters, &$message) { |
3042
|
|
|
// get the available body preference types |
3043
|
|
|
$bpTypes = $contentparameters->GetBodyPreference(); |
3044
|
|
|
if ($bpTypes !== false) { |
3045
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("BodyPreference types: %s", implode(', ', $bpTypes))); |
3046
|
|
|
// do not send mime data if the client requests it |
3047
|
|
|
if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) && ($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) { |
3048
|
|
|
unset($bpTypes[$key]); |
3049
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("Remove mime body preference type because the device required no mime support. BodyPreference types: %s", implode(', ', $bpTypes))); |
3050
|
|
|
} |
3051
|
|
|
// get the best fitting preference type |
3052
|
|
|
$bpReturnType = Utils::GetBodyPreferenceBestMatch($bpTypes); |
3053
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("GetBodyPreferenceBestMatch: %d", $bpReturnType)); |
3054
|
|
|
$bpo = $contentparameters->BodyPreference($bpReturnType); |
3055
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("bpo: truncation size:'%d', allornone:'%d', preview:'%d'", $bpo->GetTruncationSize(), $bpo->GetAllOrNone(), $bpo->GetPreview())); |
3056
|
|
|
|
3057
|
|
|
// Android Blackberry expects a full mime message for signed emails |
3058
|
|
|
// @TODO change this when refactoring |
3059
|
|
|
$props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]); |
|
|
|
|
3060
|
|
|
if (isset($props[PR_MESSAGE_CLASS]) && |
3061
|
|
|
stripos($props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME.MultipartSigned') !== false && |
3062
|
|
|
($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) { |
|
|
|
|
3063
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setMessageBody(): enforcing SYNC_BODYPREFERENCE_MIME type for a signed message")); |
3064
|
|
|
$bpReturnType = SYNC_BODYPREFERENCE_MIME; |
3065
|
|
|
} |
3066
|
|
|
|
3067
|
|
|
$this->setMessageBodyForType($mapimessage, $bpReturnType, $message); |
3068
|
|
|
// only set the truncation size data if device set it in request |
3069
|
|
|
if ($bpo->GetTruncationSize() != false && |
3070
|
|
|
$bpReturnType != SYNC_BODYPREFERENCE_MIME && |
3071
|
|
|
$message->asbody->estimatedDataSize > $bpo->GetTruncationSize() |
3072
|
|
|
) { |
3073
|
|
|
// Truncated plaintext requests are used on iOS for the preview in the email list. All images and links should be removed. |
3074
|
|
|
if ($bpReturnType == SYNC_BODYPREFERENCE_PLAIN) { |
3075
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setMessageBody(): truncated plain-text body requested, stripping all links and images"); |
3076
|
|
|
// Get more data because of the filtering it's most probably going down in size. It's going to be truncated to the correct size below. |
3077
|
|
|
$plainbody = stream_get_contents($message->asbody->data, $bpo->GetTruncationSize() * 5); |
3078
|
|
|
$message->asbody->data = StringStreamWrapper::Open(preg_replace('/<http(s){0,1}:\/\/.*?>/i', '', $plainbody)); |
3079
|
|
|
} |
3080
|
|
|
|
3081
|
|
|
// truncate data stream |
3082
|
|
|
ftruncate($message->asbody->data, $bpo->GetTruncationSize()); |
3083
|
|
|
$message->asbody->truncated = 1; |
3084
|
|
|
} |
3085
|
|
|
// set the preview or windows phones won't show the preview of an email |
3086
|
|
|
if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) { |
3087
|
|
|
$message->asbody->preview = Utils::Utf8_truncate(MAPIUtils::readPropStream($mapimessage, PR_BODY), $bpo->GetPreview()); |
|
|
|
|
3088
|
|
|
} |
3089
|
|
|
} |
3090
|
|
|
else { |
3091
|
|
|
// Override 'body' for truncation |
3092
|
|
|
$truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); |
|
|
|
|
3093
|
|
|
$this->setMessageBodyForType($mapimessage, SYNC_BODYPREFERENCE_PLAIN, $message); |
3094
|
|
|
|
3095
|
|
|
if ($message->bodysize > $truncsize) { |
3096
|
|
|
$message->body = Utils::Utf8_truncate($message->body, $truncsize); |
3097
|
|
|
$message->bodytruncated = 1; |
3098
|
|
|
} |
3099
|
|
|
|
3100
|
|
|
if (!isset($message->body) || strlen($message->body) == 0) { |
3101
|
|
|
$message->body = " "; |
3102
|
|
|
} |
3103
|
|
|
|
3104
|
|
|
if ($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_ALWAYS) { |
3105
|
|
|
// set the html body for iphone in AS 2.5 version |
3106
|
|
|
$this->imtoinet($mapimessage, $message); |
3107
|
|
|
} |
3108
|
|
|
} |
3109
|
|
|
} |
3110
|
|
|
|
3111
|
|
|
/** |
3112
|
|
|
* Sets properties for an email message. |
3113
|
|
|
* |
3114
|
|
|
* @param mixed $mapimessage |
3115
|
|
|
* @param SyncMail $message |
3116
|
|
|
*/ |
3117
|
|
|
private function setFlag($mapimessage, &$message) { |
3118
|
|
|
// do nothing if protocol version is lower than 12.0 as flags haven't been defined before |
3119
|
|
|
if (Request::GetProtocolVersion() < 12.0) { |
3120
|
|
|
return; |
3121
|
|
|
} |
3122
|
|
|
|
3123
|
|
|
$message->flag = new SyncMailFlags(); |
3124
|
|
|
|
3125
|
|
|
$this->getPropsFromMAPI($message->flag, $mapimessage, MAPIMapping::GetMailFlagsMapping()); |
3126
|
|
|
} |
3127
|
|
|
|
3128
|
|
|
/** |
3129
|
|
|
* Sets information from SyncBaseBody type for a MAPI message. |
3130
|
|
|
* |
3131
|
|
|
* @param SyncBaseBody $asbody |
3132
|
|
|
* @param array $props |
3133
|
|
|
* @param array $appointmentprops |
3134
|
|
|
*/ |
3135
|
|
|
private function setASbody($asbody, &$props, $appointmentprops) { |
3136
|
|
|
// TODO: fix checking for the length |
3137
|
|
|
if (isset($asbody->type, $asbody->data) /* && strlen($asbody->data) > 0 */) { |
3138
|
|
|
switch ($asbody->type) { |
3139
|
|
|
case SYNC_BODYPREFERENCE_PLAIN: |
3140
|
|
|
default: |
3141
|
|
|
// set plain body if the type is not in valid range |
3142
|
|
|
$props[$appointmentprops["body"]] = stream_get_contents($asbody->data); |
3143
|
|
|
break; |
3144
|
|
|
|
3145
|
|
|
case SYNC_BODYPREFERENCE_HTML: |
3146
|
|
|
$props[$appointmentprops["html"]] = stream_get_contents($asbody->data); |
3147
|
|
|
break; |
3148
|
|
|
|
3149
|
|
|
case SYNC_BODYPREFERENCE_MIME: |
3150
|
|
|
break; |
3151
|
|
|
} |
3152
|
|
|
} |
3153
|
|
|
else { |
3154
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setASbody either type or data are not set. Setting to empty body"); |
3155
|
|
|
$props[$appointmentprops["body"]] = ""; |
3156
|
|
|
} |
3157
|
|
|
} |
3158
|
|
|
|
3159
|
|
|
/** |
3160
|
|
|
* Sets attachments from an email message to a SyncObject. |
3161
|
|
|
* |
3162
|
|
|
* @param mixed $mapimessage |
3163
|
|
|
* @param SyncObject $message |
3164
|
|
|
* @param string $entryid |
3165
|
|
|
* @param string $parentSourcekey |
3166
|
|
|
* @param mixed $exceptionBasedate |
3167
|
|
|
*/ |
3168
|
|
|
private function setAttachment($mapimessage, &$message, $entryid, $parentSourcekey, $exceptionBasedate = 0) { |
3169
|
|
|
// Add attachments |
3170
|
|
|
$attachtable = mapi_message_getattachmenttable($mapimessage); |
3171
|
|
|
$rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]); |
|
|
|
|
3172
|
|
|
|
3173
|
|
|
foreach ($rows as $row) { |
3174
|
|
|
if (isset($row[PR_ATTACH_NUM])) { |
3175
|
|
|
if (Request::GetProtocolVersion() >= 12.0) { |
3176
|
|
|
$attach = new SyncBaseAttachment(); |
3177
|
|
|
} |
3178
|
|
|
else { |
3179
|
|
|
$attach = new SyncAttachment(); |
3180
|
|
|
} |
3181
|
|
|
|
3182
|
|
|
$mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); |
3183
|
|
|
$attachprops = mapi_getprops($mapiattach, [PR_ATTACH_LONG_FILENAME, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_ID_A, PR_ATTACH_MIME_TAG, PR_ATTACH_METHOD, PR_DISPLAY_NAME, PR_ATTACH_SIZE, PR_ATTACH_FLAGS]); |
|
|
|
|
3184
|
|
|
if (isset($attachprops[PR_ATTACH_MIME_TAG]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG]), 'signed') !== false) { |
3185
|
|
|
continue; |
3186
|
|
|
} |
3187
|
|
|
|
3188
|
|
|
// the displayname is handled equally for all AS versions |
3189
|
|
|
$attach->displayname = (isset($attachprops[PR_ATTACH_LONG_FILENAME])) ? $attachprops[PR_ATTACH_LONG_FILENAME] : ((isset($attachprops[PR_ATTACH_FILENAME])) ? $attachprops[PR_ATTACH_FILENAME] : ((isset($attachprops[PR_DISPLAY_NAME])) ? $attachprops[PR_DISPLAY_NAME] : "attachment.bin")); |
3190
|
|
|
// fix attachment name in case of inline images |
3191
|
|
|
if (($attach->displayname == "inline.txt" && isset($attachprops[PR_ATTACH_MIME_TAG])) || |
3192
|
|
|
(substr_compare($attach->displayname, "attachment", 0, 10, true) === 0 && substr_compare($attach->displayname, ".dat", -4, 4, true) === 0)) { |
3193
|
|
|
$mimetype = $attachprops[PR_ATTACH_MIME_TAG] ?? 'application/octet-stream'; |
3194
|
|
|
$mime = explode("/", $mimetype); |
3195
|
|
|
|
3196
|
|
|
if (count($mime) == 2 && $mime[0] == "image") { |
3197
|
|
|
$attach->displayname = "inline." . $mime[1]; |
3198
|
|
|
} |
3199
|
|
|
} |
3200
|
|
|
|
3201
|
|
|
// set AS version specific parameters |
3202
|
|
|
if (Request::GetProtocolVersion() >= 12.0) { |
3203
|
|
|
$attach->filereference = sprintf("%s:%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey, $exceptionBasedate); |
|
|
|
|
3204
|
|
|
$attach->method = (isset($attachprops[PR_ATTACH_METHOD])) ? $attachprops[PR_ATTACH_METHOD] : ATTACH_BY_VALUE; |
|
|
|
|
3205
|
|
|
|
3206
|
|
|
// if displayname does not have the eml extension for embedde messages, android and WP devices won't open it |
3207
|
|
|
if ($attach->method == ATTACH_EMBEDDED_MSG) { |
|
|
|
|
3208
|
|
|
if (strtolower(substr($attach->displayname, -4)) != '.eml') { |
3209
|
|
|
$attach->displayname .= '.eml'; |
3210
|
|
|
} |
3211
|
|
|
} |
3212
|
|
|
// android devices require attachment size in order to display an attachment properly |
3213
|
|
|
if (!isset($attachprops[PR_ATTACH_SIZE])) { |
3214
|
|
|
$stream = mapi_openproperty($mapiattach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); |
|
|
|
|
3215
|
|
|
// It's not possible to open some (embedded only?) messages, so we need to open the attachment object itself to get the data |
3216
|
|
|
if (mapi_last_hresult()) { |
3217
|
|
|
$embMessage = mapi_attach_openobj($mapiattach); |
3218
|
|
|
$addrbook = $this->getAddressbook(); |
3219
|
|
|
$stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]); |
3220
|
|
|
} |
3221
|
|
|
$stat = mapi_stream_stat($stream); |
3222
|
|
|
$attach->estimatedDataSize = $stat['cb']; |
|
|
|
|
3223
|
|
|
} |
3224
|
|
|
else { |
3225
|
|
|
$attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE]; |
3226
|
|
|
} |
3227
|
|
|
|
3228
|
|
|
if (isset($attachprops[PR_ATTACH_CONTENT_ID]) && $attachprops[PR_ATTACH_CONTENT_ID]) { |
3229
|
|
|
$attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID]; |
|
|
|
|
3230
|
|
|
} |
3231
|
|
|
|
3232
|
|
|
if (!isset($attach->contentid) && isset($attachprops[PR_ATTACH_CONTENT_ID_A]) && $attachprops[PR_ATTACH_CONTENT_ID_A]) { |
3233
|
|
|
$attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID_A]; |
3234
|
|
|
} |
3235
|
|
|
|
3236
|
|
|
if (isset($attachprops[PR_ATTACHMENT_HIDDEN]) && $attachprops[PR_ATTACHMENT_HIDDEN]) { |
3237
|
|
|
$attach->isinline = 1; |
|
|
|
|
3238
|
|
|
} |
3239
|
|
|
|
3240
|
|
|
if (isset($attach->contentid, $attachprops[PR_ATTACH_FLAGS]) && $attachprops[PR_ATTACH_FLAGS] & 4) { |
3241
|
|
|
$attach->isinline = 1; |
3242
|
|
|
} |
3243
|
|
|
|
3244
|
|
|
if (!isset($message->asattachments)) { |
3245
|
|
|
$message->asattachments = []; |
3246
|
|
|
} |
3247
|
|
|
|
3248
|
|
|
array_push($message->asattachments, $attach); |
3249
|
|
|
} |
3250
|
|
|
else { |
3251
|
|
|
$attach->attsize = $attachprops[PR_ATTACH_SIZE]; |
|
|
|
|
3252
|
|
|
$attach->attname = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey); |
|
|
|
|
3253
|
|
|
if (!isset($message->attachments)) { |
3254
|
|
|
$message->attachments = []; |
3255
|
|
|
} |
3256
|
|
|
|
3257
|
|
|
array_push($message->attachments, $attach); |
3258
|
|
|
} |
3259
|
|
|
} |
3260
|
|
|
} |
3261
|
|
|
} |
3262
|
|
|
|
3263
|
|
|
/** |
3264
|
|
|
* Update attachments of a mapimessage based on asattachments received. |
3265
|
|
|
* |
3266
|
|
|
* @param MAPIMessage $mapimessage |
3267
|
|
|
* @param array $asattachments |
3268
|
|
|
* @param SyncObject $response |
3269
|
|
|
*/ |
3270
|
|
|
public function editAttachments($mapimessage, $asattachments, &$response) { |
3271
|
|
|
foreach ($asattachments as $att) { |
3272
|
|
|
// new attachment to be saved |
3273
|
|
|
if ($att instanceof SyncBaseAttachmentAdd) { |
3274
|
|
|
if (!isset($att->content)) { |
3275
|
|
|
SLog::Write(LOGLEVEL_WARN, sprintf("MAPIProvider->editAttachments(): Ignoring attachment %s to be added as it has no content: %s", $att->clientid, $att->displayname)); |
|
|
|
|
3276
|
|
|
|
3277
|
|
|
continue; |
3278
|
|
|
} |
3279
|
|
|
|
3280
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->editAttachments(): Saving attachment %s with name: %s", $att->clientid, $att->displayname)); |
3281
|
|
|
// only create if the attachment does not already exist |
3282
|
|
|
if ($this->getFileReferenceForClientId($mapimessage, $att->clientid, 0, 0) === false) { |
3283
|
|
|
// TODO: check: contentlocation |
3284
|
|
|
$props = [ |
3285
|
|
|
PR_ATTACH_LONG_FILENAME => $att->displayname, |
|
|
|
|
3286
|
|
|
PR_DISPLAY_NAME => $att->displayname, |
|
|
|
|
3287
|
|
|
PR_ATTACH_METHOD => $att->method, // is this correct ?? |
|
|
|
|
3288
|
|
|
PR_ATTACH_DATA_BIN => "", |
|
|
|
|
3289
|
|
|
PR_ATTACHMENT_HIDDEN => false, |
|
|
|
|
3290
|
|
|
PR_ATTACH_EXTENSION => pathinfo($att->displayname, PATHINFO_EXTENSION), |
|
|
|
|
3291
|
|
|
PR_EC_WA_ATTACHMENT_ID => $att->clientid, |
|
|
|
|
3292
|
|
|
]; |
3293
|
|
|
if (!empty($att->contenttype)) { |
|
|
|
|
3294
|
|
|
$props[PR_ATTACH_MIME_TAG] = $att->contenttype; |
|
|
|
|
3295
|
|
|
} |
3296
|
|
|
if (!empty($att->contentid)) { |
3297
|
|
|
$props[PR_ATTACH_CONTENT_ID] = $att->contentid; |
|
|
|
|
3298
|
|
|
} |
3299
|
|
|
|
3300
|
|
|
$attachment = mapi_message_createattach($mapimessage); |
3301
|
|
|
mapi_setprops($attachment, $props); |
3302
|
|
|
|
3303
|
|
|
// Stream the file to the PR_ATTACH_DATA_BIN property |
3304
|
|
|
$stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
|
|
|
|
3305
|
|
|
mapi_stream_write($stream, stream_get_contents($att->content)); |
3306
|
|
|
|
3307
|
|
|
// Commit the stream and save changes |
3308
|
|
|
mapi_stream_commit($stream); |
3309
|
|
|
mapi_savechanges($attachment); |
3310
|
|
|
} |
3311
|
|
|
if (!isset($response->asattachments)) { |
3312
|
|
|
$response->asattachments = []; |
3313
|
|
|
} |
3314
|
|
|
// respond linking the clientid with the newly created filereference |
3315
|
|
|
$attResp = new SyncBaseAttachment(); |
3316
|
|
|
$attResp->clientid = $att->clientid; |
|
|
|
|
3317
|
|
|
$attResp->filereference = $this->getFileReferenceForClientId($mapimessage, $att->clientid, 0, 0); |
3318
|
|
|
$response->asattachments[] = $attResp; |
3319
|
|
|
$response->hasResponse = true; |
|
|
|
|
3320
|
|
|
} |
3321
|
|
|
// attachment to be removed |
3322
|
|
|
elseif ($att instanceof SyncBaseAttachmentDelete) { |
3323
|
|
|
list($id, $attachnum, $parentEntryid, $exceptionBasedate) = explode(":", $att->filereference); |
3324
|
|
|
SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->editAttachments(): Deleting attachment with num: %s", $attachnum)); |
3325
|
|
|
mapi_message_deleteattach($mapimessage, (int) $attachnum); |
3326
|
|
|
} |
3327
|
|
|
} |
3328
|
|
|
} |
3329
|
|
|
|
3330
|
|
|
/** |
3331
|
|
|
* Sets information from SyncLocation type for a MAPI message. |
3332
|
|
|
* |
3333
|
|
|
* @param SyncBaseBody $aslocation |
3334
|
|
|
* @param array $props |
3335
|
|
|
* @param array $appointmentprops |
3336
|
|
|
*/ |
3337
|
|
|
private function setASlocation($aslocation, &$props, $appointmentprops) { |
3338
|
|
|
$fullAddress = ""; |
3339
|
|
|
if ($aslocation->street || $aslocation->city || $aslocation->state || $aslocation->country || $aslocation->postalcode) { |
|
|
|
|
3340
|
|
|
$fullAddress = $aslocation->street . ", " . $aslocation->city . "-" . $aslocation->state . "," . $aslocation->country . "," . $aslocation->postalcode; |
3341
|
|
|
} |
3342
|
|
|
|
3343
|
|
|
// Determine which data to use as DisplayName. This is also set to the traditional location property for backwards compatibility (this is currently displayed in OL). |
3344
|
|
|
$useStreet = false; |
3345
|
|
|
if ($aslocation->displayname) { |
|
|
|
|
3346
|
|
|
$props[$appointmentprops["location"]] = $aslocation->displayname; |
3347
|
|
|
} |
3348
|
|
|
elseif ($aslocation->street) { |
3349
|
|
|
$useStreet = true; |
3350
|
|
|
$props[$appointmentprops["location"]] = $fullAddress; |
3351
|
|
|
} |
3352
|
|
|
elseif ($aslocation->city) { |
3353
|
|
|
$props[$appointmentprops["location"]] = $aslocation->city; |
3354
|
|
|
} |
3355
|
|
|
$loc = []; |
3356
|
|
|
$loc["DisplayName"] = ($useStreet) ? $aslocation->street : $props[$appointmentprops["location"]]; |
3357
|
|
|
$loc["LocationAnnotation"] = ($aslocation->annotation) ? $aslocation->annotation : ""; |
|
|
|
|
3358
|
|
|
$loc["LocationSource"] = "None"; |
3359
|
|
|
$loc["Unresolved"] = ($aslocation->locationuri) ? false : true; |
|
|
|
|
3360
|
|
|
$loc["LocationUri"] = $aslocation->locationuri ?? ""; |
3361
|
|
|
$loc["Latitude"] = ($aslocation->latitude) ? floatval($aslocation->latitude) : null; |
|
|
|
|
3362
|
|
|
$loc["Longitude"] = ($aslocation->longitude) ? floatval($aslocation->longitude) : null; |
|
|
|
|
3363
|
|
|
$loc["Altitude"] = ($aslocation->altitude) ? floatval($aslocation->altitude) : null; |
|
|
|
|
3364
|
|
|
$loc["Accuracy"] = ($aslocation->accuracy) ? floatval($aslocation->accuracy) : null; |
|
|
|
|
3365
|
|
|
$loc["AltitudeAccuracy"] = ($aslocation->altitudeaccuracy) ? floatval($aslocation->altitudeaccuracy) : null; |
|
|
|
|
3366
|
|
|
$loc["LocationStreet"] = $aslocation->street ?? ""; |
3367
|
|
|
$loc["LocationCity"] = $aslocation->city ?? ""; |
3368
|
|
|
$loc["LocationState"] = $aslocation->state ?? ""; |
3369
|
|
|
$loc["LocationCountry"] = $aslocation->country ?? ""; |
3370
|
|
|
$loc["LocationPostalCode"] = $aslocation->postalcode ?? ""; |
3371
|
|
|
$loc["LocationFullAddress"] = $fullAddress; |
3372
|
|
|
|
3373
|
|
|
$props[$appointmentprops["locations"]] = json_encode([$loc], JSON_UNESCAPED_UNICODE); |
3374
|
|
|
} |
3375
|
|
|
|
3376
|
|
|
/** |
3377
|
|
|
* Gets information from a MAPI message and applies it to a SyncLocation object. |
3378
|
|
|
* |
3379
|
|
|
* @param MAPIMessage $mapimessage |
3380
|
|
|
* @param SyncObject $aslocation |
3381
|
|
|
* @param array $appointmentprops |
3382
|
|
|
*/ |
3383
|
|
|
private function getASlocation($mapimessage, &$aslocation, $appointmentprops) { |
3384
|
|
|
$props = mapi_getprops($mapimessage, [$appointmentprops["locations"], $appointmentprops["location"]]); |
3385
|
|
|
// set the old location as displayname - this is also the "correct" approach if there is more than one location in the "locations" property json |
3386
|
|
|
if (isset($props[$appointmentprops["location"]])) { |
3387
|
|
|
$aslocation->displayname = $props[$appointmentprops["location"]]; |
3388
|
|
|
} |
3389
|
|
|
|
3390
|
|
|
if (isset($props[$appointmentprops["locations"]])) { |
3391
|
|
|
$loc = json_decode($props[$appointmentprops["locations"]], true); |
3392
|
|
|
if (is_array($loc) && count($loc) == 1) { |
3393
|
|
|
$l = $loc[0]; |
3394
|
|
|
if (!empty($l['DisplayName'])) { |
3395
|
|
|
$aslocation->displayname = $l['DisplayName']; |
3396
|
|
|
} |
3397
|
|
|
if (!empty($l['LocationAnnotation'])) { |
3398
|
|
|
$aslocation->annotation = $l['LocationAnnotation']; |
3399
|
|
|
} |
3400
|
|
|
if (!empty($l['LocationStreet'])) { |
3401
|
|
|
$aslocation->street = $l['LocationStreet']; |
3402
|
|
|
} |
3403
|
|
|
if (!empty($l['LocationCity'])) { |
3404
|
|
|
$aslocation->city = $l['LocationCity']; |
3405
|
|
|
} |
3406
|
|
|
if (!empty($l['LocationState'])) { |
3407
|
|
|
$aslocation->state = $l['LocationState']; |
3408
|
|
|
} |
3409
|
|
|
if (!empty($l['LocationCountry'])) { |
3410
|
|
|
$aslocation->country = $l['LocationCountry']; |
3411
|
|
|
} |
3412
|
|
|
if (!empty($l['LocationPostalCode'])) { |
3413
|
|
|
$aslocation->postalcode = $l['LocationPostalCode']; |
3414
|
|
|
} |
3415
|
|
|
if (isset($l['Latitude']) && is_numeric($l['Latitude'])) { |
3416
|
|
|
$aslocation->latitude = floatval($l['Latitude']); |
3417
|
|
|
} |
3418
|
|
|
if (isset($l['Longitude']) && is_numeric($l['Longitude'])) { |
3419
|
|
|
$aslocation->longitude = floatval($l['Longitude']); |
3420
|
|
|
} |
3421
|
|
|
if (isset($l['Accuracy']) && is_numeric($l['Accuracy'])) { |
3422
|
|
|
$aslocation->accuracy = floatval($l['Accuracy']); |
3423
|
|
|
} |
3424
|
|
|
if (isset($l['Altitude']) && is_numeric($l['Altitude'])) { |
3425
|
|
|
$aslocation->altitude = floatval($l['Altitude']); |
3426
|
|
|
} |
3427
|
|
|
if (isset($l['AltitudeAccuracy']) && is_numeric($l['AltitudeAccuracy'])) { |
3428
|
|
|
$aslocation->altitudeaccuracy = floatval($l['AltitudeAccuracy']); |
3429
|
|
|
} |
3430
|
|
|
if (!empty($l['LocationUri'])) { |
3431
|
|
|
$aslocation->locationuri = $l['LocationUri']; |
3432
|
|
|
} |
3433
|
|
|
} |
3434
|
|
|
} |
3435
|
|
|
} |
3436
|
|
|
|
3437
|
|
|
/** |
3438
|
|
|
* Get MAPI addressbook object. |
3439
|
|
|
* |
3440
|
|
|
* @return MAPIAddressbook object to be used with mapi_ab_* or false on failure |
|
|
|
|
3441
|
|
|
*/ |
3442
|
|
|
private function getAddressbook() { |
3443
|
|
|
if (isset($this->addressbook) && $this->addressbook) { |
3444
|
|
|
return $this->addressbook; |
3445
|
|
|
} |
3446
|
|
|
$this->addressbook = mapi_openaddressbook($this->session); |
3447
|
|
|
$result = mapi_last_hresult(); |
3448
|
|
|
if ($result && $this->addressbook === false) { |
3449
|
|
|
SLog::Write(LOGLEVEL_ERROR, sprintf("MAPIProvider->getAddressbook error opening addressbook 0x%X", $result)); |
3450
|
|
|
|
3451
|
|
|
return false; |
|
|
|
|
3452
|
|
|
} |
3453
|
|
|
|
3454
|
|
|
return $this->addressbook; |
3455
|
|
|
} |
3456
|
|
|
|
3457
|
|
|
/** |
3458
|
|
|
* Gets the required store properties. |
3459
|
|
|
* |
3460
|
|
|
* @return array |
3461
|
|
|
*/ |
3462
|
|
|
public function GetStoreProps() { |
3463
|
|
|
if (!isset($this->storeProps) || empty($this->storeProps)) { |
3464
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetStoreProps(): Getting store properties."); |
3465
|
|
|
$this->storeProps = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]); |
|
|
|
|
3466
|
|
|
// make sure all properties are set |
3467
|
|
|
if (!isset($this->storeProps[PR_IPM_WASTEBASKET_ENTRYID])) { |
3468
|
|
|
$this->storeProps[PR_IPM_WASTEBASKET_ENTRYID] = false; |
3469
|
|
|
} |
3470
|
|
|
if (!isset($this->storeProps[PR_IPM_SENTMAIL_ENTRYID])) { |
3471
|
|
|
$this->storeProps[PR_IPM_SENTMAIL_ENTRYID] = false; |
3472
|
|
|
} |
3473
|
|
|
if (!isset($this->storeProps[PR_IPM_OUTBOX_ENTRYID])) { |
3474
|
|
|
$this->storeProps[PR_IPM_OUTBOX_ENTRYID] = false; |
3475
|
|
|
} |
3476
|
|
|
if (!isset($this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) { |
3477
|
|
|
$this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID] = false; |
3478
|
|
|
} |
3479
|
|
|
} |
3480
|
|
|
|
3481
|
|
|
return $this->storeProps; |
3482
|
|
|
} |
3483
|
|
|
|
3484
|
|
|
/** |
3485
|
|
|
* Gets the required inbox properties. |
3486
|
|
|
* |
3487
|
|
|
* @return array |
3488
|
|
|
*/ |
3489
|
|
|
public function GetInboxProps() { |
3490
|
|
|
if (!isset($this->inboxProps) || empty($this->inboxProps)) { |
3491
|
|
|
SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetInboxProps(): Getting inbox properties."); |
3492
|
|
|
$this->inboxProps = []; |
3493
|
|
|
$inbox = mapi_msgstore_getreceivefolder($this->store); |
3494
|
|
|
if ($inbox) { |
3495
|
|
|
$this->inboxProps = mapi_getprops($inbox, [PR_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_TASK_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_JOURNAL_ENTRYID]); |
|
|
|
|
3496
|
|
|
// make sure all properties are set |
3497
|
|
|
if (!isset($this->inboxProps[PR_ENTRYID])) { |
3498
|
|
|
$this->inboxProps[PR_ENTRYID] = false; |
3499
|
|
|
} |
3500
|
|
|
if (!isset($this->inboxProps[PR_IPM_DRAFTS_ENTRYID])) { |
3501
|
|
|
$this->inboxProps[PR_IPM_DRAFTS_ENTRYID] = false; |
3502
|
|
|
} |
3503
|
|
|
if (!isset($this->inboxProps[PR_IPM_TASK_ENTRYID])) { |
3504
|
|
|
$this->inboxProps[PR_IPM_TASK_ENTRYID] = false; |
3505
|
|
|
} |
3506
|
|
|
if (!isset($this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID])) { |
3507
|
|
|
$this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID] = false; |
3508
|
|
|
} |
3509
|
|
|
if (!isset($this->inboxProps[PR_IPM_CONTACT_ENTRYID])) { |
3510
|
|
|
$this->inboxProps[PR_IPM_CONTACT_ENTRYID] = false; |
3511
|
|
|
} |
3512
|
|
|
if (!isset($this->inboxProps[PR_IPM_NOTE_ENTRYID])) { |
3513
|
|
|
$this->inboxProps[PR_IPM_NOTE_ENTRYID] = false; |
3514
|
|
|
} |
3515
|
|
|
if (!isset($this->inboxProps[PR_IPM_JOURNAL_ENTRYID])) { |
3516
|
|
|
$this->inboxProps[PR_IPM_JOURNAL_ENTRYID] = false; |
3517
|
|
|
} |
3518
|
|
|
} |
3519
|
|
|
} |
3520
|
|
|
|
3521
|
|
|
return $this->inboxProps; |
3522
|
|
|
} |
3523
|
|
|
|
3524
|
|
|
/** |
3525
|
|
|
* Gets the required store root properties. |
3526
|
|
|
* |
3527
|
|
|
* @return array |
3528
|
|
|
*/ |
3529
|
|
|
private function getRootProps() { |
3530
|
|
|
if (!isset($this->rootProps)) { |
3531
|
|
|
$root = mapi_msgstore_openentry($this->store, null); |
3532
|
|
|
$this->rootProps = mapi_getprops($root, [PR_ADDITIONAL_REN_ENTRYIDS_EX]); |
|
|
|
|
3533
|
|
|
} |
3534
|
|
|
|
3535
|
|
|
return $this->rootProps; |
3536
|
|
|
} |
3537
|
|
|
|
3538
|
|
|
/** |
3539
|
|
|
* Returns an array with entryids of some special folders. |
3540
|
|
|
* |
3541
|
|
|
* @return array |
3542
|
|
|
*/ |
3543
|
|
|
private function getSpecialFoldersData() { |
3544
|
|
|
// The persist data of an entry in PR_ADDITIONAL_REN_ENTRYIDS_EX consists of: |
3545
|
|
|
// PersistId - e.g. RSF_PID_SUGGESTED_CONTACTS (2 bytes) |
3546
|
|
|
// DataElementsSize - size of DataElements field (2 bytes) |
3547
|
|
|
// DataElements - array of PersistElement structures (variable size) |
3548
|
|
|
// PersistElement Structure consists of |
3549
|
|
|
// ElementID - e.g. RSF_ELID_ENTRYID (2 bytes) |
3550
|
|
|
// ElementDataSize - size of ElementData (2 bytes) |
3551
|
|
|
// ElementData - The data for the special folder identified by the PersistID (variable size) |
3552
|
|
|
if (empty($this->specialFoldersData)) { |
3553
|
|
|
$this->specialFoldersData = []; |
3554
|
|
|
$rootProps = $this->getRootProps(); |
3555
|
|
|
if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS_EX])) { |
|
|
|
|
3556
|
|
|
$persistData = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS_EX]; |
3557
|
|
|
while (strlen($persistData) > 0) { |
3558
|
|
|
// PERSIST_SENTINEL marks the end of the persist data |
3559
|
|
|
if (strlen($persistData) == 4 && intval($persistData) == PERSIST_SENTINEL) { |
|
|
|
|
3560
|
|
|
break; |
3561
|
|
|
} |
3562
|
|
|
$unpackedData = unpack("vdataSize/velementID/velDataSize", substr($persistData, 2, 6)); |
3563
|
|
|
if (isset($unpackedData['dataSize'], $unpackedData['elementID']) && $unpackedData['elementID'] == RSF_ELID_ENTRYID && isset($unpackedData['elDataSize'])) { |
|
|
|
|
3564
|
|
|
$this->specialFoldersData[] = substr($persistData, 8, $unpackedData['elDataSize']); |
3565
|
|
|
// Add PersistId and DataElementsSize lengths to the data size as they're not part of it |
3566
|
|
|
$persistData = substr($persistData, $unpackedData['dataSize'] + 4); |
3567
|
|
|
} |
3568
|
|
|
else { |
3569
|
|
|
SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getSpecialFoldersData(): persistent data is not valid"); |
3570
|
|
|
break; |
3571
|
|
|
} |
3572
|
|
|
} |
3573
|
|
|
} |
3574
|
|
|
} |
3575
|
|
|
|
3576
|
|
|
return $this->specialFoldersData; |
3577
|
|
|
} |
3578
|
|
|
|
3579
|
|
|
/** |
3580
|
|
|
* Extracts email address from PR_SEARCH_KEY property if possible. |
3581
|
|
|
* |
3582
|
|
|
* @param string $searchKey |
3583
|
|
|
* |
3584
|
|
|
* @return string |
3585
|
|
|
*/ |
3586
|
|
|
private function getEmailAddressFromSearchKey($searchKey) { |
3587
|
|
|
if (strpos($searchKey, ':') !== false && strpos($searchKey, '@') !== false) { |
3588
|
|
|
SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getEmailAddressFromSearchKey(): fall back to PR_SEARCH_KEY or PR_SENT_REPRESENTING_SEARCH_KEY to resolve user and get email address"); |
3589
|
|
|
|
3590
|
|
|
return trim(strtolower(explode(':', $searchKey)[1])); |
3591
|
|
|
} |
3592
|
|
|
|
3593
|
|
|
return ""; |
3594
|
|
|
} |
3595
|
|
|
|
3596
|
|
|
/** |
3597
|
|
|
* Returns categories for a message. |
3598
|
|
|
* |
3599
|
|
|
* @param binary $parentsourcekey |
|
|
|
|
3600
|
|
|
* @param binary $sourcekey |
3601
|
|
|
* |
3602
|
|
|
* @return array or false on failure |
3603
|
|
|
*/ |
3604
|
|
|
public function GetMessageCategories($parentsourcekey, $sourcekey) { |
3605
|
|
|
$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $parentsourcekey, $sourcekey); |
3606
|
|
|
if (!$entryid) { |
3607
|
|
|
SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->GetMessageCategories(): Couldn't retrieve message, sourcekey: '%s', parentsourcekey: '%s'", bin2hex($sourcekey), bin2hex($parentsourcekey))); |
3608
|
|
|
|
3609
|
|
|
return false; |
|
|
|
|
3610
|
|
|
} |
3611
|
|
|
$mapimessage = mapi_msgstore_openentry($this->store, $entryid); |
3612
|
|
|
$emailMapping = MAPIMapping::GetEmailMapping(); |
3613
|
|
|
$emailMapping = ["categories" => $emailMapping["categories"]]; |
3614
|
|
|
$messageCategories = $this->getProps($mapimessage, $emailMapping); |
3615
|
|
|
if (isset($messageCategories[$emailMapping["categories"]]) && is_array($messageCategories[$emailMapping["categories"]])) { |
3616
|
|
|
return $messageCategories[$emailMapping["categories"]]; |
3617
|
|
|
} |
3618
|
|
|
|
3619
|
|
|
return false; |
|
|
|
|
3620
|
|
|
} |
3621
|
|
|
|
3622
|
|
|
/** |
3623
|
|
|
* Adds recipients to the recips array. |
3624
|
|
|
* |
3625
|
|
|
* @param string $recip |
3626
|
|
|
* @param int $type |
3627
|
|
|
* @param array $recips |
3628
|
|
|
*/ |
3629
|
|
|
private function addRecips($recip, $type, &$recips) { |
3630
|
|
|
if (!empty($recip) && is_array($recip)) { |
|
|
|
|
3631
|
|
|
$emails = $recip; |
3632
|
|
|
// Recipients should be comma separated, but android devices separate |
3633
|
|
|
// them with semicolon, hence the additional processing |
3634
|
|
|
if (count($recip) === 1 && strpos($recip[0], ';') !== false) { |
3635
|
|
|
$emails = explode(';', $recip[0]); |
3636
|
|
|
} |
3637
|
|
|
|
3638
|
|
|
foreach ($emails as $email) { |
3639
|
|
|
$extEmail = $this->extractEmailAddress($email); |
3640
|
|
|
if ($extEmail !== false) { |
3641
|
|
|
$r = $this->createMapiRecipient($extEmail, $type); |
3642
|
|
|
$recips[] = $r; |
3643
|
|
|
} |
3644
|
|
|
} |
3645
|
|
|
} |
3646
|
|
|
} |
3647
|
|
|
|
3648
|
|
|
/** |
3649
|
|
|
* Creates a MAPI recipient to use with mapi_message_modifyrecipients(). |
3650
|
|
|
* |
3651
|
|
|
* @param string $email |
3652
|
|
|
* @param int $type |
3653
|
|
|
* |
3654
|
|
|
* @return array |
3655
|
|
|
*/ |
3656
|
|
|
private function createMapiRecipient($email, $type) { |
3657
|
|
|
// Open address book for user resolve |
3658
|
|
|
$addrbook = $this->getAddressbook(); |
3659
|
|
|
$recip = []; |
3660
|
|
|
$recip[PR_EMAIL_ADDRESS] = $email; |
|
|
|
|
3661
|
|
|
$recip[PR_SMTP_ADDRESS] = $email; |
|
|
|
|
3662
|
|
|
|
3663
|
|
|
// lookup information in GAB if possible so we have up-to-date name for given address |
3664
|
|
|
$userinfo = [[PR_DISPLAY_NAME => $recip[PR_EMAIL_ADDRESS]]]; |
|
|
|
|
3665
|
|
|
$userinfo = mapi_ab_resolvename($addrbook, $userinfo, EMS_AB_ADDRESS_LOOKUP); |
|
|
|
|
3666
|
|
|
if (mapi_last_hresult() == NOERROR) { |
|
|
|
|
3667
|
|
|
$recip[PR_DISPLAY_NAME] = $userinfo[0][PR_DISPLAY_NAME]; |
3668
|
|
|
$recip[PR_EMAIL_ADDRESS] = $userinfo[0][PR_EMAIL_ADDRESS]; |
3669
|
|
|
$recip[PR_SEARCH_KEY] = $userinfo[0][PR_SEARCH_KEY]; |
|
|
|
|
3670
|
|
|
$recip[PR_ADDRTYPE] = $userinfo[0][PR_ADDRTYPE]; |
|
|
|
|
3671
|
|
|
$recip[PR_ENTRYID] = $userinfo[0][PR_ENTRYID]; |
3672
|
|
|
$recip[PR_RECIPIENT_TYPE] = $type; |
|
|
|
|
3673
|
|
|
} |
3674
|
|
|
else { |
3675
|
|
|
$recip[PR_DISPLAY_NAME] = $email; |
3676
|
|
|
$recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0"; |
3677
|
|
|
$recip[PR_ADDRTYPE] = "SMTP"; |
3678
|
|
|
$recip[PR_RECIPIENT_TYPE] = $type; |
3679
|
|
|
$recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]); |
3680
|
|
|
} |
3681
|
|
|
|
3682
|
|
|
return $recip; |
3683
|
|
|
} |
3684
|
|
|
} |
3685
|
|
|
|