Test Failed
Branch develop (a67043)
by Andreas
13:06
created

TaskItemModule::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 6
rs 10
c 1
b 1
f 0
1
<?php
2
3
/**
4
 * General operations.
5
 *
6
 * All mapi operations, like create, change and delete, are set in this class.
7
 * A module calls one of these methods.
8
 *
9
 * Note: All entryids in this class are binary
10
 *
11
 * @todo This class is bloated. It also returns data in various arbitrary formats
12
 * that other functions depend on, making lots of code almost completely unreadable.
13
 */
14
class Operations {
15
	/**
16
	 * Gets the hierarchy list of all required stores.
17
	 *
18
	 * getHierarchyList builds an entire hierarchy list of all folders that should be shown in various places. Most importantly,
19
	 * it generates the list of folders to be show in the hierarchylistmodule (left-hand folder browser) on the client.
20
	 *
21
	 * It is also used to generate smaller hierarchy lists, for example for the 'create folder' dialog.
22
	 *
23
	 * The returned array is a flat array of folders, so if the caller wishes to build a tree, it is up to the caller to correlate
24
	 * the entryids and the parent_entryids of all the folders to build the tree.
25
	 *
26
	 * The return value is an associated array with the following keys:
27
	 * - store: array of stores
28
	 *
29
	 * Each store contains:
30
	 * - array("store_entryid" => entryid of store, name => name of store, subtree => entryid of viewable root, type => default|public|other, folder_type => "all")
31
	 * - folder: array of folders with each an array of properties (see Operations::setFolder() for properties)
32
	 *
33
	 * @param array  $properties   MAPI property mapping for folders
34
	 * @param int    $type         Which stores to fetch (HIERARCHY_GET_ALL | HIERARCHY_GET_DEFAULT | HIERARCHY_GET_ONE)
35
	 * @param object $store        Only when $type == HIERARCHY_GET_ONE
36
	 * @param array  $storeOptions Only when $type == HIERARCHY_GET_ONE, this overrides the  loading options which is normally
37
	 *                             obtained from the settings for loading the store (e.g. only load calendar).
38
	 * @param string $username     The username
39
	 *
40
	 * @return array Return structure
41
	 */
42
	public function getHierarchyList($properties, $type = HIERARCHY_GET_ALL, $store = null, $storeOptions = null, $username = null) {
43
		switch ($type) {
44
			case HIERARCHY_GET_ALL:
45
				$storelist = $GLOBALS["mapisession"]->getAllMessageStores();
46
				break;
47
48
			case HIERARCHY_GET_DEFAULT:
49
				$storelist = [$GLOBALS["mapisession"]->getDefaultMessageStore()];
50
				break;
51
52
			case HIERARCHY_GET_ONE:
53
				// Get single store and it's archive store as well
54
				$storelist = $GLOBALS["mapisession"]->getSingleMessageStores($store, $storeOptions, $username);
55
				break;
56
		}
57
58
		$data = [];
59
		$data["item"] = [];
60
61
		// Get the other store options
62
		if (isset($storeOptions)) {
63
			$otherUsers = $storeOptions;
64
		}
65
		else {
66
			$otherUsers = $GLOBALS["mapisession"]->retrieveOtherUsersFromSettings();
67
		}
68
69
		foreach ($storelist as $store) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $storelist does not seem to be defined for all execution paths leading up to this point.
Loading history...
70
			$msgstore_props = mapi_getprops($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_OBJECT_TYPE, PR_STORE_SUPPORT_MASK, PR_MAILBOX_OWNER_ENTRYID, PR_MAILBOX_OWNER_NAME, PR_USER_ENTRYID, PR_USER_NAME, PR_QUOTA_WARNING_THRESHOLD, PR_QUOTA_SEND_THRESHOLD, PR_QUOTA_RECEIVE_THRESHOLD, PR_MESSAGE_SIZE_EXTENDED, PR_COMMON_VIEWS_ENTRYID, PR_FINDER_ENTRYID]);
71
72
			$inboxProps = [];
73
			$storeType = $msgstore_props[PR_MDB_PROVIDER];
74
75
			/*
76
			 * storetype is public and if public folder is disabled
77
			 * then continue in loop for next store.
78
			 */
79
			if ($storeType == ZARAFA_STORE_PUBLIC_GUID && ENABLE_PUBLIC_FOLDERS == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
80
				continue;
81
			}
82
83
			// Obtain the real username for the store when dealing with a shared store
84
			if ($storeType == ZARAFA_STORE_DELEGATE_GUID) {
85
				$storeUserName = $GLOBALS["mapisession"]->getUserNameOfStore($msgstore_props[PR_ENTRYID]);
86
			}
87
			else {
88
				$storeUserName = $msgstore_props[PR_USER_NAME] ?? $GLOBALS["mapisession"]->getUserName();
89
			}
90
91
			$storeData = [
92
				"store_entryid" => bin2hex($msgstore_props[PR_ENTRYID]),
93
				"props" => [
94
					// Showing the store as 'Inbox - Name' is confusing, so we strip the 'Inbox - ' part.
95
					"display_name" => str_replace('Inbox - ', '', $msgstore_props[PR_DISPLAY_NAME]),
96
					"subtree_entryid" => bin2hex($msgstore_props[PR_IPM_SUBTREE_ENTRYID]),
97
					"mdb_provider" => bin2hex($msgstore_props[PR_MDB_PROVIDER]),
98
					"object_type" => $msgstore_props[PR_OBJECT_TYPE],
99
					"store_support_mask" => $msgstore_props[PR_STORE_SUPPORT_MASK],
100
					"user_name" => $storeUserName,
101
					"store_size" => round($msgstore_props[PR_MESSAGE_SIZE_EXTENDED] / 1024),
102
					"quota_warning" => isset($msgstore_props[PR_QUOTA_WARNING_THRESHOLD]) ? $msgstore_props[PR_QUOTA_WARNING_THRESHOLD] : 0,
103
					"quota_soft" => isset($msgstore_props[PR_QUOTA_SEND_THRESHOLD]) ? $msgstore_props[PR_QUOTA_SEND_THRESHOLD] : 0,
104
					"quota_hard" => isset($msgstore_props[PR_QUOTA_RECEIVE_THRESHOLD]) ? $msgstore_props[PR_QUOTA_RECEIVE_THRESHOLD] : 0,
105
					"common_view_entryid" => isset($msgstore_props[PR_COMMON_VIEWS_ENTRYID]) ? bin2hex($msgstore_props[PR_COMMON_VIEWS_ENTRYID]) : "",
106
					"finder_entryid" => isset($msgstore_props[PR_FINDER_ENTRYID]) ? bin2hex($msgstore_props[PR_FINDER_ENTRYID]) : "",
107
					"todolist_entryid" => bin2hex(TodoList::getEntryId()),
108
				],
109
			];
110
111
			// these properties doesn't exist in public store
112
			if (isset($msgstore_props[PR_MAILBOX_OWNER_ENTRYID], $msgstore_props[PR_MAILBOX_OWNER_NAME])) {
113
				$storeData["props"]["mailbox_owner_entryid"] = bin2hex($msgstore_props[PR_MAILBOX_OWNER_ENTRYID]);
114
				$storeData["props"]["mailbox_owner_name"] = $msgstore_props[PR_MAILBOX_OWNER_NAME];
115
			}
116
117
			// public store doesn't have inbox
118
			try {
119
				$inbox = mapi_msgstore_getreceivefolder($store);
120
				$inboxProps = mapi_getprops($inbox, [PR_ENTRYID]);
121
			}
122
			catch (MAPIException $e) {
123
				// don't propagate this error to parent handlers, if store doesn't support it
124
				if ($e->getCode() === MAPI_E_NO_SUPPORT) {
125
					$e->setHandled();
126
				}
127
			}
128
129
			$root = mapi_msgstore_openentry($store, null);
130
			$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]);
131
132
			$additional_ren_entryids = [];
133
			if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
134
				$additional_ren_entryids = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS];
135
			}
136
137
			$defaultfolders = [
138
				"default_folder_inbox" => ["inbox" => PR_ENTRYID],
139
				"default_folder_outbox" => ["store" => PR_IPM_OUTBOX_ENTRYID],
140
				"default_folder_sent" => ["store" => PR_IPM_SENTMAIL_ENTRYID],
141
				"default_folder_wastebasket" => ["store" => PR_IPM_WASTEBASKET_ENTRYID],
142
				"default_folder_favorites" => ["store" => PR_IPM_FAVORITES_ENTRYID],
143
				"default_folder_publicfolders" => ["store" => PR_IPM_PUBLIC_FOLDERS_ENTRYID],
144
				"default_folder_calendar" => ["root" => PR_IPM_APPOINTMENT_ENTRYID],
145
				"default_folder_contact" => ["root" => PR_IPM_CONTACT_ENTRYID],
146
				"default_folder_drafts" => ["root" => PR_IPM_DRAFTS_ENTRYID],
147
				"default_folder_journal" => ["root" => PR_IPM_JOURNAL_ENTRYID],
148
				"default_folder_note" => ["root" => PR_IPM_NOTE_ENTRYID],
149
				"default_folder_task" => ["root" => PR_IPM_TASK_ENTRYID],
150
				"default_folder_junk" => ["additional" => 4],
151
				"default_folder_syncissues" => ["additional" => 1],
152
				"default_folder_conflicts" => ["additional" => 0],
153
				"default_folder_localfailures" => ["additional" => 2],
154
				"default_folder_serverfailures" => ["additional" => 3],
155
			];
156
157
			foreach ($defaultfolders as $key => $prop) {
158
				$tag = reset($prop);
159
				$from = key($prop);
160
161
				switch ($from) {
162
					case "inbox":
163
						if (isset($inboxProps[$tag])) {
164
							$storeData["props"][$key] = bin2hex($inboxProps[$tag]);
165
						}
166
						break;
167
168
					case "store":
169
						if (isset($msgstore_props[$tag])) {
170
							$storeData["props"][$key] = bin2hex($msgstore_props[$tag]);
171
						}
172
						break;
173
174
					case "root":
175
						if (isset($rootProps[$tag])) {
176
							$storeData["props"][$key] = bin2hex($rootProps[$tag]);
177
						}
178
						break;
179
180
					case "additional":
181
						if (isset($additional_ren_entryids[$tag])) {
182
							$storeData["props"][$key] = bin2hex($additional_ren_entryids[$tag]);
183
						}
184
						break;
185
				}
186
			}
187
188
			$storeData["folders"] = ["item" => []];
189
190
			if (isset($msgstore_props[PR_IPM_SUBTREE_ENTRYID])) {
191
				$subtreeFolderEntryID = $msgstore_props[PR_IPM_SUBTREE_ENTRYID];
192
193
				$openWholeStore = true;
194
				if ($storeType == ZARAFA_STORE_DELEGATE_GUID) {
195
					$username = strtolower($storeData["props"]["user_name"]);
196
					$sharedFolders = [];
197
198
					// Check whether we should open the whole store or just single folders
199
					if (isset($otherUsers[$username])) {
200
						$sharedFolders = $otherUsers[$username];
201
						if (!isset($otherUsers[$username]['all'])) {
202
							$openWholeStore = false;
203
						}
204
					}
205
206
					// Update the store properties when this function was called to
207
					// only open a particular shared store.
208
					if (is_array($storeOptions)) {
209
						// Update the store properties to mark previously opened
210
						$prevSharedFolders = $GLOBALS["settings"]->get("zarafa/v1/contexts/hierarchy/shared_stores/" . $username, null);
211
						if (!empty($prevSharedFolders)) {
212
							foreach ($prevSharedFolders as $type => $prevSharedFolder) {
213
								// Update the store properties to refer to the shared folder,
214
								// note that we don't care if we have access to the folder or not.
215
								$type = $prevSharedFolder["folder_type"];
216
								if ($type == "all") {
217
									$propname = "subtree_entryid";
218
								}
219
								else {
220
									$propname = "default_folder_" . $prevSharedFolder["folder_type"];
221
								}
222
223
								if (isset($storeData["props"][$propname])) {
224
									$folderEntryID = hex2bin($storeData["props"][$propname]);
225
									$storeData["props"]["shared_folder_" . $prevSharedFolder["folder_type"]] = bin2hex($folderEntryID);
226
								}
227
							}
228
						}
229
					}
230
				}
231
232
				// Get the IPMSUBTREE object
233
				$storeAccess = true;
234
235
				try {
236
					$subtreeFolder = mapi_msgstore_openentry($store, $subtreeFolderEntryID);
237
					// Add root folder
238
					$subtree = $this->setFolder(mapi_getprops($subtreeFolder, $properties));
239
					if (!$openWholeStore) {
240
						$subtree['props']['access'] = 0;
241
					}
242
					array_push($storeData["folders"]["item"], $subtree);
243
				}
244
				catch (MAPIException $e) {
245
					if ($openWholeStore) {
246
						/*
247
						 * if we are going to open whole store and we are not able to open the subtree folder
248
						 * then it should be considered as an error
249
						 * but if we are only opening single folder then it could be possible that we don't have
250
						 * permission to open subtree folder so add a dummy subtree folder in the response and don't consider this as an error
251
						 */
252
						$storeAccess = false;
253
254
						// Add properties to the store response to indicate to the client
255
						// that the store could not be loaded.
256
						$this->invalidateResponseStore($storeData, 'all', $subtreeFolderEntryID);
257
					}
258
					else {
259
						// Add properties to the store response to add a placeholder IPMSubtree.
260
						$this->getDummyIPMSubtreeFolder($storeData, $subtreeFolderEntryID);
261
					}
262
263
					// We've handled the event
264
					$e->setHandled();
265
				}
266
267
				if ($storeAccess) {
268
					// Open the whole store and be done with it
269
					if ($openWholeStore) {
270
						try {
271
							// Update the store properties to refer to the shared folder,
272
							// note that we don't care if we have access to the folder or not.
273
							$storeData["props"]["shared_folder_all"] = bin2hex($subtreeFolderEntryID);
274
							$this->getSubFolders($subtreeFolder, $store, $properties, $storeData);
275
276
							if ($storeType == ZARAFA_SERVICE_GUID) {
277
								// If store type ZARAFA_SERVICE_GUID (own store) then get the
278
								// IPM_COMMON_VIEWS folder and set it to folders array.
279
								$storeData["favorites"] = ["item" => []];
280
								$commonViewFolderEntryid = $msgstore_props[PR_COMMON_VIEWS_ENTRYID];
281
282
								$this->setDefaultFavoritesFolder($commonViewFolderEntryid, $store, $storeData);
283
284
								$commonViewFolder = mapi_msgstore_openentry($store, $commonViewFolderEntryid);
285
								$this->getFavoritesFolders($commonViewFolder, $storeData);
286
287
								$commonViewFolderProps = mapi_getprops($commonViewFolder);
288
								array_push($storeData["folders"]["item"], $this->setFolder($commonViewFolderProps));
289
290
								// Get the To-do list folder and add it to the hierarchy
291
								$todoSearchFolder = todoList::getTodoSearchFolder($store);
292
								if ($todoSearchFolder) {
293
									$todoSearchFolderProps = mapi_getprops($todoSearchFolder);
294
295
									// Change the parent so the folder will be shown in the hierarchy
296
									$todoSearchFolderProps[PR_PARENT_ENTRYID] = $subtreeFolderEntryID;
297
									// Change the display name of the folder
298
									$todoSearchFolderProps[PR_DISPLAY_NAME] = _('To-Do List');
299
									// Never show unread content for the To-do list
300
									$todoSearchFolderProps[PR_CONTENT_UNREAD] = 0;
301
									$todoSearchFolderProps[PR_CONTENT_COUNT] = 0;
302
									array_push($storeData["folders"]["item"], $this->setFolder($todoSearchFolderProps));
303
									$storeData["props"]['default_folder_todolist'] = bin2hex($todoSearchFolderProps[PR_ENTRYID]);
304
								}
305
							}
306
						}
307
						catch (MAPIException $e) {
308
							// Add properties to the store response to indicate to the client
309
							// that the store could not be loaded.
310
							$this->invalidateResponseStore($storeData, 'all', $subtreeFolderEntryID);
311
312
							// We've handled the event
313
							$e->setHandled();
314
						}
315
316
					// Open single folders under the store object
317
					}
318
					else {
319
						foreach ($sharedFolders as $type => $sharedFolder) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sharedFolders does not seem to be defined for all execution paths leading up to this point.
Loading history...
320
							$openSubFolders = ($sharedFolder["show_subfolders"] == true);
321
322
							// See if the folders exists by checking if it is in the default folders entryid list
323
							$store_access = true;
324
							if (!isset($storeData["props"]["default_folder_" . $sharedFolder["folder_type"]])) {
325
								// Create a fake folder entryid which must be used for referencing this folder
326
								$folderEntryID = "default_folder_" . $sharedFolder["folder_type"];
327
328
								// Add properties to the store response to indicate to the client
329
								// that the store could not be loaded.
330
								$this->invalidateResponseStore($storeData, $type, $folderEntryID);
0 ignored issues
show
Bug introduced by
$folderEntryID of type string is incompatible with the type array expected by parameter $folderEntryID of Operations::invalidateResponseStore(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

330
								$this->invalidateResponseStore($storeData, $type, /** @scrutinizer ignore-type */ $folderEntryID);
Loading history...
331
332
								// Update the store properties to refer to the shared folder,
333
								// note that we don't care if we have access to the folder or not.
334
								$storeData["props"]["shared_folder_" . $sharedFolder["folder_type"]] = bin2hex($folderEntryID);
335
336
								// Indicate that we don't have access to the store,
337
								// so no more attempts to read properties or open entries.
338
								$store_access = false;
339
340
							// If you access according to the above check, go ahead and retrieve the MAPIFolder object
341
							}
342
							else {
343
								$folderEntryID = hex2bin($storeData["props"]["default_folder_" . $sharedFolder["folder_type"]]);
344
345
								// Update the store properties to refer to the shared folder,
346
								// note that we don't care if we have access to the folder or not.
347
								$storeData["props"]["shared_folder_" . $sharedFolder["folder_type"]] = bin2hex($folderEntryID);
348
349
								try {
350
									// load folder props
351
									$folder = mapi_msgstore_openentry($store, $folderEntryID);
352
								}
353
								catch (MAPIException $e) {
354
									// Add properties to the store response to indicate to the client
355
									// that the store could not be loaded.
356
									$this->invalidateResponseStore($storeData, $type, $folderEntryID);
357
358
									// Indicate that we don't have access to the store,
359
									// so no more attempts to read properties or open entries.
360
									$store_access = false;
361
362
									// We've handled the event
363
									$e->setHandled();
364
								}
365
							}
366
367
							// Check if a error handler already inserted a error folder,
368
							// or if we can insert the real folders here.
369
							if ($store_access === true) {
370
								// check if we need subfolders or not
371
								if ($openSubFolders === true) {
372
									// add folder data (with all subfolders recursively)
373
									// get parent folder's properties
374
									$folderProps = mapi_getprops($folder, $properties);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $folder does not seem to be defined for all execution paths leading up to this point.
Loading history...
375
									$tempFolderProps = $this->setFolder($folderProps);
376
377
									array_push($storeData["folders"]["item"], $tempFolderProps);
378
379
									// get subfolders
380
									if ($tempFolderProps["props"]["has_subfolder"] != false) {
381
										$subfoldersData = [];
382
										$subfoldersData["folders"]["item"] = [];
383
										$this->getSubFolders($folder, $store, $properties, $subfoldersData);
384
385
										$storeData["folders"]["item"] = array_merge($storeData["folders"]["item"], $subfoldersData["folders"]["item"]);
386
									}
387
								}
388
								else {
389
									$folderProps = mapi_getprops($folder, $properties);
390
									$tempFolderProps = $this->setFolder($folderProps);
391
									// We don't load subfolders, this means the user isn't allowed
392
									// to create subfolders, as they should normally be hidden immediately.
393
									$tempFolderProps["props"]["access"] = ($tempFolderProps["props"]["access"] & ~MAPI_ACCESS_CREATE_HIERARCHY);
394
									// We don't load subfolders, so force the 'has_subfolder' property
395
									// to be false, so the UI will not consider loading subfolders.
396
									$tempFolderProps["props"]["has_subfolder"] = false;
397
									array_push($storeData["folders"]["item"], $tempFolderProps);
398
								}
399
							}
400
						}
401
					}
402
				}
403
				array_push($data["item"], $storeData);
404
			}
405
		}
406
407
		return $data;
408
	}
409
410
	/**
411
	 * Helper function to get the subfolders of a Personal Store.
412
	 *
413
	 * @param object $folder        mapi Folder Object
414
	 * @param object $store         Message Store Object
415
	 * @param array  $properties    MAPI property mappings for folders
416
	 * @param array  $storeData     Reference to an array. The folder properties are added to this array.
417
	 * @param mixed  $parentEntryid
418
	 */
419
	public function getSubFolders($folder, $store, $properties, &$storeData, $parentEntryid = false) {
420
		/**
421
		 * remove hidden folders, folders with PR_ATTR_HIDDEN property set
422
		 * should not be shown to the client.
423
		 */
424
		$restriction = [RES_OR, [
425
			[RES_PROPERTY,
426
				[
427
					RELOP => RELOP_EQ,
428
					ULPROPTAG => PR_ATTR_HIDDEN,
429
					VALUE => [PR_ATTR_HIDDEN => false],
430
				],
431
			],
432
			[RES_NOT,
433
				[
434
					[RES_EXIST,
435
						[
436
							ULPROPTAG => PR_ATTR_HIDDEN,
437
						],
438
					],
439
				],
440
			],
441
		]];
442
443
		$hierarchyTable = mapi_folder_gethierarchytable($folder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS);
444
		mapi_table_restrict($hierarchyTable, $restriction, TBL_BATCH);
445
446
		// Also request PR_DEPTH
447
		$columns = array_merge($properties, [PR_DEPTH]);
448
449
		mapi_table_setcolumns($hierarchyTable, $columns);
450
		$columns = null;
451
452
		// Load the hierarchy in bulks
453
		$rows = mapi_table_queryrows($hierarchyTable, $columns, 0, 0x7FFFFFFF);
454
455
		foreach ($rows as $subfolder) {
456
			if ($parentEntryid !== false && isset($subfolder[PR_DEPTH]) && $subfolder[PR_DEPTH] === 1) {
457
				$subfolder[PR_PARENT_ENTRYID] = $parentEntryid;
458
			}
459
			array_push($storeData["folders"]["item"], $this->setFolder($subfolder));
460
		}
461
	}
462
463
	/**
464
	 * Convert MAPI properties into useful XML properties for a folder.
465
	 *
466
	 * @param array $folderProps Properties of a folder
467
	 *
468
	 * @return array List of properties of a folder
469
	 *
470
	 * @todo The name of this function is misleading because it doesn't 'set' anything, it just reads some properties.
471
	 */
472
	public function setFolder($folderProps) {
473
		$props = [
474
			// Identification properties
475
			"entryid" => bin2hex($folderProps[PR_ENTRYID]),
476
			"parent_entryid" => bin2hex($folderProps[PR_PARENT_ENTRYID]),
477
			"store_entryid" => bin2hex($folderProps[PR_STORE_ENTRYID]),
478
			// Scalar properties
479
			"props" => [
480
				"display_name" => $folderProps[PR_DISPLAY_NAME],
481
				"object_type" => isset($folderProps[PR_OBJECT_TYPE]) ? $folderProps[PR_OBJECT_TYPE] : MAPI_FOLDER, // FIXME: Why isn't this always set?
482
				"content_count" => isset($folderProps[PR_CONTENT_COUNT]) ? $folderProps[PR_CONTENT_COUNT] : 0,
483
				"content_unread" => isset($folderProps[PR_CONTENT_UNREAD]) ? $folderProps[PR_CONTENT_UNREAD] : 0,
484
				"has_subfolder" => isset($folderProps[PR_SUBFOLDERS]) ? $folderProps[PR_SUBFOLDERS] : false,
485
				"container_class" => isset($folderProps[PR_CONTAINER_CLASS]) ? $folderProps[PR_CONTAINER_CLASS] : "IPF.Note",
486
				"access" => $folderProps[PR_ACCESS],
487
				"rights" => isset($folderProps[PR_RIGHTS]) ? $folderProps[PR_RIGHTS] : ecRightsNone,
488
				"assoc_content_count" => isset($folderProps[PR_ASSOC_CONTENT_COUNT]) ? $folderProps[PR_ASSOC_CONTENT_COUNT] : 0,
489
			],
490
		];
491
492
		$this->setExtendedFolderFlags($folderProps, $props);
493
494
		return $props;
495
	}
496
497
	/**
498
	 * Function is used to retrieve the favorites and search folders from
499
	 * respective favorites(IPM.Microsoft.WunderBar.Link) and search (IPM.Microsoft.WunderBar.SFInfo)
500
	 * link messages which belongs to associated contains table of IPM_COMMON_VIEWS folder.
501
	 *
502
	 * @param object $commonViewFolder MAPI Folder Object in which the favorites link messages lives
503
	 * @param array  $storeData        Reference to an array. The favorites folder properties are added to this array.
504
	 */
505
	public function getFavoritesFolders($commonViewFolder, &$storeData) {
506
		$table = mapi_folder_getcontentstable($commonViewFolder, MAPI_ASSOCIATED);
507
508
		$restriction = [RES_OR,
509
			[
510
				[RES_PROPERTY,
511
					[
512
						RELOP => RELOP_EQ,
513
						ULPROPTAG => PR_MESSAGE_CLASS,
514
						VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link"],
515
					],
516
				],
517
				[RES_PROPERTY,
518
					[
519
						RELOP => RELOP_EQ,
520
						ULPROPTAG => PR_MESSAGE_CLASS,
521
						VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo"],
522
					],
523
				],
524
			],
525
		];
526
527
		// Get hierarchy table from all FINDERS_ROOT folders of
528
		// all message stores.
529
		$stores = $GLOBALS["mapisession"]->getAllMessageStores();
530
		$finderHierarchyTables = [];
531
		foreach ($stores as $entryid => $store) {
532
			$props = mapi_getprops($store, [PR_DEFAULT_STORE, PR_FINDER_ENTRYID]);
533
			if (!$props[PR_DEFAULT_STORE]) {
534
				continue;
535
			}
536
537
			try {
538
				$finderFolder = mapi_msgstore_openentry($store, $props[PR_FINDER_ENTRYID]);
539
				$hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS);
540
				$finderHierarchyTables[$props[PR_FINDER_ENTRYID]] = $hierarchyTable;
541
			}
542
			catch (MAPIException $e) {
543
				$e->setHandled();
544
				$props = mapi_getprops($store, [PR_DISPLAY_NAME]);
545
				error_log(sprintf(
546
					"Unable to open FINDER_ROOT for store \"%s\": %s (%s)",
547
					$props[PR_DISPLAY_NAME],
548
					mapi_strerror($e->getCode()),
549
					get_mapi_error_name($e->getCode())
550
				));
551
			}
552
		}
553
554
		$rows = mapi_table_queryallrows($table, $GLOBALS["properties"]->getFavoritesFolderProperties(), $restriction);
555
		$faultyLinkMsg = [];
556
		foreach ($rows as $row) {
557
			if (isset($row[PR_WLINK_TYPE]) && $row[PR_WLINK_TYPE] > wblSharedFolder) {
558
				continue;
559
			}
560
561
			try {
562
				if ($row[PR_MESSAGE_CLASS] === "IPM.Microsoft.WunderBar.Link") {
563
					// Find faulty link messages which does not linked to any message. if link message
564
					// does not contains store entryid in which actual message is located then it consider as
565
					// faulty link message.
566
					if (isset($row[PR_WLINK_STORE_ENTRYID]) && empty($row[PR_WLINK_STORE_ENTRYID]) ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && empty($row...TRYID])) || ! IssetNode, Probably Intended Meaning: IssetNode && (empty($row...TRYID]) || ! IssetNode)
Loading history...
567
						!isset($row[PR_WLINK_STORE_ENTRYID])) {
568
						array_push($faultyLinkMsg, $row[PR_ENTRYID]);
569
570
						continue;
571
					}
572
					$props = $this->getFavoriteLinkedFolderProps($row);
573
					if (empty($props)) {
574
						continue;
575
					}
576
				}
577
				elseif ($row[PR_MESSAGE_CLASS] === "IPM.Microsoft.WunderBar.SFInfo") {
578
					$props = $this->getFavoritesLinkedSearchFolderProps($row[PR_WB_SF_ID], $finderHierarchyTables);
579
					if (empty($props)) {
580
						continue;
581
					}
582
				}
583
			}
584
			catch (MAPIException $e) {
585
				continue;
586
			}
587
588
			array_push($storeData['favorites']['item'], $this->setFavoritesFolder($props));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $props does not seem to be defined for all execution paths leading up to this point.
Loading history...
589
		}
590
591
		if (!empty($faultyLinkMsg)) {
592
			// remove faulty link messages from common view folder.
593
			mapi_folder_deletemessages($commonViewFolder, $faultyLinkMsg);
594
		}
595
	}
596
597
	/**
598
	 * Function which checks whether given linked Message is faulty or not.
599
	 * It will store faulty linked messages in given &$faultyLinkMsg array.
600
	 * Returns true if linked message of favorite item is faulty.
601
	 *
602
	 * @param array  &$faultyLinkMsg   reference in which faulty linked messages will be stored
603
	 * @param array  $allMessageStores Associative array with entryid -> mapistore of all open stores (private, public, delegate)
604
	 * @param object $linkedMessage    link message which belongs to associated contains table of IPM_COMMON_VIEWS folder
605
	 *
606
	 * @return true if linked message of favorite item is faulty or false
607
	 */
608
	public function checkFaultyFavoritesLinkedFolder(&$faultyLinkMsg, $allMessageStores, $linkedMessage) {
609
		// Find faulty link messages which does not linked to any message. if link message
610
		// does not contains store entryid in which actual message is located then it consider as
611
		// faulty link message.
612
		if (isset($linkedMessage[PR_WLINK_STORE_ENTRYID]) && empty($linkedMessage[PR_WLINK_STORE_ENTRYID])) {
613
			array_push($faultyLinkMsg, $linkedMessage[PR_ENTRYID]);
614
615
			return true;
616
		}
617
618
		// Check if store of a favorite Item does not exist in Hierarchy then
619
		// delete link message of that favorite item.
620
		// i.e. If a user is unhooked then remove its favorite items.
621
		$storeExist = array_key_exists($linkedMessage[PR_WLINK_STORE_ENTRYID], $allMessageStores);
622
		if (!$storeExist) {
623
			array_push($faultyLinkMsg, $linkedMessage[PR_ENTRYID]);
624
625
			return true;
626
		}
627
628
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type true.
Loading history...
629
	}
630
631
	/**
632
	 * Function which get the favorites marked folders from favorites link message
633
	 * which belongs to associated contains table of IPM_COMMON_VIEWS folder.
634
	 *
635
	 * @param array $linkMessageProps properties of link message which belongs to
636
	 *                                associated contains table of IPM_COMMON_VIEWS folder
637
	 *
638
	 * @return array List of properties of a folder
639
	 */
640
	public function getFavoriteLinkedFolderProps($linkMessageProps) {
641
		// In webapp we use IPM_SUBTREE as root folder for the Hierarchy but OL is use IMsgStore as a
642
		// Root folder. OL never mark favorites to IPM_SUBTREE. So to make favorites compatible with OL
643
		// we need this check.
644
		// Here we check PR_WLINK_STORE_ENTRYID and PR_WLINK_ENTRYID is same. Which same only in one case
645
		// where some user has mark favorites to root(Inbox-<user name>) folder from OL. So here if condition
646
		// gets true we get the IPM_SUBTREE and send it to response as favorites folder to webapp.
647
		try {
648
			if ($GLOBALS['entryid']->compareEntryIds($linkMessageProps[PR_WLINK_STORE_ENTRYID], $linkMessageProps[PR_WLINK_ENTRYID])) {
649
				$storeObj = $GLOBALS["mapisession"]->openMessageStore($linkMessageProps[PR_WLINK_STORE_ENTRYID]);
650
				$subTreeEntryid = mapi_getprops($storeObj, [PR_IPM_SUBTREE_ENTRYID]);
651
				$folderObj = mapi_msgstore_openentry($storeObj, $subTreeEntryid[PR_IPM_SUBTREE_ENTRYID]);
652
			}
653
			else {
654
				$storeObj = $GLOBALS["mapisession"]->openMessageStore($linkMessageProps[PR_WLINK_STORE_ENTRYID]);
655
				if (!is_resource($storeObj)) {
656
					return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
657
				}
658
				$folderObj = mapi_msgstore_openentry($storeObj, $linkMessageProps[PR_WLINK_ENTRYID]);
659
			}
660
661
			return mapi_getprops($folderObj, $GLOBALS["properties"]->getFavoritesFolderProperties());
662
		}
663
		catch (Exception $e) {
664
			// in some cases error_log was causing an endless loop, so disable it for now
665
			// error_log($e);
666
		}
667
668
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
669
	}
670
671
	/**
672
	 * Function which retrieve the search folder from FINDERS_ROOT folder of all open
673
	 * message store.
674
	 *
675
	 * @param string $searchFolderId        contains a GUID that identifies the search folder.
676
	 *                                      The value of this property MUST NOT change.
677
	 * @param array  $finderHierarchyTables hierarchy tables which belongs to FINDERS_ROOT
678
	 *                                      folder of message stores
679
	 *
680
	 * @return array list of search folder properties
681
	 */
682
	public function getFavoritesLinkedSearchFolderProps($searchFolderId, $finderHierarchyTables) {
683
		$restriction = [RES_EXIST,
684
			[
685
				ULPROPTAG => PR_EXTENDED_FOLDER_FLAGS,
686
			],
687
		];
688
689
		foreach ($finderHierarchyTables as $finderEntryid => $hierarchyTable) {
690
			$rows = mapi_table_queryallrows($hierarchyTable, $GLOBALS["properties"]->getFavoritesFolderProperties(), $restriction);
691
			foreach ($rows as $row) {
692
				$flags = unpack("H2ExtendedFlags-Id/H2ExtendedFlags-Cb/H8ExtendedFlags-Data/H2SearchFolderTag-Id/H2SearchFolderTag-Cb/H8SearchFolderTag-Data/H2SearchFolderId-Id/H2SearchFolderId-Cb/H32SearchFolderId-Data", $row[PR_EXTENDED_FOLDER_FLAGS]);
693
				if ($flags["SearchFolderId-Data"] === bin2hex($searchFolderId)) {
694
					return $row;
695
				}
696
			}
697
		}
698
	}
699
700
	/**
701
	 * Create link messages for default favorites(Inbox and Sent Items) folders in associated contains table of IPM_COMMON_VIEWS folder
702
	 * and remove all other link message from the same.
703
	 *
704
	 * @param string $commonViewFolderEntryid IPM_COMMON_VIEWS folder entryid
705
	 * @param object $store                   Message Store Object
706
	 * @param array  $storeData               the store data which use to create restriction
707
	 */
708
	public function setDefaultFavoritesFolder($commonViewFolderEntryid, $store, $storeData) {
709
		if ($GLOBALS["settings"]->get("zarafa/v1/contexts/hierarchy/show_default_favorites") !== false) {
710
			$commonViewFolder = mapi_msgstore_openentry($store, $commonViewFolderEntryid);
711
712
			$inboxFolderEntryid = hex2bin($storeData["props"]["default_folder_inbox"]);
713
			$sentFolderEntryid = hex2bin($storeData["props"]["default_folder_sent"]);
714
715
			$table = mapi_folder_getcontentstable($commonViewFolder, MAPI_ASSOCIATED);
716
717
			// Restriction for get all link message(IPM.Microsoft.WunderBar.Link)
718
			// and search link message (IPM.Microsoft.WunderBar.SFInfo) from
719
			// Associated contains table of IPM_COMMON_VIEWS folder.
720
			$findLinkMsgRestriction = [RES_OR,
721
				[
722
					[RES_PROPERTY,
723
						[
724
							RELOP => RELOP_EQ,
725
							ULPROPTAG => PR_MESSAGE_CLASS,
726
							VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link"],
727
						],
728
					],
729
					[RES_PROPERTY,
730
						[
731
							RELOP => RELOP_EQ,
732
							ULPROPTAG => PR_MESSAGE_CLASS,
733
							VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo"],
734
						],
735
					],
736
				],
737
			];
738
739
			// Restriction for find Inbox and/or Sent folder link message from
740
			// Associated contains table of IPM_COMMON_VIEWS folder.
741
			$findInboxOrSentLinkMessage = [RES_OR,
742
				[
743
					[RES_PROPERTY,
744
						[
745
							RELOP => RELOP_EQ,
746
							ULPROPTAG => PR_WLINK_ENTRYID,
747
							VALUE => [PR_WLINK_ENTRYID => $inboxFolderEntryid],
748
						],
749
					],
750
					[RES_PROPERTY,
751
						[
752
							RELOP => RELOP_EQ,
753
							ULPROPTAG => PR_WLINK_ENTRYID,
754
							VALUE => [PR_WLINK_ENTRYID => $sentFolderEntryid],
755
						],
756
					],
757
				],
758
			];
759
760
			// Restriction to get all link messages except Inbox and Sent folder's link messages from
761
			// Associated contains table of IPM_COMMON_VIEWS folder, if exist in it.
762
			$restriction = [RES_AND,
763
				[
764
					$findLinkMsgRestriction,
765
					[RES_NOT,
766
						[
767
							$findInboxOrSentLinkMessage,
768
						],
769
					],
770
				],
771
			];
772
773
			$rows = mapi_table_queryallrows($table, [PR_ENTRYID], $restriction);
774
			if (!empty($rows)) {
775
				$deleteMessages = [];
776
				foreach ($rows as $row) {
777
					array_push($deleteMessages, $row[PR_ENTRYID]);
778
				}
779
				mapi_folder_deletemessages($commonViewFolder, $deleteMessages);
780
			}
781
782
			// We need to remove all search folder from FIND_ROOT(search root folder)
783
			// when reset setting was triggered because on reset setting we remove all
784
			// link messages from common view folder which are linked with either
785
			// favorites or search folder.
786
			$finderFolderEntryid = hex2bin($storeData["props"]["finder_entryid"]);
787
			$finderFolder = mapi_msgstore_openentry($store, $finderFolderEntryid);
788
			$hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS);
789
			$folders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]);
790
			foreach ($folders as $folder) {
791
				try {
792
					mapi_folder_deletefolder($finderFolder, $folder[PR_ENTRYID]);
793
				}
794
				catch (MAPIException $e) {
795
					$msg = "Problem in deleting search folder while reset settings. MAPI Error %s.";
796
					$formattedMsg = sprintf($msg, get_mapi_error_name($e->getCode()));
797
					error_log($formattedMsg);
798
					Log::Write(LOGLEVEL_ERROR, "Operations:setDefaultFavoritesFolder() " . $formattedMsg);
799
				}
800
			}
801
			// Restriction used to find only Inbox and Sent folder's link messages from
802
			// Associated contains table of IPM_COMMON_VIEWS folder, if exist in it.
803
			$restriction = [RES_AND,
804
				[
805
					$findLinkMsgRestriction,
806
					$findInboxOrSentLinkMessage,
807
				],
808
			];
809
810
			$rows = mapi_table_queryallrows($table, [PR_WLINK_ENTRYID], $restriction);
811
812
			// If Inbox and Sent folder's link messages are not exist then create the
813
			// link message for those in associated contains table of IPM_COMMON_VIEWS folder.
814
			if (empty($rows)) {
815
				$defaultFavFoldersKeys = ["inbox", "sent"];
816
				foreach ($defaultFavFoldersKeys as $folderKey) {
817
					$folderObj = $GLOBALS["mapisession"]->openMessage(hex2bin($storeData["props"]["default_folder_" . $folderKey]));
818
					$props = mapi_getprops($folderObj, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]);
819
					$this->createFavoritesLink($commonViewFolder, $props);
820
				}
821
			}
822
			elseif (count($rows) < 2) {
823
				// If rows count is less than 2 it means associated contains table of IPM_COMMON_VIEWS folder
824
				// can have either Inbox or Sent folder link message in it. So we have to create link message
825
				// for Inbox or Sent folder which ever not exist in associated contains table of IPM_COMMON_VIEWS folder
826
				// to maintain default favorites folder.
827
				$row = $rows[0];
828
				$wlinkEntryid = $row[PR_WLINK_ENTRYID];
829
830
				$isInboxFolder = $GLOBALS['entryid']->compareEntryIds($wlinkEntryid, $inboxFolderEntryid);
831
832
				if (!$isInboxFolder) {
833
					$folderObj = $GLOBALS["mapisession"]->openMessage($inboxFolderEntryid);
834
				}
835
				else {
836
					$folderObj = $GLOBALS["mapisession"]->openMessage($sentFolderEntryid);
837
				}
838
839
				$props = mapi_getprops($folderObj, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]);
840
				$this->createFavoritesLink($commonViewFolder, $props);
841
			}
842
			$GLOBALS["settings"]->set("zarafa/v1/contexts/hierarchy/show_default_favorites", false, true);
843
		}
844
	}
845
846
	/**
847
	 * Create favorites link message (IPM.Microsoft.WunderBar.Link) or
848
	 * search link message ("IPM.Microsoft.WunderBar.SFInfo") in associated
849
	 * contains table of IPM_COMMON_VIEWS folder.
850
	 *
851
	 * @param object      $commonViewFolder MAPI Message Folder Object
852
	 * @param array       $folderProps      Properties of a folder
853
	 * @param bool|string $searchFolderId   search folder id which is used to identify the
854
	 *                                      linked search folder from search link message. by default it is false.
855
	 */
856
	public function createFavoritesLink($commonViewFolder, $folderProps, $searchFolderId = false) {
857
		if ($searchFolderId) {
858
			$props = [
859
				PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo",
860
				PR_WB_SF_ID => $searchFolderId,
861
				PR_WLINK_TYPE => wblSearchFolder,
862
			];
863
		}
864
		else {
865
			$defaultStoreEntryId = hex2bin($GLOBALS['mapisession']->getDefaultMessageStoreEntryId());
866
			$props = [
867
				PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link",
868
				PR_WLINK_ENTRYID => $folderProps[PR_ENTRYID],
869
				PR_WLINK_STORE_ENTRYID => $folderProps[PR_STORE_ENTRYID],
870
				PR_WLINK_TYPE => $GLOBALS['entryid']->compareEntryIds($defaultStoreEntryId, $folderProps[PR_STORE_ENTRYID]) ? wblNormalFolder : wblSharedFolder,
871
			];
872
		}
873
874
		$favoritesLinkMsg = mapi_folder_createmessage($commonViewFolder, MAPI_ASSOCIATED);
875
		mapi_setprops($favoritesLinkMsg, $props);
876
		mapi_savechanges($favoritesLinkMsg);
877
	}
878
879
	/**
880
	 * Convert MAPI properties into useful and human readable string for favorites folder.
881
	 *
882
	 * @param array $folderProps Properties of a folder
883
	 *
884
	 * @return array List of properties of a folder
885
	 */
886
	public function setFavoritesFolder($folderProps) {
887
		$props = $this->setFolder($folderProps);
888
		// Add and Make isFavorites to true, this allows the client to properly
889
		// indicate to the user that this is a favorites item/folder.
890
		$props["props"]["isFavorites"] = true;
891
		$props["props"]["folder_type"] = $folderProps[PR_FOLDER_TYPE];
892
893
		return $props;
894
	}
895
896
	/**
897
	 * Fetches extended flags for folder. If PR_EXTENDED_FLAGS is not set then we assume that client
898
	 * should handle which property to display.
899
	 *
900
	 * @param array $folderProps Properties of a folder
901
	 * @param array $props       properties in which flags should be set
902
	 */
903
	public function setExtendedFolderFlags($folderProps, &$props) {
904
		if (isset($folderProps[PR_EXTENDED_FOLDER_FLAGS])) {
905
			$flags = unpack("Cid/Cconst/Cflags", $folderProps[PR_EXTENDED_FOLDER_FLAGS]);
906
907
			// ID property is '1' this means 'Data' property contains extended flags
908
			if ($flags["id"] == 1) {
909
				$props["props"]["extended_flags"] = $flags["flags"];
910
			}
911
		}
912
	}
913
914
	/**
915
	 * Used to update the storeData with a folder and properties that will
916
	 * inform the user that the store could not be opened.
917
	 *
918
	 * @param array  &$storeData    The store data which will be updated
919
	 * @param string $folderType    The foldertype which was attempted to be loaded
920
	 * @param array  $folderEntryID The entryid of the which was attempted to be opened
921
	 */
922
	public function invalidateResponseStore(&$storeData, $folderType, $folderEntryID) {
923
		$folderName = "Folder";
924
		$containerClass = "IPF.Note";
925
926
		switch ($folderType) {
927
			case "all":
928
				$folderName = "IPM_SUBTREE";
929
				$containerClass = "IPF.Note";
930
				break;
931
932
			case "calendar":
933
				$folderName = _("Calendar");
934
				$containerClass = "IPF.Appointment";
935
				break;
936
937
			case "contact":
938
				$folderName = _("Contacts");
939
				$containerClass = "IPF.Contact";
940
				break;
941
942
			case "inbox":
943
				$folderName = _("Inbox");
944
				$containerClass = "IPF.Note";
945
				break;
946
947
			case "note":
948
				$folderName = _("Notes");
949
				$containerClass = "IPF.StickyNote";
950
				break;
951
952
			case "task":
953
				$folderName = _("Tasks");
954
				$containerClass = "IPF.Task";
955
				break;
956
		}
957
958
		// Insert a fake folder which will be shown to the user
959
		// to acknowledge that he has a shared store, but also
960
		// to indicate that he can't open it.
961
		$tempFolderProps = $this->setFolder([
962
			PR_ENTRYID => $folderEntryID,
963
			PR_PARENT_ENTRYID => hex2bin($storeData["props"]["subtree_entryid"]),
964
			PR_STORE_ENTRYID => hex2bin($storeData["store_entryid"]),
965
			PR_DISPLAY_NAME => $folderName,
966
			PR_OBJECT_TYPE => MAPI_FOLDER,
967
			PR_SUBFOLDERS => false,
968
			PR_CONTAINER_CLASS => $containerClass,
969
			PR_ACCESS => 0,
970
		]);
971
972
		// Mark the folder as unavailable, this allows the client to properly
973
		// indicate to the user that this is a fake entry.
974
		$tempFolderProps['props']['is_unavailable'] = true;
975
976
		array_push($storeData["folders"]["item"], $tempFolderProps);
977
978
		/* TRANSLATORS: This indicates that the opened folder belongs to a particular user,
979
		 * for example: 'Calendar of Holiday', in this case %1$s is 'Calendar' (the foldername)
980
		 * and %2$s is 'Holiday' (the username).
981
		 */
982
		$storeData["props"]["display_name"] = ($folderType === "all") ? $storeData["props"]["display_name"] : sprintf(_('%1$s of %2$s'), $folderName, $storeData["props"]["mailbox_owner_name"]);
983
		$storeData["props"]["subtree_entryid"] = $tempFolderProps["parent_entryid"];
984
		$storeData["props"]["folder_type"] = $folderType;
985
	}
986
987
	/**
988
	 * Used to update the storeData with a folder and properties that will function as a
989
	 * placeholder for the IPMSubtree that could not be opened.
990
	 *
991
	 * @param array &$storeData    The store data which will be updated
992
	 * @param array $folderEntryID The entryid of the which was attempted to be opened
993
	 */
994
	public function getDummyIPMSubtreeFolder(&$storeData, $folderEntryID) {
995
		// Insert a fake folder which will be shown to the user
996
		// to acknowledge that he has a shared store.
997
		$tempFolderProps = $this->setFolder([
998
			PR_ENTRYID => $folderEntryID,
999
			PR_PARENT_ENTRYID => hex2bin($storeData["props"]["subtree_entryid"]),
1000
			PR_STORE_ENTRYID => hex2bin($storeData["store_entryid"]),
1001
			PR_DISPLAY_NAME => "IPM_SUBTREE",
1002
			PR_OBJECT_TYPE => MAPI_FOLDER,
1003
			PR_SUBFOLDERS => true,
1004
			PR_CONTAINER_CLASS => "IPF.Note",
1005
			PR_ACCESS => 0,
1006
		]);
1007
1008
		array_push($storeData["folders"]["item"], $tempFolderProps);
1009
		$storeData["props"]["subtree_entryid"] = $tempFolderProps["parent_entryid"];
1010
	}
1011
1012
	/**
1013
	 * Create a MAPI folder.
1014
	 *
1015
	 * This function simply creates a MAPI folder at a specific location with a specific folder
1016
	 * type.
1017
	 *
1018
	 * @param object $store         MAPI Message Store Object in which the folder lives
1019
	 * @param string $parententryid The parent entryid in which the new folder should be created
1020
	 * @param string $name          The name of the new folder
1021
	 * @param string $type          The type of the folder (PR_CONTAINER_CLASS, so value should be 'IPM.Appointment', etc)
1022
	 * @param array  $folderProps   reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of new folder
1023
	 *
1024
	 * @return bool true if action succeeded, false if not
1025
	 */
1026
	public function createFolder($store, $parententryid, $name, $type, &$folderProps) {
1027
		$result = false;
1028
		$folder = mapi_msgstore_openentry($store, $parententryid);
1029
1030
		if ($folder) {
1031
			/**
1032
			 * @TODO: If parent folder has any sub-folder with the same name than this will return
1033
			 * MAPI_E_COLLISION error, so show this error to client and don't close the dialog.
1034
			 */
1035
			$new_folder = mapi_folder_createfolder($folder, $name);
1036
1037
			if ($new_folder) {
1038
				mapi_setprops($new_folder, [PR_CONTAINER_CLASS => $type]);
1039
				$result = true;
1040
1041
				$folderProps = mapi_getprops($new_folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1042
			}
1043
		}
1044
1045
		return $result;
1046
	}
1047
1048
	/**
1049
	 * Rename a folder.
1050
	 *
1051
	 * This function renames the specified folder. However, a conflict situation can arise
1052
	 * if the specified folder name already exists. In this case, the folder name is postfixed with
1053
	 * an ever-higher integer to create a unique folder name.
1054
	 *
1055
	 * @param object $store       MAPI Message Store Object
1056
	 * @param string $entryid     The entryid of the folder to rename
1057
	 * @param string $name        The new name of the folder
1058
	 * @param array  $folderProps reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID
1059
	 *
1060
	 * @return bool true if action succeeded, false if not
1061
	 */
1062
	public function renameFolder($store, $entryid, $name, &$folderProps) {
1063
		$folder = mapi_msgstore_openentry($store, $entryid);
1064
		if (!$folder) {
1065
			return false;
1066
		}
1067
		$result = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
1068
		$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]);
1069
1070
		/*
1071
		 * If parent folder has any sub-folder with the same name than this will return
1072
		 * MAPI_E_COLLISION error while renaming folder, so show this error to client,
1073
		 * and revert changes in view.
1074
		 */
1075
		try {
1076
			mapi_setprops($folder, [PR_DISPLAY_NAME => $name]);
1077
			mapi_savechanges($folder);
1078
			$result = true;
1079
		}
1080
		catch (MAPIException $e) {
1081
			if ($e->getCode() == MAPI_E_COLLISION) {
1082
				/*
1083
				 * revert folder name to original one
1084
				 * There is a bug in php-mapi that updates folder name in hierarchy table with null value
1085
				 * so we need to revert those change by again setting the old folder name
1086
				 * (ZCP-11586)
1087
				 */
1088
				mapi_setprops($folder, [PR_DISPLAY_NAME => $folderProps[PR_DISPLAY_NAME]]);
1089
				mapi_savechanges($folder);
1090
			}
1091
1092
			// rethrow exception so we will send error to client
1093
			throw $e;
1094
		}
1095
1096
		return $result;
1097
	}
1098
1099
	/**
1100
	 * Check if a folder is 'special'.
1101
	 *
1102
	 * All default MAPI folders such as 'inbox', 'outbox', etc have special permissions; you can not rename them for example. This
1103
	 * function returns TRUE if the specified folder is 'special'.
1104
	 *
1105
	 * @param object $store   MAPI Message Store Object
1106
	 * @param string $entryid The entryid of the folder
1107
	 *
1108
	 * @return bool true if folder is a special folder, false if not
1109
	 */
1110
	public function isSpecialFolder($store, $entryid) {
1111
		$msgstore_props = mapi_getprops($store, [PR_MDB_PROVIDER]);
1112
1113
		// "special" folders don't exists in public store
1114
		if ($msgstore_props[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
1115
			return false;
1116
		}
1117
1118
		// Check for the Special folders which are provided on the store
1119
		$msgstore_props = mapi_getprops($store, [
1120
			PR_IPM_SUBTREE_ENTRYID,
1121
			PR_IPM_OUTBOX_ENTRYID,
1122
			PR_IPM_SENTMAIL_ENTRYID,
1123
			PR_IPM_WASTEBASKET_ENTRYID,
1124
			PR_IPM_PUBLIC_FOLDERS_ENTRYID,
1125
			PR_IPM_FAVORITES_ENTRYID,
1126
		]);
1127
1128
		if (array_search($entryid, $msgstore_props)) {
1129
			return true;
1130
		}
1131
1132
		// Check for the Special folders which are provided on the root folder
1133
		$root = mapi_msgstore_openentry($store, null);
1134
		$rootProps = mapi_getprops($root, [
1135
			PR_IPM_APPOINTMENT_ENTRYID,
1136
			PR_IPM_CONTACT_ENTRYID,
1137
			PR_IPM_DRAFTS_ENTRYID,
1138
			PR_IPM_JOURNAL_ENTRYID,
1139
			PR_IPM_NOTE_ENTRYID,
1140
			PR_IPM_TASK_ENTRYID,
1141
			PR_ADDITIONAL_REN_ENTRYIDS,
1142
		]);
1143
1144
		if (array_search($entryid, $rootProps)) {
1145
			return true;
1146
		}
1147
1148
		// The PR_ADDITIONAL_REN_ENTRYIDS are a bit special
1149
		if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS]) && is_array($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
1150
			if (array_search($entryid, $rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
1151
				return true;
1152
			}
1153
		}
1154
1155
		// Check if the given folder is the inbox, note that we are unsure
1156
		// if we have permissions on that folder, so we need a try catch.
1157
		try {
1158
			$inbox = mapi_msgstore_getreceivefolder($store);
1159
			$props = mapi_getprops($inbox, [PR_ENTRYID]);
1160
1161
			if ($props[PR_ENTRYID] == $entryid) {
1162
				return true;
1163
			}
1164
		}
1165
		catch (MAPIException $e) {
1166
			if ($e->getCode() !== MAPI_E_NO_ACCESS) {
1167
				throw $e;
1168
			}
1169
		}
1170
1171
		return false;
1172
	}
1173
1174
	/**
1175
	 * Delete a folder.
1176
	 *
1177
	 * Deleting a folder normally just moves the folder to the wastebasket, which is what this function does. However,
1178
	 * if the folder was already in the wastebasket, then the folder is really deleted.
1179
	 *
1180
	 * @param object $store         MAPI Message Store Object
1181
	 * @param string $parententryid The parent in which the folder should be deleted
1182
	 * @param string $entryid       The entryid of the folder which will be deleted
1183
	 * @param array  $folderProps   reference to an array which will be filled with PR_ENTRYID, PR_STORE_ENTRYID of the deleted object
1184
	 * @param bool   $softDelete    flag for indicating that folder should be soft deleted which can be recovered from
1185
	 *                              restore deleted items
1186
	 * @param bool   $hardDelete    flag for indicating that folder should be hard deleted from system and can not be
1187
	 *                              recovered from restore soft deleted items
1188
	 *
1189
	 * @return bool true if action succeeded, false if not
1190
	 *
1191
	 * @todo subfolders of folders in the wastebasket should also be hard-deleted
1192
	 */
1193
	public function deleteFolder($store, $parententryid, $entryid, &$folderProps, $softDelete = false, $hardDelete = false) {
1194
		$result = false;
1195
		$msgprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID]);
1196
		$folder = mapi_msgstore_openentry($store, $parententryid);
1197
1198
		if ($folder && !$this->isSpecialFolder($store, $entryid)) {
1199
			if ($hardDelete === true) {
1200
				// hard delete the message if requested
1201
				// beware that folder can not be recovered after this and will be deleted from system entirely
1202
				if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS | DELETE_HARD_DELETE)) {
1203
					$result = true;
1204
1205
					// if exists, also delete settings made for this folder (client don't need an update for this)
1206
					$GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid));
1207
				}
1208
			}
1209
			else {
1210
				if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID])) {
1211
					// TODO: check if not only $parententryid=wastebasket, but also the parents of that parent...
1212
					// if folder is already in wastebasket or softDelete is requested then delete the message
1213
					if ($msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete === true) {
1214
						if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS)) {
1215
							$result = true;
1216
1217
							// if exists, also delete settings made for this folder (client don't need an update for this)
1218
							$GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid));
1219
						}
1220
					}
1221
					else {
1222
						// move the folder to wastebasket
1223
						$wastebasket = mapi_msgstore_openentry($store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID]);
1224
1225
						$deleted_folder = mapi_msgstore_openentry($store, $entryid);
1226
						$props = mapi_getprops($deleted_folder, [PR_DISPLAY_NAME]);
1227
1228
						try {
1229
							/*
1230
							 * To decrease overload of checking for conflicting folder names on modification of every folder
1231
							 * we should first try to copy folder and if it returns MAPI_E_COLLISION then
1232
							 * only we should check for the conflicting folder names and generate a new name
1233
							 * and copy folder with the generated name.
1234
							 */
1235
							mapi_folder_copyfolder($folder, $entryid, $wastebasket, $props[PR_DISPLAY_NAME], FOLDER_MOVE);
1236
							$folderProps = mapi_getprops($deleted_folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1237
							$result = true;
1238
						}
1239
						catch (MAPIException $e) {
1240
							if ($e->getCode() == MAPI_E_COLLISION) {
1241
								$foldername = $this->checkFolderNameConflict($store, $wastebasket, $props[PR_DISPLAY_NAME]);
1242
1243
								mapi_folder_copyfolder($folder, $entryid, $wastebasket, $foldername, FOLDER_MOVE);
1244
								$folderProps = mapi_getprops($deleted_folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1245
								$result = true;
1246
							}
1247
							else {
1248
								// all other errors should be propagated to higher level exception handlers
1249
								throw $e;
1250
							}
1251
						}
1252
					}
1253
				}
1254
				else {
1255
					if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS)) {
1256
						$result = true;
1257
1258
						// if exists, also delete settings made for this folder (client don't need an update for this)
1259
						$GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid));
1260
					}
1261
				}
1262
			}
1263
		}
1264
1265
		return $result;
1266
	}
1267
1268
	/**
1269
	 * Empty folder.
1270
	 *
1271
	 * Removes all items from a folder. This is a real delete, not a move.
1272
	 *
1273
	 * @param object $store           MAPI Message Store Object
1274
	 * @param string $entryid         The entryid of the folder which will be emptied
1275
	 * @param array  $folderProps     reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of the emptied folder
1276
	 * @param bool   $hardDelete      flag to indicate if messages will be hard deleted and can not be recoved using restore soft deleted items
1277
	 * @param bool   $emptySubFolders true to remove all messages with child folders of selected folder else false will
1278
	 *                                remove only message of selected folder
1279
	 *
1280
	 * @return bool true if action succeeded, false if not
1281
	 */
1282
	public function emptyFolder($store, $entryid, &$folderProps, $hardDelete = false, $emptySubFolders = true) {
1283
		$result = false;
1284
		$folder = mapi_msgstore_openentry($store, $entryid);
1285
1286
		if ($folder) {
1287
			$flag = DEL_ASSOCIATED;
1288
1289
			if ($hardDelete) {
1290
				$flag |= DELETE_HARD_DELETE;
1291
			}
1292
1293
			if ($emptySubFolders) {
1294
				$result = mapi_folder_emptyfolder($folder, $flag);
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
1295
			}
1296
			else {
1297
				// Delete all items of selected folder without
1298
				// removing child folder and it's content.
1299
				// FIXME: it is effecting performance because mapi_folder_emptyfolder function not provide facility to
1300
				// remove only selected folder items without touching child folder and it's items.
1301
				// for more check KC-1268
1302
				$table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS);
1303
				$rows = mapi_table_queryallrows($table, [PR_ENTRYID]);
1304
				$messages = [];
1305
				foreach ($rows as $row) {
1306
					array_push($messages, $row[PR_ENTRYID]);
1307
				}
1308
				$result = mapi_folder_deletemessages($folder, $messages, $flag);
1309
			}
1310
1311
			$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1312
			$result = true;
1313
		}
1314
1315
		return $result;
1316
	}
1317
1318
	/**
1319
	 * Copy or move a folder.
1320
	 *
1321
	 * @param object $store               MAPI Message Store Object
1322
	 * @param string $parentfolderentryid The parent entryid of the folder which will be copied or moved
1323
	 * @param string $sourcefolderentryid The entryid of the folder which will be copied or moved
1324
	 * @param string $destfolderentryid   The entryid of the folder which the folder will be copied or moved to
1325
	 * @param bool   $moveFolder          true - move folder, false - copy folder
1326
	 * @param array  $folderProps         reference to an array which will be filled with entryids
1327
	 * @param mixed  $deststore
1328
	 *
1329
	 * @return bool true if action succeeded, false if not
1330
	 */
1331
	public function copyFolder($store, $parentfolderentryid, $sourcefolderentryid, $destfolderentryid, $deststore, $moveFolder, &$folderProps) {
1332
		$result = false;
1333
		$sourceparentfolder = mapi_msgstore_openentry($store, $parentfolderentryid);
1334
		$destfolder = mapi_msgstore_openentry($deststore, $destfolderentryid);
1335
		if (!$this->isSpecialFolder($store, $sourcefolderentryid) && $sourceparentfolder && $destfolder && $deststore) {
1336
			$folder = mapi_msgstore_openentry($store, $sourcefolderentryid);
1337
			$props = mapi_getprops($folder, [PR_DISPLAY_NAME]);
1338
1339
			try {
1340
				/*
1341
				  * To decrease overload of checking for conflicting folder names on modification of every folder
1342
				  * we should first try to copy/move folder and if it returns MAPI_E_COLLISION then
1343
				  * only we should check for the conflicting folder names and generate a new name
1344
				  * and copy/move folder with the generated name.
1345
				  */
1346
				if ($moveFolder) {
1347
					mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $props[PR_DISPLAY_NAME], FOLDER_MOVE);
1348
					$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1349
					// In some cases PR_PARENT_ENTRYID is not available in mapi_getprops, add it manually
1350
					$folderProps[PR_PARENT_ENTRYID] = $destfolderentryid;
1351
					$result = true;
1352
				}
1353
				else {
1354
					mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $props[PR_DISPLAY_NAME], COPY_SUBFOLDERS);
1355
					$result = true;
1356
				}
1357
			}
1358
			catch (MAPIException $e) {
1359
				if ($e->getCode() == MAPI_E_COLLISION) {
1360
					$foldername = $this->checkFolderNameConflict($deststore, $destfolder, $props[PR_DISPLAY_NAME]);
1361
					if ($moveFolder) {
1362
						mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $foldername, FOLDER_MOVE);
1363
						$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1364
						$result = true;
1365
					}
1366
					else {
1367
						mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $foldername, COPY_SUBFOLDERS);
1368
						$result = true;
1369
					}
1370
				}
1371
				else {
1372
					// all other errors should be propagated to higher level exception handlers
1373
					throw $e;
1374
				}
1375
			}
1376
		}
1377
1378
		return $result;
1379
	}
1380
1381
	/**
1382
	 * Read MAPI table.
1383
	 *
1384
	 * This function performs various operations to open, setup, and read all rows from a MAPI table.
1385
	 *
1386
	 * The output from this function is an XML array structure which can be sent directly to XML serialisation.
1387
	 *
1388
	 * @param object $store        MAPI Message Store Object
1389
	 * @param string $entryid      The entryid of the folder to read the table from
1390
	 * @param array  $properties   The set of properties which will be read
1391
	 * @param array  $sort         The set properties which the table will be sort on (formatted as a MAPI sort order)
1392
	 * @param int    $start        Starting row at which to start reading rows
1393
	 * @param int    $rowcount     Number of rows which should be read
1394
	 * @param array  $restriction  Table restriction to apply to the table (formatted as MAPI restriction)
1395
	 * @param mixed  $getHierarchy
1396
	 * @param mixed  $flags
1397
	 *
1398
	 * @return array XML array structure with row data
1399
	 */
1400
	public function getTable($store, $entryid, $properties, $sort, $start, $rowcount = false, $restriction = false, $getHierarchy = false, $flags = MAPI_DEFERRED_ERRORS) {
1401
		$data = [];
1402
		$folder = mapi_msgstore_openentry($store, $entryid);
1403
1404
		if ($folder) {
1405
			$table = $getHierarchy ? mapi_folder_gethierarchytable($folder, $flags) : mapi_folder_getcontentstable($folder, $flags);
1406
1407
			if (!$rowcount) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rowcount of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1408
				$rowcount = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50);
1409
			}
1410
1411
			if (is_array($restriction)) {
1412
				mapi_table_restrict($table, $restriction, TBL_BATCH);
1413
			}
1414
1415
			if (is_array($sort) && !empty($sort)) {
1416
				/*
1417
				 * If the sort array contains the PR_SUBJECT column we should change this to
1418
				 * PR_NORMALIZED_SUBJECT to make sure that when sorting on subjects: "sweet" and
1419
				 * "RE: sweet", the first one is displayed before the latter one. If the subject
1420
				 * is used for sorting the PR_MESSAGE_DELIVERY_TIME must be added as well as
1421
				 * Outlook behaves the same way in this case.
1422
				 */
1423
				if (isset($sort[PR_SUBJECT])) {
1424
					$sortReplace = [];
1425
					foreach ($sort as $key => $value) {
1426
						if ($key == PR_SUBJECT) {
1427
							$sortReplace[PR_NORMALIZED_SUBJECT] = $value;
1428
							$sortReplace[PR_MESSAGE_DELIVERY_TIME] = TABLE_SORT_DESCEND;
1429
						}
1430
						else {
1431
							$sortReplace[$key] = $value;
1432
						}
1433
					}
1434
					$sort = $sortReplace;
1435
				}
1436
1437
				mapi_table_sort($table, $sort, TBL_BATCH);
1438
			}
1439
1440
			$data["item"] = [];
1441
1442
			$rows = mapi_table_queryrows($table, $properties, $start, $rowcount);
1443
			foreach ($rows as $row) {
1444
				$itemData = Conversion::mapMAPI2XML($properties, $row);
1445
1446
				// For ZARAFA type users the email_address properties are filled with the username
1447
				// Here we will copy that property to the *_username property for consistency with
1448
				// the getMessageProps() function
1449
				// We will not retrieve the real email address (like the getMessageProps function does)
1450
				// for all items because that would be a performance decrease!
1451
				if (isset($itemData['props']["sent_representing_email_address"])) {
1452
					$itemData['props']["sent_representing_username"] = $itemData['props']["sent_representing_email_address"];
1453
				}
1454
				if (isset($itemData['props']["sender_email_address"])) {
1455
					$itemData['props']["sender_username"] = $itemData['props']["sender_email_address"];
1456
				}
1457
				if (isset($itemData['props']["received_by_email_address"])) {
1458
					$itemData['props']["received_by_username"] = $itemData['props']["received_by_email_address"];
1459
				}
1460
1461
				array_push($data["item"], $itemData);
1462
			}
1463
1464
			// Update the page information
1465
			$data["page"] = [];
1466
			$data["page"]["start"] = $start;
1467
			$data["page"]["rowcount"] = $rowcount;
1468
			$data["page"]["totalrowcount"] = mapi_table_getrowcount($table);
1469
		}
1470
1471
		return $data;
1472
	}
1473
1474
	/**
1475
	 * Returns TRUE of the MAPI message only has inline attachments.
1476
	 *
1477
	 * @param resource $message The MAPI message object to check
1478
	 *
1479
	 * @return bool TRUE if the item contains only inline attachments, FALSE otherwise
1480
	 *
1481
	 * @deprecated This function is not used, because it is much too slow to run on all messages in your inbox
1482
	 */
1483
	public function hasOnlyInlineAttachments($message) {
1484
		$attachmentTable = mapi_message_getattachmenttable($message);
1485
		if ($attachmentTable) {
1486
			$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACHMENT_HIDDEN]);
1487
			foreach ($attachments as $attachmentRow) {
1488
				if (!isset($attachmentRow[PR_ATTACHMENT_HIDDEN]) || !$attachmentRow[PR_ATTACHMENT_HIDDEN]) {
1489
					return false;
1490
				}
1491
			}
1492
		}
1493
1494
		return true;
1495
	}
1496
1497
	/**
1498
	 * Read message properties.
1499
	 *
1500
	 * Reads a message and returns the data as an XML array structure with all data from the message that is needed
1501
	 * to show a message (for example in the preview pane)
1502
	 *
1503
	 * @param object $store      MAPI Message Store Object
1504
	 * @param object $message    The MAPI Message Object
1505
	 * @param array  $properties Mapping of properties that should be read
1506
	 * @param bool   $html2text  true - body will be converted from html to text, false - html body will be returned
1507
	 *
1508
	 * @return array item properties
1509
	 *
1510
	 * @todo Function name is misleading as it doesn't just get message properties
1511
	 */
1512
	public function getMessageProps($store, $message, $properties, $html2text = false) {
1513
		$props = [];
1514
1515
		if ($message) {
0 ignored issues
show
introduced by
$message is of type object, thus it always evaluated to true.
Loading history...
1516
			$itemprops = mapi_getprops($message, $properties);
1517
1518
			/* If necessary stream the property, if it's > 8KB */
1519
			if (isset($itemprops[PR_TRANSPORT_MESSAGE_HEADERS]) || propIsError(PR_TRANSPORT_MESSAGE_HEADERS, $itemprops) == MAPI_E_NOT_ENOUGH_MEMORY) {
1520
				$itemprops[PR_TRANSPORT_MESSAGE_HEADERS] = mapi_openproperty($message, PR_TRANSPORT_MESSAGE_HEADERS);
1521
			}
1522
1523
			$props = Conversion::mapMAPI2XML($properties, $itemprops);
1524
1525
			// Get actual SMTP address for sent_representing_email_address and received_by_email_address
1526
			$smtpprops = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_RECEIVED_BY_ENTRYID, PR_SENDER_ENTRYID]);
1527
1528
			if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID])) {
1529
				try {
1530
					$user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(true), $smtpprops[PR_SENT_REPRESENTING_ENTRYID]);
1531
					if (isset($user)) {
1532
						$user_props = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]);
1533
						if (isset($user_props[PR_EMS_AB_THUMBNAIL_PHOTO])) {
1534
							$props["props"]['thumbnail_photo'] = "data:image/jpeg;base64," . base64_encode($user_props[PR_EMS_AB_THUMBNAIL_PHOTO]);
1535
						}
1536
					}
1537
				}
1538
				catch (MAPIException $e) {
1539
					// do nothing
1540
				}
1541
			}
1542
1543
			/*
1544
			 * Check that we have PR_SENT_REPRESENTING_ENTRYID for the item, and also
1545
			 * Check that we have sent_representing_email_address property there in the message,
1546
			 * but for contacts we are not using sent_representing_* properties so we are not
1547
			 * getting it from the message. So basically this will be used for mail items only
1548
			 */
1549
			if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $props["props"]["sent_representing_email_address"])) {
1550
				$props["props"]["sent_representing_username"] = $props["props"]["sent_representing_email_address"];
1551
				$sentRepresentingSearchKey = isset($props['props']['sent_representing_search_key']) ? hex2bin($props['props']['sent_representing_search_key']) : false;
1552
				$props["props"]["sent_representing_email_address"] = $this->getEmailAddress($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $sentRepresentingSearchKey);
1553
			}
1554
1555
			if (isset($smtpprops[PR_SENDER_ENTRYID], $props["props"]["sender_email_address"])) {
1556
				$props["props"]["sender_username"] = $props["props"]["sender_email_address"];
1557
				$senderSearchKey = isset($props['props']['sender_search_key']) ? hex2bin($props['props']['sender_search_key']) : false;
1558
				$props["props"]["sender_email_address"] = $this->getEmailAddress($smtpprops[PR_SENDER_ENTRYID], $senderSearchKey);
1559
			}
1560
1561
			if (isset($smtpprops[PR_RECEIVED_BY_ENTRYID], $props["props"]["received_by_email_address"])) {
1562
				$props["props"]["received_by_username"] = $props["props"]["received_by_email_address"];
1563
				$receivedSearchKey = isset($props['props']['received_by_search_key']) ? hex2bin($props['props']['received_by_search_key']) : false;
1564
				$props["props"]["received_by_email_address"] = $this->getEmailAddress($smtpprops[PR_RECEIVED_BY_ENTRYID], $receivedSearchKey);
1565
			}
1566
1567
			// Get body content
1568
			// TODO: Move retrieving the body to a separate function.
1569
			$plaintext = $this->isPlainText(/** @scrutinizer ignore-type */$message);
1570
			$tmpProps = mapi_getprops($message, [PR_BODY, PR_HTML]);
1571
1572
			if (empty($tmpProps[PR_HTML])) {
1573
				$tmpProps = mapi_getprops($message, [PR_BODY, PR_RTF_COMPRESSED]);
1574
				if (isset($tmpProps[PR_RTF_COMPRESSED])) {
1575
					$tmpProps[PR_HTML] = mapi_decompressrtf($tmpProps[PR_RTF_COMPRESSED]);
1576
				}
1577
			}
1578
1579
			$htmlcontent = '';
1580
			$plaincontent = '';
1581
			if (!$plaintext && isset($tmpProps[PR_HTML])) {
1582
				$cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]);
1583
				$codepage = isset($cpprops[PR_INTERNET_CPID]) ? $cpprops[PR_INTERNET_CPID] : 65001;
1584
				$htmlcontent = Conversion::convertCodepageStringToUtf8($codepage, $tmpProps[PR_HTML]);
1585
				if (!empty($htmlcontent)) {
1586
					if ($html2text) {
1587
						$htmlcontent = '';
1588
					}
1589
					else {
1590
						$props["props"]["isHTML"] = true;
1591
					}
1592
				}
1593
1594
				$htmlcontent = trim($htmlcontent, "\0");
1595
			}
1596
1597
			if (isset($tmpProps[PR_BODY])) {
1598
				// only open property if it exists
1599
				$plaincontent = mapi_message_openproperty($message, PR_BODY);
1600
				$plaincontent = trim($plaincontent, "\0");
1601
			}
1602
			else {
1603
				if ($html2text && isset($tmpProps[PR_HTML])) {
1604
					$plaincontent = strip_tags($tmpProps[PR_HTML]);
1605
				}
1606
			}
1607
1608
			if (!empty($htmlcontent)) {
1609
				$props["props"]["html_body"] = $htmlcontent;
1610
				$props["props"]["isHTML"] = true;
1611
			}
1612
			else {
1613
				$props["props"]["isHTML"] = false;
1614
			}
1615
			$props["props"]["body"] = $plaincontent;
1616
1617
			// Get reply-to information, otherwise consider the sender to be the reply-to person.
1618
			$props['reply-to'] = ['item' => []];
1619
			$messageprops = mapi_getprops($message, [PR_REPLY_RECIPIENT_ENTRIES]);
1620
			if (isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES])) {
1621
				$props['reply-to']['item'] = $this->readReplyRecipientEntry($messageprops[PR_REPLY_RECIPIENT_ENTRIES]);
1622
			}
1623
			if (!isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES]) || count($props['reply-to']['item']) === 0) {
1624
				if (isset($props['props']['sent_representing_email_address']) && !empty($props['props']['sent_representing_email_address'])) {
1625
					$props['reply-to']['item'][] = [
1626
						'rowid' => 0,
1627
						'props' => [
1628
							'entryid' => $props['props']['sent_representing_entryid'],
1629
							'display_name' => $props['props']['sent_representing_name'],
1630
							'smtp_address' => $props['props']['sent_representing_email_address'],
1631
							'address_type' => $props['props']['sent_representing_address_type'],
1632
							'object_type' => MAPI_MAILUSER,
1633
							'search_key' => isset($props['props']['sent_representing_search_key']) ? $props['props']['sent_representing_search_key'] : '',
1634
						],
1635
					];
1636
				}
1637
				elseif (!empty($props['props']['sender_email_address'])) {
1638
					$props['reply-to']['item'][] = [
1639
						'rowid' => 0,
1640
						'props' => [
1641
							'entryid' => $props['props']['sender_entryid'],
1642
							'display_name' => $props['props']['sender_name'],
1643
							'smtp_address' => $props['props']['sender_email_address'],
1644
							'address_type' => $props['props']['sender_address_type'],
1645
							'object_type' => MAPI_MAILUSER,
1646
							'search_key' => $props['props']['sender_search_key'],
1647
						],
1648
					];
1649
				}
1650
			}
1651
1652
			// Get recipients
1653
			$recipients = $GLOBALS["operations"]->getRecipientsInfo($message);
1654
			if (!empty($recipients)) {
1655
				$props["recipients"] = [
1656
					"item" => $recipients,
1657
				];
1658
			}
1659
1660
			// Get attachments
1661
			$attachments = $GLOBALS["operations"]->getAttachmentsInfo($message);
1662
			if (!empty($attachments)) {
1663
				$props["attachments"] = [
1664
					"item" => $attachments,
1665
				];
1666
				$cid_found = false;
1667
				foreach ($attachments as $attachement) {
1668
					if (isset($attachement["props"]["cid"])) {
1669
						$cid_found = true;
1670
					}
1671
				}
1672
				if ($cid_found == true && isset($htmlcontent)) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1673
					preg_match_all('/src="cid:(.*)"/Uims', $htmlcontent, $matches);
1674
					if (count($matches) > 0) {
1675
						$search = [];
1676
						$replace = [];
1677
						foreach ($matches[1] as $match) {
1678
							$idx = -1;
1679
							foreach ($attachments as $key => $attachement) {
1680
								if (isset($attachement["props"]["cid"]) &&
1681
									strcasecmp($match, $attachement["props"]["cid"]) == 0) {
1682
									$idx = $key;
1683
									$num = $attachement["props"]["attach_num"];
1684
								}
1685
							}
1686
							if ($idx == -1) {
1687
								continue;
1688
							}
1689
							$attach = mapi_message_openattach($message, $num);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $num does not seem to be defined for all execution paths leading up to this point.
Loading history...
1690
							if (empty($attach)) {
1691
								continue;
1692
							}
1693
							$attachprop = mapi_getprops($attach, [PR_ATTACH_DATA_BIN, PR_ATTACH_MIME_TAG]);
1694
							if (empty($attachprop) || !isset($attachprop[PR_ATTACH_DATA_BIN])) {
1695
								continue;
1696
							}
1697
							if (!isset($attachprop[PR_ATTACH_MIME_TAG])) {
1698
								$mime_tag = "text/plain";
1699
							}
1700
							else {
1701
								$mime_tag = $attachprop[PR_ATTACH_MIME_TAG];
1702
							}
1703
							$search[] = "src=\"cid:{$match}\"";
1704
							$replace[] = "src=\"data:{$mime_tag};base64," . base64_encode($attachprop[PR_ATTACH_DATA_BIN]) . "\"";
1705
							unset($props["attachments"]["item"][$idx]);
1706
						}
1707
						$props["attachments"]["item"] = array_values($props["attachments"]["item"]);
1708
						$htmlcontent = str_replace($search, $replace, $htmlcontent);
1709
						$props["props"]["html_body"] = $htmlcontent;
1710
					}
1711
				}
1712
			}
1713
1714
			// for distlists, we need to get members data
1715
			if (isset($props["props"]["oneoff_members"], $props["props"]["members"])) {
1716
				// remove non-client props
1717
				unset($props["props"]["members"], $props["props"]["oneoff_members"]);
1718
1719
				// get members
1720
				$members = $GLOBALS["operations"]->getMembersFromDistributionList($store, $message, $properties);
1721
				if (!empty($members)) {
1722
					$props["members"] = [
1723
						"item" => $members,
1724
					];
1725
				}
1726
			}
1727
		}
1728
1729
		return $props;
1730
	}
1731
1732
	/**
1733
	 * Get the email address either from entryid or search key. Function is helpful
1734
	 * to retrieve the email address of already deleted contact which is use as a
1735
	 * recipient in message.
1736
	 *
1737
	 * @param string      $entryId   the entryId of an item/recipient
1738
	 * @param bool|string $searchKey then search key of an item/recipient
1739
	 *
1740
	 * @return string email address if found else return empty string
1741
	 */
1742
	public function getEmailAddress($entryId, $searchKey = false) {
1743
		$emailAddress = $this->getEmailAddressFromEntryID($entryId);
1744
		if (empty($emailAddress) && $searchKey !== false) {
1745
			$emailAddress = $this->getEmailAddressFromSearchKey($searchKey);
0 ignored issues
show
Bug introduced by
It seems like $searchKey can also be of type true; however, parameter $searchKey of Operations::getEmailAddressFromSearchKey() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1745
			$emailAddress = $this->getEmailAddressFromSearchKey(/** @scrutinizer ignore-type */ $searchKey);
Loading history...
1746
		}
1747
1748
		return $emailAddress;
1749
	}
1750
1751
	/**
1752
	 * Get and convert properties of a message into an XML array structure.
1753
	 *
1754
	 * @param object $item       The MAPI Object
1755
	 * @param array  $properties Mapping of properties that should be read
1756
	 *
1757
	 * @return array XML array structure
1758
	 *
1759
	 * @todo Function name is misleading, especially compared to getMessageProps()
1760
	 */
1761
	public function getProps($item, $properties) {
1762
		$props = [];
1763
1764
		if ($item) {
0 ignored issues
show
introduced by
$item is of type object, thus it always evaluated to true.
Loading history...
1765
			$itemprops = mapi_getprops($item, $properties);
1766
			$props = Conversion::mapMAPI2XML($properties, $itemprops);
1767
		}
1768
1769
		return $props;
1770
	}
1771
1772
	/**
1773
	 * Get embedded message data.
1774
	 *
1775
	 * Returns the same data as getMessageProps, but then for a specific sub/sub/sub message
1776
	 * of a MAPI message.
1777
	 *
1778
	 * @param object $store         MAPI Message Store Object
1779
	 * @param object $message       MAPI Message Object
1780
	 * @param array  $properties    a set of properties which will be selected
1781
	 * @param array  $parentMessage MAPI Message Object of parent
1782
	 * @param array  $attach_num    a list of attachment numbers (aka 2,1 means 'attachment nr 1 of attachment nr 2')
1783
	 *
1784
	 * @return array item XML array structure of the embedded message
1785
	 */
1786
	public function getEmbeddedMessageProps($store, $message, $properties, $parentMessage, $attach_num) {
1787
		$msgprops = mapi_getprops($message, [PR_MESSAGE_CLASS]);
1788
1789
		switch ($msgprops[PR_MESSAGE_CLASS]) {
1790
			case 'IPM.Note':
1791
				$html2text = false;
1792
				break;
1793
1794
			default:
1795
				$html2text = true;
1796
		}
1797
1798
		$props = $this->getMessageProps($store, $message, $properties, $html2text);
1799
1800
		// sub message will not be having entryid, so use parent's entryid
1801
		$parentProps = mapi_getprops($parentMessage, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
1802
		$props['entryid'] = bin2hex($parentProps[PR_ENTRYID]);
1803
		$props['parent_entryid'] = bin2hex($parentProps[PR_PARENT_ENTRYID]);
1804
		$props['store_entryid'] = bin2hex($parentProps[PR_STORE_ENTRYID]);
1805
		$props['attach_num'] = $attach_num;
1806
1807
		return $props;
1808
	}
1809
1810
	/**
1811
	 * Create a MAPI message.
1812
	 *
1813
	 * @param object $store         MAPI Message Store Object
1814
	 * @param string $parententryid The entryid of the folder in which the new message is to be created
1815
	 *
1816
	 * @return resource Created MAPI message resource
1817
	 */
1818
	public function createMessage($store, $parententryid) {
1819
		$folder = mapi_msgstore_openentry($store, $parententryid);
1820
1821
		return mapi_folder_createmessage($folder);
1822
	}
1823
1824
	/**
1825
	 * Open a MAPI message.
1826
	 *
1827
	 * @param object $store       MAPI Message Store Object
1828
	 * @param string $entryid     entryid of the message
1829
	 * @param array  $attach_num  a list of attachment numbers (aka 2,1 means 'attachment nr 1 of attachment nr 2')
1830
	 * @param bool   $parse_smime (optional) call parse_smime on the opened message or not
1831
	 *
1832
	 * @return mixed MAPI Message
1833
	 */
1834
	public function openMessage($store, $entryid, $attach_num = false, $parse_smime = false) {
1835
		$message = mapi_msgstore_openentry($store, $entryid);
1836
1837
		// Needed for S/MIME messages with embedded message attachments
1838
		if ($parse_smime) {
1839
			parse_smime(/** @scrutinizer ignore-type */$store, $message);
1840
		}
1841
1842
		if ($message && $attach_num) {
1843
			for ($index = 0, $count = count($attach_num); $index < $count; ++$index) {
1844
				// attach_num cannot have value of -1
1845
				// if we get that then we are trying to open an embedded message which
1846
				// is not present in the attachment table to parent message (because parent message is unsaved yet)
1847
				// so return the message which is opened using entryid which will point to actual message which is
1848
				// attached as embedded message
1849
				if ($attach_num[$index] === -1) {
1850
					return $message;
1851
				}
1852
1853
				$attachment = mapi_message_openattach($message, $attach_num[$index]);
1854
1855
				if ($attachment) {
1856
					$message = mapi_attach_openobj($attachment);
1857
				}
1858
				else {
1859
					return false;
1860
				}
1861
			}
1862
		}
1863
1864
		return $message;
1865
	}
1866
1867
	/**
1868
	 * Save a MAPI message.
1869
	 *
1870
	 * The to-be-saved message can be of any type, including e-mail items, appointments, contacts, etc. The message may be pre-existing
1871
	 * or it may be a new message.
1872
	 *
1873
	 * The dialog_attachments parameter represents a unique ID which for the dialog in the client for which this function was called; This
1874
	 * is used as follows; Whenever a user uploads an attachment, the attachment is stored in a temporary place on the server. At the same time,
1875
	 * the temporary server location of the attachment is saved in the session information, accompanied by the $dialog_attachments unique ID. This
1876
	 * way, when we save the message into MAPI, we know which attachment was previously uploaded ready for this message, because when the user saves
1877
	 * the message, we pass the same $dialog_attachments ID as when we uploaded the file.
1878
	 *
1879
	 * @param object   $store                     MAPI Message Store Object
1880
	 * @param string   $entryid                   entryid of the message
1881
	 * @param string   $parententryid             Parent entryid of the message
1882
	 * @param array    $props                     The MAPI properties to be saved
1883
	 * @param array    $messageProps              reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of the saved message
1884
	 * @param array    $recipients                XML array structure of recipients for the recipient table
1885
	 * @param array    $attachments               attachments array containing unique check number which checks if attachments should be added
1886
	 * @param array    $propertiesToDelete        Properties specified in this array are deleted from the MAPI message
1887
	 * @param resource $copyFromMessage           resource of the message from which we should
1888
	 *                                            copy attachments and/or recipients to the current message
1889
	 * @param bool     $copyAttachments           if set we copy all attachments from the $copyFromMessage
1890
	 * @param bool     $copyRecipients            if set we copy all recipients from the $copyFromMessage
1891
	 * @param bool     $copyInlineAttachmentsOnly if true then copy only inline attachments
1892
	 * @param bool     $saveChanges               if true then save all change in mapi message
1893
	 * @param bool     $send                      true if this function is called from submitMessage else false
1894
	 * @param bool     $isPlainText               if true then message body will be generated using PR_BODY otherwise PR_HTML will be used in saveMessage() function
1895
	 *
1896
	 * @return resource Saved MAPI message resource
1897
	 */
1898
	public function saveMessage($store, $entryid, $parententryid, $props, &$messageProps, $recipients = [], $attachments = [], $propertiesToDelete = [], $copyFromMessage = false, $copyAttachments = false, $copyRecipients = false, $copyInlineAttachmentsOnly = false, $saveChanges = true, $send = false, $isPlainText = false) {
1899
		$message = false;
1900
1901
		// Check if an entryid is set, otherwise create a new message
1902
		if ($entryid && !empty($entryid)) {
1903
			$message = $this->openMessage($store, $entryid);
1904
		}
1905
		else {
1906
			$message = $this->createMessage($store, $parententryid);
1907
		}
1908
1909
		if ($message) {
1910
			$property = false;
1911
			$body = "";
1912
1913
			// Check if the body is set.
1914
			if (isset($props[PR_BODY])) {
1915
				$body = $props[PR_BODY];
1916
				$property = PR_BODY;
1917
				$bodyPropertiesToDelete = [PR_HTML, PR_RTF_COMPRESSED];
1918
1919
				if (isset($props[PR_HTML])) {
1920
					$subject = '';
1921
					if (isset($props[PR_SUBJECT])) {
1922
						$subject = $props[PR_SUBJECT];
1923
					// If subject is not updated we need to get it from the message
1924
					}
1925
					else {
1926
						$subjectProp = mapi_getprops($message, [PR_SUBJECT]);
1927
						if (isset($subjectProp[PR_SUBJECT])) {
1928
							$subject = $subjectProp[PR_SUBJECT];
1929
						}
1930
					}
1931
					$body = $this->generateBodyHTML($isPlainText ? $props[PR_BODY] : $props[PR_HTML], $subject);
1932
					$property = PR_HTML;
1933
					$bodyPropertiesToDelete = [PR_BODY, PR_RTF_COMPRESSED];
1934
					unset($props[PR_HTML]);
1935
				}
1936
				unset($props[PR_BODY]);
1937
1938
				$propertiesToDelete = array_unique(array_merge($propertiesToDelete, $bodyPropertiesToDelete));
1939
			}
1940
1941
			if (!isset($props[PR_SENT_REPRESENTING_ENTRYID]) &&
1942
			   isset($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && !empty($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) &&
1943
			   isset($props[PR_SENT_REPRESENTING_ADDRTYPE]) && !empty($props[PR_SENT_REPRESENTING_ADDRTYPE]) &&
1944
			   isset($props[PR_SENT_REPRESENTING_NAME]) && !empty($props[PR_SENT_REPRESENTING_NAME])) {
1945
				// Set FROM field properties
1946
				$props[PR_SENT_REPRESENTING_ENTRYID] = mapi_createoneoff($props[PR_SENT_REPRESENTING_NAME], $props[PR_SENT_REPRESENTING_ADDRTYPE], $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]);
1947
			}
1948
1949
			/*
1950
			 * Delete PR_SENT_REPRESENTING_ENTRYID and PR_SENT_REPRESENTING_SEARCH_KEY properties, if PR_SENT_REPRESENTING_* properties are configured with empty string.
1951
			 * Because, this is the case while user removes recipient from FROM field and send that particular draft without saving it.
1952
			 */
1953
			if (isset($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && empty($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) &&
1954
			   isset($props[PR_SENT_REPRESENTING_ADDRTYPE]) && empty($props[PR_SENT_REPRESENTING_ADDRTYPE]) &&
1955
			   isset($props[PR_SENT_REPRESENTING_NAME]) && empty($props[PR_SENT_REPRESENTING_NAME])) {
1956
				array_push($propertiesToDelete, PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_SEARCH_KEY);
1957
			}
1958
1959
			// remove mv properties when needed
1960
			foreach ($props as $propTag => $propVal) {
1961
				switch (mapi_prop_type($propTag)) {
1962
					case PT_SYSTIME:
1963
						// Empty PT_SYSTIME values mean they should be deleted (there is no way to set an empty PT_SYSTIME)
1964
						// case PT_STRING8:	// not enabled at this moment
1965
						// Empty Strings
1966
					case PT_MV_LONG:
1967
						// Empty multivalued long
1968
					case PT_MV_STRING8:
1969
						// Empty multivalued string
1970
						if (empty($propVal)) {
1971
							$propertiesToDelete[] = $propTag;
1972
						}
1973
						break;
1974
				}
1975
			}
1976
1977
			foreach ($propertiesToDelete as $prop) {
1978
				unset($props[$prop]);
1979
			}
1980
1981
			// Set the properties
1982
			mapi_setprops($message, $props);
1983
1984
			// Delete the properties we don't need anymore
1985
			mapi_deleteprops($message, $propertiesToDelete);
1986
1987
			if ($property != false) {
1988
				// Stream the body to the PR_BODY or PR_HTML property
1989
				$stream = mapi_openproperty($message, $property, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
1990
				mapi_stream_setsize($stream, strlen($body));
1991
				mapi_stream_write($stream, $body);
1992
				mapi_stream_commit($stream);
1993
			}
1994
1995
			/*
1996
			 * Save recipients
1997
			 *
1998
			 * If we are sending mail from delegator's folder, then we need to copy
1999
			 * all recipients from original message first - need to pass message
2000
			 *
2001
			 * if delegate has added or removed any recipients then they will be
2002
			 * added/removed using recipients array.
2003
			 */
2004
			if ($copyRecipients !== false && $copyFromMessage !== false) {
2005
				$this->copyRecipients($message, $copyFromMessage);
2006
			}
2007
2008
			$this->setRecipients($message, $recipients, $send);
2009
2010
			// Save the attachments with the $dialog_attachments, for attachments we have to obtain
2011
			// some additional information from the state.
2012
			if (!empty($attachments)) {
2013
				$attachment_state = new AttachmentState();
2014
				$attachment_state->open();
2015
2016
				if ($copyFromMessage !== false) {
2017
					$this->copyAttachments($message, $attachments, $copyFromMessage, $copyInlineAttachmentsOnly, $attachment_state);
2018
				}
2019
2020
				$this->setAttachments($message, $attachments, $attachment_state);
2021
2022
				$attachment_state->close();
2023
			}
2024
2025
			// Set 'hideattachments' if message has only inline attachments.
2026
			$properties = $GLOBALS['properties']->getMailProperties();
2027
			if ($this->hasOnlyInlineAttachments($message)) {
0 ignored issues
show
Deprecated Code introduced by
The function Operations::hasOnlyInlineAttachments() has been deprecated: This function is not used, because it is much too slow to run on all messages in your inbox ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2027
			if (/** @scrutinizer ignore-deprecated */ $this->hasOnlyInlineAttachments($message)) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2028
				mapi_setprops($message, [$properties['hide_attachments'] => true]);
2029
			}
2030
			else {
2031
				mapi_deleteprops($message, [$properties['hide_attachments']]);
2032
			}
2033
2034
			$this->convertInlineImage($message);
2035
			// Save changes
2036
			if ($saveChanges) {
2037
				mapi_savechanges($message);
2038
			}
2039
2040
			// Get the PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID properties of this message
2041
			$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
2042
		}
2043
2044
		return $message;
2045
	}
2046
2047
	/**
2048
	 * Save an appointment item.
2049
	 *
2050
	 * This is basically the same as saving any other type of message with the added complexity that
2051
	 * we support saving exceptions to recurrence here. This means that if the client sends a basedate
2052
	 * in the action, that we will attempt to open an existing exception and change that, and if that
2053
	 * fails, create a new exception with the specified data.
2054
	 *
2055
	 * @param resource $store                       MAPI store of the message
2056
	 * @param string   $entryid                     entryid of the message
2057
	 * @param string   $parententryid               Parent entryid of the message (folder entryid, NOT message entryid)
2058
	 * @param array    $action                      Action array containing XML request
2059
	 * @param string   $actionType                  The action type which triggered this action
2060
	 * @param bool     $directBookingMeetingRequest Indicates if a Meeting Request should use direct booking or not. Defaults to true.
2061
	 *
2062
	 * @return array of PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID properties of modified item
2063
	 */
2064
	public function saveAppointment($store, $entryid, $parententryid, $action, $actionType = 'save', $directBookingMeetingRequest = true) {
2065
		$messageProps = [];
2066
		// It stores the values that is exception allowed or not false -> not allowed
2067
		$isExceptionAllowed = true;
2068
		$delete = $actionType == 'delete';	// Flag for MeetingRequest Class whether to send update or cancel mail.
2069
		$basedate = false;	// Flag for MeetingRequest Class whether to send an exception or not.
2070
		$isReminderTimeAllowed = true;	// Flag to check reminder minutes is in range of the occurrences
2071
		$properties = $GLOBALS['properties']->getAppointmentProperties();
2072
		$send = false;
2073
		$oldProps = [];
2074
		$pasteRecord = false;
2075
2076
		if (isset($action['message_action'], $action['message_action']['send'])) {
2077
			$send = $action['message_action']['send'];
2078
		}
2079
2080
		if (isset($action['message_action'], $action['message_action']['paste'])) {
2081
			$pasteRecord = true;
2082
		}
2083
2084
		if (!empty($action['recipients'])) {
2085
			$recips = $action['recipients'];
2086
		}
2087
		else {
2088
			$recips = false;
2089
		}
2090
2091
		// Set PidLidAppointmentTimeZoneDefinitionStartDisplay and
2092
		// PidLidAppointmentTimeZoneDefinitionEndDisplay so that the allday
2093
		// events are displayed correctly
2094
		if (!empty($action['props']['timezone_iana'])) {
2095
			try {
2096
				$tzdef = mapi_ianatz_to_tzdef($action['props']['timezone_iana']);
2097
			}
2098
			catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2099
			}
2100
			if ($tzdef !== false) {
2101
				$action['props']['tzdefstart'] = $action['props']['tzdefend'] = bin2hex($tzdef);
2102
			}
2103
		}
2104
2105
		if (is_resource($store) && $parententryid) {
2106
			// @FIXME: check for $action['props'] array
2107
			if (isset($entryid) && $entryid) {
2108
				// Modify existing or add/change exception
2109
				$message = mapi_msgstore_openentry($store, $entryid);
2110
2111
				if ($message) {
2112
					$props = mapi_getprops($message, $properties);
0 ignored issues
show
Unused Code introduced by
The assignment to $props is dead and can be removed.
Loading history...
2113
					// Do not update timezone information if the appointment times haven't changed
2114
					if (!isset($action['props']['commonstart']) &&
2115
						!isset($action['props']['commonend']) &&
2116
						!isset($action['props']['startdate']) &&
2117
						!isset($action['props']['enddate'])
2118
					) {
2119
						unset($action['props']['tzdefstart'], $action['props']['tzdefend']);
2120
					}
2121
					// Check if appointment is an exception to a recurring item
2122
					if (isset($action['basedate']) && $action['basedate'] > 0) {
2123
						// Create recurrence object
2124
						$recurrence = new Recurrence($store, $message);
0 ignored issues
show
Bug introduced by
The type Recurrence was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2125
2126
						$basedate = $action['basedate'];
2127
						$exceptionatt = $recurrence->getExceptionAttachment($basedate);
2128
						if ($exceptionatt) {
2129
							// get properties of existing exception.
2130
							$exceptionattProps = mapi_getprops($exceptionatt, [PR_ATTACH_NUM]);
2131
							$attach_num = $exceptionattProps[PR_ATTACH_NUM];
2132
						}
2133
2134
						if ($delete === true) {
2135
							$isExceptionAllowed = $recurrence->createException([], $basedate, true);
2136
						}
2137
						else {
2138
							$exception_recips = [];
2139
							if (isset($recips['add'])) {
2140
								$savedUnsavedRecipients = [];
2141
								foreach ($recips["add"] as $recip) {
2142
									$savedUnsavedRecipients["unsaved"][] = $recip;
2143
								}
2144
								// convert all local distribution list members to ideal recipient.
2145
								$members = $this->convertLocalDistlistMembersToRecipients($savedUnsavedRecipients);
2146
2147
								$recips['add'] = $members['add'];
2148
								$exception_recips['add'] = $this->createRecipientList($recips['add'], 'add', true, true);
2149
							}
2150
							if (isset($recips['remove'])) {
2151
								$exception_recips['remove'] = $this->createRecipientList($recips['remove'], 'remove');
2152
							}
2153
							if (isset($recips['modify'])) {
2154
								$exception_recips['modify'] = $this->createRecipientList($recips['modify'], 'modify', true, true);
2155
							}
2156
2157
							if (isset($action['props']['reminder_minutes'], $action['props']['startdate'])) {
2158
								$isReminderTimeAllowed = $recurrence->isValidReminderTime($basedate, $action['props']['reminder_minutes'], $action['props']['startdate']);
2159
							}
2160
2161
							// As the reminder minutes occurs before other occurrences don't modify the item.
2162
							if ($isReminderTimeAllowed) {
2163
								if ($recurrence->isException($basedate)) {
2164
									$oldProps = $recurrence->getExceptionProperties($recurrence->getChangeException($basedate));
2165
2166
									$isExceptionAllowed = $recurrence->modifyException(Conversion::mapXML2MAPI($properties, $action['props']), $basedate, $exception_recips);
2167
								}
2168
								else {
2169
									$oldProps[$properties['startdate']] = $recurrence->getOccurrenceStart($basedate);
2170
									$oldProps[$properties['duedate']] = $recurrence->getOccurrenceEnd($basedate);
2171
2172
									$isExceptionAllowed = $recurrence->createException(Conversion::mapXML2MAPI($properties, $action['props']), $basedate, false, $exception_recips);
2173
								}
2174
								mapi_savechanges($message);
2175
							}
2176
						}
2177
					}
2178
					else {
2179
						$oldProps = mapi_getprops($message, [$properties['startdate'], $properties['duedate']]);
2180
						// Modifying non-exception (the series) or normal appointment item
2181
						$message = $GLOBALS['operations']->saveMessage($store, $entryid, $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ? $recips : [], isset($action['attachments']) ? $action['attachments'] : [], [], false, false, false, false, false, false, $send);
2182
2183
						$recurrenceProps = mapi_getprops($message, [$properties['startdate_recurring'], $properties['enddate_recurring'], $properties["recurring"]]);
2184
						// Check if the meeting is recurring
2185
						if ($recips && $recurrenceProps[$properties["recurring"]] && isset($recurrenceProps[$properties['startdate_recurring']], $recurrenceProps[$properties['enddate_recurring']])) {
2186
							// If recipient of meeting is modified than that modification needs to be applied
2187
							// to recurring exception as well, if any.
2188
							$exception_recips = [];
2189
							if (isset($recips['add'])) {
2190
								$exception_recips['add'] = $this->createRecipientList($recips['add'], 'add', true, true);
2191
							}
2192
							if (isset($recips['remove'])) {
2193
								$exception_recips['remove'] = $this->createRecipientList($recips['remove'], 'remove');
2194
							}
2195
							if (isset($recips['modify'])) {
2196
								$exception_recips['modify'] = $this->createRecipientList($recips['modify'], 'modify', true, true);
2197
							}
2198
2199
							// Create recurrence object
2200
							$recurrence = new Recurrence($store, $message);
2201
2202
							$recurItems = $recurrence->getItems($recurrenceProps[$properties['startdate_recurring']], $recurrenceProps[$properties['enddate_recurring']]);
2203
2204
							foreach ($recurItems as $recurItem) {
2205
								if (isset($recurItem["exception"])) {
2206
									$recurrence->modifyException([], $recurItem["basedate"], $exception_recips);
2207
								}
2208
							}
2209
						}
2210
2211
						// Only save recurrence if it has been changed by the user (because otherwise we'll reset
2212
						// the exceptions)
2213
						if (isset($action['props']['recurring_reset']) && $action['props']['recurring_reset'] == true) {
2214
							$recur = new Recurrence($store, $message);
2215
2216
							if (isset($action['props']['timezone'])) {
2217
								$tzprops = ['timezone', 'timezonedst', 'dststartmonth', 'dststartweek', 'dststartday', 'dststarthour', 'dstendmonth', 'dstendweek', 'dstendday', 'dstendhour'];
2218
2219
								// Get timezone info
2220
								$tz = [];
2221
								foreach ($tzprops as $tzprop) {
2222
									$tz[$tzprop] = $action['props'][$tzprop];
2223
								}
2224
							}
2225
2226
							/**
2227
							 * Check if any recurrence property is missing, if yes then prepare
2228
							 * the set of properties required to update the recurrence. For more info
2229
							 * please refer detailed description of parseRecurrence function of
2230
							 * BaseRecurrence class".
2231
							 *
2232
							 * Note : this is a special case of changing the time of
2233
							 * recurrence meeting from scheduling tab.
2234
							 */
2235
							$recurrence = $recur->getRecurrence();
2236
							if (isset($recurrence)) {
2237
								unset($recurrence['changed_occurrences'], $recurrence['deleted_occurrences']);
2238
2239
								foreach ($recurrence as $key => $value) {
2240
									if (!isset($action['props'][$key])) {
2241
										$action['props'][$key] = $value;
2242
									}
2243
								}
2244
							}
2245
							// Act like the 'props' are the recurrence pattern; it has more information but that
2246
							// is ignored
2247
							$recur->setRecurrence(isset($tz) ? $tz : false, $action['props']);
2248
						}
2249
					}
2250
2251
					// Get the properties of the main object of which the exception was changed, and post
2252
					// that message as being modified. This will cause the update function to update all
2253
					// occurrences of the item to the client
2254
					$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
2255
2256
					// if opened appointment is exception then it will add
2257
					// the attach_num and basedate in messageProps.
2258
					if (isset($attach_num)) {
2259
						$messageProps[PR_ATTACH_NUM] = [$attach_num];
2260
						$messageProps[$properties["basedate"]] = $action['basedate'];
2261
					}
2262
				}
2263
			}
2264
			else {
2265
				$tz = null;
2266
				$hasRecipient = false;
2267
				$copyAttachments = false;
2268
				$sourceRecord = false;
2269
				if (isset($action['message_action'], $action['message_action']['source_entryid'])) {
2270
					$sourceEntryId = $action['message_action']['source_entryid'];
2271
					$sourceStoreEntryId = $action['message_action']['source_store_entryid'];
2272
2273
					$sourceStore = $GLOBALS['mapisession']->openMessageStore(hex2bin($sourceStoreEntryId));
2274
					$sourceRecord = mapi_msgstore_openentry($sourceStore, hex2bin($sourceEntryId));
2275
					if ($pasteRecord) {
2276
						$sourceRecordProps = mapi_getprops($sourceRecord, [$properties["meeting"], $properties["responsestatus"]]);
2277
						// Don't copy recipient if source record is received message.
2278
						if ($sourceRecordProps[$properties["meeting"]] === olMeeting &&
2279
							$sourceRecordProps[$properties["meeting"]] === olResponseOrganized) {
2280
							$table = mapi_message_getrecipienttable($sourceRecord);
2281
							$hasRecipient = mapi_table_getrowcount($table) > 0;
2282
						}
2283
					}
2284
					else {
2285
						$copyAttachments = true;
2286
						// Set sender of new Appointment.
2287
						$this->setSenderAddress($store, $action);
2288
					}
2289
				}
2290
				else {
2291
					// Set sender of new Appointment.
2292
					$this->setSenderAddress($store, $action);
2293
				}
2294
2295
				$message = $this->saveMessage($store, $entryid, $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ? $recips : [], isset($action['attachments']) ? $action['attachments'] : [], [], $sourceRecord, $copyAttachments, $hasRecipient, false, false, false, $send);
0 ignored issues
show
Bug introduced by
$store of type resource is incompatible with the type object expected by parameter $store of Operations::saveMessage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2295
				$message = $this->saveMessage(/** @scrutinizer ignore-type */ $store, $entryid, $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ? $recips : [], isset($action['attachments']) ? $action['attachments'] : [], [], $sourceRecord, $copyAttachments, $hasRecipient, false, false, false, $send);
Loading history...
2296
2297
				if (isset($action['props']['timezone'])) {
2298
					$tzprops = ['timezone', 'timezonedst', 'dststartmonth', 'dststartweek', 'dststartday', 'dststarthour', 'dstendmonth', 'dstendweek', 'dstendday', 'dstendhour'];
2299
2300
					// Get timezone info
2301
					$tz = [];
2302
					foreach ($tzprops as $tzprop) {
2303
						$tz[$tzprop] = $action['props'][$tzprop];
2304
					}
2305
				}
2306
2307
				// Set recurrence
2308
				if (isset($action['props']['recurring']) && $action['props']['recurring'] == true) {
2309
					$recur = new Recurrence($store, $message);
2310
					$recur->setRecurrence($tz, $action['props']);
2311
				}
2312
			}
2313
		}
2314
2315
		$result = false;
2316
		// Check to see if it should be sent as a meeting request
2317
		if ($send === true && $isExceptionAllowed) {
2318
			$savedUnsavedRecipients = [];
2319
			$remove = [];
2320
			if (!isset($action['basedate'])) {
2321
				// retrieve recipients from saved message
2322
				$savedRecipients = $GLOBALS['operations']->getRecipientsInfo($message);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $message does not seem to be defined for all execution paths leading up to this point.
Loading history...
2323
				foreach ($savedRecipients as $recipient) {
2324
					$savedUnsavedRecipients["saved"][] = $recipient['props'];
2325
				}
2326
2327
				// retrieve removed recipients.
2328
				if (!empty($recips) && !empty($recips["remove"])) {
2329
					$remove = $recips["remove"];
2330
				}
2331
2332
				// convert all local distribution list members to ideal recipient.
2333
				$members = $this->convertLocalDistlistMembersToRecipients($savedUnsavedRecipients, $remove);
2334
2335
				// Before sending meeting request we set the recipient to message
2336
				// which are converted from local distribution list members.
2337
				$this->setRecipients($message, $members);
2338
			}
2339
2340
			$request = new Meetingrequest($store, $message, $GLOBALS['mapisession']->getSession(), $directBookingMeetingRequest);
0 ignored issues
show
Bug introduced by
The type Meetingrequest was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2341
2342
			/*
2343
			 * check write access for delegate, make sure that we will not send meeting request
2344
			 * if we don't have permission to save calendar item
2345
			 */
2346
			if ($request->checkFolderWriteAccess($parententryid, $store) !== true) {
2347
				// Throw an exception that we don't have write permissions on calendar folder,
2348
				// error message will be filled by module
2349
				throw new MAPIException(null, MAPI_E_NO_ACCESS);
2350
			}
2351
2352
			$request->updateMeetingRequest($basedate);
2353
2354
			$isRecurrenceChanged = isset($action['props']['recurring_reset']) && $action['props']['recurring_reset'] == true;
2355
			$request->checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged);
2356
2357
			// Update extra body information
2358
			if (isset($action['message_action']['meetingTimeInfo']) && !empty($action['message_action']['meetingTimeInfo'])) {
2359
				// Append body if the request action requires this
2360
				if (isset($action['message_action'], $action['message_action']['append_body'])) {
2361
					$bodyProps = mapi_getprops($message, [PR_BODY]);
2362
					if (isset($bodyProps[PR_BODY]) || propIsError(PR_BODY, $bodyProps) == MAPI_E_NOT_ENOUGH_MEMORY) {
2363
						$bodyProps[PR_BODY] = streamProperty($message, PR_BODY);
2364
					}
2365
2366
					if (isset($action['message_action']['meetingTimeInfo'], $bodyProps[PR_BODY])) {
2367
						$action['message_action']['meetingTimeInfo'] .= $bodyProps[PR_BODY];
2368
					}
2369
				}
2370
2371
				$request->setMeetingTimeInfo($action['message_action']['meetingTimeInfo']);
2372
				unset($action['message_action']['meetingTimeInfo']);
2373
			}
2374
2375
			$modifiedRecipients = false;
2376
			$deletedRecipients = false;
2377
			if ($recips) {
2378
				if (isset($action['message_action']['send_update']) && $action['message_action']['send_update'] == 'modified') {
2379
					if (isset($recips['add']) && !empty($recips['add'])) {
2380
						$modifiedRecipients = $modifiedRecipients ? $modifiedRecipients : [];
0 ignored issues
show
introduced by
The condition $modifiedRecipients is always false.
Loading history...
2381
						$modifiedRecipients = array_merge($modifiedRecipients, $this->createRecipientList($recips['add'], 'add'));
2382
					}
2383
2384
					if (isset($recips['modify']) && !empty($recips['modify'])) {
2385
						$modifiedRecipients = $modifiedRecipients ? $modifiedRecipients : [];
2386
						$modifiedRecipients = array_merge($modifiedRecipients, $this->createRecipientList($recips['modify'], 'modify'));
2387
					}
2388
				}
2389
2390
				// lastUpdateCounter is represent that how many times this message is updated(send)
2391
				$lastUpdateCounter = $request->getLastUpdateCounter();
2392
				if ($lastUpdateCounter !== false && $lastUpdateCounter > 0) {
2393
					if (isset($recips['remove']) && !empty($recips['remove'])) {
2394
						$deletedRecipients = $deletedRecipients ? $deletedRecipients : [];
0 ignored issues
show
introduced by
The condition $deletedRecipients is always false.
Loading history...
2395
						$deletedRecipients = array_merge($deletedRecipients, $this->createRecipientList($recips['remove'], 'remove'));
2396
						if (isset($action['message_action']['send_update']) && $action['message_action']['send_update'] != 'all') {
2397
							$modifiedRecipients = $modifiedRecipients ? $modifiedRecipients : [];
2398
						}
2399
					}
2400
				}
2401
			}
2402
2403
			$sendMeetingRequestResult = $request->sendMeetingRequest($delete, false, $basedate, $modifiedRecipients, $deletedRecipients);
2404
2405
			$this->addRecipientsToRecipientHistory($this->getRecipientsInfo($message));
2406
2407
			if ($sendMeetingRequestResult === true) {
2408
				$this->parseDistListAndAddToRecipientHistory($savedUnsavedRecipients, $remove);
2409
2410
				mapi_savechanges($message);
2411
2412
				// We want to sent the 'request_sent' property, to have it properly
2413
				// deserialized we must also send some type properties.
2414
				$props = mapi_getprops($message, [PR_MESSAGE_CLASS, PR_OBJECT_TYPE]);
2415
				$messageProps[PR_MESSAGE_CLASS] = $props[PR_MESSAGE_CLASS];
2416
				$messageProps[PR_OBJECT_TYPE] = $props[PR_OBJECT_TYPE];
2417
2418
				// Indicate that the message was correctly sent
2419
				$messageProps[$properties['request_sent']] = true;
2420
2421
				// Return message properties that can be sent to the bus to notify changes
2422
				$result = $messageProps;
2423
			}
2424
			else {
2425
				$sendMeetingRequestResult[PR_ENTRYID] = $messageProps[PR_ENTRYID];
2426
				$sendMeetingRequestResult[PR_PARENT_ENTRYID] = $messageProps[PR_PARENT_ENTRYID];
2427
				$sendMeetingRequestResult[PR_STORE_ENTRYID] = $messageProps[PR_STORE_ENTRYID];
2428
				$result = $sendMeetingRequestResult;
2429
			}
2430
		}
2431
		else {
2432
			mapi_savechanges($message);
2433
2434
			if (isset($isExceptionAllowed)) {
2435
				if ($isExceptionAllowed === false) {
2436
					$messageProps['isexceptionallowed'] = false;
2437
				}
2438
			}
2439
2440
			if (isset($isReminderTimeAllowed)) {
2441
				if ($isReminderTimeAllowed === false) {
2442
					$messageProps['remindertimeerror'] = false;
2443
				}
2444
			}
2445
			// Return message properties that can be sent to the bus to notify changes
2446
			$result = $messageProps;
2447
		}
2448
2449
		return $result;
2450
	}
2451
2452
	/**
2453
	 * Function is used to identify the local distribution list from all recipients and
2454
	 * convert all local distribution list members to recipients.
2455
	 *
2456
	 * @param array $recipients array of recipients either saved or add
2457
	 * @param array $remove     array of recipients that was removed
2458
	 *
2459
	 * @return array $newRecipients a list of recipients as XML array structure
2460
	 */
2461
	public function convertLocalDistlistMembersToRecipients($recipients, $remove = []) {
2462
		$addRecipients = [];
2463
		$removeRecipients = [];
2464
2465
		foreach ($recipients as $key => $recipient) {
2466
			foreach ($recipient as $recipientItem) {
2467
				$recipientEntryid = $recipientItem["entryid"];
2468
				$isExistInRemove = $this->isExistInRemove($recipientEntryid, $remove);
2469
2470
				/*
2471
				 * Condition is only gets true, if recipient is distribution list and it`s belongs
2472
				 * to shared/internal(belongs in contact folder) folder.
2473
				 */
2474
				if ($recipientItem['address_type'] == 'MAPIPDL') {
2475
					if (!$isExistInRemove) {
2476
						$recipientItems = $GLOBALS["operations"]->expandDistList($recipientEntryid, true);
2477
						foreach ($recipientItems as $recipient) {
0 ignored issues
show
Comprehensibility Bug introduced by
$recipient is overwriting a variable from outer foreach loop.
Loading history...
2478
							// set recipient type of each members as per the distribution list recipient type
2479
							$recipient['recipient_type'] = $recipientItem['recipient_type'];
2480
							array_push($addRecipients, $recipient);
2481
						}
2482
2483
						if ($key === "saved") {
2484
							array_push($removeRecipients, $recipientItem);
2485
						}
2486
					}
2487
				}
2488
				else {
2489
					/*
2490
					 * Only Add those recipients which are not saved earlier in message and
2491
					 * not present in remove array.
2492
					 */
2493
					if (!$isExistInRemove && $key === "unsaved") {
2494
						array_push($addRecipients, $recipientItem);
2495
					}
2496
				}
2497
			}
2498
		}
2499
		$newRecipients["add"] = $addRecipients;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$newRecipients was never initialized. Although not strictly required by PHP, it is generally a good practice to add $newRecipients = array(); before regardless.
Loading history...
2500
		$newRecipients["remove"] = $removeRecipients;
2501
2502
		return $newRecipients;
2503
	}
2504
2505
	/**
2506
	 * Function used to identify given recipient was already available in remove array of recipients array or not.
2507
	 * which was sent from client side. If it is found the entry in the $remove array will be deleted, since
2508
	 * we do not want to find it again for other recipients. (if a user removes and adds an user again it
2509
	 * should be added once!).
2510
	 *
2511
	 * @param string $recipientEntryid recipient entryid
2512
	 * @param array  $remove           removed recipients array
2513
	 *
2514
	 * @return bool return false if recipient not exist in remove array else return true
2515
	 */
2516
	public function isExistInRemove($recipientEntryid, &$remove) {
2517
		if (!empty($remove)) {
2518
			foreach ($remove as $index => $removeItem) {
2519
				if (array_search($recipientEntryid, $removeItem, true)) {
2520
					unset($remove[$index]);
2521
2522
					return true;
2523
				}
2524
			}
2525
		}
2526
2527
		return false;
2528
	}
2529
2530
	/**
2531
	 * Function is used to identify the local distribution list from all recipients and
2532
	 * Add distribution list to recipient history.
2533
	 *
2534
	 * @param array $savedUnsavedRecipients array of recipients either saved or add
2535
	 * @param array $remove                 array of recipients that was removed
2536
	 */
2537
	public function parseDistListAndAddToRecipientHistory($savedUnsavedRecipients, $remove) {
2538
		$distLists = [];
2539
		foreach ($savedUnsavedRecipients as $key => $recipient) {
2540
			foreach ($recipient as $recipientItem) {
2541
				if ($recipientItem['address_type'] == 'MAPIPDL') {
2542
					$isExistInRemove = $this->isExistInRemove($recipientItem['entryid'], $remove);
2543
					if (!$isExistInRemove) {
2544
						array_push($distLists, ["props" => $recipientItem]);
2545
					}
2546
				}
2547
			}
2548
		}
2549
2550
		$this->addRecipientsToRecipientHistory($distLists);
2551
	}
2552
2553
	/**
2554
	 * Set sent_representing_email_address property of Appointment.
2555
	 *
2556
	 * Before saving any new appointment, sent_representing_email_address property of appointment
2557
	 * should contain email_address of user, who is the owner of store(in which the appointment
2558
	 * is created).
2559
	 *
2560
	 * @param resource $store  MAPI store of the message
2561
	 * @param array    $action reference to action array containing XML request
2562
	 */
2563
	public function setSenderAddress($store, &$action) {
2564
		$storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]);
2565
		// check for public store
2566
		if (!isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])) {
2567
			$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
2568
			$storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]);
2569
		}
2570
		$mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $storeProps[PR_MAILBOX_OWNER_ENTRYID]);
2571
		if ($mailuser) {
2572
			$userprops = mapi_getprops($mailuser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]);
2573
			$action["props"]["sent_representing_entryid"] = bin2hex($storeProps[PR_MAILBOX_OWNER_ENTRYID]);
2574
			// we do conversion here, because before passing props to saveMessage() props are converted from utf8-to-w
2575
			$action["props"]["sent_representing_name"] = $userprops[PR_DISPLAY_NAME];
2576
			$action["props"]["sent_representing_address_type"] = $userprops[PR_ADDRTYPE];
2577
			if ($userprops[PR_ADDRTYPE] == 'SMTP') {
2578
				$emailAddress = $userprops[PR_SMTP_ADDRESS];
2579
			}
2580
			else {
2581
				$emailAddress = $userprops[PR_EMAIL_ADDRESS];
2582
			}
2583
			$action["props"]["sent_representing_email_address"] = $emailAddress;
2584
			$action["props"]["sent_representing_search_key"] = bin2hex(strtoupper($userprops[PR_ADDRTYPE] . ':' . $emailAddress)) . '00';
2585
		}
2586
	}
2587
2588
	/**
2589
	 * Submit a message for sending.
2590
	 *
2591
	 * This function is an extension of the saveMessage() function, with the extra functionality
2592
	 * that the item is actually sent and queued for moving to 'Sent Items'. Also, the e-mail addresses
2593
	 * used in the message are processed for later auto-suggestion.
2594
	 *
2595
	 * @see Operations::saveMessage() for more information on the parameters, which are identical.
2596
	 *
2597
	 * @param resource $store                     MAPI Message Store Object
2598
	 * @param string   $entryid                   Entryid of the message
2599
	 * @param array    $props                     The properties to be saved
2600
	 * @param array    $messageProps              reference to an array which will be filled with PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID
2601
	 * @param array    $recipients                XML array structure of recipients for the recipient table
2602
	 * @param array    $attachments               array of attachments consisting unique ID of attachments for this message
2603
	 * @param resource $copyFromMessage           resource of the message from which we should
2604
	 *                                            copy attachments and/or recipients to the current message
2605
	 * @param bool     $copyAttachments           if set we copy all attachments from the $copyFromMessage
2606
	 * @param bool     $copyRecipients            if set we copy all recipients from the $copyFromMessage
2607
	 * @param bool     $copyInlineAttachmentsOnly if true then copy only inline attachments
2608
	 * @param bool     $isPlainText               if true then message body will be generated using PR_BODY otherwise PR_HTML will be used in saveMessage() function
2609
	 *
2610
	 * @return bool false if action succeeded, anything else indicates an error (e.g. a string)
2611
	 */
2612
	public function submitMessage($store, $entryid, $props, &$messageProps, $recipients = [], $attachments = [], $copyFromMessage = false, $copyAttachments = false, $copyRecipients = false, $copyInlineAttachmentsOnly = false, $isPlainText = false) {
2613
		$message = false;
2614
		$origStore = $store;
2615
2616
		// Get the outbox and sent mail entryid, ignore the given $store, use the default store for submitting messages
2617
		$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
2618
		$storeprops = mapi_getprops($store, [PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_ENTRYID]);
2619
		$origStoreprops = mapi_getprops($origStore, [PR_ENTRYID]);
2620
2621
		if (!isset($storeprops[PR_IPM_OUTBOX_ENTRYID])) {
2622
			return false;
2623
		}
2624
		if (isset($storeprops[PR_IPM_SENTMAIL_ENTRYID])) {
2625
			$props[PR_SENTMAIL_ENTRYID] = $storeprops[PR_IPM_SENTMAIL_ENTRYID];
2626
		}
2627
2628
		// Check if replying then set PR_INTERNET_REFERENCES and PR_IN_REPLY_TO_ID properties in props.
2629
		// flag is probably used wrong here but the same flag indicates if this is reply or replyall
2630
		if ($copyInlineAttachmentsOnly) {
2631
			$origMsgProps = mapi_getprops($copyFromMessage, [PR_INTERNET_MESSAGE_ID, PR_INTERNET_REFERENCES]);
2632
			if (isset($origMsgProps[PR_INTERNET_MESSAGE_ID])) {
2633
				// The references header should indicate the message-id of the original
2634
				// header plus any of the references which were set on the previous mail.
2635
				$props[PR_INTERNET_REFERENCES] = $origMsgProps[PR_INTERNET_MESSAGE_ID];
2636
				if (isset($origMsgProps[PR_INTERNET_REFERENCES])) {
2637
					$props[PR_INTERNET_REFERENCES] = $origMsgProps[PR_INTERNET_REFERENCES] . ' ' . $props[PR_INTERNET_REFERENCES];
2638
				}
2639
				$props[PR_IN_REPLY_TO_ID] = $origMsgProps[PR_INTERNET_MESSAGE_ID];
2640
			}
2641
		}
2642
2643
		if (!$GLOBALS["entryid"]->compareStoreEntryIds(bin2hex($origStoreprops[PR_ENTRYID]), bin2hex($storeprops[PR_ENTRYID]))) {
2644
			// set properties for "on behalf of" mails
2645
			$origStoreProps = mapi_getprops($origStore, [PR_MAILBOX_OWNER_ENTRYID, PR_MDB_PROVIDER]);
2646
2647
			// set PR_SENDER_* properties, which contains currently logged users data
2648
			$ab = $GLOBALS['mapisession']->getAddressbook();
2649
			$abitem = mapi_ab_openentry($ab, $GLOBALS["mapisession"]->getUserEntryID());
2650
			$abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2651
2652
			$props[PR_SENDER_ENTRYID] = $GLOBALS["mapisession"]->getUserEntryID();
2653
			$props[PR_SENDER_NAME] = $abitemprops[PR_DISPLAY_NAME];
2654
			$props[PR_SENDER_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS];
2655
			$props[PR_SENDER_ADDRTYPE] = "EX";
2656
			$props[PR_SENDER_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY];
2657
2658
			/*
2659
			 * if delegate store then set PR_SENT_REPRESENTING_* properties
2660
			 * based on delegate store's owner data
2661
			 * if public store then set PR_SENT_REPRESENTING_* properties based on
2662
			 * default store's owner data
2663
			 */
2664
			if ($origStoreProps[PR_MDB_PROVIDER] === ZARAFA_STORE_DELEGATE_GUID) {
2665
				$abitem = mapi_ab_openentry($ab, $origStoreProps[PR_MAILBOX_OWNER_ENTRYID]);
2666
				$abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2667
2668
				$props[PR_SENT_REPRESENTING_ENTRYID] = $origStoreProps[PR_MAILBOX_OWNER_ENTRYID];
2669
				$props[PR_SENT_REPRESENTING_NAME] = $abitemprops[PR_DISPLAY_NAME];
2670
				$props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS];
2671
				$props[PR_SENT_REPRESENTING_ADDRTYPE] = "EX";
2672
				$props[PR_SENT_REPRESENTING_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY];
2673
			}
2674
			elseif ($origStoreProps[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) {
2675
				$props[PR_SENT_REPRESENTING_ENTRYID] = $props[PR_SENDER_ENTRYID];
2676
				$props[PR_SENT_REPRESENTING_NAME] = $props[PR_SENDER_NAME];
2677
				$props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $props[PR_SENDER_EMAIL_ADDRESS];
2678
				$props[PR_SENT_REPRESENTING_ADDRTYPE] = $props[PR_SENDER_ADDRTYPE];
2679
				$props[PR_SENT_REPRESENTING_SEARCH_KEY] = $props[PR_SEARCH_KEY];
2680
			}
2681
2682
			/**
2683
			 * we are sending mail from delegate's account, so we can't use delegate's outbox and sent items folder
2684
			 * so we have to copy the mail from delegate's store to logged user's store and in outbox folder and then
2685
			 * we can send mail from logged user's outbox folder.
2686
			 *
2687
			 * if we set $entryid to false before passing it to saveMessage function then it will assume
2688
			 * that item doesn't exist and it will create a new item (in outbox of logged in user)
2689
			 */
2690
			if ($entryid) {
2691
				$oldEntryId = $entryid;
2692
				$entryid = false;
2693
2694
				// if we are sending mail from drafts folder then we have to copy
2695
				// its recipients and attachments also. $origStore and $oldEntryId points to mail
2696
				// saved in delegators draft folder
2697
				if ($copyFromMessage === false) {
2698
					$copyFromMessage = mapi_msgstore_openentry($origStore, $oldEntryId);
2699
					$copyRecipients = true;
2700
2701
					// Decode smime signed messages on this message
2702
					parse_smime($origStore, $copyFromMessage);
2703
				}
2704
			}
2705
2706
			if ($copyFromMessage) {
2707
				// Get properties of original message, to copy recipients and attachments in new message
2708
				$copyMessageProps = mapi_getprops($copyFromMessage);
2709
				$oldParentEntryId = $copyMessageProps[PR_PARENT_ENTRYID];
2710
2711
				// unset id properties before merging the props, so we will be creating new item instead of sending same item
2712
				unset($copyMessageProps[PR_ENTRYID], $copyMessageProps[PR_PARENT_ENTRYID], $copyMessageProps[PR_STORE_ENTRYID]);
2713
2714
				// grommunio generates PR_HTML on the fly, but it's necessary to unset it
2715
				// if the original message didn't have PR_HTML property.
2716
				if (!isset($props[PR_HTML]) && isset($copyMessageProps[PR_HTML])) {
2717
					unset($copyMessageProps[PR_HTML]);
2718
				}
2719
				/* New EMAIL_ADDRESSes were set (various cases above), kill off old SMTP_ADDRESS. */
2720
				unset($copyMessageProps[PR_SENDER_SMTP_ADDRESS], $copyMessageProps[PR_SENT_REPRESENTING_SMTP_ADDRESS]);
2721
2722
				// Merge original message props with props sent by client
2723
				$props = $props + $copyMessageProps;
2724
			}
2725
2726
			// Save the new message properties
2727
			$message = $this->saveMessage($store, /** @scrutinizer ignore-type */$entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], $copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText);
2728
2729
			// FIXME: currently message is deleted from original store and new message is created
2730
			// in current user's store, but message should be moved
2731
2732
			// delete message from it's original location
2733
			if (!empty($oldEntryId) && !empty($oldParentEntryId)) {
2734
				$folder = mapi_msgstore_openentry($origStore, $oldParentEntryId);
2735
				mapi_folder_deletemessages($folder, [$oldEntryId], DELETE_HARD_DELETE);
2736
			}
2737
		}
2738
		else {
2739
			// When the message is in your own store, just move it to your outbox. We move it manually so we know the new entryid after it has been moved.
2740
			$outbox = mapi_msgstore_openentry($store, $storeprops[PR_IPM_OUTBOX_ENTRYID]);
2741
2742
			// Open the old and the new message
2743
			$newmessage = mapi_folder_createmessage($outbox);
2744
			$oldEntryId = $entryid;
2745
2746
			// Remember the new entryid
2747
			$newprops = mapi_getprops($newmessage, [PR_ENTRYID]);
2748
			$entryid = $newprops[PR_ENTRYID];
2749
2750
			if (!empty($oldEntryId)) {
2751
				$message = mapi_msgstore_openentry($store, $oldEntryId);
2752
				// Copy the entire message
2753
				mapi_copyto($message, [], [], $newmessage);
2754
				$tmpProps = mapi_getprops($message);
2755
				$oldParentEntryId = $tmpProps[PR_PARENT_ENTRYID];
2756
				if ($storeprops[PR_IPM_OUTBOX_ENTRYID] == $oldParentEntryId) {
2757
					$folder = $outbox;
2758
				}
2759
				else {
2760
					$folder = mapi_msgstore_openentry($store, $oldParentEntryId);
2761
				}
2762
2763
				// Copy message_class for S/MIME plugin
2764
				if (isset($tmpProps[PR_MESSAGE_CLASS])) {
2765
					$props[PR_MESSAGE_CLASS] = $tmpProps[PR_MESSAGE_CLASS];
2766
				}
2767
				// Delete the old message
2768
				mapi_folder_deletemessages($folder, [$oldEntryId]);
2769
			}
2770
2771
			// save changes to new message created in outbox
2772
			mapi_savechanges($newmessage);
2773
2774
			$reprProps = mapi_getprops($newmessage, [PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ENTRYID]);
2775
			if (isset($reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], $reprProps[PR_SENDER_EMAIL_ADDRESS], $reprProps[PR_SENT_REPRESENTING_ENTRYID]) &&
2776
				strcasecmp($reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], $reprProps[PR_SENDER_EMAIL_ADDRESS]) != 0) {
2777
				$ab = $GLOBALS['mapisession']->getAddressbook();
2778
				$abitem = mapi_ab_openentry($ab, $reprProps[PR_SENT_REPRESENTING_ENTRYID]);
2779
				$abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2780
2781
				$props[PR_SENT_REPRESENTING_NAME] = $abitemprops[PR_DISPLAY_NAME];
2782
				$props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS];
2783
				$props[PR_SENT_REPRESENTING_ADDRTYPE] = "EX";
2784
				$props[PR_SENT_REPRESENTING_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY];
2785
			}
2786
			// Save the new message properties
2787
			$message = $this->saveMessage($store, $entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], /** @scrutinizer ignore-type */$copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText);
2788
		}
2789
2790
		if (!$message) {
0 ignored issues
show
introduced by
$message is of type resource, thus it always evaluated to false.
Loading history...
2791
			return false;
2792
		}
2793
		// Allowing to hook in just before the data sent away to be sent to the client
2794
		$GLOBALS['PluginManager']->triggerHook('server.core.operations.submitmessage', [
2795
			'moduleObject' => $this,
2796
			'store' => $store,
2797
			'entryid' => $entryid,
2798
			'message' => &$message,
2799
		]);
2800
2801
		// Submit the message (send)
2802
		try {
2803
			mapi_message_submitmessage($message);
2804
		}
2805
		catch (MAPIException $e) {
2806
			$username = $GLOBALS["mapisession"]->getUserName();
2807
			$errorName = get_mapi_error_name($e->getCode());
2808
			error_log(sprintf(
2809
				'Unable to submit message for %s, MAPI error: %s. ' .
2810
				'SMTP server may be down or it refused the message or the message' .
2811
				' is too large to submit or user does not have the permission ...',
2812
				$username,
2813
				$errorName
2814
			));
2815
2816
			return $errorName;
2817
		}
2818
2819
		$tmp_props = mapi_getprops($message, [PR_PARENT_ENTRYID]);
2820
		$messageProps[PR_PARENT_ENTRYID] = $tmp_props[PR_PARENT_ENTRYID];
2821
2822
		$this->addRecipientsToRecipientHistory($this->getRecipientsInfo($message));
2823
2824
		return false;
2825
	}
2826
2827
	/**
2828
	 * Delete messages.
2829
	 *
2830
	 * This function does what is needed when a user presses 'delete' on a MAPI message. This means that:
2831
	 *
2832
	 * - Items in the own store are moved to the wastebasket
2833
	 * - Items in the wastebasket are deleted
2834
	 * - Items in other users stores are moved to our own wastebasket
2835
	 * - Items in the public store are deleted
2836
	 *
2837
	 * @param resource $store         MAPI Message Store Object
2838
	 * @param string   $parententryid parent entryid of the messages to be deleted
2839
	 * @param array    $entryids      a list of entryids which will be deleted
2840
	 * @param bool     $softDelete    flag for soft-deleteing (when user presses Shift+Del)
2841
	 * @param bool     $unread        message is unread
2842
	 *
2843
	 * @return bool true if action succeeded, false if not
2844
	 */
2845
	public function deleteMessages($store, $parententryid, $entryids, $softDelete = false, $unread = false) {
2846
		$result = false;
2847
		if (!is_array($entryids)) {
0 ignored issues
show
introduced by
The condition is_array($entryids) is always true.
Loading history...
2848
			$entryids = [$entryids];
2849
		}
2850
2851
		$folder = mapi_msgstore_openentry($store, $parententryid);
2852
		$flags = $unread ? GX_DELMSG_NOTIFY_UNREAD : 0;
2853
		$msgprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_OUTBOX_ENTRYID]);
2854
2855
		switch ($msgprops[PR_MDB_PROVIDER]) {
2856
			case ZARAFA_STORE_DELEGATE_GUID:
2857
				$softDelete = $softDelete || defined('ENABLE_DEFAULT_SOFT_DELETE') ? ENABLE_DEFAULT_SOFT_DELETE : false;
2858
				// with a store from an other user we need our own waste basket...
2859
				if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) && $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $msgprops[...entryid) || $softDelete, Probably Intended Meaning: IssetNode && ($msgprops[...entryid || $softDelete)
Loading history...
2860
					// except when it is the waste basket itself
2861
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2862
					break;
2863
				}
2864
				$defaultstore = $GLOBALS["mapisession"]->getDefaultMessageStore();
2865
				$msgprops = mapi_getprops($defaultstore, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER]);
2866
2867
				if (!isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) ||
2868
					$msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid) {
2869
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2870
					break;
2871
				}
2872
2873
				try {
2874
					$result = $this->copyMessages(/** @scrutinizer ignore-type */$store, $parententryid, $defaultstore, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true);
2875
				}
2876
				catch (MAPIException $e) {
2877
					$e->setHandled();
2878
					// if moving fails, try normal delete
2879
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2880
				}
2881
				break;
2882
2883
			case ZARAFA_STORE_ARCHIVER_GUID:
2884
			case ZARAFA_STORE_PUBLIC_GUID:
2885
				// always delete in public store and archive store
2886
				$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2887
				break;
2888
2889
			case ZARAFA_SERVICE_GUID:
2890
				// delete message when in your own waste basket, else move it to the waste basket
2891
				if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) && $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete == true) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $msgprops[... || $softDelete == true, Probably Intended Meaning: IssetNode && ($msgprops[...|| $softDelete == true)
Loading history...
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2892
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2893
					break;
2894
				}
2895
2896
				try {
2897
					// if the message is deleting from outbox then first delete the
2898
					// message from an outgoing queue.
2899
					if (function_exists("mapi_msgstore_abortsubmit") && isset($msgprops[PR_IPM_OUTBOX_ENTRYID]) && $msgprops[PR_IPM_OUTBOX_ENTRYID] === $parententryid) {
2900
						foreach ($entryids as $entryid) {
2901
							$message = mapi_msgstore_openentry($store, $entryid);
2902
							$messageProps = mapi_getprops($message, [PR_DEFERRED_SEND_TIME]);
2903
							if (isset($messageProps[PR_DEFERRED_SEND_TIME])) {
2904
								mapi_msgstore_abortsubmit($store, $entryid);
2905
							}
2906
						}
2907
					}
2908
					$result = $this->copyMessages($store, $parententryid, $store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true);
0 ignored issues
show
Bug introduced by
$store of type resource is incompatible with the type object expected by parameter $store of Operations::copyMessages(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2908
					$result = $this->copyMessages(/** @scrutinizer ignore-type */ $store, $parententryid, $store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true);
Loading history...
2909
				}
2910
				catch (MAPIException $e) {
2911
					if ($e->getCode() === MAPI_E_NOT_IN_QUEUE || $e->getCode() === MAPI_E_UNABLE_TO_ABORT) {
2912
						throw $e;
2913
					}
2914
2915
					$e->setHandled();
2916
					// if moving fails, try normal delete
2917
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2918
				}
2919
				break;
2920
		}
2921
2922
		return $result;
2923
	}
2924
2925
	/**
2926
	 * Copy or move messages.
2927
	 *
2928
	 * @param object $store         MAPI Message Store Object
2929
	 * @param string $parententryid parent entryid of the messages
2930
	 * @param string $destentryid   destination folder
2931
	 * @param array  $entryids      a list of entryids which will be copied or moved
2932
	 * @param array  $ignoreProps   a list of proptags which should not be copied over
2933
	 *                              to the new message
2934
	 * @param bool   $moveMessages  true - move messages, false - copy messages
2935
	 * @param array  $props         a list of proptags which should set in new messages
2936
	 * @param mixed  $destStore
2937
	 *
2938
	 * @return bool true if action succeeded, false if not
2939
	 */
2940
	public function copyMessages($store, $parententryid, $destStore, $destentryid, $entryids, $ignoreProps, $moveMessages, $props = []) {
2941
		$sourcefolder = mapi_msgstore_openentry($store, $parententryid);
2942
		$destfolder = mapi_msgstore_openentry($destStore, $destentryid);
2943
2944
		if (!$sourcefolder || !$destfolder) {
2945
			error_log("Could not open source or destination folder. Aborting.");
2946
2947
			return false;
2948
		}
2949
2950
		if (!is_array($entryids)) {
0 ignored issues
show
introduced by
The condition is_array($entryids) is always true.
Loading history...
2951
			$entryids = [$entryids];
2952
		}
2953
2954
		/*
2955
		 * If there are no properties to ignore as well as set then we can use mapi_folder_copymessages instead
2956
		 * of mapi_copyto. mapi_folder_copymessages is much faster then copyto since it executes
2957
		 * the copying on the server instead of in client.
2958
		 */
2959
		if (empty($ignoreProps) && empty($props)) {
2960
			try {
2961
				mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0);
2962
			}
2963
			catch (MAPIException $e) {
2964
				error_log(sprintf("mapi_folder_copymessages failed with code: 0x%08X. Wait 250ms and try again", mapi_last_hresult()));
2965
				// wait 250ms before trying again
2966
				usleep(250000);
2967
2968
				try {
2969
					mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0);
2970
				}
2971
				catch (MAPIException $e) {
2972
					error_log(sprintf("2nd attempt of mapi_folder_copymessages also failed with code: 0x%08X. Abort.", mapi_last_hresult()));
2973
2974
					return false;
2975
				}
2976
			}
2977
		}
2978
		else {
2979
			foreach ($entryids as $entryid) {
2980
				$oldmessage = mapi_msgstore_openentry($store, $entryid);
2981
				$newmessage = mapi_folder_createmessage($destfolder);
2982
2983
				mapi_copyto($oldmessage, [], $ignoreProps, $newmessage, 0);
2984
				if (!empty($props)) {
2985
					mapi_setprops($newmessage, $props);
2986
				}
2987
				mapi_savechanges($newmessage);
2988
			}
2989
			if ($moveMessages) {
2990
				// while moving message we actually copy that particular message into
2991
				// destination folder, and remove it from source folder. so we must have
2992
				// to hard delete the message.
2993
				mapi_folder_deletemessages($sourcefolder, $entryids, DELETE_HARD_DELETE);
2994
			}
2995
		}
2996
2997
		return true;
2998
	}
2999
3000
	/**
3001
	 * Set message read flag.
3002
	 *
3003
	 * @param object $store      MAPI Message Store Object
3004
	 * @param string $entryid    entryid of the message
3005
	 * @param int    $flags      Bitmask of values (read, has attachment etc.)
3006
	 * @param array  $props      properties of the message
3007
	 * @param mixed  $msg_action
3008
	 *
3009
	 * @return bool true if action succeeded, false if not
3010
	 */
3011
	public function setMessageFlag($store, $entryid, $flags, $msg_action = false, &$props = false) {
3012
		$message = $this->openMessage($store, $entryid);
3013
3014
		if ($message) {
3015
			/**
3016
			 * convert flags of PR_MESSAGE_FLAGS property to flags that is
3017
			 * used in mapi_message_setreadflag.
3018
			 */
3019
			$flag = MAPI_DEFERRED_ERRORS;		// set unread flag, read receipt will be sent
3020
3021
			if (($flags & MSGFLAG_RN_PENDING) && isset($msg_action['send_read_receipt']) && $msg_action['send_read_receipt'] == false) {
3022
				$flag |= SUPPRESS_RECEIPT;
3023
			}
3024
			else {
3025
				if (!($flags & MSGFLAG_READ)) {
3026
					$flag |= CLEAR_READ_FLAG;
3027
				}
3028
			}
3029
3030
			mapi_message_setreadflag($message, $flag);
3031
3032
			if (is_array($props)) {
3033
				$props = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]);
3034
			}
3035
		}
3036
3037
		return true;
3038
	}
3039
3040
	/**
3041
	 * Create a unique folder name based on a provided new folder name.
3042
	 *
3043
	 * checkFolderNameConflict() checks if a folder name conflict is caused by the given $foldername.
3044
	 * This function is used for copying of moving a folder to another folder. It returns
3045
	 * a unique foldername.
3046
	 *
3047
	 * @param object $store      MAPI Message Store Object
3048
	 * @param object $folder     MAPI Folder Object
3049
	 * @param string $foldername the folder name
3050
	 *
3051
	 * @return string correct foldername
3052
	 */
3053
	public function checkFolderNameConflict($store, $folder, $foldername) {
3054
		$folderNames = [];
3055
3056
		$hierarchyTable = mapi_folder_gethierarchytable($folder, MAPI_DEFERRED_ERRORS);
3057
		mapi_table_sort($hierarchyTable, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
3058
3059
		$subfolders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]);
3060
3061
		if (is_array($subfolders)) {
3062
			foreach ($subfolders as $subfolder) {
3063
				$folderObject = mapi_msgstore_openentry($store, $subfolder[PR_ENTRYID]);
3064
				$folderProps = mapi_getprops($folderObject, [PR_DISPLAY_NAME]);
3065
3066
				array_push($folderNames, strtolower($folderProps[PR_DISPLAY_NAME]));
3067
			}
3068
		}
3069
3070
		if (array_search(strtolower($foldername), $folderNames) !== false) {
3071
			$i = 2;
3072
			while (array_search(strtolower($foldername) . " ({$i})", $folderNames) !== false) {
3073
				++$i;
3074
			}
3075
			$foldername .= " ({$i})";
3076
		}
3077
3078
		return $foldername;
3079
	}
3080
3081
	/**
3082
	 * Set the recipients of a MAPI message.
3083
	 *
3084
	 * @param resource $message    MAPI Message Object
3085
	 * @param array    $recipients XML array structure of recipients
3086
	 * @param bool     $send       true if we are going to send this message else false
3087
	 */
3088
	public function setRecipients($message, $recipients, $send = false) {
3089
		if (empty($recipients)) {
3090
			// no recipients are sent from client
3091
			return;
3092
		}
3093
3094
		$newRecipients = [];
3095
		$removeRecipients = [];
3096
		$modifyRecipients = [];
3097
3098
		if (isset($recipients['add']) && !empty($recipients['add'])) {
3099
			$newRecipients = $this->createRecipientList($recipients['add'], 'add', false, $send);
3100
		}
3101
3102
		if (isset($recipients['remove']) && !empty($recipients['remove'])) {
3103
			$removeRecipients = $this->createRecipientList($recipients['remove'], 'remove');
3104
		}
3105
3106
		if (isset($recipients['modify']) && !empty($recipients['modify'])) {
3107
			$modifyRecipients = $this->createRecipientList($recipients['modify'], 'modify', false, $send);
3108
		}
3109
3110
		if (!empty($removeRecipients)) {
3111
			mapi_message_modifyrecipients($message, MODRECIP_REMOVE, $removeRecipients);
3112
		}
3113
3114
		if (!empty($modifyRecipients)) {
3115
			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $modifyRecipients);
3116
		}
3117
3118
		if (!empty($newRecipients)) {
3119
			mapi_message_modifyrecipients($message, MODRECIP_ADD, $newRecipients);
3120
		}
3121
	}
3122
3123
	/**
3124
	 * Copy recipients from original message.
3125
	 *
3126
	 * If we are sending mail from a delegator's folder, we need to copy all recipients from the original message
3127
	 *
3128
	 * @param mixed    $message         MAPI Message Object
3129
	 * @param resource $copyFromMessage If set we copy all recipients from this message
3130
	 */
3131
	public function copyRecipients($message, $copyFromMessage = false) {
3132
		$recipienttable = mapi_message_getrecipienttable($copyFromMessage);
3133
		$messageRecipients = mapi_table_queryallrows($recipienttable, $GLOBALS["properties"]->getRecipientProperties());
3134
		if (!empty($messageRecipients)) {
3135
			mapi_message_modifyrecipients($message, MODRECIP_ADD, $messageRecipients);
3136
		}
3137
	}
3138
3139
	/**
3140
	 * Set attachments in a MAPI message.
3141
	 *
3142
	 * This function reads any attachments that have been previously uploaded and copies them into
3143
	 * the passed MAPI message resource. For a description of the dialog_attachments variable and
3144
	 * generally how attachments work when uploading, see Operations::saveMessage()
3145
	 *
3146
	 * @see Operations::saveMessage()
3147
	 *
3148
	 * @param resource        $message          MAPI Message Object
3149
	 * @param array           $attachments      XML array structure of attachments
3150
	 * @param AttachmentState $attachment_state the state object in which the attachments are saved
3151
	 *                                          between different requests
3152
	 */
3153
	public function setAttachments($message, $attachments, $attachment_state) {
3154
		// Check if attachments should be deleted. This is set in the "upload_attachment.php" file
3155
		if (isset($attachments['dialog_attachments'])) {
3156
			$deleted = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']);
3157
			if ($deleted) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $deleted of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3158
				foreach ($deleted as $attach_num) {
3159
					try {
3160
						mapi_message_deleteattach($message, (int) $attach_num);
3161
					}
3162
					catch (Exception $e) {
3163
						continue;
3164
					}
3165
				}
3166
				$attachment_state->clearDeletedAttachments($attachments['dialog_attachments']);
3167
			}
3168
		}
3169
3170
		$addedInlineAttachmentCidMapping = [];
3171
		if (is_array($attachments) && !empty($attachments)) {
3172
			// Set contentId to saved attachments.
3173
			if (isset($attachments['add']) && is_array($attachments['add']) && !empty($attachments['add'])) {
3174
				foreach ($attachments['add'] as $key => $attach) {
3175
					if ($attach && isset($attach['inline']) && $attach['inline']) {
3176
						$addedInlineAttachmentCidMapping[$attach['attach_num']] = $attach['cid'];
3177
						$msgattachment = mapi_message_openattach($message, $attach['attach_num']);
3178
						if ($msgattachment) {
3179
							$props = [PR_ATTACH_CONTENT_ID => $attach['cid'], PR_ATTACHMENT_HIDDEN => true];
3180
							mapi_setprops($msgattachment, $props);
3181
							mapi_savechanges($msgattachment);
3182
						}
3183
					}
3184
				}
3185
			}
3186
3187
			// Delete saved inline images if removed from body.
3188
			if (isset($attachments['remove']) && is_array($attachments['remove']) && !empty($attachments['remove'])) {
3189
				foreach ($attachments['remove'] as $key => $attach) {
3190
					if ($attach && isset($attach['inline']) && $attach['inline']) {
3191
						$msgattachment = mapi_message_openattach($message, $attach['attach_num']);
3192
						if ($msgattachment) {
3193
							mapi_message_deleteattach($message, $attach['attach_num']);
3194
							mapi_savechanges($message);
3195
						}
3196
					}
3197
				}
3198
			}
3199
		}
3200
3201
		if ($attachments['dialog_attachments']) {
3202
			$dialog_attachments = $attachments['dialog_attachments'];
3203
		}
3204
		else {
3205
			return;
3206
		}
3207
3208
		$files = $attachment_state->getAttachmentFiles($dialog_attachments);
3209
		if ($files) {
3210
			// Loop through the uploaded attachments
3211
			foreach ($files as $tmpname => $fileinfo) {
3212
				if ($fileinfo['sourcetype'] === 'embedded') {
3213
					// open message which needs to be embedded
3214
					$copyFromStore = $GLOBALS['mapisession']->openMessageStore(hex2bin($fileinfo['store_entryid']));
3215
					$copyFrom = mapi_msgstore_openentry($copyFromStore, hex2bin($fileinfo['entryid']));
3216
3217
					$msgProps = mapi_getprops($copyFrom, [PR_SUBJECT]);
3218
3219
					// get message and copy it to attachment table as embedded attachment
3220
					$props = [];
3221
					$props[PR_EC_WA_ATTACHMENT_ID] = $fileinfo['attach_id'];
3222
					$props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG;
3223
					$props[PR_DISPLAY_NAME] = !empty($msgProps[PR_SUBJECT]) ? $msgProps[PR_SUBJECT] : _('Untitled');
3224
3225
					// Create new attachment.
3226
					$attachment = mapi_message_createattach($message);
3227
					mapi_setprops($attachment, $props);
3228
3229
					$imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
3230
3231
					// Copy the properties from the source message to the attachment
3232
					mapi_copyto($copyFrom, [], [], $imessage, 0); // includes attachments and recipients
3233
3234
					// save changes in the embedded message and the final attachment
3235
					mapi_savechanges($imessage);
3236
					mapi_savechanges($attachment);
3237
				}
3238
				elseif ($fileinfo['sourcetype'] === 'icsfile') {
3239
					$messageStore = $GLOBALS['mapisession']->openMessageStore(hex2bin($fileinfo['store_entryid']));
3240
					$copyFrom = mapi_msgstore_openentry($messageStore, hex2bin($fileinfo['entryid']));
3241
3242
					// Get addressbook for current session
3243
					$addrBook = $GLOBALS['mapisession']->getAddressbook();
3244
3245
					// get message properties.
3246
					$messageProps = mapi_getprops($copyFrom, [PR_SUBJECT]);
3247
3248
					// Read the appointment as RFC2445-formatted ics stream.
3249
					$appointmentStream = mapi_mapitoical($GLOBALS['mapisession']->getSession(), $addrBook, $copyFrom, []);
3250
3251
					$filename = (!empty($messageProps[PR_SUBJECT])) ? $messageProps[PR_SUBJECT] : _('Untitled');
3252
					$filename .= '.ics';
3253
3254
					$props = [
3255
						PR_ATTACH_LONG_FILENAME => $filename,
3256
						PR_DISPLAY_NAME => $filename,
3257
						PR_ATTACH_METHOD => ATTACH_BY_VALUE,
3258
						PR_ATTACH_DATA_BIN => "",
3259
						PR_ATTACH_MIME_TAG => "application/octet-stream",
3260
						PR_ATTACHMENT_HIDDEN => false,
3261
						PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(),
3262
						PR_ATTACH_EXTENSION => pathinfo($filename, PATHINFO_EXTENSION),
3263
					];
3264
3265
					$attachment = mapi_message_createattach($message);
3266
					mapi_setprops($attachment, $props);
3267
3268
					// Stream the file to the PR_ATTACH_DATA_BIN property
3269
					$stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
3270
					mapi_stream_write($stream, $appointmentStream);
3271
3272
					// Commit the stream and save changes
3273
					mapi_stream_commit($stream);
3274
					mapi_savechanges($attachment);
3275
				}
3276
				else {
3277
					$filepath = $attachment_state->getAttachmentPath($tmpname);
3278
					if (is_file($filepath)) {
3279
						// Set contentId if attachment is inline
3280
						$cid = '';
3281
						if (isset($addedInlineAttachmentCidMapping[$tmpname])) {
3282
							$cid = $addedInlineAttachmentCidMapping[$tmpname];
3283
						}
3284
3285
						// If a .p7m file was manually uploaded by the user, we must change the mime type because
3286
						// otherwise mail applications will think the containing email is an encrypted email.
3287
						// That will make Outlook crash, and it will make grommunio Web show the original mail as encrypted
3288
						// without showing the attachment
3289
						$mimeType = $fileinfo["type"];
3290
						$smimeTags = ['multipart/signed', 'application/pkcs7-mime', 'application/x-pkcs7-mime'];
3291
						if (in_array($mimeType, $smimeTags)) {
3292
							$mimeType = "application/octet-stream";
3293
						}
3294
3295
						// Set attachment properties
3296
						$props = [
3297
							PR_ATTACH_LONG_FILENAME => $fileinfo["name"],
3298
							PR_DISPLAY_NAME => $fileinfo["name"],
3299
							PR_ATTACH_METHOD => ATTACH_BY_VALUE,
3300
							PR_ATTACH_DATA_BIN => "",
3301
							PR_ATTACH_MIME_TAG => $mimeType,
3302
							PR_ATTACHMENT_HIDDEN => !empty($cid) ? true : false,
3303
							PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(),
3304
							PR_ATTACH_EXTENSION => pathinfo($fileinfo["name"], PATHINFO_EXTENSION),
3305
						];
3306
3307
						if (isset($fileinfo['sourcetype']) && $fileinfo['sourcetype'] === 'contactphoto') {
3308
							$props[PR_ATTACHMENT_HIDDEN] = true;
3309
							$props[PR_ATTACHMENT_CONTACTPHOTO] = true;
3310
						}
3311
3312
						if (!empty($cid)) {
3313
							$props[PR_ATTACH_CONTENT_ID] = $cid;
3314
						}
3315
3316
						// Create attachment and set props
3317
						$attachment = mapi_message_createattach($message);
3318
						mapi_setprops($attachment, $props);
3319
3320
						// Stream the file to the PR_ATTACH_DATA_BIN property
3321
						$stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
3322
						$handle = fopen($filepath, "r");
3323
						while (!feof($handle)) {
3324
							$contents = fread($handle, BLOCK_SIZE);
3325
							mapi_stream_write($stream, $contents);
3326
						}
3327
3328
						// Commit the stream and save changes
3329
						mapi_stream_commit($stream);
3330
						mapi_savechanges($attachment);
3331
						fclose($handle);
3332
						unlink($filepath);
3333
					}
3334
				}
3335
			}
3336
3337
			// Delete all the files in the state.
3338
			$attachment_state->clearAttachmentFiles($dialog_attachments);
3339
		}
3340
	}
3341
3342
	/**
3343
	 * Copy attachments from original message.
3344
	 *
3345
	 * @see Operations::saveMessage()
3346
	 *
3347
	 * @param object          $message                   MAPI Message Object
3348
	 * @param array           $attachments
3349
	 * @param resource        $copyFromMessage           if set, copy the attachments from this message in addition to the uploaded attachments
3350
	 * @param bool            $copyInlineAttachmentsOnly if true then copy only inline attachments
3351
	 * @param AttachmentState $attachment_state          the state object in which the attachments are saved
3352
	 *                                                   between different requests
3353
	 */
3354
	public function copyAttachments($message, $attachments, $copyFromMessage, $copyInlineAttachmentsOnly, $attachment_state) {
3355
		$attachmentTable = mapi_message_getattachmenttable($copyFromMessage);
3356
		if ($attachmentTable && isset($attachments['dialog_attachments'])) {
3357
			$existingAttachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD, PR_ATTACH_CONTENT_ID]);
3358
			$deletedAttachments = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']);
3359
3360
			$plainText = $this->isPlainText(/** @scrutinizer ignore-type */$message);
3361
3362
			$properties = $GLOBALS['properties']->getMailProperties();
3363
			$blockStatus = mapi_getprops($copyFromMessage, [PR_BLOCK_STATUS]);
3364
			$blockStatus = Conversion::mapMAPI2XML($properties, $blockStatus);
3365
			$isSafeSender = false;
3366
3367
			// Here if message is HTML and block status is empty then and then call isSafeSender function
3368
			// to check that sender or sender's domain of original message was part of safe sender list.
3369
			if (!$plainText && empty($blockStatus)) {
3370
				$isSafeSender = $this->isSafeSender($copyFromMessage);
3371
			}
3372
3373
			$body = false;
3374
			foreach ($existingAttachments as $props) {
3375
				// check if this attachment is "deleted"
3376
3377
				if ($deletedAttachments && in_array($props[PR_ATTACH_NUM], $deletedAttachments)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $deletedAttachments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3378
					// skip attachment, remove reference from state as it no longer applies.
3379
					$attachment_state->removeDeletedAttachment($attachments['dialog_attachments'], $props[PR_ATTACH_NUM]);
3380
3381
					continue;
3382
				}
3383
3384
				$old = mapi_message_openattach($copyFromMessage, $props[PR_ATTACH_NUM]);
3385
				$isInlineAttachment = $attachment_state->isInlineAttachment($old);
3386
3387
				/*
3388
				 * If reply/reply all message, then copy only inline attachments.
3389
				 */
3390
				if ($copyInlineAttachmentsOnly) {
3391
					/*
3392
					 * if message is reply/reply all and format is plain text than ignore inline attachments
3393
					 * and normal attachments to copy from original mail.
3394
					 */
3395
					if ($plainText || !$isInlineAttachment) {
3396
						continue;
3397
					}
3398
				}
3399
				elseif ($plainText && $isInlineAttachment) {
3400
					/*
3401
					 * If message is forward and format of message is plain text then ignore only inline attachments from the
3402
					 * original mail.
3403
					 */
3404
					continue;
3405
				}
3406
3407
				/*
3408
				 * If the inline attachment is referenced with an content-id,
3409
				 * manually check if it's still referenced in the body otherwise remove it
3410
				 */
3411
				if ($isInlineAttachment) {
3412
					// Cache body, so we stream it once
3413
					if ($body === false) {
3414
						$body = streamProperty(/** @scrutinizer ignore-type */$message, PR_HTML);
3415
					}
3416
3417
					$contentID = $props[PR_ATTACH_CONTENT_ID];
3418
					if (strpos($body, $contentID) === false) {
0 ignored issues
show
Bug introduced by
It seems like $body can also be of type false; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3418
					if (strpos(/** @scrutinizer ignore-type */ $body, $contentID) === false) {
Loading history...
3419
						continue;
3420
					}
3421
				}
3422
3423
				/*
3424
				 * if message is reply/reply all or forward and format of message is HTML but
3425
				 * - inline attachments are not downloaded from external source
3426
				 * - sender of original message is not safe sender
3427
				 * - domain of sender is not part of safe sender list
3428
				 * then ignore inline attachments from original message.
3429
				 *
3430
				 * NOTE : blockStatus is only generated when user has download inline image from external source.
3431
				 * it should remains empty if user add the sender in to safe sender list.
3432
				 */
3433
				if (!$plainText && $isInlineAttachment && empty($blockStatus) && !$isSafeSender) {
3434
					continue;
3435
				}
3436
3437
				$new = mapi_message_createattach($message);
3438
3439
				try {
3440
					mapi_copyto($old, [], [], $new, 0);
3441
					mapi_savechanges($new);
3442
				}
3443
				catch (MAPIException $e) {
3444
					// This is a workaround for the grommunio-web issue 75
3445
					// Remove it after gromox issue 253 is resolved
3446
					if ($e->getCode() == ecMsgCycle) {
3447
						$oldstream = mapi_openproperty($old, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
3448
						$stat = mapi_stream_stat($oldstream);
3449
						$props = mapi_attach_getprops($old, [PR_ATTACH_LONG_FILENAME, PR_ATTACH_MIME_TAG, PR_DISPLAY_NAME, PR_ATTACH_METHOD, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_EXTENSION, PR_ATTACH_FLAGS]);
3450
3451
						mapi_setprops($new, [
3452
							PR_ATTACH_LONG_FILENAME => $props[PR_ATTACH_LONG_FILENAME] ?? '',
3453
							PR_ATTACH_MIME_TAG => $props[PR_ATTACH_MIME_TAG] ?? "application/octet-stream",
3454
							PR_DISPLAY_NAME => $props[PR_DISPLAY_NAME] ?? '',
3455
							PR_ATTACH_METHOD => $props[PR_ATTACH_METHOD] ?? ATTACH_BY_VALUE,
3456
							PR_ATTACH_FILENAME => $props[PR_ATTACH_FILENAME] ?? '',
3457
							PR_ATTACH_DATA_BIN => "",
3458
							PR_ATTACHMENT_HIDDEN => $props[PR_ATTACHMENT_HIDDEN] ?? false,
3459
							PR_ATTACH_EXTENSION => $props[PR_ATTACH_EXTENSION] ?? '',
3460
							PR_ATTACH_FLAGS => $props[PR_ATTACH_FLAGS] ?? 0,
3461
						]);
3462
						$newstream = mapi_openproperty($new, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
3463
						mapi_stream_setsize($newstream, $stat['cb']);
3464
						for ($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) {
3465
							mapi_stream_write($newstream, mapi_stream_read($oldstream, BLOCK_SIZE));
3466
						}
3467
						mapi_stream_commit($newstream);
3468
						mapi_savechanges($new);
3469
					}
3470
				}
3471
			}
3472
		}
3473
	}
3474
3475
	/**
3476
	 * Function was used to identify the sender or domain of original mail in safe sender list.
3477
	 *
3478
	 * @param resource $copyFromMessage resource of the message from which we should get
3479
	 *                                  the sender of message
3480
	 *
3481
	 * @return bool true if sender of original mail was safe sender else false
3482
	 */
3483
	public function isSafeSender($copyFromMessage) {
3484
		$safeSenderList = $GLOBALS['settings']->get('zarafa/v1/contexts/mail/safe_senders_list');
3485
		$senderEntryid = mapi_getprops($copyFromMessage, [PR_SENT_REPRESENTING_ENTRYID]);
3486
		$senderEntryid = $senderEntryid[PR_SENT_REPRESENTING_ENTRYID];
3487
3488
		// If sender is user himself (which happens in case of "Send as New message") consider sender as safe
3489
		if ($GLOBALS['entryid']->compareEntryIds($senderEntryid, $GLOBALS["mapisession"]->getUserEntryID())) {
3490
			return true;
3491
		}
3492
3493
		try {
3494
			$mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $senderEntryid);
3495
		}
3496
		catch (MAPIException $e) {
3497
			// The user might have a new uidNumber, which makes the user not resolve, see WA-7673
3498
			// FIXME: Lookup the user by PR_SENDER_NAME or another attribute if PR_SENDER_ADDRTYPE is "EX"
3499
			return false;
3500
		}
3501
3502
		$addressType = mapi_getprops($mailuser, [PR_ADDRTYPE]);
3503
3504
		// Here it will check that sender of original mail was address book user.
3505
		// If PR_ADDRTYPE is ZARAFA, it means sender of original mail was address book contact.
3506
		if ($addressType[PR_ADDRTYPE] === 'EX') {
3507
			$address = mapi_getprops($mailuser, [PR_SMTP_ADDRESS]);
3508
			$address = $address[PR_SMTP_ADDRESS];
3509
		}
3510
		elseif ($addressType[PR_ADDRTYPE] === 'SMTP') {
3511
			// If PR_ADDRTYPE is SMTP, it means sender of original mail was external sender.
3512
			$address = mapi_getprops($mailuser, [PR_EMAIL_ADDRESS]);
3513
			$address = $address[PR_EMAIL_ADDRESS];
3514
		}
3515
3516
		// Obtain the Domain address from smtp/email address.
3517
		$domain = substr($address, strpos($address, "@") + 1);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $address does not seem to be defined for all execution paths leading up to this point.
Loading history...
3518
3519
		if (!empty($safeSenderList)) {
3520
			foreach ($safeSenderList as $safeSender) {
3521
				if ($safeSender === $address || $safeSender === $domain) {
3522
					return true;
3523
				}
3524
			}
3525
		}
3526
3527
		return false;
3528
	}
3529
3530
	/**
3531
	 * get attachments information of a particular message.
3532
	 *
3533
	 * @param resource $message       MAPI Message Object
3534
	 * @param bool     $excludeHidden exclude hidden attachments
3535
	 */
3536
	public function getAttachmentsInfo($message, $excludeHidden = false) {
3537
		$attachmentsInfo = [];
3538
3539
		$hasattachProp = mapi_getprops($message, [PR_HASATTACH]);
3540
		if (isset($hasattachProp[PR_HASATTACH]) && $hasattachProp[PR_HASATTACH]) {
3541
			$attachmentTable = mapi_message_getattachmenttable($message);
3542
3543
			$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME,
3544
				PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD,
3545
				PR_ATTACH_CONTENT_ID, PR_ATTACH_MIME_TAG,
3546
				PR_ATTACHMENT_CONTACTPHOTO, PR_RECORD_KEY, PR_EC_WA_ATTACHMENT_ID, PR_OBJECT_TYPE, PR_ATTACH_EXTENSION, ]);
3547
			foreach ($attachments as $attachmentRow) {
3548
				$props = [];
3549
3550
				if (isset($attachmentRow[PR_ATTACH_MIME_TAG])) {
3551
					if ($attachmentRow[PR_ATTACH_MIME_TAG]) {
3552
						$props["filetype"] = $attachmentRow[PR_ATTACH_MIME_TAG];
3553
					}
3554
3555
					$smimeTags = ['multipart/signed', 'application/pkcs7-mime', 'application/x-pkcs7-mime'];
3556
					if (in_array($attachmentRow[PR_ATTACH_MIME_TAG], $smimeTags)) {
3557
						// Ignore the message with attachment types set as smime as they are for smime
3558
						continue;
3559
					}
3560
				}
3561
3562
				$attach_id = '';
3563
				if (isset($attachmentRow[PR_EC_WA_ATTACHMENT_ID])) {
3564
					$attach_id = $attachmentRow[PR_EC_WA_ATTACHMENT_ID];
3565
				}
3566
				elseif (isset($attachmentRow[PR_RECORD_KEY])) {
3567
					$attach_id = bin2hex($attachmentRow[PR_RECORD_KEY]);
3568
				}
3569
				else {
3570
					$attach_id = uniqid();
3571
				}
3572
3573
				$props["object_type"] = $attachmentRow[PR_OBJECT_TYPE];
3574
				$props["attach_id"] = $attach_id;
3575
				$props["attach_num"] = $attachmentRow[PR_ATTACH_NUM];
3576
				$props["attach_method"] = $attachmentRow[PR_ATTACH_METHOD];
3577
				$props["size"] = $attachmentRow[PR_ATTACH_SIZE];
3578
3579
				if (isset($attachmentRow[PR_ATTACH_CONTENT_ID]) && $attachmentRow[PR_ATTACH_CONTENT_ID]) {
3580
					$props["cid"] = $attachmentRow[PR_ATTACH_CONTENT_ID];
3581
				}
3582
3583
				$props["hidden"] = isset($attachmentRow[PR_ATTACHMENT_HIDDEN]) ? $attachmentRow[PR_ATTACHMENT_HIDDEN] : false;
3584
				if ($excludeHidden && $props["hidden"]) {
3585
					continue;
3586
				}
3587
3588
				if (isset($attachmentRow[PR_ATTACH_LONG_FILENAME])) {
3589
					$props["name"] = $attachmentRow[PR_ATTACH_LONG_FILENAME];
3590
				}
3591
				elseif (isset($attachmentRow[PR_ATTACH_FILENAME])) {
3592
					$props["name"] = $attachmentRow[PR_ATTACH_FILENAME];
3593
				}
3594
				elseif (isset($attachmentRow[PR_DISPLAY_NAME])) {
3595
					$props["name"] = $attachmentRow[PR_DISPLAY_NAME];
3596
				}
3597
				else {
3598
					$props["name"] = "untitled";
3599
				}
3600
3601
				if (isset($attachmentRow[PR_ATTACH_EXTENSION]) && $attachmentRow[PR_ATTACH_EXTENSION]) {
3602
					$props["extension"] = $attachmentRow[PR_ATTACH_EXTENSION];
3603
				}
3604
				else {
3605
					// For backward compatibility where attachments doesn't have the extension property
3606
					$props["extension"] = pathinfo($props["name"], PATHINFO_EXTENSION);
3607
				}
3608
3609
				if (isset($attachmentRow[PR_ATTACHMENT_CONTACTPHOTO]) && $attachmentRow[PR_ATTACHMENT_CONTACTPHOTO]) {
3610
					$props["attachment_contactphoto"] = $attachmentRow[PR_ATTACHMENT_CONTACTPHOTO];
3611
					$props["hidden"] = true;
3612
3613
					// Open contact photo attachement in binary format.
3614
					$attach = mapi_message_openattach($message, $props["attach_num"]);
0 ignored issues
show
Unused Code introduced by
The assignment to $attach is dead and can be removed.
Loading history...
3615
				}
3616
3617
				if ($props["attach_method"] == ATTACH_EMBEDDED_MSG) {
3618
					// open attachment to get the message class
3619
					$attach = mapi_message_openattach($message, $props["attach_num"]);
3620
					$embMessage = mapi_attach_openobj($attach);
3621
					$embProps = mapi_getprops($embMessage, [PR_MESSAGE_CLASS]);
3622
					if (isset($embProps[PR_MESSAGE_CLASS])) {
3623
						$props["attach_message_class"] = $embProps[PR_MESSAGE_CLASS];
3624
					}
3625
				}
3626
3627
				array_push($attachmentsInfo, ["props" => $props]);
3628
			}
3629
		}
3630
3631
		return $attachmentsInfo;
3632
	}
3633
3634
	/**
3635
	 * get recipients information of a particular message.
3636
	 *
3637
	 * @param resource $message        MAPI Message Object
3638
	 * @param bool     $excludeDeleted exclude deleted recipients
3639
	 */
3640
	public function getRecipientsInfo($message, $excludeDeleted = true) {
3641
		$recipientsInfo = [];
3642
3643
		$recipientTable = mapi_message_getrecipienttable($message);
3644
		if ($recipientTable) {
3645
			$recipients = mapi_table_queryallrows($recipientTable, $GLOBALS['properties']->getRecipientProperties());
3646
3647
			foreach ($recipients as $recipientRow) {
3648
				if ($excludeDeleted && isset($recipientRow[PR_RECIPIENT_FLAGS]) && (($recipientRow[PR_RECIPIENT_FLAGS] & recipExceptionalDeleted) == recipExceptionalDeleted)) {
3649
					continue;
3650
				}
3651
3652
				$props = [];
3653
				$props['rowid'] = $recipientRow[PR_ROWID];
3654
				$props['search_key'] = isset($recipientRow[PR_SEARCH_KEY]) ? bin2hex($recipientRow[PR_SEARCH_KEY]) : '';
3655
				$props['display_name'] = $recipientRow[PR_DISPLAY_NAME] ?? '';
3656
				$props['email_address'] = $recipientRow[PR_EMAIL_ADDRESS] ?? '';
3657
				$props['smtp_address'] = $recipientRow[PR_SMTP_ADDRESS] ?? '';
3658
				$props['address_type'] = $recipientRow[PR_ADDRTYPE] ?? '';
3659
				$props['object_type'] = $recipientRow[PR_OBJECT_TYPE] ?? MAPI_MAILUSER;
3660
				$props['recipient_type'] = $recipientRow[PR_RECIPIENT_TYPE];
3661
				$props['display_type'] = $recipientRow[PR_DISPLAY_TYPE] ?? DT_MAILUSER;
3662
				$props['display_type_ex'] = $recipientRow[PR_DISPLAY_TYPE_EX] ?? DT_MAILUSER;
3663
3664
				if (isset($recipientRow[PR_RECIPIENT_FLAGS])) {
3665
					$props['recipient_flags'] = $recipientRow[PR_RECIPIENT_FLAGS];
3666
				}
3667
3668
				if (isset($recipientRow[PR_ENTRYID])) {
3669
					$props['entryid'] = bin2hex($recipientRow[PR_ENTRYID]);
3670
3671
					// Get the SMTP address from the addressbook if no address is found
3672
					if (empty($props['smtp_address']) && ($recipientRow[PR_ADDRTYPE] == 'EX' || $props['address_type'] === 'ZARAFA')) {
3673
						$recipientSearchKey = isset($recipientRow[PR_SEARCH_KEY]) ? $recipientRow[PR_SEARCH_KEY] : false;
3674
						$props['smtp_address'] = $this->getEmailAddress($recipientRow[PR_ENTRYID], $recipientSearchKey);
3675
					}
3676
				}
3677
3678
				// smtp address is still empty(in case of external email address) than
3679
				// value of email address is copied into smtp address.
3680
				if ($props['address_type'] == 'SMTP' && empty($props['smtp_address'])) {
3681
					$props['smtp_address'] = $props['email_address'];
3682
				}
3683
3684
				// PST importer imports items without an entryid and as SMTP recipient, this causes issues for
3685
				// opening meeting requests with removed users as recipient.
3686
				// gromox-kdb2mt might import items without an entryid and
3687
				// PR_ADDRTYPE 'ZARAFA' which causes issues when opening such messages.
3688
				if (empty($props['entryid']) && ($props['address_type'] === 'SMTP' || $props['address_type'] === 'ZARAFA')) {
3689
					$props['entryid'] = bin2hex(mapi_createoneoff($props['display_name'], $props['address_type'], $props['smtp_address'], MAPI_UNICODE));
3690
				}
3691
3692
				// Set propose new time properties
3693
				if (isset($recipientRow[PR_RECIPIENT_PROPOSED], $recipientRow[PR_RECIPIENT_PROPOSEDSTARTTIME], $recipientRow[PR_RECIPIENT_PROPOSEDENDTIME])) {
3694
					$props['proposednewtime_start'] = $recipientRow[PR_RECIPIENT_PROPOSEDSTARTTIME];
3695
					$props['proposednewtime_end'] = $recipientRow[PR_RECIPIENT_PROPOSEDENDTIME];
3696
					$props['proposednewtime'] = $recipientRow[PR_RECIPIENT_PROPOSED];
3697
				}
3698
				else {
3699
					$props['proposednewtime'] = false;
3700
				}
3701
3702
				$props['recipient_trackstatus'] = $recipientRow[PR_RECIPIENT_TRACKSTATUS] ?? olRecipientTrackStatusNone;
3703
				$props['recipient_trackstatus_time'] = $recipientRow[PR_RECIPIENT_TRACKSTATUS_TIME] ?? null;
3704
3705
				array_push($recipientsInfo, ["props" => $props]);
3706
			}
3707
		}
3708
3709
		return $recipientsInfo;
3710
	}
3711
3712
	/**
3713
	 * Extracts email address from PR_SEARCH_KEY property if possible.
3714
	 *
3715
	 * @param string $searchKey The PR_SEARCH_KEY property
3716
	 *
3717
	 * @return string email address if possible else return empty string
3718
	 */
3719
	public function getEmailAddressFromSearchKey($searchKey) {
3720
		if (strpos($searchKey, ':') !== false && strpos($searchKey, '@') !== false) {
3721
			return trim(strtolower(explode(':', $searchKey)[1]));
3722
		}
3723
3724
		return "";
3725
	}
3726
3727
	/**
3728
	 * Create a MAPI recipient list from an XML array structure.
3729
	 *
3730
	 * This functions is used for setting the recipient table of a message.
3731
	 *
3732
	 * @param array  $recipientList a list of recipients as XML array structure
3733
	 * @param string $opType        the type of operation that will be performed on this recipient list (add, remove, modify)
3734
	 * @param bool   $send          true if we are going to send this message else false
3735
	 * @param mixed  $isException
3736
	 *
3737
	 * @return array list of recipients with the correct MAPI properties ready for mapi_message_modifyrecipients()
3738
	 */
3739
	public function createRecipientList($recipientList, $opType = 'add', $isException = false, $send = false) {
3740
		$recipients = [];
3741
		$addrbook = $GLOBALS["mapisession"]->getAddressbook();
3742
3743
		foreach ($recipientList as $recipientItem) {
3744
			if ($isException) {
3745
				// We do not add organizer to exception msg in organizer's calendar.
3746
				if (isset($recipientItem[PR_RECIPIENT_FLAGS]) && $recipientItem[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
3747
					continue;
3748
				}
3749
3750
				$recipient[PR_RECIPIENT_FLAGS] = (recipSendable | recipExceptionalResponse | recipReserved);
3751
			}
3752
3753
			if (!empty($recipientItem["smtp_address"]) && empty($recipientItem["email_address"])) {
3754
				$recipientItem["email_address"] = $recipientItem["smtp_address"];
3755
			}
3756
3757
			// When saving a mail we can allow an empty email address or entryid, but not when sending it
3758
			if ($send && empty($recipientItem["email_address"]) && empty($recipientItem['entryid'])) {
3759
				return;
3760
			}
3761
3762
			// to modify or remove recipients we need PR_ROWID property
3763
			if ($opType !== 'add' && (!isset($recipientItem['rowid']) || !is_numeric($recipientItem['rowid']))) {
3764
				continue;
3765
			}
3766
3767
			if (isset($recipientItem['search_key']) && !empty($recipientItem['search_key'])) {
3768
				// search keys sent from client are in hex format so convert it to binary format
3769
				$recipientItem['search_key'] = hex2bin($recipientItem['search_key']);
3770
			}
3771
3772
			if (isset($recipientItem["entryid"]) && !empty($recipientItem["entryid"])) {
3773
				// entryids sent from client are in hex format so convert it to binary format
3774
				$recipientItem["entryid"] = hex2bin($recipientItem["entryid"]);
3775
3776
			// Only resolve the recipient when no entryid is set
3777
			}
3778
			else {
3779
				/**
3780
				 * For external contacts (DT_REMOTE_MAILUSER) email_address contains display name of contact
3781
				 * which is obviously not unique so for that we need to resolve address based on smtp_address
3782
				 * if provided.
3783
				 */
3784
				$addressToResolve = $recipientItem["email_address"];
3785
				if (!empty($recipientItem["smtp_address"])) {
3786
					$addressToResolve = $recipientItem["smtp_address"];
3787
				}
3788
3789
				// Resolve the recipient
3790
				$user = [[PR_DISPLAY_NAME => $addressToResolve]];
3791
3792
				try {
3793
					// resolve users based on email address with strict matching
3794
					$user = mapi_ab_resolvename($addrbook, $user, EMS_AB_ADDRESS_LOOKUP);
3795
					$recipientItem["display_name"] = $user[0][PR_DISPLAY_NAME];
3796
					$recipientItem["entryid"] = $user[0][PR_ENTRYID];
3797
					$recipientItem["search_key"] = $user[0][PR_SEARCH_KEY];
3798
					$recipientItem["email_address"] = $user[0][PR_EMAIL_ADDRESS];
3799
					$recipientItem["address_type"] = $user[0][PR_ADDRTYPE];
3800
				}
3801
				catch (MAPIException $e) {
3802
					// recipient is not resolved or it got multiple matches,
3803
					// so ignore this error and continue with normal processing
3804
					$e->setHandled();
3805
				}
3806
			}
3807
3808
			$recipient = [];
3809
			$recipient[PR_DISPLAY_NAME] = $recipientItem["display_name"];
3810
			$recipient[PR_DISPLAY_TYPE] = $recipientItem["display_type"];
3811
			$recipient[PR_DISPLAY_TYPE_EX] = $recipientItem["display_type_ex"];
3812
			$recipient[PR_EMAIL_ADDRESS] = $recipientItem["email_address"];
3813
			$recipient[PR_SMTP_ADDRESS] = $recipientItem["smtp_address"];
3814
			if (isset($recipientItem["search_key"])) {
3815
				$recipient[PR_SEARCH_KEY] = $recipientItem["search_key"];
3816
			}
3817
			$recipient[PR_ADDRTYPE] = $recipientItem["address_type"];
3818
			$recipient[PR_OBJECT_TYPE] = $recipientItem["object_type"];
3819
			$recipient[PR_RECIPIENT_TYPE] = $recipientItem["recipient_type"];
3820
			if ($opType != 'add') {
3821
				$recipient[PR_ROWID] = $recipientItem["rowid"];
3822
			}
3823
3824
			if (isset($recipientItem["recipient_status"]) && !empty($recipientItem["recipient_status"])) {
3825
				$recipient[PR_RECIPIENT_TRACKSTATUS] = $recipientItem["recipient_status"];
3826
			}
3827
3828
			if (isset($recipientItem["recipient_flags"]) && !empty($recipient["recipient_flags"])) {
3829
				$recipient[PR_RECIPIENT_FLAGS] = $recipientItem["recipient_flags"];
3830
			}
3831
			else {
3832
				$recipient[PR_RECIPIENT_FLAGS] = recipSendable;
3833
			}
3834
3835
			if (isset($recipientItem["proposednewtime"]) && !empty($recipientItem["proposednewtime"]) && isset($recipientItem["proposednewtime_start"], $recipientItem["proposednewtime_end"])) {
3836
				$recipient[PR_RECIPIENT_PROPOSED] = $recipientItem["proposednewtime"];
3837
				$recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $recipientItem["proposednewtime_start"];
3838
				$recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $recipientItem["proposednewtime_end"];
3839
			}
3840
			else {
3841
				$recipient[PR_RECIPIENT_PROPOSED] = false;
3842
			}
3843
3844
			// Use given entryid if possible, otherwise create a one-off entryid
3845
			if (isset($recipientItem["entryid"]) && !empty($recipientItem["entryid"])) {
3846
				$recipient[PR_ENTRYID] = $recipientItem["entryid"];
3847
			}
3848
			elseif ($send) {
3849
				// only create one-off entryid when we are actually sending the message not saving it
3850
				$recipient[PR_ENTRYID] = mapi_createoneoff($recipient[PR_DISPLAY_NAME], $recipient[PR_ADDRTYPE], $recipient[PR_EMAIL_ADDRESS]);
3851
			}
3852
3853
			array_push($recipients, $recipient);
3854
		}
3855
3856
		return $recipients;
3857
	}
3858
3859
	/**
3860
	 * Function which is get store of external resource from entryid.
3861
	 *
3862
	 * @param string $entryid entryid of the shared folder record
3863
	 *
3864
	 * @return object/boolean $store store of shared folder if found otherwise false
0 ignored issues
show
Documentation Bug introduced by
The doc comment object/boolean at position 0 could not be parsed: Unknown type name 'object/boolean' at position 0 in object/boolean.
Loading history...
3865
	 *
3866
	 * FIXME: this function is pretty inefficient, since it opens the store for every
3867
	 * shared user in the worst case. Might be that we could extract the guid from
3868
	 * the $entryid and compare it and fetch the guid from the userentryid.
3869
	 * C++ has a GetStoreGuidFromEntryId() function.
3870
	 */
3871
	public function getOtherStoreFromEntryid($entryid) {
3872
		// Get all external user from settings
3873
		$otherUsers = $GLOBALS['mapisession']->retrieveOtherUsersFromSettings();
3874
3875
		// Fetch the store of each external user and
3876
		// find the record with given entryid
3877
		foreach ($otherUsers as $sharedUser => $values) {
3878
			$userEntryid = mapi_msgstore_createentryid($GLOBALS['mapisession']->getDefaultMessageStore(), $sharedUser);
3879
			$store = $GLOBALS['mapisession']->openMessageStore($userEntryid);
3880
			if ($GLOBALS['entryid']->hasContactProviderGUID($entryid)) {
3881
				$entryid = $GLOBALS["entryid"]->unwrapABEntryIdObj($entryid);
3882
			}
3883
3884
			try {
3885
				$record = mapi_msgstore_openentry($store, hex2bin($entryid));
3886
				if ($record) {
3887
					return $store;
3888
				}
3889
			}
3890
			catch (MAPIException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
3891
			}
3892
		}
3893
3894
		return false;
3895
	}
3896
3897
	/**
3898
	 * Function which is use to check the contact item (distribution list / contact)
3899
	 * belongs to any external folder or not.
3900
	 *
3901
	 * @param string $entryid entryid of contact item
3902
	 *
3903
	 * @return bool true if contact item from external folder otherwise false.
3904
	 *
3905
	 * FIXME: this function is broken and returns true if the user is a contact in a shared store.
3906
	 * Also research if we cannot just extract the GUID and compare it with our own GUID.
3907
	 * FIXME This function should be renamed, because it's also meant for normal shared folder contacts.
3908
	 */
3909
	public function isExternalContactItem($entryid) {
3910
		try {
3911
			if (!$GLOBALS['entryid']->hasContactProviderGUID(bin2hex($entryid))) {
3912
				$entryid = hex2bin($GLOBALS['entryid']->wrapABEntryIdObj(bin2hex($entryid), MAPI_DISTLIST));
3913
			}
3914
			mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $entryid);
3915
		}
3916
		catch (MAPIException $e) {
3917
			return true;
3918
		}
3919
3920
		return false;
3921
	}
3922
3923
	/**
3924
	 * Get object type from distlist type of member of distribution list.
3925
	 *
3926
	 * @param int $distlistType distlist type of distribution list
3927
	 *
3928
	 * @return int object type of distribution list
3929
	 */
3930
	public function getObjectTypeFromDistlistType($distlistType) {
3931
		switch ($distlistType) {
3932
			case DL_DIST :
3933
			case DL_DIST_AB :
3934
				return MAPI_DISTLIST;
3935
3936
			case DL_USER :
3937
			case DL_USER2 :
3938
			case DL_USER3 :
3939
			case DL_USER_AB :
3940
			default:
3941
				return MAPI_MAILUSER;
3942
		}
3943
	}
3944
3945
	/**
3946
	 * Function which fetches all members of shared/internal(Local Contact Folder)
3947
	 * folder's distribution list.
3948
	 *
3949
	 * @param string $distlistEntryid entryid of distribution list
3950
	 * @param bool   $isRecursive     if there is/are distribution list(s) inside the distlist
3951
	 *                                to expand all the members, pass true to expand distlist recursively, false to not expand
3952
	 *
3953
	 * @return array $members all members of a distribution list
3954
	 */
3955
	public function expandDistList($distlistEntryid, $isRecursive = false) {
3956
		$properties = $GLOBALS['properties']->getDistListProperties();
3957
		$eidObj = $GLOBALS['entryid']->createABEntryIdObj($distlistEntryid);
3958
		$extidObj = $GLOBALS['entryid']->createMessageEntryIdObj($eidObj['extid']);
3959
3960
		$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
3961
		$contactFolderId = $this->getPropertiesFromStoreRoot($store, [PR_IPM_CONTACT_ENTRYID]);
3962
		$contactFolderidObj = $GLOBALS['entryid']->createFolderEntryIdObj(bin2hex($contactFolderId[PR_IPM_CONTACT_ENTRYID]));
3963
3964
		if ($contactFolderidObj['providerguid'] != $extidObj['providerguid'] && $contactFolderidObj['folderdbguid'] != $extidObj['folderdbguid']) {
3965
			$storelist = $GLOBALS["mapisession"]->getAllMessageStores();
3966
			foreach ($storelist as $storeObj) {
3967
				$contactFolderId = $this->getPropertiesFromStoreRoot($storeObj, [PR_IPM_CONTACT_ENTRYID]);
3968
				$contactFolderidObj = $GLOBALS['entryid']->createFolderEntryIdObj(bin2hex($contactFolderId[PR_IPM_CONTACT_ENTRYID]));
3969
				if ($contactFolderidObj['providerguid'] == $extidObj['providerguid'] && $contactFolderidObj['folderdbguid'] == $extidObj['folderdbguid']) {
3970
					$store = $storeObj;
3971
					break;
3972
				}
3973
			}
3974
		}
3975
3976
		$distlistEntryid = $GLOBALS["entryid"]->unwrapABEntryIdObj($distlistEntryid);
3977
3978
		try {
3979
			$distlist = $this->openMessage($store, hex2bin($distlistEntryid));
3980
		}
3981
		catch (Exception $e) {
3982
			// the distribution list is in a public folder
3983
			$distlist = $this->openMessage($GLOBALS["mapisession"]->getPublicMessageStore(), hex2bin($distlistEntryid));
3984
		}
3985
3986
		// Retrieve the members from distribution list.
3987
		$distlistMembers = $this->getMembersFromDistributionList($store, $distlist, $properties, $isRecursive);
3988
		$recipients = [];
3989
3990
		foreach ($distlistMembers as $member) {
3991
			$props = $this->convertDistlistMemberToRecipient($store, $member);
3992
			array_push($recipients, $props);
3993
		}
3994
3995
		return $recipients;
3996
	}
3997
3998
	/**
3999
	 * Function Which convert the shared/internal(local contact folder distlist)
4000
	 * folder's distlist members to recipient type.
4001
	 *
4002
	 * @param resource $store  MAPI store of the message
4003
	 * @param array    $member of distribution list contacts
4004
	 *
4005
	 * @return array members properties converted in to recipient
4006
	 */
4007
	public function convertDistlistMemberToRecipient($store, $member) {
4008
		$entryid = $member["props"]["entryid"];
4009
		$memberProps = $member["props"];
4010
		$props = [];
4011
4012
		$distlistType = $memberProps["distlist_type"];
4013
		$addressType = $memberProps["address_type"];
4014
4015
		$isGABDistlList = $distlistType == DL_DIST_AB && $addressType === "EX";
4016
		$isLocalDistlist = $distlistType == DL_DIST && $addressType === "MAPIPDL";
4017
4018
		$isGABContact = $memberProps["address_type"] === 'EX';
4019
		// If distlist_type is 0 then it means distlist member is external contact.
4020
		// For mare please read server/core/constants.php
4021
		$isLocalContact = !$isGABContact && $distlistType !== 0;
4022
4023
		/*
4024
		 * If distribution list belongs to the local contact folder then open that contact and
4025
		 * retrieve all properties which requires to prepare ideal recipient to send mail.
4026
		 */
4027
		if ($isLocalDistlist) {
4028
			try {
4029
				$distlist = $this->openMessage($store, hex2bin($entryid));
0 ignored issues
show
Bug introduced by
$store of type resource is incompatible with the type object expected by parameter $store of Operations::openMessage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4029
				$distlist = $this->openMessage(/** @scrutinizer ignore-type */ $store, hex2bin($entryid));
Loading history...
4030
			}
4031
			catch (Exception $e) {
4032
				$distlist = $this->openMessage($GLOBALS["mapisession"]->getPublicMessageStore(), hex2bin($entryid));
4033
			}
4034
4035
			$abProps = $this->getProps($distlist, $GLOBALS['properties']->getRecipientProperties());
4036
			$props = $abProps["props"];
4037
4038
			$props["entryid"] = $GLOBALS["entryid"]->wrapABEntryIdObj($abProps["entryid"], MAPI_DISTLIST);
4039
			$props["display_type"] = DT_DISTLIST;
4040
			$props["display_type_ex"] = DT_DISTLIST;
4041
			$props["address_type"] = $memberProps["address_type"];
4042
			$emailAddress = !empty($memberProps["email_address"]) ? $memberProps["email_address"] : "";
4043
			$props["smtp_address"] = $emailAddress;
4044
			$props["email_address"] = $emailAddress;
4045
		}
4046
		elseif ($isGABContact || $isGABDistlList) {
4047
			/*
4048
			 * If contact or distribution list belongs to GAB then open that contact and
4049
			 * retrieve all properties which requires to prepare ideal recipient to send mail.
4050
			 */
4051
			try {
4052
				$abentry = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), hex2bin($entryid));
4053
				$abProps = $this->getProps($abentry, $GLOBALS['properties']->getRecipientProperties());
4054
				$props = $abProps["props"];
4055
				$props["entryid"] = $abProps["entryid"];
4056
			}
4057
			catch (Exception $e) {
4058
				// Throw MAPI_E_NOT_FOUND or MAPI_E_UNKNOWN_ENTRYID it may possible that contact is already
4059
				// deleted from server. so just create recipient
4060
				// with existing information of distlist member.
4061
				// recipient is not valid so sender get report mail for that
4062
				// particular recipient to inform that recipient is not exist.
4063
				if ($e->getCode() == MAPI_E_NOT_FOUND || $e->getCode() == MAPI_E_UNKNOWN_ENTRYID) {
4064
					$props["entryid"] = $memberProps["entryid"];
4065
					$props["display_type"] = DT_MAILUSER;
4066
					$props["display_type_ex"] = DT_MAILUSER;
4067
					$props["display_name"] = $memberProps["display_name"];
4068
					$props["smtp_address"] = $memberProps["email_address"];
4069
					$props["email_address"] = $memberProps["email_address"];
4070
					$props["address_type"] = !empty($memberProps["address_type"]) ? $memberProps["address_type"] : 'SMTP';
4071
				}
4072
				else {
4073
					throw $e;
4074
				}
4075
			}
4076
		}
4077
		else {
4078
			/*
4079
			 * If contact is belongs to local/shared folder then prepare ideal recipient to send mail
4080
			 * as per the contact type.
4081
			 */
4082
			$props["entryid"] = $isLocalContact ? $GLOBALS["entryid"]->wrapABEntryIdObj($entryid, MAPI_MAILUSER) : $memberProps["entryid"];
4083
			$props["display_type"] = DT_MAILUSER;
4084
			$props["display_type_ex"] = $isLocalContact ? DT_MAILUSER : DT_REMOTE_MAILUSER;
4085
			$props["display_name"] = $memberProps["display_name"];
4086
			$props["smtp_address"] = $memberProps["email_address"];
4087
			$props["email_address"] = $memberProps["email_address"];
4088
			$props["address_type"] = !empty($memberProps["address_type"]) ? $memberProps["address_type"] : 'SMTP';
4089
		}
4090
4091
		// Set object type property into each member of distribution list
4092
		$props["object_type"] = $this->getObjectTypeFromDistlistType($memberProps["distlist_type"]);
4093
4094
		return $props;
4095
	}
4096
4097
	/**
4098
	 * Parse reply-to value from PR_REPLY_RECIPIENT_ENTRIES property.
4099
	 *
4100
	 * @param string $flatEntryList the PR_REPLY_RECIPIENT_ENTRIES value
4101
	 *
4102
	 * @return array list of recipients in array structure
4103
	 */
4104
	public function readReplyRecipientEntry($flatEntryList) {
4105
		$addressbook = $GLOBALS["mapisession"]->getAddressbook();
4106
		$entryids = [];
4107
4108
		// Unpack number of entries, the byte count and the entries
4109
		$unpacked = unpack('V1cEntries/V1cbEntries/a*', $flatEntryList);
4110
4111
		// $unpacked consists now of the following fields:
4112
		//	'cEntries' => The number of entryids in our list
4113
		//	'cbEntries' => The total number of bytes inside 'abEntries'
4114
		//	'abEntries' => The list of Entryids
4115
		//
4116
		// Each 'abEntries' can be broken down into groups of 2 fields
4117
		//	'cb' => The length of the entryid
4118
		//	'entryid' => The entryid
4119
4120
		$position = 8; // sizeof(cEntries) + sizeof(cbEntries);
4121
4122
		for ($i = 0, $len = $unpacked['cEntries']; $i < $len; ++$i) {
4123
			// Obtain the size for the current entry
4124
			$size = unpack('a' . $position . '/V1cb/a*', $flatEntryList);
4125
4126
			// We have the size, now can obtain the bytes
4127
			$entryid = unpack('a' . $position . '/V1cb/a' . $size['cb'] . 'entryid/a*', $flatEntryList);
4128
4129
			// unpack() will remove the NULL characters, readd
4130
			// them until we match the 'cb' length.
4131
			while ($entryid['cb'] > strlen($entryid['entryid'])) {
4132
				$entryid['entryid'] .= chr(0x00);
4133
			}
4134
4135
			$entryids[] = $entryid['entryid'];
4136
4137
			// sizeof(cb) + strlen(entryid)
4138
			$position += 4 + $entryid['cb'];
4139
		}
4140
4141
		$recipients = [];
4142
		foreach ($entryids as $entryid) {
4143
			// Check if entryid extracted, since unpack errors can not be caught.
4144
			if (!$entryid) {
4145
				continue;
4146
			}
4147
4148
			// Handle malformed entryids
4149
			try {
4150
				$entry = mapi_ab_openentry($addressbook, $entryid);
4151
				$props = mapi_getprops($entry, [PR_ENTRYID, PR_SEARCH_KEY, PR_OBJECT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS]);
4152
4153
				// Put data in recipient array
4154
				$recipients[] = $this->composeRecipient(count($recipients), $props);
4155
			}
4156
			catch (MAPIException $e) {
4157
				$oneoff = mapi_parseoneoff($entryid);
4158
				if (!isset($oneoff['address'])) {
4159
					Log::Write(LOGLEVEL_WARN, "readReplyRecipientEntry unable to open AB entry and oneoff address is not available: " . get_mapi_error_name($e->getCode()), $e->getDisplayMessage());
0 ignored issues
show
Unused Code introduced by
The call to Log::Write() has too many arguments starting with $e->getDisplayMessage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

4159
					Log::/** @scrutinizer ignore-call */ 
4160
          Write(LOGLEVEL_WARN, "readReplyRecipientEntry unable to open AB entry and oneoff address is not available: " . get_mapi_error_name($e->getCode()), $e->getDisplayMessage());

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
4160
4161
					continue;
4162
				}
4163
4164
				$entryid = mapi_createoneoff($oneoff['name'] ?? '', $oneoff['type'] ?? 'SMTP', $oneoff['address']);
4165
				$props = [
4166
					PR_ENTRYID => $entryid,
4167
					PR_DISPLAY_NAME => !empty($oneoff['name']) ? $oneoff['name'] : $oneoff['address'],
4168
					PR_ADDRTYPE => $oneoff['type'] ?? 'SMTP',
4169
					PR_EMAIL_ADDRESS => $oneoff['address'],
4170
				];
4171
				$recipients[] = $this->composeRecipient(count($recipients), $props);
4172
			}
4173
		}
4174
4175
		return $recipients;
4176
	}
4177
4178
	private function composeRecipient($rowid, $props) {
4179
		return [
4180
			'rowid' => $rowid,
4181
			'props' => [
4182
				'entryid' => bin2hex($props[PR_ENTRYID]),
4183
				'object_type' => $props[PR_OBJECT_TYPE] ?? MAPI_MAILUSER,
4184
				'search_key' => $props[PR_SEARCH_KEY] ?? '',
4185
				'display_name' => $props[PR_DISPLAY_NAME] ?? $props[PR_EMAIL_ADDRESS],
4186
				'address_type' => $props[PR_ADDRTYPE] ?? 'SMTP',
4187
				'email_address' => $props[PR_EMAIL_ADDRESS] ?? '',
4188
				'smtp_address' => $props[PR_EMAIL_ADDRESS] ?? '',
4189
			],
4190
		];
4191
	}
4192
4193
	/**
4194
	 * Build full-page HTML from the TinyMCE HTML.
4195
	 *
4196
	 * This function basically takes the generated HTML from TinyMCE and embeds it in
4197
	 * a standalone HTML page (including header and CSS) to form.
4198
	 *
4199
	 * @param string $body  This is the HTML created by the TinyMCE
4200
	 * @param string $title Optional, this string is placed in the <title>
4201
	 *
4202
	 * @return string full HTML message
4203
	 */
4204
	public function generateBodyHTML($body, $title = "grommunio-web") {
4205
		$html = "<!DOCTYPE html>" .
4206
				"<html>\n" .
4207
				"<head>\n" .
4208
				"  <meta name=\"Generator\" content=\"grommunio-web v" . trim(file_get_contents('version')) . "\">\n" .
4209
				"  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" .
4210
				"  <title>" . htmlspecialchars($title) . "</title>\n";
4211
4212
		$html .= "</head>\n" .
4213
				"<body>\n" .
4214
				$body . "\n" .
4215
				"</body>\n" .
4216
				"</html>";
4217
4218
		return $html;
4219
	}
4220
4221
	/**
4222
	 * Calculate the total size for all items in the given folder.
4223
	 *
4224
	 * @param resource $folder The folder for which the size must be calculated
4225
	 *
4226
	 * @return number The folder size
4227
	 */
4228
	public function calcFolderMessageSize($folder) {
4229
		$folderProps = mapi_getprops($folder, [PR_MESSAGE_SIZE_EXTENDED]);
4230
		if (isset($folderProps[PR_MESSAGE_SIZE_EXTENDED])) {
4231
			return $folderProps[PR_MESSAGE_SIZE_EXTENDED];
4232
		}
4233
4234
		return 0;
4235
	}
4236
4237
	/**
4238
	 * Detect plaintext body type of message.
4239
	 *
4240
	 * @param resource $message MAPI message resource to check
4241
	 *
4242
	 * @return bool TRUE if the message is a plaintext message, FALSE if otherwise
4243
	 */
4244
	public function isPlainText($message) {
4245
		$props = mapi_getprops($message, [PR_NATIVE_BODY_INFO]);
4246
		if (isset($props[PR_NATIVE_BODY_INFO]) && $props[PR_NATIVE_BODY_INFO] == 1) {
4247
			return true;
4248
		}
4249
4250
		return false;
4251
	}
4252
4253
	/**
4254
	 * Parse email recipient list and add all e-mail addresses to the recipient history.
4255
	 *
4256
	 * The recipient history is used for auto-suggestion when writing e-mails. This function
4257
	 * opens the recipient history property (PR_EC_RECIPIENT_HISTORY_JSON) and updates or appends
4258
	 * it with the passed email addresses.
4259
	 *
4260
	 * @param array $recipients list of recipients
4261
	 */
4262
	public function addRecipientsToRecipientHistory($recipients) {
4263
		$emailAddress = [];
4264
		foreach ($recipients as $key => $value) {
4265
			$emailAddresses[] = $value['props'];
4266
		}
4267
4268
		if (empty($emailAddresses)) {
4269
			return;
4270
		}
4271
4272
		// Retrieve the recipient history
4273
		$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
4274
		$storeProps = mapi_getprops($store, [PR_EC_RECIPIENT_HISTORY_JSON]);
4275
		$recipient_history = false;
4276
4277
		if (isset($storeProps[PR_EC_RECIPIENT_HISTORY_JSON]) || propIsError(PR_EC_RECIPIENT_HISTORY_JSON, $storeProps) == MAPI_E_NOT_ENOUGH_MEMORY) {
4278
			$datastring = streamProperty($store, PR_EC_RECIPIENT_HISTORY_JSON);
4279
4280
			if (!empty($datastring)) {
4281
				$recipient_history = json_decode_data($datastring, true);
4282
			}
4283
		}
4284
4285
		$l_aNewHistoryItems = [];
4286
		// Loop through all new recipients
4287
		for ($i = 0, $len = count($emailAddresses); $i < $len; ++$i) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $emailAddresses seems to be defined by a foreach iteration on line 4264. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
4288
			if ($emailAddresses[$i]['address_type'] == 'SMTP') {
4289
				$emailAddress = $emailAddresses[$i]['smtp_address'];
4290
				if (empty($emailAddress)) {
4291
					$emailAddress = $emailAddresses[$i]['email_address'];
4292
				}
4293
			}
4294
			else { // address_type == 'EX' || address_type == 'MAPIPDL'
4295
				$emailAddress = $emailAddresses[$i]['email_address'];
4296
				if (empty($emailAddress)) {
4297
					$emailAddress = $emailAddresses[$i]['smtp_address'];
4298
				}
4299
			}
4300
4301
			// If no email address property is found, then we can't
4302
			// generate a valid suggestion.
4303
			if (empty($emailAddress)) {
4304
				continue;
4305
			}
4306
4307
			$l_bFoundInHistory = false;
4308
			// Loop through all the recipients in history
4309
			if (is_array($recipient_history) && !empty($recipient_history['recipients'])) {
4310
				for ($j = 0, $lenJ = count($recipient_history['recipients']); $j < $lenJ; ++$j) {
4311
					// Email address already found in history
4312
					$l_bFoundInHistory = false;
4313
4314
					// The address_type property must exactly match,
4315
					// when it does, a recipient matches the suggestion
4316
					// if it matches to either the email_address or smtp_address.
4317
					if ($emailAddresses[$i]['address_type'] === $recipient_history['recipients'][$j]['address_type']) {
4318
						if ($emailAddress == $recipient_history['recipients'][$j]['email_address'] ||
4319
							$emailAddress == $recipient_history['recipients'][$j]['smtp_address']) {
4320
							$l_bFoundInHistory = true;
4321
						}
4322
					}
4323
4324
					if ($l_bFoundInHistory == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
4325
						// Check if a name has been supplied.
4326
						$newDisplayName = trim($emailAddresses[$i]['display_name']);
4327
						if (!empty($newDisplayName)) {
4328
							$oldDisplayName = trim($recipient_history['recipients'][$j]['display_name']);
4329
4330
							// Check if the name is not the same as the email address
4331
							if ($newDisplayName != $emailAddresses[$i]['smtp_address']) {
4332
								$recipient_history['recipients'][$j]['display_name'] = $newDisplayName;
4333
							// Check if the recipient history has no name for this email
4334
							}
4335
							elseif (empty($oldDisplayName)) {
4336
								$recipient_history['recipients'][$j]['display_name'] = $newDisplayName;
4337
							}
4338
						}
4339
						++$recipient_history['recipients'][$j]['count'];
4340
						$recipient_history['recipients'][$j]['last_used'] = time();
4341
						break;
4342
					}
4343
				}
4344
			}
4345
			if (!$l_bFoundInHistory && !isset($l_aNewHistoryItems[$emailAddress])) {
4346
				$l_aNewHistoryItems[$emailAddress] = [
4347
					'display_name' => $emailAddresses[$i]['display_name'],
4348
					'smtp_address' => $emailAddresses[$i]['smtp_address'],
4349
					'email_address' => $emailAddresses[$i]['email_address'],
4350
					'address_type' => $emailAddresses[$i]['address_type'],
4351
					'count' => 1,
4352
					'last_used' => time(),
4353
					'object_type' => $emailAddresses[$i]['object_type'],
4354
				];
4355
			}
4356
		}
4357
		if (!empty($l_aNewHistoryItems)) {
4358
			foreach ($l_aNewHistoryItems as $l_aValue) {
4359
				$recipient_history['recipients'][] = $l_aValue;
4360
			}
4361
		}
4362
4363
		$l_sNewRecipientHistoryJSON = json_encode($recipient_history);
4364
4365
		$stream = mapi_openproperty($store, PR_EC_RECIPIENT_HISTORY_JSON, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
4366
		mapi_stream_setsize($stream, strlen($l_sNewRecipientHistoryJSON));
4367
		mapi_stream_write($stream, $l_sNewRecipientHistoryJSON);
4368
		mapi_stream_commit($stream);
4369
		mapi_savechanges($store);
4370
	}
4371
4372
	/**
4373
	 * Get the SMTP e-mail of an addressbook entry.
4374
	 *
4375
	 * @param string $entryid Addressbook entryid of object
4376
	 *
4377
	 * @return string SMTP e-mail address of that entry or FALSE on error
4378
	 */
4379
	public function getEmailAddressFromEntryID($entryid) {
4380
		try {
4381
			$mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $entryid);
4382
		}
4383
		catch (MAPIException $e) {
4384
			// if any invalid entryid is passed in this function then it should silently ignore it
4385
			// and continue with execution
4386
			if ($e->getCode() == MAPI_E_UNKNOWN_ENTRYID) {
4387
				$e->setHandled();
4388
4389
				return "";
4390
			}
4391
		}
4392
4393
		if (!isset($mailuser)) {
4394
			return "";
4395
		}
4396
4397
		$abprops = mapi_getprops($mailuser, [PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]);
4398
		if (isset($abprops[PR_SMTP_ADDRESS])) {
4399
			return $abprops[PR_SMTP_ADDRESS];
4400
		}
4401
		if (isset($abprops[PR_EMAIL_ADDRESS])) {
4402
			return $abprops[PR_EMAIL_ADDRESS];
4403
		}
4404
4405
		return "";
4406
	}
4407
4408
	/**
4409
	 * Function which fetches all members of a distribution list recursively.
4410
	 *
4411
	 * @param resource $store        MAPI Message Store Object
4412
	 * @param resource $message      the distribution list message
4413
	 * @param array    $properties   array of properties to get properties of distlist
4414
	 * @param bool     $isRecursive  function will be called recursively if there is/are
4415
	 *                               distribution list inside the distlist to expand all the members,
4416
	 *                               pass true to expand distlist recursively, false to not expand
4417
	 * @param array    $listEntryIDs list of already expanded Distribution list from contacts folder,
4418
	 *                               This parameter is used for recursive call of the function
4419
	 *
4420
	 * @return object $items all members of a distlist
4421
	 */
4422
	public function getMembersFromDistributionList($store, $message, $properties, $isRecursive = false, $listEntryIDs = []) {
4423
		$items = [];
4424
4425
		$props = mapi_getprops($message, [$properties['oneoff_members'], $properties['members'], PR_ENTRYID]);
4426
4427
		// only continue when we have something to expand
4428
		if (!isset($props[$properties['oneoff_members']]) || !isset($props[$properties['members']])) {
4429
			return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type object.
Loading history...
4430
		}
4431
4432
		if ($isRecursive) {
4433
			// when opening sub message we will not have entryid, so use entryid only when we have it
4434
			if (isset($props[PR_ENTRYID])) {
4435
				// for preventing recursion we need to store entryids, and check if the same distlist is going to be expanded again
4436
				if (in_array($props[PR_ENTRYID], $listEntryIDs)) {
4437
					// don't expand a distlist that is already expanded
4438
					return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type object.
Loading history...
4439
				}
4440
4441
				$listEntryIDs[] = $props[PR_ENTRYID];
4442
			}
4443
		}
4444
4445
		$members = $props[$properties['members']];
4446
4447
		// parse oneoff members
4448
		$oneoffmembers = [];
4449
		foreach ($props[$properties['oneoff_members']] as $key => $item) {
4450
			$oneoffmembers[$key] = mapi_parseoneoff($item);
4451
		}
4452
4453
		foreach ($members as $key => $item) {
4454
			/*
4455
			 * PHP 5.5.0 and greater has made the unpack function incompatible with previous versions by changing:
4456
			 * - a = code now retains trailing NULL bytes.
4457
			 * - A = code now strips all trailing ASCII whitespace (spaces, tabs, newlines, carriage
4458
			 * returns, and NULL bytes).
4459
			 * for more http://php.net/manual/en/function.unpack.php
4460
			 */
4461
			if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
4462
				$parts = unpack('Vnull/A16guid/Ctype/a*entryid', $item);
4463
			}
4464
			else {
4465
				$parts = unpack('Vnull/A16guid/Ctype/A*entryid', $item);
4466
			}
4467
4468
			$memberItem = [];
4469
			$memberItem['props'] = [];
4470
			$memberItem['props']['distlist_type'] = $parts['type'];
4471
4472
			if ($parts['guid'] === hex2bin('812b1fa4bea310199d6e00dd010f5402')) {
4473
				// custom e-mail address (no user or contact)
4474
				$oneoff = mapi_parseoneoff($item);
4475
4476
				$memberItem['props']['display_name'] = $oneoff['name'];
4477
				$memberItem['props']['address_type'] = $oneoff['type'];
4478
				$memberItem['props']['email_address'] = $oneoff['address'];
4479
				$memberItem['props']['smtp_address'] = $oneoff['address'];
4480
				$memberItem['props']['entryid'] = bin2hex($members[$key]);
4481
4482
				$items[] = $memberItem;
4483
			}
4484
			else {
4485
				if ($parts['type'] === DL_DIST && $isRecursive) {
4486
					// Expand distribution list to get distlist members inside the distributionlist.
4487
					$distlist = mapi_msgstore_openentry($store, $parts['entryid']);
4488
					$items = array_merge($items, $this->getMembersFromDistributionList($store, $distlist, $properties, true, $listEntryIDs));
0 ignored issues
show
Bug introduced by
$this->getMembersFromDis...s, true, $listEntryIDs) of type object is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4488
					$items = array_merge($items, /** @scrutinizer ignore-type */ $this->getMembersFromDistributionList($store, $distlist, $properties, true, $listEntryIDs));
Loading history...
4489
				}
4490
				else {
4491
					$memberItem['props']['entryid'] = bin2hex($parts['entryid']);
4492
					$memberItem['props']['display_name'] = $oneoffmembers[$key]['name'];
4493
					$memberItem['props']['address_type'] = $oneoffmembers[$key]['type'];
4494
					// distribution lists don't have valid email address so ignore that property
4495
4496
					if ($parts['type'] !== DL_DIST) {
4497
						$memberItem['props']['email_address'] = $oneoffmembers[$key]['address'];
4498
4499
						// internal members in distribution list don't have smtp address so add add that property
4500
						$memberProps = $this->convertDistlistMemberToRecipient($store, $memberItem);
4501
						$memberItem['props']['smtp_address'] = isset($memberProps["smtp_address"]) ? $memberProps["smtp_address"] : $memberProps["email_address"];
4502
					}
4503
4504
					$items[] = $memberItem;
4505
				}
4506
			}
4507
		}
4508
4509
		return $items;
4510
	}
4511
4512
	/**
4513
	 * Convert inline image <img src="data:image/mimetype;.date> links in HTML email
4514
	 * to CID embedded images. Which are supported in major mail clients or
4515
	 * providers such as outlook.com or gmail.com.
4516
	 *
4517
	 * grommunio Web now extracts the base64 image, saves it as hidden attachment,
4518
	 * replace the img src tag with the 'cid' which corresponds with the attachments
4519
	 * cid.
4520
	 *
4521
	 * @param resource $message the distribution list message
4522
	 */
4523
	public function convertInlineImage($message) {
4524
		$body = streamProperty($message, PR_HTML);
4525
		$imageIDs = [];
4526
4527
		// Only load the DOM if the HTML contains a img or data:text/plain due to a bug
4528
		// in Chrome on Windows in combination with TinyMCE.
4529
		if (strpos($body, "img") !== false || strpos($body, "data:text/plain") !== false) {
4530
			$doc = new DOMDocument();
4531
			$cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]);
4532
			$codepage = isset($cpprops[PR_INTERNET_CPID]) ? $cpprops[PR_INTERNET_CPID] : 1252;
4533
			$hackEncoding = '<meta http-equiv="Content-Type" content="text/html; charset=' . Conversion::getCodepageCharset($codepage) . '">';
4534
			// TinyMCE does not generate valid HTML, so we must suppress warnings.
4535
			@$doc->loadHTML($hackEncoding . $body);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for loadHTML(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

4535
			/** @scrutinizer ignore-unhandled */ @$doc->loadHTML($hackEncoding . $body);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
4536
			$images = $doc->getElementsByTagName('img');
4537
			$saveChanges = false;
4538
4539
			foreach ($images as $image) {
4540
				$src = $image->getAttribute('src');
4541
4542
				if (strpos($src, "cid:") === false && (strpos($src, "data:image") !== false ||
4543
					strpos($body, "data:text/plain") !== false)) {
4544
					$saveChanges = true;
4545
4546
					// Extract mime type data:image/jpeg;
4547
					$firstOffset = strpos($src, '/') + 1;
4548
					$endOffset = strpos($src, ';');
4549
					$mimeType = substr($src, $firstOffset, $endOffset - $firstOffset);
4550
4551
					$dataPosition = strpos($src, ",");
4552
					// Extract encoded data
4553
					$rawImage = base64_decode(substr($src, $dataPosition + 1, strlen($src)));
4554
4555
					$uniqueId = uniqid();
4556
					$image->setAttribute('src', 'cid:' . $uniqueId);
4557
					// TinyMCE adds an extra inline image for some reason, remove it.
4558
					$image->setAttribute('data-mce-src', '');
4559
4560
					array_push($imageIDs, $uniqueId);
4561
4562
					// Create hidden attachment with CID
4563
					$inlineImage = mapi_message_createattach($message);
4564
					$props = [
4565
						PR_ATTACH_METHOD => ATTACH_BY_VALUE,
4566
						PR_ATTACH_CONTENT_ID => $uniqueId,
4567
						PR_ATTACHMENT_HIDDEN => true,
4568
						PR_ATTACH_FLAGS => 4,
4569
						PR_ATTACH_MIME_TAG => $mimeType !== 'plain' ? 'image/' . $mimeType : 'image/png',
4570
					];
4571
					mapi_setprops($inlineImage, $props);
4572
4573
					$stream = mapi_openproperty($inlineImage, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
4574
					mapi_stream_setsize($stream, strlen($rawImage));
4575
					mapi_stream_write($stream, $rawImage);
4576
					mapi_stream_commit($stream);
4577
					mapi_savechanges($inlineImage);
4578
				}
4579
				elseif (strstr($src, "cid:") !== false) {
4580
					// Check for the cid(there may be http: ) is in the image src. push the cid
4581
					// to $imageIDs array. which further used in clearDeletedInlineAttachments function.
4582
4583
					$firstOffset = strpos($src, ":") + 1;
4584
					$cid = substr($src, $firstOffset);
4585
					array_push($imageIDs, $cid);
4586
				}
4587
			}
4588
4589
			if ($saveChanges) {
4590
				// Write the <img src="cid:data"> changes to the HTML property
4591
				$body = $doc->saveHTML();
4592
				$stream = mapi_openproperty($message, PR_HTML, IID_IStream, 0, MAPI_MODIFY);
4593
				mapi_stream_setsize($stream, strlen($body));
4594
				mapi_stream_write($stream, $body);
4595
				mapi_stream_commit($stream);
4596
				mapi_savechanges($message);
4597
			}
4598
		}
4599
		$this->clearDeletedInlineAttachments($message, $imageIDs);
4600
	}
4601
4602
	/**
4603
	 * Delete the deleted inline image attachment from attachment store.
4604
	 *
4605
	 * @param resource $message  the distribution list message
4606
	 * @param array    $imageIDs Array of existing inline image PR_ATTACH_CONTENT_ID
4607
	 */
4608
	public function clearDeletedInlineAttachments($message, $imageIDs = []) {
4609
		$attachmentTable = mapi_message_getattachmenttable($message);
4610
4611
		$restriction = [RES_AND, [
4612
			[RES_PROPERTY,
4613
				[
4614
					RELOP => RELOP_EQ,
4615
					ULPROPTAG => PR_ATTACHMENT_HIDDEN,
4616
					VALUE => [PR_ATTACHMENT_HIDDEN => true],
4617
				],
4618
			],
4619
			[RES_EXIST,
4620
				[
4621
					ULPROPTAG => PR_ATTACH_CONTENT_ID,
4622
				],
4623
			],
4624
		]];
4625
4626
		$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_CONTENT_ID, PR_ATTACH_NUM], $restriction);
4627
		foreach ($attachments as $attachment) {
4628
			$clearDeletedInlineAttach = array_search($attachment[PR_ATTACH_CONTENT_ID], $imageIDs) === false;
4629
			if ($clearDeletedInlineAttach) {
4630
				mapi_message_deleteattach($message, $attachment[PR_ATTACH_NUM]);
4631
			}
4632
		}
4633
	}
4634
4635
	/**
4636
	 * This function will fetch the user from mapi session and retrieve its LDAP image.
4637
	 * It will return the compressed image using php's GD library.
4638
	 *
4639
	 * @param string $userEntryId       The user entryid which is going to open
4640
	 * @param int    $compressedQuality The compression factor ranges from 0 (high) to 100 (low)
4641
	 *                                  Default value is set to 10 which is nearly
4642
	 *                                  extreme compressed image
4643
	 *
4644
	 * @return string A base64 encoded string (data url)
4645
	 */
4646
	public function getCompressedUserImage($userEntryId, $compressedQuality = 10) {
4647
		try {
4648
			$user = $GLOBALS['mapisession']->getUser($userEntryId);
4649
		}
4650
		catch (Exception $e) {
4651
			$msg = "Problem while getting a user from the addressbook. Error %s : %s.";
4652
			$formattedMsg = sprintf($msg, $e->getCode(), $e->getMessage());
4653
			error_log($formattedMsg);
4654
			Log::Write(LOGLEVEL_ERROR, "Operations:getCompressedUserImage() " . $formattedMsg);
4655
4656
			return "";
4657
		}
4658
4659
		$userImageProp = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]);
4660
		if (isset($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO])) {
4661
			return $this->compressedImage($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO], $compressedQuality);
4662
		}
4663
4664
		return "";
4665
	}
4666
4667
	/**
4668
	 * Function used to compressed the image.
4669
	 *
4670
	 * @param string $image the image which is going to compress
4671
	 * @param int compressedQuality The compression factor range from 0 (high) to 100 (low)
0 ignored issues
show
Bug introduced by
The type compressedQuality was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
4672
	 * Default value is set to 10 which is nearly extreme compressed image
4673
	 * @param mixed $compressedQuality
4674
	 *
4675
	 * @return string A base64 encoded string (data url)
4676
	 */
4677
	public function compressedImage($image, $compressedQuality = 10) {
4678
		// Proceed only when GD library's functions and user image data are available.
4679
		if (function_exists('imagecreatefromstring')) {
4680
			try {
4681
				$image = imagecreatefromstring($image);
4682
			}
4683
			catch (Exception $e) {
4684
				$msg = "Problem while creating image from string. Error %s : %s.";
4685
				$formattedMsg = sprintf($msg, $e->getCode(), $e->getMessage());
4686
				error_log($formattedMsg);
4687
				Log::Write(LOGLEVEL_ERROR, "Operations:compressedImage() " . $formattedMsg);
4688
			}
4689
4690
			if ($image !== false) {
4691
				// We need to use buffer because imagejpeg will give output as image in browser or file.
4692
				ob_start();
4693
				imagejpeg($image, null, $compressedQuality);
4694
				$compressedImg = ob_get_contents();
4695
				ob_end_clean();
4696
4697
				// Free up memory space acquired by image.
4698
				imagedestroy($image);
4699
4700
				return strlen($compressedImg) > 0 ? "data:image/jpg;base64," . base64_encode($compressedImg) : "";
4701
			}
4702
		}
4703
4704
		return "";
4705
	}
4706
4707
	public function getPropertiesFromStoreRoot($store, $props) {
4708
		$root = mapi_msgstore_openentry($store, null);
4709
4710
		return mapi_getprops($root, $props);
4711
	}
4712
4713
	/**
4714
	 * Returns the encryption key for sodium functions.
4715
	 *
4716
	 * It will generate a new one if the user doesn't have an encryption key yet.
4717
	 * It will also save the key into EncryptionStore for this session if the key
4718
	 * wasn't there yet.
4719
	 *
4720
	 * @return string
4721
	 */
4722
	public function getFilesEncryptionKey() {
4723
		// fallback if FILES_ACCOUNTSTORE_V1_SECRET_KEY is defined globally
4724
		$key = defined('FILES_ACCOUNTSTORE_V1_SECRET_KEY') ? hex2bin(constant('FILES_ACCOUNTSTORE_V1_SECRET_KEY')) : null;
4725
		if ($key === null) {
4726
			$encryptionStore = EncryptionStore::getInstance();
4727
			$key = $encryptionStore->get('filesenckey');
4728
			if ($key === null) {
4729
				$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
4730
				$props = mapi_getprops($store, [PR_EC_WA_FILES_ENCRYPTION_KEY]);
4731
				if (isset($props[PR_EC_WA_FILES_ENCRYPTION_KEY])) {
4732
					$key = $props[PR_EC_WA_FILES_ENCRYPTION_KEY];
4733
				}
4734
				else {
4735
					$key = sodium_crypto_secretbox_keygen();
4736
					$encryptionStore->add('filesenckey', $key);
4737
					mapi_setprops($store, [PR_EC_WA_FILES_ENCRYPTION_KEY => $key]);
4738
					mapi_savechanges($store);
4739
				}
4740
			}
4741
		}
4742
4743
		return $key;
4744
	}
4745
}
4746