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