Operations::expandDistList()   B
last analyzed

Complexity

Conditions 11
Paths 80

Size

Total Lines 48
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 11
eloc 30
c 1
b 1
f 0
nc 80
nop 2
dl 0
loc 48
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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) {
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((string) $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((string) $msgstore_props[PR_IPM_SUBTREE_ENTRYID]),
97
					"mdb_provider" => bin2hex((string) $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" => $msgstore_props[PR_QUOTA_WARNING_THRESHOLD] ?? 0,
103
					"quota_soft" => $msgstore_props[PR_QUOTA_SEND_THRESHOLD] ?? 0,
104
					"quota_hard" => $msgstore_props[PR_QUOTA_RECEIVE_THRESHOLD] ?? 0,
105
					"common_view_entryid" => isset($msgstore_props[PR_COMMON_VIEWS_ENTRYID]) ? bin2hex((string) $msgstore_props[PR_COMMON_VIEWS_ENTRYID]) : "",
106
					"finder_entryid" => isset($msgstore_props[PR_FINDER_ENTRYID]) ? bin2hex((string) $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((string) $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) {
0 ignored issues
show
Bug introduced by
The type MAPIException 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...
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);
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((string) $inboxProps[$tag]);
165
						}
166
						break;
167
168
					case "store":
169
						if (isset($msgstore_props[$tag])) {
170
							$storeData["props"][$key] = bin2hex((string) $msgstore_props[$tag]);
171
						}
172
						break;
173
174
					case "root":
175
						if (isset($rootProps[$tag])) {
176
							$storeData["props"][$key] = bin2hex((string) $rootProps[$tag]);
177
						}
178
						break;
179
180
					case "additional":
181
						if (isset($additional_ren_entryids[$tag])) {
182
							$storeData["props"][$key] = bin2hex((string) $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((string) $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((string) $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((string) $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((string) $folderProps[PR_ENTRYID]),
476
			"parent_entryid" => bin2hex((string) $folderProps[PR_PARENT_ENTRYID]),
477
			"store_entryid" => bin2hex((string) $folderProps[PR_STORE_ENTRYID]),
478
			// Scalar properties
479
			"props" => [
480
				"display_name" => $folderProps[PR_DISPLAY_NAME],
481
				"object_type" => $folderProps[PR_OBJECT_TYPE] ?? MAPI_FOLDER, // FIXME: Why isn't this always set?
482
				"content_count" => $folderProps[PR_CONTENT_COUNT] ?? 0,
483
				"content_unread" => $folderProps[PR_CONTENT_UNREAD] ?? 0,
484
				"has_subfolder" => $folderProps[PR_SUBFOLDERS] ?? false,
485
				"container_class" => $folderProps[PR_CONTAINER_CLASS] ?? "IPF.Note",
486
				"access" => $folderProps[PR_ACCESS] ?? 0,
487
				"rights" => $folderProps[PR_RIGHTS] ?? ecRightsNone,
488
				"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
						// Outlook apparently doesn't set PR_WLINK_STORE_ENTRYID
569
						// for with free/busy permission only opened shared calendars,
570
						// so do not remove them from the IPM_COMMON_VIEWS
571
						if ((isset($row[PR_WLINK_SECTION]) && $row[PR_WLINK_SECTION] != wbsidCalendar) ||
572
							!isset($row[PR_WLINK_SECTION])) {
573
							array_push($faultyLinkMsg, $row[PR_ENTRYID]);
574
						}
575
576
						continue;
577
					}
578
					$props = $this->getFavoriteLinkedFolderProps($row);
579
					if (empty($props)) {
580
						continue;
581
					}
582
				}
583
				elseif ($row[PR_MESSAGE_CLASS] === "IPM.Microsoft.WunderBar.SFInfo") {
584
					$props = $this->getFavoritesLinkedSearchFolderProps($row[PR_WB_SF_ID], $finderHierarchyTables);
585
					if (empty($props)) {
586
						continue;
587
					}
588
				}
589
			}
590
			catch (MAPIException) {
591
				continue;
592
			}
593
594
			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...
595
		}
596
597
		if (!empty($faultyLinkMsg)) {
598
			// remove faulty link messages from common view folder.
599
			mapi_folder_deletemessages($commonViewFolder, $faultyLinkMsg);
600
		}
601
	}
602
603
	/**
604
	 * Function which checks whether given linked Message is faulty or not.
605
	 * It will store faulty linked messages in given &$faultyLinkMsg array.
606
	 * Returns true if linked message of favorite item is faulty.
607
	 *
608
	 * @param array  &$faultyLinkMsg   reference in which faulty linked messages will be stored
609
	 * @param array  $allMessageStores Associative array with entryid -> mapistore of all open stores (private, public, delegate)
610
	 * @param object $linkedMessage    link message which belongs to associated contains table of IPM_COMMON_VIEWS folder
611
	 *
612
	 * @return true if linked message of favorite item is faulty or false
613
	 */
614
	public function checkFaultyFavoritesLinkedFolder(&$faultyLinkMsg, $allMessageStores, $linkedMessage) {
615
		// Find faulty link messages which does not linked to any message. if link message
616
		// does not contains store entryid in which actual message is located then it consider as
617
		// faulty link message.
618
		if (isset($linkedMessage[PR_WLINK_STORE_ENTRYID]) && empty($linkedMessage[PR_WLINK_STORE_ENTRYID])) {
619
			array_push($faultyLinkMsg, $linkedMessage[PR_ENTRYID]);
620
621
			return true;
622
		}
623
624
		// Check if store of a favorite Item does not exist in Hierarchy then
625
		// delete link message of that favorite item.
626
		// i.e. If a user is unhooked then remove its favorite items.
627
		$storeExist = array_key_exists($linkedMessage[PR_WLINK_STORE_ENTRYID], $allMessageStores);
628
		if (!$storeExist) {
629
			array_push($faultyLinkMsg, $linkedMessage[PR_ENTRYID]);
630
631
			return true;
632
		}
633
634
		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...
635
	}
636
637
	/**
638
	 * Function which get the favorites marked folders from favorites link message
639
	 * which belongs to associated contains table of IPM_COMMON_VIEWS folder.
640
	 *
641
	 * @param array $linkMessageProps properties of link message which belongs to
642
	 *                                associated contains table of IPM_COMMON_VIEWS folder
643
	 *
644
	 * @return array List of properties of a folder
645
	 */
646
	public function getFavoriteLinkedFolderProps($linkMessageProps) {
647
		// In webapp we use IPM_SUBTREE as root folder for the Hierarchy but OL is use IMsgStore as a
648
		// Root folder. OL never mark favorites to IPM_SUBTREE. So to make favorites compatible with OL
649
		// we need this check.
650
		// Here we check PR_WLINK_STORE_ENTRYID and PR_WLINK_ENTRYID is same. Which same only in one case
651
		// where some user has mark favorites to root(Inbox-<user name>) folder from OL. So here if condition
652
		// gets true we get the IPM_SUBTREE and send it to response as favorites folder to webapp.
653
		try {
654
			if ($GLOBALS['entryid']->compareEntryIds($linkMessageProps[PR_WLINK_STORE_ENTRYID], $linkMessageProps[PR_WLINK_ENTRYID])) {
655
				$storeObj = $GLOBALS["mapisession"]->openMessageStore($linkMessageProps[PR_WLINK_STORE_ENTRYID]);
656
				$subTreeEntryid = mapi_getprops($storeObj, [PR_IPM_SUBTREE_ENTRYID]);
657
				$folderObj = mapi_msgstore_openentry($storeObj, $subTreeEntryid[PR_IPM_SUBTREE_ENTRYID]);
658
			}
659
			else {
660
				$storeObj = $GLOBALS["mapisession"]->openMessageStore($linkMessageProps[PR_WLINK_STORE_ENTRYID]);
661
				if (!is_resource($storeObj)) {
662
					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...
663
				}
664
				$folderObj = mapi_msgstore_openentry($storeObj, $linkMessageProps[PR_WLINK_ENTRYID]);
665
			}
666
667
			return mapi_getprops($folderObj, $GLOBALS["properties"]->getFavoritesFolderProperties());
668
		}
669
		catch (Exception) {
670
			// in some cases error_log was causing an endless loop, so disable it for now
671
			// error_log($e);
672
		}
673
674
		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...
675
	}
676
677
	/**
678
	 * Function which retrieve the search folder from FINDERS_ROOT folder of all open
679
	 * message store.
680
	 *
681
	 * @param string $searchFolderId        contains a GUID that identifies the search folder.
682
	 *                                      The value of this property MUST NOT change.
683
	 * @param array  $finderHierarchyTables hierarchy tables which belongs to FINDERS_ROOT
684
	 *                                      folder of message stores
685
	 *
686
	 * @return array list of search folder properties
687
	 */
688
	public function getFavoritesLinkedSearchFolderProps($searchFolderId, $finderHierarchyTables) {
689
		$restriction = [RES_EXIST,
690
			[
691
				ULPROPTAG => PR_EXTENDED_FOLDER_FLAGS,
692
			],
693
		];
694
695
		foreach ($finderHierarchyTables as $finderEntryid => $hierarchyTable) {
696
			$rows = mapi_table_queryallrows($hierarchyTable, $GLOBALS["properties"]->getFavoritesFolderProperties(), $restriction);
697
			foreach ($rows as $row) {
698
				$flags = unpack("H2ExtendedFlags-Id/H2ExtendedFlags-Cb/H8ExtendedFlags-Data/H2SearchFolderTag-Id/H2SearchFolderTag-Cb/H8SearchFolderTag-Data/H2SearchFolderId-Id/H2SearchFolderId-Cb/H32SearchFolderId-Data", (string) $row[PR_EXTENDED_FOLDER_FLAGS]);
699
				if ($flags["SearchFolderId-Data"] === bin2hex($searchFolderId)) {
700
					return $row;
701
				}
702
			}
703
		}
704
	}
705
706
	/**
707
	 * Create link messages for default favorites(Inbox and Sent Items) folders in associated contains table of IPM_COMMON_VIEWS folder
708
	 * and remove all other link message from the same.
709
	 *
710
	 * @param string $commonViewFolderEntryid IPM_COMMON_VIEWS folder entryid
711
	 * @param object $store                   Message Store Object
712
	 * @param array  $storeData               the store data which use to create restriction
713
	 */
714
	public function setDefaultFavoritesFolder($commonViewFolderEntryid, $store, $storeData) {
715
		if ($GLOBALS["settings"]->get("zarafa/v1/contexts/hierarchy/show_default_favorites") !== false) {
716
			$commonViewFolder = mapi_msgstore_openentry($store, $commonViewFolderEntryid);
717
718
			$inboxFolderEntryid = hex2bin((string) $storeData["props"]["default_folder_inbox"]);
719
			$sentFolderEntryid = hex2bin((string) $storeData["props"]["default_folder_sent"]);
720
721
			$table = mapi_folder_getcontentstable($commonViewFolder, MAPI_ASSOCIATED);
722
723
			// Restriction for get all link message(IPM.Microsoft.WunderBar.Link)
724
			// and search link message (IPM.Microsoft.WunderBar.SFInfo) from
725
			// Associated contains table of IPM_COMMON_VIEWS folder.
726
			$findLinkMsgRestriction = [RES_OR,
727
				[
728
					[RES_PROPERTY,
729
						[
730
							RELOP => RELOP_EQ,
731
							ULPROPTAG => PR_MESSAGE_CLASS,
732
							VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link"],
733
						],
734
					],
735
					[RES_PROPERTY,
736
						[
737
							RELOP => RELOP_EQ,
738
							ULPROPTAG => PR_MESSAGE_CLASS,
739
							VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo"],
740
						],
741
					],
742
				],
743
			];
744
745
			// Restriction for find Inbox and/or Sent folder link message from
746
			// Associated contains table of IPM_COMMON_VIEWS folder.
747
			$findInboxOrSentLinkMessage = [RES_OR,
748
				[
749
					[RES_PROPERTY,
750
						[
751
							RELOP => RELOP_EQ,
752
							ULPROPTAG => PR_WLINK_ENTRYID,
753
							VALUE => [PR_WLINK_ENTRYID => $inboxFolderEntryid],
754
						],
755
					],
756
					[RES_PROPERTY,
757
						[
758
							RELOP => RELOP_EQ,
759
							ULPROPTAG => PR_WLINK_ENTRYID,
760
							VALUE => [PR_WLINK_ENTRYID => $sentFolderEntryid],
761
						],
762
					],
763
				],
764
			];
765
766
			// Restriction to get all link messages except Inbox and Sent folder's link messages from
767
			// Associated contains table of IPM_COMMON_VIEWS folder, if exist in it.
768
			$restriction = [RES_AND,
769
				[
770
					$findLinkMsgRestriction,
771
					[RES_NOT,
772
						[
773
							$findInboxOrSentLinkMessage,
774
						],
775
					],
776
				],
777
			];
778
779
			$rows = mapi_table_queryallrows($table, [PR_ENTRYID], $restriction);
780
			if (!empty($rows)) {
781
				$deleteMessages = [];
782
				foreach ($rows as $row) {
783
					array_push($deleteMessages, $row[PR_ENTRYID]);
784
				}
785
				mapi_folder_deletemessages($commonViewFolder, $deleteMessages);
786
			}
787
788
			// We need to remove all search folder from FIND_ROOT(search root folder)
789
			// when reset setting was triggered because on reset setting we remove all
790
			// link messages from common view folder which are linked with either
791
			// favorites or search folder.
792
			$finderFolderEntryid = hex2bin((string) $storeData["props"]["finder_entryid"]);
793
			$finderFolder = mapi_msgstore_openentry($store, $finderFolderEntryid);
794
			$hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS);
795
			$folders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]);
796
			foreach ($folders as $folder) {
797
				try {
798
					mapi_folder_deletefolder($finderFolder, $folder[PR_ENTRYID]);
799
				}
800
				catch (MAPIException $e) {
801
					$msg = "Problem in deleting search folder while reset settings. MAPI Error %s.";
802
					$formattedMsg = sprintf($msg, get_mapi_error_name($e->getCode()));
803
					error_log($formattedMsg);
804
					Log::Write(LOGLEVEL_ERROR, "Operations:setDefaultFavoritesFolder() " . $formattedMsg);
805
				}
806
			}
807
			// Restriction used to find only Inbox and Sent folder's link messages from
808
			// Associated contains table of IPM_COMMON_VIEWS folder, if exist in it.
809
			$restriction = [RES_AND,
810
				[
811
					$findLinkMsgRestriction,
812
					$findInboxOrSentLinkMessage,
813
				],
814
			];
815
816
			$rows = mapi_table_queryallrows($table, [PR_WLINK_ENTRYID], $restriction);
817
818
			// If Inbox and Sent folder's link messages are not exist then create the
819
			// link message for those in associated contains table of IPM_COMMON_VIEWS folder.
820
			if (empty($rows)) {
821
				$defaultFavFoldersKeys = ["inbox", "sent"];
822
				foreach ($defaultFavFoldersKeys as $folderKey) {
823
					$folderObj = $GLOBALS["mapisession"]->openMessage(hex2bin((string) $storeData["props"]["default_folder_" . $folderKey]));
824
					$props = mapi_getprops($folderObj, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]);
825
					$this->createFavoritesLink($commonViewFolder, $props);
826
				}
827
			}
828
			elseif (count($rows) < 2) {
829
				// If rows count is less than 2 it means associated contains table of IPM_COMMON_VIEWS folder
830
				// can have either Inbox or Sent folder link message in it. So we have to create link message
831
				// for Inbox or Sent folder which ever not exist in associated contains table of IPM_COMMON_VIEWS folder
832
				// to maintain default favorites folder.
833
				$row = $rows[0];
834
				$wlinkEntryid = $row[PR_WLINK_ENTRYID];
835
836
				$isInboxFolder = $GLOBALS['entryid']->compareEntryIds($wlinkEntryid, $inboxFolderEntryid);
837
838
				if (!$isInboxFolder) {
839
					$folderObj = $GLOBALS["mapisession"]->openMessage($inboxFolderEntryid);
840
				}
841
				else {
842
					$folderObj = $GLOBALS["mapisession"]->openMessage($sentFolderEntryid);
843
				}
844
845
				$props = mapi_getprops($folderObj, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]);
846
				$this->createFavoritesLink($commonViewFolder, $props);
847
			}
848
			$GLOBALS["settings"]->set("zarafa/v1/contexts/hierarchy/show_default_favorites", false, true);
849
		}
850
	}
851
852
	/**
853
	 * Create favorites link message (IPM.Microsoft.WunderBar.Link) or
854
	 * search link message ("IPM.Microsoft.WunderBar.SFInfo") in associated
855
	 * contains table of IPM_COMMON_VIEWS folder.
856
	 *
857
	 * @param object      $commonViewFolder MAPI Message Folder Object
858
	 * @param array       $folderProps      Properties of a folder
859
	 * @param bool|string $searchFolderId   search folder id which is used to identify the
860
	 *                                      linked search folder from search link message. by default it is false.
861
	 */
862
	public function createFavoritesLink($commonViewFolder, $folderProps, $searchFolderId = false) {
863
		if ($searchFolderId) {
864
			$props = [
865
				PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo",
866
				PR_WB_SF_ID => $searchFolderId,
867
				PR_WLINK_TYPE => wblSearchFolder,
868
			];
869
		}
870
		else {
871
			$defaultStoreEntryId = hex2bin((string) $GLOBALS['mapisession']->getDefaultMessageStoreEntryId());
872
			$props = [
873
				PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link",
874
				PR_WLINK_ENTRYID => $folderProps[PR_ENTRYID],
875
				PR_WLINK_STORE_ENTRYID => $folderProps[PR_STORE_ENTRYID],
876
				PR_WLINK_TYPE => $GLOBALS['entryid']->compareEntryIds($defaultStoreEntryId, $folderProps[PR_STORE_ENTRYID]) ? wblNormalFolder : wblSharedFolder,
877
			];
878
		}
879
880
		$favoritesLinkMsg = mapi_folder_createmessage($commonViewFolder, MAPI_ASSOCIATED);
881
		mapi_setprops($favoritesLinkMsg, $props);
882
		mapi_savechanges($favoritesLinkMsg);
883
	}
884
885
	/**
886
	 * Convert MAPI properties into useful and human readable string for favorites folder.
887
	 *
888
	 * @param array $folderProps Properties of a folder
889
	 *
890
	 * @return array List of properties of a folder
891
	 */
892
	public function setFavoritesFolder($folderProps) {
893
		$props = $this->setFolder($folderProps);
894
		// Add and Make isFavorites to true, this allows the client to properly
895
		// indicate to the user that this is a favorites item/folder.
896
		$props["props"]["isFavorites"] = true;
897
		$props["props"]["folder_type"] = $folderProps[PR_FOLDER_TYPE];
898
899
		return $props;
900
	}
901
902
	/**
903
	 * Fetches extended flags for folder. If PR_EXTENDED_FLAGS is not set then we assume that client
904
	 * should handle which property to display.
905
	 *
906
	 * @param array $folderProps Properties of a folder
907
	 * @param array $props       properties in which flags should be set
908
	 */
909
	public function setExtendedFolderFlags($folderProps, &$props) {
910
		if (isset($folderProps[PR_EXTENDED_FOLDER_FLAGS])) {
911
			$flags = unpack("Cid/Cconst/Cflags", $folderProps[PR_EXTENDED_FOLDER_FLAGS]);
912
913
			// ID property is '1' this means 'Data' property contains extended flags
914
			if ($flags["id"] == 1) {
915
				$props["props"]["extended_flags"] = $flags["flags"];
916
			}
917
		}
918
	}
919
920
	/**
921
	 * Used to update the storeData with a folder and properties that will
922
	 * inform the user that the store could not be opened.
923
	 *
924
	 * @param array  &$storeData    The store data which will be updated
925
	 * @param string $folderType    The foldertype which was attempted to be loaded
926
	 * @param array  $folderEntryID The entryid of the which was attempted to be opened
927
	 */
928
	public function invalidateResponseStore(&$storeData, $folderType, $folderEntryID) {
929
		$folderName = "Folder";
930
		$containerClass = "IPF.Note";
931
932
		switch ($folderType) {
933
			case "all":
934
				$folderName = "IPM_SUBTREE";
935
				$containerClass = "IPF.Note";
936
				break;
937
938
			case "calendar":
939
				$folderName = _("Calendar");
940
				$containerClass = "IPF.Appointment";
941
				break;
942
943
			case "contact":
944
				$folderName = _("Contacts");
945
				$containerClass = "IPF.Contact";
946
				break;
947
948
			case "inbox":
949
				$folderName = _("Inbox");
950
				$containerClass = "IPF.Note";
951
				break;
952
953
			case "note":
954
				$folderName = _("Notes");
955
				$containerClass = "IPF.StickyNote";
956
				break;
957
958
			case "task":
959
				$folderName = _("Tasks");
960
				$containerClass = "IPF.Task";
961
				break;
962
		}
963
964
		// Insert a fake folder which will be shown to the user
965
		// to acknowledge that he has a shared store, but also
966
		// to indicate that he can't open it.
967
		$tempFolderProps = $this->setFolder([
968
			PR_ENTRYID => $folderEntryID,
969
			PR_PARENT_ENTRYID => hex2bin((string) $storeData["props"]["subtree_entryid"]),
970
			PR_STORE_ENTRYID => hex2bin((string) $storeData["store_entryid"]),
971
			PR_DISPLAY_NAME => $folderName,
972
			PR_OBJECT_TYPE => MAPI_FOLDER,
973
			PR_SUBFOLDERS => false,
974
			PR_CONTAINER_CLASS => $containerClass,
975
			PR_ACCESS => 0,
976
		]);
977
978
		// Mark the folder as unavailable, this allows the client to properly
979
		// indicate to the user that this is a fake entry.
980
		$tempFolderProps['props']['is_unavailable'] = true;
981
982
		array_push($storeData["folders"]["item"], $tempFolderProps);
983
984
		/* TRANSLATORS: This indicates that the opened folder belongs to a particular user,
985
		 * for example: 'Calendar of Holiday', in this case %1$s is 'Calendar' (the foldername)
986
		 * and %2$s is 'Holiday' (the username).
987
		 */
988
		$storeData["props"]["display_name"] = ($folderType === "all") ? $storeData["props"]["display_name"] : sprintf(_('%1$s of %2$s'), $folderName, $storeData["props"]["mailbox_owner_name"]);
989
		$storeData["props"]["subtree_entryid"] = $tempFolderProps["parent_entryid"];
990
		$storeData["props"]["folder_type"] = $folderType;
991
	}
992
993
	/**
994
	 * Used to update the storeData with a folder and properties that will function as a
995
	 * placeholder for the IPMSubtree that could not be opened.
996
	 *
997
	 * @param array &$storeData    The store data which will be updated
998
	 * @param array $folderEntryID The entryid of the which was attempted to be opened
999
	 */
1000
	public function getDummyIPMSubtreeFolder(&$storeData, $folderEntryID) {
1001
		// Insert a fake folder which will be shown to the user
1002
		// to acknowledge that he has a shared store.
1003
		$tempFolderProps = $this->setFolder([
1004
			PR_ENTRYID => $folderEntryID,
1005
			PR_PARENT_ENTRYID => hex2bin((string) $storeData["props"]["subtree_entryid"]),
1006
			PR_STORE_ENTRYID => hex2bin((string) $storeData["store_entryid"]),
1007
			PR_DISPLAY_NAME => "IPM_SUBTREE",
1008
			PR_OBJECT_TYPE => MAPI_FOLDER,
1009
			PR_SUBFOLDERS => true,
1010
			PR_CONTAINER_CLASS => "IPF.Note",
1011
			PR_ACCESS => 0,
1012
		]);
1013
1014
		array_push($storeData["folders"]["item"], $tempFolderProps);
1015
		$storeData["props"]["subtree_entryid"] = $tempFolderProps["parent_entryid"];
1016
	}
1017
1018
	/**
1019
	 * Create a MAPI folder.
1020
	 *
1021
	 * This function simply creates a MAPI folder at a specific location with a specific folder
1022
	 * type.
1023
	 *
1024
	 * @param object $store         MAPI Message Store Object in which the folder lives
1025
	 * @param string $parententryid The parent entryid in which the new folder should be created
1026
	 * @param string $name          The name of the new folder
1027
	 * @param string $type          The type of the folder (PR_CONTAINER_CLASS, so value should be 'IPM.Appointment', etc)
1028
	 * @param array  $folderProps   reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of new folder
1029
	 *
1030
	 * @return bool true if action succeeded, false if not
1031
	 */
1032
	public function createFolder($store, $parententryid, $name, $type, &$folderProps) {
1033
		$result = false;
1034
		$folder = mapi_msgstore_openentry($store, $parententryid);
1035
1036
		if ($folder) {
1037
			/**
1038
			 * @TODO: If parent folder has any sub-folder with the same name than this will return
1039
			 * MAPI_E_COLLISION error, so show this error to client and don't close the dialog.
1040
			 */
1041
			$new_folder = mapi_folder_createfolder($folder, $name);
1042
1043
			if ($new_folder) {
1044
				mapi_setprops($new_folder, [PR_CONTAINER_CLASS => $type]);
1045
				$result = true;
1046
1047
				$folderProps = mapi_getprops($new_folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1048
			}
1049
		}
1050
1051
		return $result;
1052
	}
1053
1054
	/**
1055
	 * Rename a folder.
1056
	 *
1057
	 * This function renames the specified folder. However, a conflict situation can arise
1058
	 * if the specified folder name already exists. In this case, the folder name is postfixed with
1059
	 * an ever-higher integer to create a unique folder name.
1060
	 *
1061
	 * @param object $store       MAPI Message Store Object
1062
	 * @param string $entryid     The entryid of the folder to rename
1063
	 * @param string $name        The new name of the folder
1064
	 * @param array  $folderProps reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID
1065
	 *
1066
	 * @return bool true if action succeeded, false if not
1067
	 */
1068
	public function renameFolder($store, $entryid, $name, &$folderProps) {
1069
		$folder = mapi_msgstore_openentry($store, $entryid);
1070
		if (!$folder) {
1071
			return false;
1072
		}
1073
		$result = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
1074
		$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]);
1075
1076
		/*
1077
		 * If parent folder has any sub-folder with the same name than this will return
1078
		 * MAPI_E_COLLISION error while renaming folder, so show this error to client,
1079
		 * and revert changes in view.
1080
		 */
1081
		try {
1082
			mapi_setprops($folder, [PR_DISPLAY_NAME => $name]);
1083
			mapi_savechanges($folder);
1084
			$result = true;
1085
		}
1086
		catch (MAPIException $e) {
1087
			if ($e->getCode() == MAPI_E_COLLISION) {
1088
				/*
1089
				 * revert folder name to original one
1090
				 * There is a bug in php-mapi that updates folder name in hierarchy table with null value
1091
				 * so we need to revert those change by again setting the old folder name
1092
				 * (ZCP-11586)
1093
				 */
1094
				mapi_setprops($folder, [PR_DISPLAY_NAME => $folderProps[PR_DISPLAY_NAME]]);
1095
				mapi_savechanges($folder);
1096
			}
1097
1098
			// rethrow exception so we will send error to client
1099
			throw $e;
1100
		}
1101
1102
		return $result;
1103
	}
1104
1105
	/**
1106
	 * Check if a folder is 'special'.
1107
	 *
1108
	 * All default MAPI folders such as 'inbox', 'outbox', etc have special permissions; you can not rename them for example. This
1109
	 * function returns TRUE if the specified folder is 'special'.
1110
	 *
1111
	 * @param object $store   MAPI Message Store Object
1112
	 * @param string $entryid The entryid of the folder
1113
	 *
1114
	 * @return bool true if folder is a special folder, false if not
1115
	 */
1116
	public function isSpecialFolder($store, $entryid) {
1117
		$msgstore_props = mapi_getprops($store, [PR_MDB_PROVIDER]);
1118
1119
		// "special" folders don't exists in public store
1120
		if ($msgstore_props[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
1121
			return false;
1122
		}
1123
1124
		// Check for the Special folders which are provided on the store
1125
		$msgstore_props = mapi_getprops($store, [
1126
			PR_IPM_SUBTREE_ENTRYID,
1127
			PR_IPM_OUTBOX_ENTRYID,
1128
			PR_IPM_SENTMAIL_ENTRYID,
1129
			PR_IPM_WASTEBASKET_ENTRYID,
1130
			PR_IPM_PUBLIC_FOLDERS_ENTRYID,
1131
			PR_IPM_FAVORITES_ENTRYID,
1132
		]);
1133
1134
		if (array_search($entryid, $msgstore_props)) {
1135
			return true;
1136
		}
1137
1138
		// Check for the Special folders which are provided on the root folder
1139
		$root = mapi_msgstore_openentry($store);
1140
		$rootProps = mapi_getprops($root, [
1141
			PR_IPM_APPOINTMENT_ENTRYID,
1142
			PR_IPM_CONTACT_ENTRYID,
1143
			PR_IPM_DRAFTS_ENTRYID,
1144
			PR_IPM_JOURNAL_ENTRYID,
1145
			PR_IPM_NOTE_ENTRYID,
1146
			PR_IPM_TASK_ENTRYID,
1147
			PR_ADDITIONAL_REN_ENTRYIDS,
1148
		]);
1149
1150
		if (array_search($entryid, $rootProps)) {
1151
			return true;
1152
		}
1153
1154
		// The PR_ADDITIONAL_REN_ENTRYIDS are a bit special
1155
		if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS]) && is_array($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
1156
			if (array_search($entryid, $rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
1157
				return true;
1158
			}
1159
		}
1160
1161
		// Check if the given folder is the inbox, note that we are unsure
1162
		// if we have permissions on that folder, so we need a try catch.
1163
		try {
1164
			$inbox = mapi_msgstore_getreceivefolder($store);
1165
			$props = mapi_getprops($inbox, [PR_ENTRYID]);
1166
1167
			if ($props[PR_ENTRYID] == $entryid) {
1168
				return true;
1169
			}
1170
		}
1171
		catch (MAPIException $e) {
1172
			if ($e->getCode() !== MAPI_E_NO_ACCESS) {
1173
				throw $e;
1174
			}
1175
		}
1176
1177
		return false;
1178
	}
1179
1180
	/**
1181
	 * Delete a folder.
1182
	 *
1183
	 * Deleting a folder normally just moves the folder to the wastebasket, which is what this function does. However,
1184
	 * if the folder was already in the wastebasket, then the folder is really deleted.
1185
	 *
1186
	 * @param object $store         MAPI Message Store Object
1187
	 * @param string $parententryid The parent in which the folder should be deleted
1188
	 * @param string $entryid       The entryid of the folder which will be deleted
1189
	 * @param array  $folderProps   reference to an array which will be filled with PR_ENTRYID, PR_STORE_ENTRYID of the deleted object
1190
	 * @param bool   $softDelete    flag for indicating that folder should be soft deleted which can be recovered from
1191
	 *                              restore deleted items
1192
	 * @param bool   $hardDelete    flag for indicating that folder should be hard deleted from system and can not be
1193
	 *                              recovered from restore soft deleted items
1194
	 *
1195
	 * @return bool true if action succeeded, false if not
1196
	 *
1197
	 * @todo subfolders of folders in the wastebasket should also be hard-deleted
1198
	 */
1199
	public function deleteFolder($store, $parententryid, $entryid, &$folderProps, $softDelete = false, $hardDelete = false) {
1200
		$result = false;
1201
		$msgprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID]);
1202
		$folder = mapi_msgstore_openentry($store, $parententryid);
1203
1204
		if ($folder && !$this->isSpecialFolder($store, $entryid)) {
1205
			if ($hardDelete === true) {
1206
				// hard delete the message if requested
1207
				// beware that folder can not be recovered after this and will be deleted from system entirely
1208
				if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS | DELETE_HARD_DELETE)) {
1209
					$result = true;
1210
1211
					// if exists, also delete settings made for this folder (client don't need an update for this)
1212
					$GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid));
1213
				}
1214
			}
1215
			else {
1216
				if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID])) {
1217
					// TODO: check if not only $parententryid=wastebasket, but also the parents of that parent...
1218
					// if folder is already in wastebasket or softDelete is requested then delete the message
1219
					if ($msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete === true) {
1220
						if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS)) {
1221
							$result = true;
1222
1223
							// if exists, also delete settings made for this folder (client don't need an update for this)
1224
							$GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid));
1225
						}
1226
					}
1227
					else {
1228
						// move the folder to wastebasket
1229
						$wastebasket = mapi_msgstore_openentry($store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID]);
1230
1231
						$deleted_folder = mapi_msgstore_openentry($store, $entryid);
1232
						$props = mapi_getprops($deleted_folder, [PR_DISPLAY_NAME]);
1233
1234
						try {
1235
							/*
1236
							 * To decrease overload of checking for conflicting folder names on modification of every folder
1237
							 * we should first try to copy folder and if it returns MAPI_E_COLLISION then
1238
							 * only we should check for the conflicting folder names and generate a new name
1239
							 * and copy folder with the generated name.
1240
							 */
1241
							mapi_folder_copyfolder($folder, $entryid, $wastebasket, $props[PR_DISPLAY_NAME], FOLDER_MOVE);
1242
							$folderProps = mapi_getprops($deleted_folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1243
							$result = true;
1244
						}
1245
						catch (MAPIException $e) {
1246
							if ($e->getCode() == MAPI_E_COLLISION) {
1247
								$foldername = $this->checkFolderNameConflict($store, $wastebasket, $props[PR_DISPLAY_NAME]);
1248
1249
								mapi_folder_copyfolder($folder, $entryid, $wastebasket, $foldername, FOLDER_MOVE);
1250
								$folderProps = mapi_getprops($deleted_folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1251
								$result = true;
1252
							}
1253
							else {
1254
								// all other errors should be propagated to higher level exception handlers
1255
								throw $e;
1256
							}
1257
						}
1258
					}
1259
				}
1260
				else {
1261
					if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS)) {
1262
						$result = true;
1263
1264
						// if exists, also delete settings made for this folder (client don't need an update for this)
1265
						$GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid));
1266
					}
1267
				}
1268
			}
1269
		}
1270
1271
		return $result;
1272
	}
1273
1274
	/**
1275
	 * Empty folder.
1276
	 *
1277
	 * Removes all items from a folder. This is a real delete, not a move.
1278
	 *
1279
	 * @param object $store           MAPI Message Store Object
1280
	 * @param string $entryid         The entryid of the folder which will be emptied
1281
	 * @param array  $folderProps     reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of the emptied folder
1282
	 * @param bool   $hardDelete      flag to indicate if messages will be hard deleted and can not be recoved using restore soft deleted items
1283
	 * @param bool   $emptySubFolders true to remove all messages with child folders of selected folder else false will
1284
	 *                                remove only message of selected folder
1285
	 *
1286
	 * @return bool true if action succeeded, false if not
1287
	 */
1288
	public function emptyFolder($store, $entryid, &$folderProps, $hardDelete = false, $emptySubFolders = true) {
1289
		$result = false;
1290
		$folder = mapi_msgstore_openentry($store, $entryid);
1291
1292
		if ($folder) {
1293
			$flag = DEL_ASSOCIATED;
1294
1295
			if ($hardDelete) {
1296
				$flag |= DELETE_HARD_DELETE;
1297
			}
1298
1299
			if ($emptySubFolders) {
1300
				$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...
1301
			}
1302
			else {
1303
				// Delete all items of selected folder without
1304
				// removing child folder and it's content.
1305
				// FIXME: it is effecting performance because mapi_folder_emptyfolder function not provide facility to
1306
				// remove only selected folder items without touching child folder and it's items.
1307
				// for more check KC-1268
1308
				$table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS);
1309
				$rows = mapi_table_queryallrows($table, [PR_ENTRYID]);
1310
				$messages = [];
1311
				foreach ($rows as $row) {
1312
					array_push($messages, $row[PR_ENTRYID]);
1313
				}
1314
				$result = mapi_folder_deletemessages($folder, $messages, $flag);
1315
			}
1316
1317
			$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1318
			$result = true;
1319
		}
1320
1321
		return $result;
1322
	}
1323
1324
	/**
1325
	 * Copy or move a folder.
1326
	 *
1327
	 * @param object $store               MAPI Message Store Object
1328
	 * @param string $parentfolderentryid The parent entryid of the folder which will be copied or moved
1329
	 * @param string $sourcefolderentryid The entryid of the folder which will be copied or moved
1330
	 * @param string $destfolderentryid   The entryid of the folder which the folder will be copied or moved to
1331
	 * @param bool   $moveFolder          true - move folder, false - copy folder
1332
	 * @param array  $folderProps         reference to an array which will be filled with entryids
1333
	 * @param mixed  $deststore
1334
	 *
1335
	 * @return bool true if action succeeded, false if not
1336
	 */
1337
	public function copyFolder($store, $parentfolderentryid, $sourcefolderentryid, $destfolderentryid, $deststore, $moveFolder, &$folderProps) {
1338
		$result = false;
1339
		$sourceparentfolder = mapi_msgstore_openentry($store, $parentfolderentryid);
1340
		$destfolder = mapi_msgstore_openentry($deststore, $destfolderentryid);
1341
		if (!$this->isSpecialFolder($store, $sourcefolderentryid) && $sourceparentfolder && $destfolder && $deststore) {
1342
			$folder = mapi_msgstore_openentry($store, $sourcefolderentryid);
1343
			$props = mapi_getprops($folder, [PR_DISPLAY_NAME]);
1344
1345
			try {
1346
				/*
1347
				  * To decrease overload of checking for conflicting folder names on modification of every folder
1348
				  * we should first try to copy/move folder and if it returns MAPI_E_COLLISION then
1349
				  * only we should check for the conflicting folder names and generate a new name
1350
				  * and copy/move folder with the generated name.
1351
				  */
1352
				if ($moveFolder) {
1353
					mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $props[PR_DISPLAY_NAME], FOLDER_MOVE);
1354
					$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1355
					// In some cases PR_PARENT_ENTRYID is not available in mapi_getprops, add it manually
1356
					$folderProps[PR_PARENT_ENTRYID] = $destfolderentryid;
1357
					$result = true;
1358
				}
1359
				else {
1360
					mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $props[PR_DISPLAY_NAME], COPY_SUBFOLDERS);
1361
					$result = true;
1362
				}
1363
			}
1364
			catch (MAPIException $e) {
1365
				if ($e->getCode() == MAPI_E_COLLISION) {
1366
					$foldername = $this->checkFolderNameConflict($deststore, $destfolder, $props[PR_DISPLAY_NAME]);
1367
					if ($moveFolder) {
1368
						mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $foldername, FOLDER_MOVE);
1369
						$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1370
						$result = true;
1371
					}
1372
					else {
1373
						mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $foldername, COPY_SUBFOLDERS);
1374
						$result = true;
1375
					}
1376
				}
1377
				else {
1378
					// all other errors should be propagated to higher level exception handlers
1379
					throw $e;
1380
				}
1381
			}
1382
		}
1383
1384
		return $result;
1385
	}
1386
1387
	/**
1388
	 * Read MAPI table.
1389
	 *
1390
	 * This function performs various operations to open, setup, and read all rows from a MAPI table.
1391
	 *
1392
	 * The output from this function is an XML array structure which can be sent directly to XML serialisation.
1393
	 *
1394
	 * @param object $store        MAPI Message Store Object
1395
	 * @param string $entryid      The entryid of the folder to read the table from
1396
	 * @param array  $properties   The set of properties which will be read
1397
	 * @param array  $sort         The set properties which the table will be sort on (formatted as a MAPI sort order)
1398
	 * @param int    $start        Starting row at which to start reading rows
1399
	 * @param int    $rowcount     Number of rows which should be read
1400
	 * @param array  $restriction  Table restriction to apply to the table (formatted as MAPI restriction)
1401
	 * @param mixed  $getHierarchy
1402
	 * @param mixed  $flags
1403
	 *
1404
	 * @return array XML array structure with row data
1405
	 */
1406
	public function getTable($store, $entryid, $properties, $sort, $start, $rowcount = false, $restriction = false, $getHierarchy = false, $flags = MAPI_DEFERRED_ERRORS) {
1407
		$data = ["item" => []];
1408
		$folder = mapi_msgstore_openentry($store, $entryid);
1409
1410
		if (!$folder) {
1411
			return $data;
1412
		}
1413
1414
		try {
1415
			$table = $getHierarchy ? mapi_folder_gethierarchytable($folder, $flags) : mapi_folder_getcontentstable($folder, $flags);
1416
		}
1417
		catch (Exception $e) {
1418
			error_log(sprintf("Unable to open table: '%s' (0x%08X)", $e->getMessage(), mapi_last_hresult()));
1419
1420
			return $data;
1421
		}
1422
1423
		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...
1424
			$rowcount = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50);
1425
		}
1426
1427
		if (is_array($restriction)) {
1428
			mapi_table_restrict($table, $restriction, TBL_BATCH);
1429
		}
1430
1431
		if (is_array($sort) && !empty($sort)) {
1432
			/*
1433
			 * If the sort array contains the PR_SUBJECT column we should change this to
1434
			 * PR_NORMALIZED_SUBJECT to make sure that when sorting on subjects: "sweet" and
1435
			 * "RE: sweet", the first one is displayed before the latter one. If the subject
1436
			 * is used for sorting the PR_MESSAGE_DELIVERY_TIME must be added as well as
1437
			 * Outlook behaves the same way in this case.
1438
			 */
1439
			if (isset($sort[PR_SUBJECT])) {
1440
				$sortReplace = [];
1441
				foreach ($sort as $key => $value) {
1442
					if ($key == PR_SUBJECT) {
1443
						$sortReplace[PR_NORMALIZED_SUBJECT] = $value;
1444
						$sortReplace[PR_MESSAGE_DELIVERY_TIME] = TABLE_SORT_DESCEND;
1445
					}
1446
					else {
1447
						$sortReplace[$key] = $value;
1448
					}
1449
				}
1450
				$sort = $sortReplace;
1451
			}
1452
1453
			mapi_table_sort($table, $sort, TBL_BATCH);
1454
		}
1455
1456
		$rows = mapi_table_queryrows($table, $properties, $start, $rowcount);
1457
1458
		foreach ($rows as $row) {
1459
			$itemData = Conversion::mapMAPI2XML($properties, $row);
1460
1461
			// For ZARAFA type users the email_address properties are filled with the username
1462
			// Here we will copy that property to the *_username property for consistency with
1463
			// the getMessageProps() function
1464
			// We will not retrieve the real email address (like the getMessageProps function does)
1465
			// for all items because that would be a performance decrease!
1466
			if (isset($itemData['props']["sent_representing_email_address"])) {
1467
				$itemData['props']["sent_representing_username"] = $itemData['props']["sent_representing_email_address"];
1468
			}
1469
			if (isset($itemData['props']["sender_email_address"])) {
1470
				$itemData['props']["sender_username"] = $itemData['props']["sender_email_address"];
1471
			}
1472
			if (isset($itemData['props']["received_by_email_address"])) {
1473
				$itemData['props']["received_by_username"] = $itemData['props']["received_by_email_address"];
1474
			}
1475
1476
			array_push($data["item"], $itemData);
1477
		}
1478
1479
		// Update the page information
1480
		$data["page"] = [];
1481
		$data["page"]["start"] = $start;
1482
		$data["page"]["rowcount"] = $rowcount;
1483
		$data["page"]["totalrowcount"] = mapi_table_getrowcount($table);
1484
1485
		return $data;
1486
	}
1487
1488
	/**
1489
	 * Returns TRUE of the MAPI message only has inline attachments.
1490
	 *
1491
	 * @param mapimessage $message The MAPI message object to check
0 ignored issues
show
Bug introduced by
The type mapimessage 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...
1492
	 *
1493
	 * @return bool TRUE if the item contains only inline attachments, FALSE otherwise
1494
	 *
1495
	 * @deprecated This function is not used, because it is much too slow to run on all messages in your inbox
1496
	 */
1497
	public function hasOnlyInlineAttachments($message) {
1498
		$attachmentTable = @mapi_message_getattachmenttable($message);
1499
		if (!$attachmentTable) {
1500
			return false;
1501
		}
1502
1503
		$attachments = @mapi_table_queryallrows($attachmentTable, [PR_ATTACHMENT_HIDDEN]);
1504
		if (empty($attachments)) {
1505
			return false;
1506
		}
1507
1508
		foreach ($attachments as $attachmentRow) {
1509
			if (!isset($attachmentRow[PR_ATTACHMENT_HIDDEN]) || !$attachmentRow[PR_ATTACHMENT_HIDDEN]) {
1510
				return false;
1511
			}
1512
		}
1513
1514
		return true;
1515
	}
1516
1517
	/**
1518
	 * Retrieve and convert body content of a message.
1519
	 *
1520
	 * This function performs the heavy lifting of decompressing RTF, converting
1521
	 * code pages and extracting both the HTML and plain text bodies. It can be
1522
	 * called independently to lazily fetch body data when required.
1523
	 *
1524
	 * @param object $message   The MAPI Message Object
1525
	 * @param bool   $html2text true - body will be converted from html to text,
1526
	 *                          false - html body will be returned
1527
	 *
1528
	 * @return array associative array containing keys 'body', 'html_body' and 'isHTML'
1529
	 */
1530
	public function getMessageBody($message, $html2text = false) {
1531
		$result = [
1532
			'body' => '',
1533
			'isHTML' => false,
1534
		];
1535
1536
		if (!$message) {
0 ignored issues
show
introduced by
$message is of type object, thus it always evaluated to true.
Loading history...
1537
			return $result;
1538
		}
1539
1540
		$plaintext = $this->isPlainText($message);
1541
		$tmpProps = mapi_getprops($message, [PR_BODY, PR_HTML]);
1542
1543
		if (empty($tmpProps[PR_HTML])) {
1544
			$tmpProps = mapi_getprops($message, [PR_BODY, PR_RTF_COMPRESSED]);
1545
			if (isset($tmpProps[PR_RTF_COMPRESSED])) {
1546
				$tmpProps[PR_HTML] = mapi_decompressrtf($tmpProps[PR_RTF_COMPRESSED]);
1547
			}
1548
		}
1549
1550
		$htmlcontent = '';
1551
		if (!$plaintext && isset($tmpProps[PR_HTML])) {
1552
			$cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]);
1553
			$codepage = $cpprops[PR_INTERNET_CPID] ?? 65001;
1554
			$htmlcontent = Conversion::convertCodepageStringToUtf8($codepage, $tmpProps[PR_HTML]);
1555
			if (!empty($htmlcontent)) {
1556
				if ($html2text) {
1557
					$htmlcontent = '';
1558
				}
1559
				else {
1560
					$result['isHTML'] = true;
1561
				}
1562
			}
1563
1564
			$htmlcontent = trim($htmlcontent, "\0");
1565
		}
1566
1567
		if (isset($tmpProps[PR_BODY])) {
1568
			// only open property if it exists
1569
			$result['body'] = trim((string) mapi_message_openproperty($message, PR_BODY), "\0");
1570
		}
1571
		elseif ($html2text && isset($tmpProps[PR_HTML])) {
1572
			$result['body'] = strip_tags((string) $tmpProps[PR_HTML]);
1573
		}
1574
1575
		if (!empty($htmlcontent)) {
1576
			$result['html_body'] = $htmlcontent;
1577
		}
1578
1579
		return $result;
1580
	}
1581
1582
	/**
1583
	 * Read message properties.
1584
	 *
1585
	 * Reads a message and returns the data as an XML array structure with all data from the message that is needed
1586
	 * to show a message (for example in the preview pane)
1587
	 *
1588
	 * @param object $store      MAPI Message Store Object
1589
	 * @param object $message    The MAPI Message Object
1590
	 * @param array  $properties Mapping of properties that should be read
1591
	 * @param bool   $html2text  true - body will be converted from html to text, false - html body will be returned
1592
	 * @param bool   $loadBody   true - fetch body content, false - skip body retrieval
1593
	 *
1594
	 * @return array item properties
1595
	 *
1596
	 * @todo Function name is misleading as it doesn't just get message properties
1597
	 */
1598
	public function getMessageProps($store, $message, $properties, $html2text = false, $loadBody = false) {
1599
		$props = [];
1600
1601
		if ($message) {
0 ignored issues
show
introduced by
$message is of type object, thus it always evaluated to true.
Loading history...
1602
			$itemprops = mapi_getprops($message, $properties);
1603
1604
			/* If necessary stream the property, if it's > 8KB */
1605
			if (isset($itemprops[PR_TRANSPORT_MESSAGE_HEADERS]) || propIsError(PR_TRANSPORT_MESSAGE_HEADERS, $itemprops) == MAPI_E_NOT_ENOUGH_MEMORY) {
1606
				$itemprops[PR_TRANSPORT_MESSAGE_HEADERS] = mapi_openproperty($message, PR_TRANSPORT_MESSAGE_HEADERS);
1607
			}
1608
1609
			$props = Conversion::mapMAPI2XML($properties, $itemprops);
1610
1611
			// Get actual SMTP address for sent_representing_email_address and received_by_email_address
1612
			$smtpprops = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_RECEIVED_BY_ENTRYID, PR_SENDER_ENTRYID]);
1613
1614
			if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID])) {
1615
				try {
1616
					$user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(true), $smtpprops[PR_SENT_REPRESENTING_ENTRYID]);
1617
					if (isset($user)) {
1618
						$user_props = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]);
1619
						if (isset($user_props[PR_EMS_AB_THUMBNAIL_PHOTO])) {
1620
							$props["props"]['thumbnail_photo'] = "data:image/jpeg;base64," . base64_encode((string) $user_props[PR_EMS_AB_THUMBNAIL_PHOTO]);
1621
						}
1622
					}
1623
				}
1624
				catch (MAPIException) {
1625
					// do nothing
1626
				}
1627
			}
1628
1629
			/*
1630
			 * Check that we have PR_SENT_REPRESENTING_ENTRYID for the item, and also
1631
			 * Check that we have sent_representing_email_address property there in the message,
1632
			 * but for contacts we are not using sent_representing_* properties so we are not
1633
			 * getting it from the message. So basically this will be used for mail items only
1634
			 */
1635
			if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $props["props"]["sent_representing_email_address"])) {
1636
				$props["props"]["sent_representing_username"] = $props["props"]["sent_representing_email_address"];
1637
				$sentRepresentingSearchKey = isset($props['props']['sent_representing_search_key']) ? hex2bin($props['props']['sent_representing_search_key']) : false;
1638
				$props["props"]["sent_representing_email_address"] = $this->getEmailAddress($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $sentRepresentingSearchKey);
1639
			}
1640
1641
			if (isset($smtpprops[PR_SENDER_ENTRYID], $props["props"]["sender_email_address"])) {
1642
				$props["props"]["sender_username"] = $props["props"]["sender_email_address"];
1643
				$senderSearchKey = isset($props['props']['sender_search_key']) ? hex2bin($props['props']['sender_search_key']) : false;
1644
				$props["props"]["sender_email_address"] = $this->getEmailAddress($smtpprops[PR_SENDER_ENTRYID], $senderSearchKey);
1645
			}
1646
1647
			if (isset($smtpprops[PR_RECEIVED_BY_ENTRYID], $props["props"]["received_by_email_address"])) {
1648
				$props["props"]["received_by_username"] = $props["props"]["received_by_email_address"];
1649
				$receivedSearchKey = isset($props['props']['received_by_search_key']) ? hex2bin($props['props']['received_by_search_key']) : false;
1650
				$props["props"]["received_by_email_address"] = $this->getEmailAddress($smtpprops[PR_RECEIVED_BY_ENTRYID], $receivedSearchKey);
1651
			}
1652
1653
			$props['props']['isHTML'] = false;
1654
			$htmlcontent = null;
1655
			if ($loadBody) {
1656
				$body = $this->getMessageBody($message, $html2text);
1657
				$props['props'] = array_merge($props['props'], $body);
1658
				$htmlcontent = $body['html_body'] ?? null;
1659
			}
1660
1661
			// Get reply-to information, otherwise consider the sender to be the reply-to person.
1662
			$props['reply-to'] = ['item' => []];
1663
			$messageprops = mapi_getprops($message, [PR_REPLY_RECIPIENT_ENTRIES]);
1664
			if (isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES])) {
1665
				$props['reply-to']['item'] = $this->readReplyRecipientEntry($messageprops[PR_REPLY_RECIPIENT_ENTRIES]);
1666
			}
1667
			if (!isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES]) || count($props['reply-to']['item']) === 0) {
1668
				if (isset($props['props']['sent_representing_email_address']) && !empty($props['props']['sent_representing_email_address'])) {
1669
					$props['reply-to']['item'][] = [
1670
						'rowid' => 0,
1671
						'props' => [
1672
							'entryid' => $props['props']['sent_representing_entryid'],
1673
							'display_name' => $props['props']['sent_representing_name'],
1674
							'smtp_address' => $props['props']['sent_representing_email_address'],
1675
							'address_type' => $props['props']['sent_representing_address_type'],
1676
							'object_type' => MAPI_MAILUSER,
1677
							'search_key' => $props['props']['sent_representing_search_key'] ?? '',
1678
						],
1679
					];
1680
				}
1681
				elseif (!empty($props['props']['sender_email_address'])) {
1682
					$props['reply-to']['item'][] = [
1683
						'rowid' => 0,
1684
						'props' => [
1685
							'entryid' => $props['props']['sender_entryid'],
1686
							'display_name' => $props['props']['sender_name'],
1687
							'smtp_address' => $props['props']['sender_email_address'],
1688
							'address_type' => $props['props']['sender_address_type'],
1689
							'object_type' => MAPI_MAILUSER,
1690
							'search_key' => $props['props']['sender_search_key'],
1691
						],
1692
					];
1693
				}
1694
			}
1695
1696
			// Get recipients
1697
			$recipients = $GLOBALS["operations"]->getRecipientsInfo($message);
1698
			if (!empty($recipients)) {
1699
				$props["recipients"] = [
1700
					"item" => $recipients,
1701
				];
1702
			}
1703
1704
			// Get attachments
1705
			$attachments = $GLOBALS["operations"]->getAttachmentsInfo($message);
1706
			if (!empty($attachments)) {
1707
				$props["attachments"] = [
1708
					"item" => $attachments,
1709
				];
1710
				$cid_found = false;
1711
				foreach ($attachments as $attachment) {
1712
					if (isset($attachment["props"]["cid"])) {
1713
						$cid_found = true;
1714
					}
1715
				}
1716
				if ($loadBody && $cid_found === true && $htmlcontent !== null) {
1717
					preg_match_all('/src="cid:(.*)"/Uims', $htmlcontent, $matches);
1718
					if (count($matches) > 0) {
1719
						$search = [];
1720
						$replace = [];
1721
						foreach ($matches[1] as $match) {
1722
							$idx = -1;
1723
							foreach ($attachments as $key => $attachment) {
1724
								if (isset($attachment["props"]["cid"]) &&
1725
									strcasecmp($match, $attachment["props"]["cid"]) == 0) {
1726
									$idx = $key;
1727
									$num = $attachment["props"]["attach_num"];
1728
								}
1729
							}
1730
							if ($idx == -1) {
1731
								continue;
1732
							}
1733
							$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...
1734
							if (empty($attach)) {
1735
								continue;
1736
							}
1737
							$attachprop = mapi_getprops($attach, [PR_ATTACH_DATA_BIN, PR_ATTACH_MIME_TAG]);
1738
							if (empty($attachprop) || !isset($attachprop[PR_ATTACH_DATA_BIN])) {
1739
								continue;
1740
							}
1741
							if (!isset($attachprop[PR_ATTACH_MIME_TAG])) {
1742
								$mime_tag = "text/plain";
1743
							}
1744
							else {
1745
								$mime_tag = $attachprop[PR_ATTACH_MIME_TAG];
1746
							}
1747
							$search[] = "src=\"cid:{$match}\"";
1748
							$replace[] = "src=\"data:{$mime_tag};base64," . base64_encode((string) $attachprop[PR_ATTACH_DATA_BIN]) . "\"";
1749
							unset($props["attachments"]["item"][$idx]);
1750
						}
1751
						$props["attachments"]["item"] = array_values($props["attachments"]["item"]);
1752
						$htmlcontent = str_replace($search, $replace, $htmlcontent);
1753
						$props["props"]["html_body"] = $htmlcontent;
1754
					}
1755
				}
1756
			}
1757
1758
			// for distlists, we need to get members data
1759
			if (isset($props["props"]["oneoff_members"], $props["props"]["members"])) {
1760
				// remove non-client props
1761
				unset($props["props"]["members"], $props["props"]["oneoff_members"]);
1762
1763
				// get members
1764
				$members = $GLOBALS["operations"]->getMembersFromDistributionList($store, $message, $properties);
1765
				if (!empty($members)) {
1766
					$props["members"] = [
1767
						"item" => $members,
1768
					];
1769
				}
1770
			}
1771
		}
1772
1773
		return $props;
1774
	}
1775
1776
	/**
1777
	 * Get the email address either from entryid or search key. Function is helpful
1778
	 * to retrieve the email address of already deleted contact which is use as a
1779
	 * recipient in message.
1780
	 *
1781
	 * @param string      $entryId   the entryId of an item/recipient
1782
	 * @param bool|string $searchKey then search key of an item/recipient
1783
	 *
1784
	 * @return string email address if found else return empty string
1785
	 */
1786
	public function getEmailAddress($entryId, $searchKey = false) {
1787
		$emailAddress = $this->getEmailAddressFromEntryID($entryId);
1788
		if (empty($emailAddress) && $searchKey !== false) {
1789
			$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

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

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

2064
					$this->copyAttachments($message, /** @scrutinizer ignore-type */ $attachments, $copyFromMessage, $copyInlineAttachmentsOnly, $attachment_state);
Loading history...
2065
				}
2066
2067
				$this->setAttachments($message, $attachments, $attachment_state);
2068
2069
				$attachment_state->close();
2070
			}
2071
2072
			// Set 'hideattachments' if message has only inline attachments.
2073
			$properties = $GLOBALS['properties']->getMailProperties();
2074
			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

2074
			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...
2075
				mapi_setprops($message, [$properties['hide_attachments'] => true]);
2076
			}
2077
			else {
2078
				mapi_deleteprops($message, [$properties['hide_attachments']]);
2079
			}
2080
2081
			$this->convertInlineImage($message);
2082
			// Save changes
2083
			if ($saveChanges) {
2084
				mapi_savechanges($message);
2085
			}
2086
2087
			// Get the PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID properties of this message
2088
			$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
2089
		}
2090
2091
		return $message;
2092
	}
2093
2094
	/**
2095
	 * Save an appointment item.
2096
	 *
2097
	 * This is basically the same as saving any other type of message with the added complexity that
2098
	 * we support saving exceptions to recurrence here. This means that if the client sends a basedate
2099
	 * in the action, that we will attempt to open an existing exception and change that, and if that
2100
	 * fails, create a new exception with the specified data.
2101
	 *
2102
	 * @param mapistore $store                       MAPI store of the message
0 ignored issues
show
Bug introduced by
The type mapistore 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...
2103
	 * @param string    $entryid                     entryid of the message
2104
	 * @param string    $parententryid               Parent entryid of the message (folder entryid, NOT message entryid)
2105
	 * @param array     $action                      Action array containing XML request
2106
	 * @param string    $actionType                  The action type which triggered this action
2107
	 * @param bool      $directBookingMeetingRequest Indicates if a Meeting Request should use direct booking or not. Defaults to true.
2108
	 *
2109
	 * @return array of PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID properties of modified item
2110
	 */
2111
	public function saveAppointment($store, $entryid, $parententryid, $action, $actionType = 'save', $directBookingMeetingRequest = true) {
2112
		$messageProps = [];
2113
		// It stores the values that is exception allowed or not false -> not allowed
2114
		$isExceptionAllowed = true;
2115
		$delete = $actionType == 'delete';	// Flag for MeetingRequest Class whether to send update or cancel mail.
2116
		$basedate = false;	// Flag for MeetingRequest Class whether to send an exception or not.
2117
		$isReminderTimeAllowed = true;	// Flag to check reminder minutes is in range of the occurrences
2118
		$properties = $GLOBALS['properties']->getAppointmentProperties();
2119
		$send = false;
2120
		$oldProps = [];
2121
		$pasteRecord = false;
2122
2123
		if (isset($action['message_action'], $action['message_action']['send'])) {
2124
			$send = $action['message_action']['send'];
2125
		}
2126
2127
		if (isset($action['message_action'], $action['message_action']['paste'])) {
2128
			$pasteRecord = true;
2129
		}
2130
2131
		if (!empty($action['recipients'])) {
2132
			$recips = $action['recipients'];
2133
		}
2134
		else {
2135
			$recips = false;
2136
		}
2137
2138
		// Set PidLidAppointmentTimeZoneDefinitionStartDisplay and
2139
		// PidLidAppointmentTimeZoneDefinitionEndDisplay so that the allday
2140
		// events are displayed correctly
2141
		if (!empty($action['props']['timezone_iana'])) {
2142
			try {
2143
				$tzdef = mapi_ianatz_to_tzdef($action['props']['timezone_iana']);
2144
			}
2145
			catch (Exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2146
			}
2147
			if ($tzdef !== false) {
2148
				$action['props']['tzdefstart'] = $action['props']['tzdefend'] = bin2hex($tzdef);
2149
				if (isset($action['props']['recurring']) && $action['props']['recurring'] == true) {
2150
					$action['props']['tzdefrecur'] = $action['props']['tzdefstart'];
2151
				}
2152
			}
2153
		}
2154
2155
		if ($store && $parententryid) {
2156
			// @FIXME: check for $action['props'] array
2157
			if (isset($entryid) && $entryid) {
2158
				// Modify existing or add/change exception
2159
				$message = mapi_msgstore_openentry($store, $entryid);
2160
2161
				if ($message) {
2162
					$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...
2163
					// Do not update timezone information if the appointment times haven't changed
2164
					if (!isset($action['props']['commonstart']) &&
2165
						!isset($action['props']['commonend']) &&
2166
						!isset($action['props']['startdate']) &&
2167
						!isset($action['props']['enddate'])
2168
					) {
2169
						unset($action['props']['tzdefstart'], $action['props']['tzdefend'], $action['props']['tzdefrecur']);
2170
					}
2171
					// Check if appointment is an exception to a recurring item
2172
					if (isset($action['basedate']) && $action['basedate'] > 0) {
2173
						// Create recurrence object
2174
						$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...
2175
2176
						$basedate = $action['basedate'];
2177
						$exceptionatt = $recurrence->getExceptionAttachment($basedate);
2178
						if ($exceptionatt) {
2179
							// get properties of existing exception.
2180
							$exceptionattProps = mapi_getprops($exceptionatt, [PR_ATTACH_NUM]);
2181
							$attach_num = $exceptionattProps[PR_ATTACH_NUM];
2182
						}
2183
2184
						if ($delete === true) {
2185
							$isExceptionAllowed = $recurrence->createException([], $basedate, true);
2186
						}
2187
						else {
2188
							$exception_recips = [];
2189
							if (isset($recips['add'])) {
2190
								$savedUnsavedRecipients = [];
2191
								foreach ($recips["add"] as $recip) {
2192
									$savedUnsavedRecipients["unsaved"][] = $recip;
2193
								}
2194
								// convert all local distribution list members to ideal recipient.
2195
								$members = $this->convertLocalDistlistMembersToRecipients($savedUnsavedRecipients);
2196
2197
								$recips['add'] = $members['add'];
2198
								$exception_recips['add'] = $this->createRecipientList($recips['add'], 'add', true, true);
2199
							}
2200
							if (isset($recips['remove'])) {
2201
								$exception_recips['remove'] = $this->createRecipientList($recips['remove'], 'remove');
2202
							}
2203
							if (isset($recips['modify'])) {
2204
								$exception_recips['modify'] = $this->createRecipientList($recips['modify'], 'modify', true, true);
2205
							}
2206
2207
							if (isset($action['props']['reminder_minutes'], $action['props']['startdate'])) {
2208
								$isReminderTimeAllowed = $recurrence->isValidReminderTime($basedate, $action['props']['reminder_minutes'], $action['props']['startdate']);
2209
							}
2210
2211
							// As the reminder minutes occurs before other occurrences don't modify the item.
2212
							if ($isReminderTimeAllowed) {
2213
								if ($recurrence->isException($basedate)) {
2214
									$oldProps = $recurrence->getExceptionProperties($recurrence->getChangeException($basedate));
2215
2216
									$isExceptionAllowed = $recurrence->modifyException(Conversion::mapXML2MAPI($properties, $action['props']), $basedate, $exception_recips);
2217
								}
2218
								else {
2219
									$oldProps[$properties['startdate']] = $recurrence->getOccurrenceStart($basedate);
2220
									$oldProps[$properties['duedate']] = $recurrence->getOccurrenceEnd($basedate);
2221
2222
									$isExceptionAllowed = $recurrence->createException(Conversion::mapXML2MAPI($properties, $action['props']), $basedate, false, $exception_recips);
2223
								}
2224
								mapi_savechanges($message);
2225
							}
2226
						}
2227
					}
2228
					else {
2229
						$oldProps = mapi_getprops($message, [$properties['startdate'], $properties['duedate']]);
2230
						// Modifying non-exception (the series) or normal appointment item
2231
						$message = $GLOBALS['operations']->saveMessage($store, $entryid, $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ?: [], $action['attachments'] ?? [], [], false, false, false, false, false, false, $send);
2232
2233
						$recurrenceProps = mapi_getprops($message, [$properties['startdate_recurring'], $properties['enddate_recurring'], $properties["recurring"]]);
2234
						// Check if the meeting is recurring
2235
						if ($recips && $recurrenceProps[$properties["recurring"]] && isset($recurrenceProps[$properties['startdate_recurring']], $recurrenceProps[$properties['enddate_recurring']])) {
2236
							// If recipient of meeting is modified than that modification needs to be applied
2237
							// to recurring exception as well, if any.
2238
							$exception_recips = [];
2239
							if (isset($recips['add'])) {
2240
								$exception_recips['add'] = $this->createRecipientList($recips['add'], 'add', true, true);
2241
							}
2242
							if (isset($recips['remove'])) {
2243
								$exception_recips['remove'] = $this->createRecipientList($recips['remove'], 'remove');
2244
							}
2245
							if (isset($recips['modify'])) {
2246
								$exception_recips['modify'] = $this->createRecipientList($recips['modify'], 'modify', true, true);
2247
							}
2248
2249
							// Create recurrence object
2250
							$recurrence = new Recurrence($store, $message);
2251
2252
							$recurItems = $recurrence->getItems($recurrenceProps[$properties['startdate_recurring']], $recurrenceProps[$properties['enddate_recurring']]);
2253
2254
							foreach ($recurItems as $recurItem) {
2255
								if (isset($recurItem["exception"])) {
2256
									$recurrence->modifyException([], $recurItem["basedate"], $exception_recips);
2257
								}
2258
							}
2259
						}
2260
2261
						// Only save recurrence if it has been changed by the user (because otherwise we'll reset
2262
						// the exceptions)
2263
						if (isset($action['props']['recurring_reset']) && $action['props']['recurring_reset'] == true) {
2264
							$recur = new Recurrence($store, $message);
2265
2266
							if (isset($action['props']['timezone'])) {
2267
								$tzprops = ['timezone', 'timezonedst', 'dststartmonth', 'dststartweek', 'dststartday', 'dststarthour', 'dstendmonth', 'dstendweek', 'dstendday', 'dstendhour'];
2268
2269
								// Get timezone info
2270
								$tz = [];
2271
								foreach ($tzprops as $tzprop) {
2272
									$tz[$tzprop] = $action['props'][$tzprop];
2273
								}
2274
							}
2275
2276
							/**
2277
							 * Check if any recurrence property is missing, if yes then prepare
2278
							 * the set of properties required to update the recurrence. For more info
2279
							 * please refer detailed description of parseRecurrence function of
2280
							 * BaseRecurrence class".
2281
							 *
2282
							 * Note : this is a special case of changing the time of
2283
							 * recurrence meeting from scheduling tab.
2284
							 */
2285
							$recurrence = $recur->getRecurrence();
2286
							if (isset($recurrence)) {
2287
								unset($recurrence['changed_occurrences'], $recurrence['deleted_occurrences']);
2288
2289
								foreach ($recurrence as $key => $value) {
2290
									if (!isset($action['props'][$key])) {
2291
										$action['props'][$key] = $value;
2292
									}
2293
								}
2294
							}
2295
							// Act like the 'props' are the recurrence pattern; it has more information but that
2296
							// is ignored
2297
							$recur->setRecurrence($tz ?? false, $action['props']);
2298
						}
2299
					}
2300
2301
					// Get the properties of the main object of which the exception was changed, and post
2302
					// that message as being modified. This will cause the update function to update all
2303
					// occurrences of the item to the client
2304
					$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
2305
2306
					// if opened appointment is exception then it will add
2307
					// the attach_num and basedate in messageProps.
2308
					if (isset($attach_num)) {
2309
						$messageProps[PR_ATTACH_NUM] = [$attach_num];
2310
						$messageProps[$properties["basedate"]] = $action['basedate'];
2311
					}
2312
				}
2313
			}
2314
			else {
2315
				$tz = null;
2316
				$hasRecipient = false;
2317
				$copyAttachments = false;
2318
				$sourceRecord = false;
2319
				if (isset($action['message_action'], $action['message_action']['source_entryid'])) {
2320
					$sourceEntryId = $action['message_action']['source_entryid'];
2321
					$sourceStoreEntryId = $action['message_action']['source_store_entryid'];
2322
2323
					$sourceStore = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $sourceStoreEntryId));
2324
					$sourceRecord = mapi_msgstore_openentry($sourceStore, hex2bin($sourceEntryId));
2325
					if ($pasteRecord) {
2326
						$sourceRecordProps = mapi_getprops($sourceRecord, [$properties["meeting"], $properties["responsestatus"]]);
2327
						// Don't copy recipient if source record is received message.
2328
						if ($sourceRecordProps[$properties["meeting"]] === olMeeting &&
2329
							$sourceRecordProps[$properties["meeting"]] === olResponseOrganized) {
2330
							$table = mapi_message_getrecipienttable($sourceRecord);
2331
							$hasRecipient = mapi_table_getrowcount($table) > 0;
2332
						}
2333
					}
2334
					else {
2335
						$copyAttachments = true;
2336
						// Set sender of new Appointment.
2337
						$this->setSenderAddress($store, $action);
2338
					}
2339
				}
2340
				else {
2341
					// Set sender of new Appointment.
2342
					$this->setSenderAddress($store, $action);
2343
				}
2344
2345
				$message = $this->saveMessage($store, $entryid, $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ?: [], $action['attachments'] ?? [], [], $sourceRecord, $copyAttachments, $hasRecipient, false, false, false, $send);
0 ignored issues
show
Bug introduced by
$parententryid of type string is incompatible with the type binary expected by parameter $parententryid 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

2345
				$message = $this->saveMessage($store, $entryid, /** @scrutinizer ignore-type */ $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ?: [], $action['attachments'] ?? [], [], $sourceRecord, $copyAttachments, $hasRecipient, false, false, false, $send);
Loading history...
2346
2347
				if (isset($action['props']['timezone'])) {
2348
					$tzprops = ['timezone', 'timezonedst', 'dststartmonth', 'dststartweek', 'dststartday', 'dststarthour', 'dstendmonth', 'dstendweek', 'dstendday', 'dstendhour'];
2349
2350
					// Get timezone info
2351
					$tz = [];
2352
					foreach ($tzprops as $tzprop) {
2353
						$tz[$tzprop] = $action['props'][$tzprop];
2354
					}
2355
				}
2356
2357
				// Set recurrence
2358
				if (isset($action['props']['recurring']) && $action['props']['recurring'] == true) {
2359
					$recur = new Recurrence($store, $message);
2360
					$recur->setRecurrence($tz, $action['props']);
2361
				}
2362
			}
2363
		}
2364
2365
		$result = false;
2366
		// Check to see if it should be sent as a meeting request
2367
		if ($send === true && $isExceptionAllowed) {
2368
			$savedUnsavedRecipients = [];
2369
			$remove = [];
2370
			if (!isset($action['basedate'])) {
2371
				// retrieve recipients from saved message
2372
				$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...
2373
				foreach ($savedRecipients as $recipient) {
2374
					$savedUnsavedRecipients["saved"][] = $recipient['props'];
2375
				}
2376
2377
				// retrieve removed recipients.
2378
				if (!empty($recips) && !empty($recips["remove"])) {
2379
					$remove = $recips["remove"];
2380
				}
2381
2382
				// convert all local distribution list members to ideal recipient.
2383
				$members = $this->convertLocalDistlistMembersToRecipients($savedUnsavedRecipients, $remove);
2384
2385
				// Before sending meeting request we set the recipient to message
2386
				// which are converted from local distribution list members.
2387
				$this->setRecipients($message, $members);
2388
			}
2389
2390
			$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...
2391
2392
			/*
2393
			 * check write access for delegate, make sure that we will not send meeting request
2394
			 * if we don't have permission to save calendar item
2395
			 */
2396
			if ($request->checkFolderWriteAccess($parententryid, $store) !== true) {
2397
				// Throw an exception that we don't have write permissions on calendar folder,
2398
				// error message will be filled by module
2399
				throw new MAPIException(_("Insufficient permissions"), MAPI_E_NO_ACCESS);
2400
			}
2401
2402
			$request->updateMeetingRequest($basedate);
2403
2404
			$isRecurrenceChanged = isset($action['props']['recurring_reset']) && $action['props']['recurring_reset'] == true;
2405
			$request->checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged);
2406
2407
			// Update extra body information
2408
			if (isset($action['message_action']['meetingTimeInfo']) && !empty($action['message_action']['meetingTimeInfo'])) {
2409
				$tag = $action['message_action']['mti_html'] ? PR_HTML : PR_BODY;
2410
				// Append body if the request action requires this
2411
				if (isset($action['message_action'], $action['message_action']['append_body'])) {
2412
					$bodyProps = mapi_getprops($message, [$tag]);
2413
					if (isset($bodyProps[$tag]) || propIsError($tag, $bodyProps) == MAPI_E_NOT_ENOUGH_MEMORY)
2414
						$bodyProps[$tag] = streamProperty($message, $tag);
2415
					if (isset($action['message_action']['meetingTimeInfo'], $bodyProps[$tag]))
2416
						$action['message_action']['meetingTimeInfo'] .= $bodyProps[$tag];
2417
				}
2418
2419
				$request->setMeetingTimeInfo($action['message_action']['meetingTimeInfo'],
2420
					$action['message_action']['mti_html'] ?? false); /* cf. mapi-header-php */
2421
				unset($action['message_action']['meetingTimeInfo']);
2422
			}
2423
2424
			$modifiedRecipients = false;
2425
			$deletedRecipients = false;
2426
			if ($recips) {
2427
				if (isset($action['message_action']['send_update']) && $action['message_action']['send_update'] == 'modified') {
2428
					if (isset($recips['add']) && !empty($recips['add'])) {
2429
						$modifiedRecipients = $modifiedRecipients ?: [];
0 ignored issues
show
introduced by
The condition $modifiedRecipients is always false.
Loading history...
2430
						$modifiedRecipients = array_merge($modifiedRecipients, $this->createRecipientList($recips['add'], 'add'));
2431
					}
2432
2433
					if (isset($recips['modify']) && !empty($recips['modify'])) {
2434
						$modifiedRecipients = $modifiedRecipients ?: [];
2435
						$modifiedRecipients = array_merge($modifiedRecipients, $this->createRecipientList($recips['modify'], 'modify'));
2436
					}
2437
				}
2438
2439
				// lastUpdateCounter is represent that how many times this message is updated(send)
2440
				$lastUpdateCounter = $request->getLastUpdateCounter();
2441
				if ($lastUpdateCounter !== false && $lastUpdateCounter > 0) {
2442
					if (isset($recips['remove']) && !empty($recips['remove'])) {
2443
						$deletedRecipients = $deletedRecipients ?: [];
0 ignored issues
show
introduced by
The condition $deletedRecipients is always false.
Loading history...
2444
						$deletedRecipients = array_merge($deletedRecipients, $this->createRecipientList($recips['remove'], 'remove'));
2445
						if (isset($action['message_action']['send_update']) && $action['message_action']['send_update'] != 'all') {
2446
							$modifiedRecipients = $modifiedRecipients ?: [];
2447
						}
2448
					}
2449
				}
2450
			}
2451
2452
			$sendMeetingRequestResult = $request->sendMeetingRequest($delete, false, $basedate, $modifiedRecipients, $deletedRecipients);
2453
2454
			$this->addRecipientsToRecipientHistory($this->getRecipientsInfo($message));
2455
2456
			if ($sendMeetingRequestResult === true) {
2457
				$this->parseDistListAndAddToRecipientHistory($savedUnsavedRecipients, $remove);
2458
2459
				mapi_savechanges($message);
2460
2461
				// We want to sent the 'request_sent' property, to have it properly
2462
				// deserialized we must also send some type properties.
2463
				$props = mapi_getprops($message, [PR_MESSAGE_CLASS, PR_OBJECT_TYPE]);
2464
				$messageProps[PR_MESSAGE_CLASS] = $props[PR_MESSAGE_CLASS];
2465
				$messageProps[PR_OBJECT_TYPE] = $props[PR_OBJECT_TYPE];
2466
2467
				// Indicate that the message was correctly sent
2468
				$messageProps[$properties['request_sent']] = true;
2469
2470
				// Return message properties that can be sent to the bus to notify changes
2471
				$result = $messageProps;
2472
			}
2473
			else {
2474
				$sendMeetingRequestResult[PR_ENTRYID] = $messageProps[PR_ENTRYID];
2475
				$sendMeetingRequestResult[PR_PARENT_ENTRYID] = $messageProps[PR_PARENT_ENTRYID];
2476
				$sendMeetingRequestResult[PR_STORE_ENTRYID] = $messageProps[PR_STORE_ENTRYID];
2477
				$result = $sendMeetingRequestResult;
2478
			}
2479
		}
2480
		else {
2481
			mapi_savechanges($message);
2482
2483
			if (isset($isExceptionAllowed)) {
2484
				if ($isExceptionAllowed === false) {
2485
					$messageProps['isexceptionallowed'] = false;
2486
				}
2487
			}
2488
2489
			if (isset($isReminderTimeAllowed)) {
2490
				if ($isReminderTimeAllowed === false) {
2491
					$messageProps['remindertimeerror'] = false;
2492
				}
2493
			}
2494
			// Return message properties that can be sent to the bus to notify changes
2495
			$result = $messageProps;
2496
		}
2497
2498
		return $result;
2499
	}
2500
2501
	/**
2502
	 * Function is used to identify the local distribution list from all recipients and
2503
	 * convert all local distribution list members to recipients.
2504
	 *
2505
	 * @param array $recipients array of recipients either saved or add
2506
	 * @param array $remove     array of recipients that was removed
2507
	 *
2508
	 * @return array $newRecipients a list of recipients as XML array structure
2509
	 */
2510
	public function convertLocalDistlistMembersToRecipients($recipients, $remove = []) {
2511
		$addRecipients = [];
2512
		$removeRecipients = [];
2513
2514
		foreach ($recipients as $key => $recipient) {
2515
			foreach ($recipient as $recipientItem) {
2516
				$recipientEntryid = $recipientItem["entryid"];
2517
				$isExistInRemove = $this->isExistInRemove($recipientEntryid, $remove);
2518
2519
				/*
2520
				 * Condition is only gets true, if recipient is distribution list and it`s belongs
2521
				 * to shared/internal(belongs in contact folder) folder.
2522
				 */
2523
				if ($recipientItem['object_type'] == MAPI_DISTLIST && $recipientItem['address_type'] != 'EX') {
2524
					if (!$isExistInRemove) {
2525
						$recipientItems = $GLOBALS["operations"]->expandDistList($recipientEntryid, true);
2526
						foreach ($recipientItems as $recipient) {
0 ignored issues
show
Comprehensibility Bug introduced by
$recipient is overwriting a variable from outer foreach loop.
Loading history...
2527
							// set recipient type of each members as per the distribution list recipient type
2528
							$recipient['recipient_type'] = $recipientItem['recipient_type'];
2529
							array_push($addRecipients, $recipient);
2530
						}
2531
2532
						if ($key === "saved") {
2533
							array_push($removeRecipients, $recipientItem);
2534
						}
2535
					}
2536
				}
2537
				else {
2538
					/*
2539
					 * Only Add those recipients which are not saved earlier in message and
2540
					 * not present in remove array.
2541
					 */
2542
					if (!$isExistInRemove && $key === "unsaved") {
2543
						array_push($addRecipients, $recipientItem);
2544
					}
2545
				}
2546
			}
2547
		}
2548
		$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...
2549
		$newRecipients["remove"] = $removeRecipients;
2550
2551
		return $newRecipients;
2552
	}
2553
2554
	/**
2555
	 * Function used to identify given recipient was already available in remove array of recipients array or not.
2556
	 * which was sent from client side. If it is found the entry in the $remove array will be deleted, since
2557
	 * we do not want to find it again for other recipients. (if a user removes and adds an user again it
2558
	 * should be added once!).
2559
	 *
2560
	 * @param string $recipientEntryid recipient entryid
2561
	 * @param array  $remove           removed recipients array
2562
	 *
2563
	 * @return bool return false if recipient not exist in remove array else return true
2564
	 */
2565
	public function isExistInRemove($recipientEntryid, &$remove) {
2566
		if (!empty($remove)) {
2567
			foreach ($remove as $index => $removeItem) {
2568
				if (array_search($recipientEntryid, $removeItem, true)) {
2569
					unset($remove[$index]);
2570
2571
					return true;
2572
				}
2573
			}
2574
		}
2575
2576
		return false;
2577
	}
2578
2579
	/**
2580
	 * Function is used to identify the local distribution list from all recipients and
2581
	 * Add distribution list to recipient history.
2582
	 *
2583
	 * @param array $savedUnsavedRecipients array of recipients either saved or add
2584
	 * @param array $remove                 array of recipients that was removed
2585
	 */
2586
	public function parseDistListAndAddToRecipientHistory($savedUnsavedRecipients, $remove) {
2587
		$distLists = [];
2588
		foreach ($savedUnsavedRecipients as $key => $recipient) {
2589
			foreach ($recipient as $recipientItem) {
2590
				if ($recipientItem['address_type'] == 'MAPIPDL') {
2591
					$isExistInRemove = $this->isExistInRemove($recipientItem['entryid'], $remove);
2592
					if (!$isExistInRemove) {
2593
						array_push($distLists, ["props" => $recipientItem]);
2594
					}
2595
				}
2596
			}
2597
		}
2598
2599
		$this->addRecipientsToRecipientHistory($distLists);
2600
	}
2601
2602
	/**
2603
	 * Set sent_representing_email_address property of Appointment.
2604
	 *
2605
	 * Before saving any new appointment, sent_representing_email_address property of appointment
2606
	 * should contain email_address of user, who is the owner of store(in which the appointment
2607
	 * is created).
2608
	 *
2609
	 * @param mapistore $store  MAPI store of the message
2610
	 * @param array     $action reference to action array containing XML request
2611
	 */
2612
	public function setSenderAddress($store, &$action) {
2613
		$storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]);
2614
		// check for public store
2615
		if (!isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])) {
2616
			$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
2617
			$storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]);
2618
		}
2619
		$mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $storeProps[PR_MAILBOX_OWNER_ENTRYID]);
2620
		if ($mailuser) {
2621
			$userprops = mapi_getprops($mailuser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]);
2622
			$action["props"]["sent_representing_entryid"] = bin2hex((string) $storeProps[PR_MAILBOX_OWNER_ENTRYID]);
2623
			// we do conversion here, because before passing props to saveMessage() props are converted from utf8-to-w
2624
			$action["props"]["sent_representing_name"] = $userprops[PR_DISPLAY_NAME];
2625
			$action["props"]["sent_representing_address_type"] = $userprops[PR_ADDRTYPE];
2626
			if ($userprops[PR_ADDRTYPE] == 'SMTP') {
2627
				$emailAddress = $userprops[PR_SMTP_ADDRESS];
2628
			}
2629
			else {
2630
				$emailAddress = $userprops[PR_EMAIL_ADDRESS];
2631
			}
2632
			$action["props"]["sent_representing_email_address"] = $emailAddress;
2633
			$action["props"]["sent_representing_search_key"] = bin2hex(strtoupper($userprops[PR_ADDRTYPE] . ':' . $emailAddress)) . '00';
2634
		}
2635
	}
2636
2637
	/**
2638
	 * Submit a message for sending.
2639
	 *
2640
	 * This function is an extension of the saveMessage() function, with the extra functionality
2641
	 * that the item is actually sent and queued for moving to 'Sent Items'. Also, the e-mail addresses
2642
	 * used in the message are processed for later auto-suggestion.
2643
	 *
2644
	 * @see Operations::saveMessage() for more information on the parameters, which are identical.
2645
	 *
2646
	 * @param mapistore   $store                     MAPI Message Store Object
2647
	 * @param binary      $entryid                   Entryid of the message
2648
	 * @param array       $props                     The properties to be saved
2649
	 * @param array       $messageProps              reference to an array which will be filled with PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID
2650
	 * @param array       $recipients                XML array structure of recipients for the recipient table
2651
	 * @param array       $attachments               array of attachments consisting unique ID of attachments for this message
2652
	 * @param MAPIMessage $copyFromMessage           resource of the message from which we should
2653
	 *                                               copy attachments and/or recipients to the current message
2654
	 * @param bool        $copyAttachments           if set we copy all attachments from the $copyFromMessage
2655
	 * @param bool        $copyRecipients            if set we copy all recipients from the $copyFromMessage
2656
	 * @param bool        $copyInlineAttachmentsOnly if true then copy only inline attachments
2657
	 * @param bool        $isPlainText               if true then message body will be generated using PR_BODY otherwise PR_HTML will be used in saveMessage() function
2658
	 *
2659
	 * @return bool false if action succeeded, anything else indicates an error (e.g. a string)
2660
	 */
2661
	public function submitMessage($store, $entryid, $props, &$messageProps, $recipients = [], $attachments = [], $copyFromMessage = false, $copyAttachments = false, $copyRecipients = false, $copyInlineAttachmentsOnly = false, $isPlainText = false) {
2662
		$message = false;
2663
		$origStore = $store;
2664
		$reprMessage = false;
2665
		$delegateSentItemsStyle = $GLOBALS['settings']->get('zarafa/v1/contexts/mail/delegate_sent_items_style');
2666
		$saveBoth = strcasecmp((string) $delegateSentItemsStyle, 'both') == 0;
2667
		$saveRepresentee = strcasecmp((string) $delegateSentItemsStyle, 'representee') == 0;
2668
		$sendingAsDelegate = false;
2669
2670
		// Get the outbox and sent mail entryid, ignore the given $store, use the default store for submitting messages
2671
		$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
2672
		$storeprops = mapi_getprops($store, [PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_ENTRYID]);
2673
		$origStoreprops = mapi_getprops($origStore, [PR_ENTRYID, PR_IPM_SENTMAIL_ENTRYID]);
2674
2675
		if (!isset($storeprops[PR_IPM_OUTBOX_ENTRYID])) {
2676
			return false;
2677
		}
2678
		if (isset($storeprops[PR_IPM_SENTMAIL_ENTRYID])) {
2679
			$props[PR_SENTMAIL_ENTRYID] = $storeprops[PR_IPM_SENTMAIL_ENTRYID];
2680
		}
2681
2682
		// Check if replying then set PR_INTERNET_REFERENCES and PR_IN_REPLY_TO_ID properties in props.
2683
		// flag is probably used wrong here but the same flag indicates if this is reply or replyall
2684
		if ($copyInlineAttachmentsOnly) {
2685
			$origMsgProps = mapi_getprops($copyFromMessage, [PR_INTERNET_MESSAGE_ID, PR_INTERNET_REFERENCES]);
2686
			if (isset($origMsgProps[PR_INTERNET_MESSAGE_ID])) {
2687
				// The references header should indicate the message-id of the original
2688
				// header plus any of the references which were set on the previous mail.
2689
				$props[PR_INTERNET_REFERENCES] = $origMsgProps[PR_INTERNET_MESSAGE_ID];
2690
				if (isset($origMsgProps[PR_INTERNET_REFERENCES])) {
2691
					$props[PR_INTERNET_REFERENCES] = $origMsgProps[PR_INTERNET_REFERENCES] . ' ' . $props[PR_INTERNET_REFERENCES];
2692
				}
2693
				$props[PR_IN_REPLY_TO_ID] = $origMsgProps[PR_INTERNET_MESSAGE_ID];
2694
			}
2695
		}
2696
2697
		if (!$GLOBALS["entryid"]->compareEntryIds(bin2hex((string) $origStoreprops[PR_ENTRYID]), bin2hex((string) $storeprops[PR_ENTRYID]))) {
2698
			// set properties for "on behalf of" mails
2699
			$origStoreProps = mapi_getprops($origStore, [PR_MAILBOX_OWNER_ENTRYID, PR_MDB_PROVIDER, PR_IPM_SENTMAIL_ENTRYID]);
2700
2701
			// set PR_SENDER_* properties, which contains currently logged user's data
2702
			$ab = $GLOBALS['mapisession']->getAddressbook();
2703
			$abitem = mapi_ab_openentry($ab, $GLOBALS["mapisession"]->getUserEntryID());
2704
			$abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2705
2706
			$props[PR_SENDER_ENTRYID] = $GLOBALS["mapisession"]->getUserEntryID();
2707
			$props[PR_SENDER_NAME] = $abitemprops[PR_DISPLAY_NAME];
2708
			$props[PR_SENDER_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS];
2709
			$props[PR_SENDER_ADDRTYPE] = "EX";
2710
			$props[PR_SENDER_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY];
2711
2712
			// Use the PR_SENT_REPRESENTING_* properties sent by the client or set to the currently logged user's data
2713
			$props[PR_SENT_REPRESENTING_ENTRYID] ??= $props[PR_SENDER_ENTRYID];
2714
			$props[PR_SENT_REPRESENTING_NAME] ??= $props[PR_SENDER_NAME];
2715
			$props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] ??= $props[PR_SENDER_EMAIL_ADDRESS];
2716
			$props[PR_SENT_REPRESENTING_ADDRTYPE] ??= $props[PR_SENDER_ADDRTYPE];
2717
			$props[PR_SENT_REPRESENTING_SEARCH_KEY] ??= $props[PR_SENDER_SEARCH_KEY];
2718
2719
			/**
2720
			 * we are sending mail from delegate's account, so we can't use delegate's outbox and sent items folder
2721
			 * so we have to copy the mail from delegate's store to logged user's store and in outbox folder and then
2722
			 * we can send mail from logged user's outbox folder.
2723
			 *
2724
			 * if we set $entryid to false before passing it to saveMessage function then it will assume
2725
			 * that item doesn't exist and it will create a new item (in outbox of logged in user)
2726
			 */
2727
			if ($entryid) {
0 ignored issues
show
introduced by
$entryid is of type binary, thus it always evaluated to true.
Loading history...
2728
				$oldEntryId = $entryid;
2729
				$entryid = false;
2730
2731
				// if we are sending mail from drafts folder then we have to copy
2732
				// its recipients and attachments also. $origStore and $oldEntryId points to mail
2733
				// saved in delegators draft folder
2734
				if ($copyFromMessage === false) {
2735
					$copyFromMessage = mapi_msgstore_openentry($origStore, $oldEntryId);
2736
					$copyRecipients = true;
2737
2738
					// Decode smime signed messages on this message
2739
					parse_smime($origStore, $copyFromMessage);
2740
				}
2741
			}
2742
2743
			if ($copyFromMessage) {
2744
				// Get properties of original message, to copy recipients and attachments in new message
2745
				$copyMessageProps = mapi_getprops($copyFromMessage);
2746
				$oldParentEntryId = $copyMessageProps[PR_PARENT_ENTRYID];
2747
2748
				// unset id properties before merging the props, so we will be creating new item instead of sending same item
2749
				unset($copyMessageProps[PR_ENTRYID], $copyMessageProps[PR_PARENT_ENTRYID], $copyMessageProps[PR_STORE_ENTRYID], $copyMessageProps[PR_SEARCH_KEY]);
2750
2751
				// grommunio generates PR_HTML on the fly, but it's necessary to unset it
2752
				// if the original message didn't have PR_HTML property.
2753
				if (!isset($props[PR_HTML]) && isset($copyMessageProps[PR_HTML])) {
2754
					unset($copyMessageProps[PR_HTML]);
2755
				}
2756
				/* New EMAIL_ADDRESSes were set (various cases above), kill off old SMTP_ADDRESS. */
2757
				unset($copyMessageProps[PR_SENDER_SMTP_ADDRESS], $copyMessageProps[PR_SENT_REPRESENTING_SMTP_ADDRESS]);
2758
2759
				// Merge original message props with props sent by client
2760
				$props = $props + $copyMessageProps;
2761
			}
2762
2763
			// Save the new message properties
2764
			$message = $this->saveMessage($store, $entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], $copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText);
0 ignored issues
show
Bug introduced by
$entryid of type false is incompatible with the type binary expected by parameter $entryid 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

2764
			$message = $this->saveMessage($store, /** @scrutinizer ignore-type */ $entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], $copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText);
Loading history...
2765
2766
			// FIXME: currently message is deleted from original store and new message is created
2767
			// in current user's store, but message should be moved
2768
2769
			// delete message from it's original location
2770
			if (!empty($oldEntryId) && !empty($oldParentEntryId)) {
2771
				$folder = mapi_msgstore_openentry($origStore, $oldParentEntryId);
2772
				mapi_folder_deletemessages($folder, [$oldEntryId], DELETE_HARD_DELETE);
2773
			}
2774
			if ($saveBoth || $saveRepresentee) {
2775
				if ($origStoreProps[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) {
2776
					$userEntryid = $GLOBALS["mapisession"]->getStoreEntryIdOfUser(strtolower((string) $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]));
2777
					$origStore = $GLOBALS["mapisession"]->openMessageStore($userEntryid);
2778
					$origStoreprops = mapi_getprops($origStore, [PR_IPM_SENTMAIL_ENTRYID]);
2779
				}
2780
				$destfolder = mapi_msgstore_openentry($origStore, $origStoreprops[PR_IPM_SENTMAIL_ENTRYID]);
2781
				$reprMessage = mapi_folder_createmessage($destfolder);
2782
				mapi_copyto($message, [], [], $reprMessage, 0);
2783
			}
2784
		}
2785
		else {
2786
			// 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.
2787
			$outbox = mapi_msgstore_openentry($store, $storeprops[PR_IPM_OUTBOX_ENTRYID]);
2788
2789
			// Open the old and the new message
2790
			$newmessage = mapi_folder_createmessage($outbox);
2791
			$oldEntryId = $entryid;
2792
2793
			// Remember the new entryid
2794
			$newprops = mapi_getprops($newmessage, [PR_ENTRYID]);
2795
			$entryid = $newprops[PR_ENTRYID];
2796
2797
			if (!empty($oldEntryId)) {
2798
				$message = mapi_msgstore_openentry($store, $oldEntryId);
2799
				// Copy the entire message
2800
				mapi_copyto($message, [], [], $newmessage);
2801
				$tmpProps = mapi_getprops($message);
2802
				$oldParentEntryId = $tmpProps[PR_PARENT_ENTRYID];
2803
				if ($storeprops[PR_IPM_OUTBOX_ENTRYID] == $oldParentEntryId) {
2804
					$folder = $outbox;
2805
				}
2806
				else {
2807
					$folder = mapi_msgstore_openentry($store, $oldParentEntryId);
2808
				}
2809
2810
				// Copy message_class for S/MIME plugin
2811
				if (isset($tmpProps[PR_MESSAGE_CLASS])) {
2812
					$props[PR_MESSAGE_CLASS] = $tmpProps[PR_MESSAGE_CLASS];
2813
				}
2814
				// Delete the old message
2815
				mapi_folder_deletemessages($folder, [$oldEntryId]);
2816
			}
2817
2818
			// save changes to new message created in outbox
2819
			mapi_savechanges($newmessage);
2820
2821
			$reprProps = mapi_getprops($newmessage, [PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ENTRYID]);
2822
			if (isset($reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], $reprProps[PR_SENDER_EMAIL_ADDRESS], $reprProps[PR_SENT_REPRESENTING_ENTRYID]) &&
2823
				strcasecmp((string) $reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], (string) $reprProps[PR_SENDER_EMAIL_ADDRESS]) != 0) {
2824
				$ab = $GLOBALS['mapisession']->getAddressbook();
2825
				$abitem = mapi_ab_openentry($ab, $reprProps[PR_SENT_REPRESENTING_ENTRYID]);
2826
				$abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2827
2828
				$props[PR_SENT_REPRESENTING_NAME] = $abitemprops[PR_DISPLAY_NAME];
2829
				$props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS];
2830
				$props[PR_SENT_REPRESENTING_ADDRTYPE] = "EX";
2831
				$props[PR_SENT_REPRESENTING_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY];
2832
				$sendingAsDelegate = true;
2833
			}
2834
			// Save the new message properties
2835
			$message = $this->saveMessage($store, $entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], $copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText);
0 ignored issues
show
Bug introduced by
It seems like $copyFromMessage can also be of type false; however, parameter $copyFromMessage of Operations::saveMessage() does only seem to accept MAPIMessage, 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

2835
			$message = $this->saveMessage($store, $entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], /** @scrutinizer ignore-type */ $copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText);
Loading history...
2836
			// Sending as delegate from drafts folder
2837
			if ($sendingAsDelegate && ($saveBoth || $saveRepresentee)) {
2838
				$userEntryid = $GLOBALS["mapisession"]->getStoreEntryIdOfUser(strtolower((string) $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]));
2839
				$origStore = $GLOBALS["mapisession"]->openMessageStore($userEntryid);
2840
				if ($origStore) {
2841
					$origStoreprops = mapi_getprops($origStore, [PR_ENTRYID, PR_IPM_SENTMAIL_ENTRYID]);
2842
					$destfolder = mapi_msgstore_openentry($origStore, $origStoreprops[PR_IPM_SENTMAIL_ENTRYID]);
2843
					$reprMessage = mapi_folder_createmessage($destfolder);
2844
					mapi_copyto($message, [], [], $reprMessage, 0);
2845
				}
2846
			}
2847
		}
2848
2849
		if (!$message) {
0 ignored issues
show
introduced by
$message is of type mapimessage, thus it always evaluated to true.
Loading history...
2850
			return false;
2851
		}
2852
		// Allowing to hook in just before the data sent away to be sent to the client
2853
		$GLOBALS['PluginManager']->triggerHook('server.core.operations.submitmessage', [
2854
			'moduleObject' => $this,
2855
			'store' => $store,
2856
			'entryid' => $entryid,
2857
			'message' => &$message,
2858
		]);
2859
2860
		// Submit the message (send)
2861
		try {
2862
			mapi_message_submitmessage($message);
2863
		}
2864
		catch (MAPIException $e) {
2865
			$username = $GLOBALS["mapisession"]->getUserName();
2866
			$errorName = get_mapi_error_name($e->getCode());
2867
			error_log(sprintf(
2868
				'Unable to submit message for %s, MAPI error: %s. ' .
2869
				'SMTP server may be down or it refused the message or the message' .
2870
				' is too large to submit or user does not have the permission ...',
2871
				$username,
2872
				$errorName
2873
			));
2874
2875
			return $errorName;
2876
		}
2877
2878
		$tmp_props = mapi_getprops($message, [PR_PARENT_ENTRYID, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME, PR_SEARCH_KEY, PR_MESSAGE_FLAGS]);
2879
		$messageProps[PR_PARENT_ENTRYID] = $tmp_props[PR_PARENT_ENTRYID];
2880
		if ($reprMessage !== false) {
2881
			mapi_setprops($reprMessage, [
2882
				PR_CLIENT_SUBMIT_TIME => $tmp_props[PR_CLIENT_SUBMIT_TIME] ?? time(),
2883
				PR_MESSAGE_DELIVERY_TIME => $tmp_props[PR_MESSAGE_DELIVERY_TIME] ?? time(),
2884
				PR_MESSAGE_FLAGS => $tmp_props[PR_MESSAGE_FLAGS] | MSGFLAG_READ,
2885
			]);
2886
			mapi_savechanges($reprMessage);
2887
			if ($saveRepresentee) {
2888
				// delete the message in the delegate's Sent Items folder
2889
				$sentFolder = mapi_msgstore_openentry($store, $storeprops[PR_IPM_SENTMAIL_ENTRYID]);
2890
				$sentTable = mapi_folder_getcontentstable($sentFolder, MAPI_DEFERRED_ERRORS);
2891
				$restriction = [RES_PROPERTY, [
2892
					RELOP => RELOP_EQ,
2893
					ULPROPTAG => PR_SEARCH_KEY,
2894
					VALUE => $tmp_props[PR_SEARCH_KEY],
2895
				]];
2896
				mapi_table_restrict($sentTable, $restriction);
2897
				$sentMessageProps = mapi_table_queryallrows($sentTable, [PR_ENTRYID, PR_SEARCH_KEY]);
2898
				if (mapi_table_getrowcount($sentTable) == 1) {
2899
					mapi_folder_deletemessages($sentFolder, [$sentMessageProps[0][PR_ENTRYID]], DELETE_HARD_DELETE);
2900
				}
2901
				else {
2902
					error_log(sprintf(
2903
						"Found multiple entries in Sent Items with the same PR_SEARCH_KEY (%d)." .
2904
						" Impossible to delete email from the delegate's Sent Items folder.",
2905
						mapi_table_getrowcount($sentTable)
2906
					));
2907
				}
2908
			}
2909
		}
2910
2911
		$this->addRecipientsToRecipientHistory($this->getRecipientsInfo($message));
2912
2913
		return false;
2914
	}
2915
2916
	/**
2917
	 * Delete messages.
2918
	 *
2919
	 * This function does what is needed when a user presses 'delete' on a MAPI message. This means that:
2920
	 *
2921
	 * - Items in the own store are moved to the wastebasket
2922
	 * - Items in the wastebasket are deleted
2923
	 * - Items in other users stores are moved to our own wastebasket
2924
	 * - Items in the public store are deleted
2925
	 *
2926
	 * @param mapistore $store         MAPI Message Store Object
2927
	 * @param string    $parententryid parent entryid of the messages to be deleted
2928
	 * @param array     $entryids      a list of entryids which will be deleted
2929
	 * @param bool      $softDelete    flag for soft-deleteing (when user presses Shift+Del)
2930
	 * @param bool      $unread        message is unread
2931
	 *
2932
	 * @return bool true if action succeeded, false if not
2933
	 */
2934
	public function deleteMessages($store, $parententryid, $entryids, $softDelete = false, $unread = false) {
2935
		$result = false;
2936
		if (!is_array($entryids)) {
0 ignored issues
show
introduced by
The condition is_array($entryids) is always true.
Loading history...
2937
			$entryids = [$entryids];
2938
		}
2939
2940
		$folder = mapi_msgstore_openentry($store, $parententryid);
2941
		$flags = $unread ? GX_DELMSG_NOTIFY_UNREAD : 0;
2942
		$msgprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_OUTBOX_ENTRYID]);
2943
2944
		switch ($msgprops[PR_MDB_PROVIDER]) {
2945
			case ZARAFA_STORE_DELEGATE_GUID:
2946
				$softDelete = $softDelete || defined('ENABLE_DEFAULT_SOFT_DELETE') ? ENABLE_DEFAULT_SOFT_DELETE : false;
2947
				// with a store from an other user we need our own waste basket...
2948
				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...
2949
					// except when it is the waste basket itself
2950
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2951
					break;
2952
				}
2953
				$defaultstore = $GLOBALS["mapisession"]->getDefaultMessageStore();
2954
				$msgprops = mapi_getprops($defaultstore, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER]);
2955
2956
				if (!isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) ||
2957
					$msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid) {
2958
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2959
					break;
2960
				}
2961
2962
				try {
2963
					$result = $this->copyMessages($store, $parententryid, $defaultstore, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true);
2964
				}
2965
				catch (MAPIException $e) {
2966
					$e->setHandled();
2967
					// if moving fails, try normal delete
2968
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2969
				}
2970
				break;
2971
2972
			case ZARAFA_STORE_ARCHIVER_GUID:
2973
			case ZARAFA_STORE_PUBLIC_GUID:
2974
				// always delete in public store and archive store
2975
				$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2976
				break;
2977
2978
			case ZARAFA_SERVICE_GUID:
2979
				// delete message when in your own waste basket, else move it to the waste basket
2980
				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...
2981
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2982
					break;
2983
				}
2984
2985
				try {
2986
					// if the message is deleting from outbox then first delete the
2987
					// message from an outgoing queue.
2988
					if (function_exists("mapi_msgstore_abortsubmit") && isset($msgprops[PR_IPM_OUTBOX_ENTRYID]) && $msgprops[PR_IPM_OUTBOX_ENTRYID] === $parententryid) {
2989
						foreach ($entryids as $entryid) {
2990
							$message = mapi_msgstore_openentry($store, $entryid);
2991
							$messageProps = mapi_getprops($message, [PR_DEFERRED_SEND_TIME]);
2992
							if (isset($messageProps[PR_DEFERRED_SEND_TIME])) {
2993
								mapi_msgstore_abortsubmit($store, $entryid);
2994
							}
2995
						}
2996
					}
2997
					$result = $this->copyMessages($store, $parententryid, $store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true);
2998
				}
2999
				catch (MAPIException $e) {
3000
					if ($e->getCode() === MAPI_E_NOT_IN_QUEUE || $e->getCode() === MAPI_E_UNABLE_TO_ABORT) {
3001
						throw $e;
3002
					}
3003
3004
					$e->setHandled();
3005
					// if moving fails, try normal delete
3006
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
3007
				}
3008
				break;
3009
		}
3010
3011
		return $result;
3012
	}
3013
3014
	/**
3015
	 * Copy or move messages.
3016
	 *
3017
	 * @param object $store         MAPI Message Store Object
3018
	 * @param string $parententryid parent entryid of the messages
3019
	 * @param string $destentryid   destination folder
3020
	 * @param array  $entryids      a list of entryids which will be copied or moved
3021
	 * @param array  $ignoreProps   a list of proptags which should not be copied over
3022
	 *                              to the new message
3023
	 * @param bool   $moveMessages  true - move messages, false - copy messages
3024
	 * @param array  $props         a list of proptags which should set in new messages
3025
	 * @param mixed  $destStore
3026
	 *
3027
	 * @return bool true if action succeeded, false if not
3028
	 */
3029
	public function copyMessages($store, $parententryid, $destStore, $destentryid, $entryids, $ignoreProps, $moveMessages, $props = []) {
3030
		$sourcefolder = mapi_msgstore_openentry($store, $parententryid);
3031
		$destfolder = mapi_msgstore_openentry($destStore, $destentryid);
3032
3033
		if (!$sourcefolder || !$destfolder) {
3034
			error_log("Could not open source or destination folder. Aborting.");
3035
3036
			return false;
3037
		}
3038
3039
		if (!is_array($entryids)) {
0 ignored issues
show
introduced by
The condition is_array($entryids) is always true.
Loading history...
3040
			$entryids = [$entryids];
3041
		}
3042
3043
		/*
3044
		 * If there are no properties to ignore as well as set then we can use mapi_folder_copymessages instead
3045
		 * of mapi_copyto. mapi_folder_copymessages is much faster then copyto since it executes
3046
		 * the copying on the server instead of in client.
3047
		 */
3048
		if (empty($ignoreProps) && empty($props)) {
3049
			try {
3050
				mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0);
3051
			}
3052
			catch (MAPIException) {
3053
				error_log(sprintf("mapi_folder_copymessages failed with code: 0x%08X. Wait 250ms and try again", mapi_last_hresult()));
3054
				// wait 250ms before trying again
3055
				usleep(250000);
3056
3057
				try {
3058
					mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0);
3059
				}
3060
				catch (MAPIException) {
3061
					error_log(sprintf("2nd attempt of mapi_folder_copymessages also failed with code: 0x%08X. Abort.", mapi_last_hresult()));
3062
3063
					return false;
3064
				}
3065
			}
3066
		}
3067
		else {
3068
			foreach ($entryids as $entryid) {
3069
				$oldmessage = mapi_msgstore_openentry($store, $entryid);
3070
				$newmessage = mapi_folder_createmessage($destfolder);
3071
3072
				mapi_copyto($oldmessage, [], $ignoreProps, $newmessage, 0);
3073
				if (!empty($props)) {
3074
					mapi_setprops($newmessage, $props);
3075
				}
3076
				mapi_savechanges($newmessage);
3077
			}
3078
			if ($moveMessages) {
3079
				// while moving message we actually copy that particular message into
3080
				// destination folder, and remove it from source folder. so we must have
3081
				// to hard delete the message.
3082
				mapi_folder_deletemessages($sourcefolder, $entryids, DELETE_HARD_DELETE);
3083
			}
3084
		}
3085
3086
		return true;
3087
	}
3088
3089
	/**
3090
	 * Set message read flag.
3091
	 *
3092
	 * @param object $store      MAPI Message Store Object
3093
	 * @param string $entryid    entryid of the message
3094
	 * @param int    $flags      Bitmask of values (read, has attachment etc.)
3095
	 * @param array  $props      properties of the message
3096
	 * @param mixed  $msg_action
3097
	 *
3098
	 * @return bool true if action succeeded, false if not
3099
	 */
3100
	public function setMessageFlag($store, $entryid, $flags, $msg_action = false, &$props = false) {
3101
		$message = $this->openMessage($store, $entryid);
3102
3103
		if ($message) {
0 ignored issues
show
introduced by
$message is of type object, thus it always evaluated to true.
Loading history...
3104
			/**
3105
			 * convert flags of PR_MESSAGE_FLAGS property to flags that is
3106
			 * used in mapi_message_setreadflag.
3107
			 */
3108
			$flag = MAPI_DEFERRED_ERRORS;		// set unread flag, read receipt will be sent
3109
3110
			if (($flags & MSGFLAG_RN_PENDING) && isset($msg_action['send_read_receipt']) && $msg_action['send_read_receipt'] == false) {
3111
				$flag |= SUPPRESS_RECEIPT;
3112
			}
3113
			else {
3114
				if (!($flags & MSGFLAG_READ)) {
3115
					$flag |= CLEAR_READ_FLAG;
3116
				}
3117
			}
3118
3119
			mapi_message_setreadflag($message, $flag);
3120
3121
			if (is_array($props)) {
3122
				$props = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]);
3123
			}
3124
		}
3125
3126
		return true;
3127
	}
3128
3129
	/**
3130
	 * Create a unique folder name based on a provided new folder name.
3131
	 *
3132
	 * checkFolderNameConflict() checks if a folder name conflict is caused by the given $foldername.
3133
	 * This function is used for copying of moving a folder to another folder. It returns
3134
	 * a unique foldername.
3135
	 *
3136
	 * @param object $store      MAPI Message Store Object
3137
	 * @param object $folder     MAPI Folder Object
3138
	 * @param string $foldername the folder name
3139
	 *
3140
	 * @return string correct foldername
3141
	 */
3142
	public function checkFolderNameConflict($store, $folder, $foldername) {
3143
		$folderNames = [];
3144
3145
		$hierarchyTable = mapi_folder_gethierarchytable($folder, MAPI_DEFERRED_ERRORS);
3146
		mapi_table_sort($hierarchyTable, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
3147
3148
		$subfolders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]);
3149
3150
		if (is_array($subfolders)) {
3151
			foreach ($subfolders as $subfolder) {
3152
				$folderObject = mapi_msgstore_openentry($store, $subfolder[PR_ENTRYID]);
3153
				$folderProps = mapi_getprops($folderObject, [PR_DISPLAY_NAME]);
3154
3155
				array_push($folderNames, strtolower((string) $folderProps[PR_DISPLAY_NAME]));
3156
			}
3157
		}
3158
3159
		if (array_search(strtolower($foldername), $folderNames) !== false) {
3160
			$i = 2;
3161
			while (array_search(strtolower($foldername) . " ({$i})", $folderNames) !== false) {
3162
				++$i;
3163
			}
3164
			$foldername .= " ({$i})";
3165
		}
3166
3167
		return $foldername;
3168
	}
3169
3170
	/**
3171
	 * Set the recipients of a MAPI message.
3172
	 *
3173
	 * @param object $message    MAPI Message Object
3174
	 * @param array  $recipients XML array structure of recipients
3175
	 * @param bool   $send       true if we are going to send this message else false
3176
	 */
3177
	public function setRecipients($message, $recipients, $send = false) {
3178
		if (empty($recipients)) {
3179
			// no recipients are sent from client
3180
			return;
3181
		}
3182
3183
		$newRecipients = [];
3184
		$removeRecipients = [];
3185
		$modifyRecipients = [];
3186
3187
		if (isset($recipients['add']) && !empty($recipients['add'])) {
3188
			$newRecipients = $this->createRecipientList($recipients['add'], 'add', false, $send);
3189
		}
3190
3191
		if (isset($recipients['remove']) && !empty($recipients['remove'])) {
3192
			$removeRecipients = $this->createRecipientList($recipients['remove'], 'remove');
3193
		}
3194
3195
		if (isset($recipients['modify']) && !empty($recipients['modify'])) {
3196
			$modifyRecipients = $this->createRecipientList($recipients['modify'], 'modify', false, $send);
3197
		}
3198
3199
		if (!empty($removeRecipients)) {
3200
			mapi_message_modifyrecipients($message, MODRECIP_REMOVE, $removeRecipients);
3201
		}
3202
3203
		if (!empty($modifyRecipients)) {
3204
			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $modifyRecipients);
3205
		}
3206
3207
		if (!empty($newRecipients)) {
3208
			mapi_message_modifyrecipients($message, MODRECIP_ADD, $newRecipients);
3209
		}
3210
	}
3211
3212
	/**
3213
	 * Copy recipients from original message.
3214
	 *
3215
	 * If we are sending mail from a delegator's folder, we need to copy all recipients from the original message
3216
	 *
3217
	 * @param object      $message         MAPI Message Object
3218
	 * @param MAPIMessage $copyFromMessage If set we copy all recipients from this message
3219
	 */
3220
	public function copyRecipients($message, $copyFromMessage = false) {
3221
		$recipienttable = mapi_message_getrecipienttable($copyFromMessage);
3222
		$messageRecipients = mapi_table_queryallrows($recipienttable, $GLOBALS["properties"]->getRecipientProperties());
3223
		if (!empty($messageRecipients)) {
3224
			mapi_message_modifyrecipients($message, MODRECIP_ADD, $messageRecipients);
3225
		}
3226
	}
3227
3228
	/**
3229
	 * Set attachments in a MAPI message.
3230
	 *
3231
	 * This function reads any attachments that have been previously uploaded and copies them into
3232
	 * the passed MAPI message resource. For a description of the dialog_attachments variable and
3233
	 * generally how attachments work when uploading, see Operations::saveMessage()
3234
	 *
3235
	 * @see Operations::saveMessage()
3236
	 *
3237
	 * @param object          $message          MAPI Message Object
3238
	 * @param array           $attachments      XML array structure of attachments
3239
	 * @param AttachmentState $attachment_state the state object in which the attachments are saved
3240
	 *                                          between different requests
3241
	 */
3242
	public function setAttachments($message, $attachments, $attachment_state) {
3243
		// Check if attachments should be deleted. This is set in the "upload_attachment.php" file
3244
		if (isset($attachments['dialog_attachments'])) {
3245
			$deleted = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']);
3246
			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...
3247
				foreach ($deleted as $attach_num) {
3248
					try {
3249
						mapi_message_deleteattach($message, (int) $attach_num);
3250
					}
3251
					catch (Exception) {
3252
						continue;
3253
					}
3254
				}
3255
				$attachment_state->clearDeletedAttachments($attachments['dialog_attachments']);
3256
			}
3257
		}
3258
3259
		$addedInlineAttachmentCidMapping = [];
3260
		if (is_array($attachments) && !empty($attachments)) {
3261
			// Set contentId to saved attachments.
3262
			if (isset($attachments['add']) && is_array($attachments['add']) && !empty($attachments['add'])) {
3263
				foreach ($attachments['add'] as $key => $attach) {
3264
					if ($attach && isset($attach['inline']) && $attach['inline']) {
3265
						$addedInlineAttachmentCidMapping[$attach['attach_num']] = $attach['cid'];
3266
						$msgattachment = mapi_message_openattach($message, $attach['attach_num']);
3267
						if ($msgattachment) {
3268
							$props = [PR_ATTACH_CONTENT_ID => $attach['cid'], PR_ATTACHMENT_HIDDEN => true];
3269
							mapi_setprops($msgattachment, $props);
3270
							mapi_savechanges($msgattachment);
3271
						}
3272
					}
3273
				}
3274
			}
3275
3276
			// Delete saved inline images if removed from body.
3277
			if (isset($attachments['remove']) && is_array($attachments['remove']) && !empty($attachments['remove'])) {
3278
				foreach ($attachments['remove'] as $key => $attach) {
3279
					if ($attach && isset($attach['inline']) && $attach['inline']) {
3280
						$msgattachment = mapi_message_openattach($message, $attach['attach_num']);
3281
						if ($msgattachment) {
3282
							mapi_message_deleteattach($message, $attach['attach_num']);
3283
							mapi_savechanges($message);
3284
						}
3285
					}
3286
				}
3287
			}
3288
		}
3289
3290
		if ($attachments['dialog_attachments']) {
3291
			$dialog_attachments = $attachments['dialog_attachments'];
3292
		}
3293
		else {
3294
			return;
3295
		}
3296
3297
		$files = $attachment_state->getAttachmentFiles($dialog_attachments);
3298
		if ($files) {
3299
			// Loop through the uploaded attachments
3300
			foreach ($files as $tmpname => $fileinfo) {
3301
				if ($fileinfo['sourcetype'] === 'embedded') {
3302
					// open message which needs to be embedded
3303
					$copyFromStore = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $fileinfo['store_entryid']));
3304
					$copyFrom = mapi_msgstore_openentry($copyFromStore, hex2bin((string) $fileinfo['entryid']));
3305
3306
					$msgProps = mapi_getprops($copyFrom, [PR_SUBJECT]);
3307
3308
					// get message and copy it to attachment table as embedded attachment
3309
					$props = [];
3310
					$props[PR_EC_WA_ATTACHMENT_ID] = $fileinfo['attach_id'];
3311
					$props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG;
3312
					$props[PR_DISPLAY_NAME] = !empty($msgProps[PR_SUBJECT]) ? $msgProps[PR_SUBJECT] : _('Untitled');
3313
3314
					// Create new attachment.
3315
					$attachment = mapi_message_createattach($message);
3316
					mapi_setprops($attachment, $props);
3317
3318
					$imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
3319
3320
					// Copy the properties from the source message to the attachment
3321
					mapi_copyto($copyFrom, [], [], $imessage, 0); // includes attachments and recipients
3322
3323
					// save changes in the embedded message and the final attachment
3324
					mapi_savechanges($imessage);
3325
					mapi_savechanges($attachment);
3326
				}
3327
				elseif ($fileinfo['sourcetype'] === 'icsfile') {
3328
					$messageStore = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $fileinfo['store_entryid']));
3329
					$copyFrom = mapi_msgstore_openentry($messageStore, hex2bin((string) $fileinfo['entryid']));
3330
3331
					// Get addressbook for current session
3332
					$addrBook = $GLOBALS['mapisession']->getAddressbook();
3333
3334
					// get message properties.
3335
					$messageProps = mapi_getprops($copyFrom, [PR_SUBJECT]);
3336
3337
					// Read the appointment as RFC2445-formatted ics stream.
3338
					$appointmentStream = mapi_mapitoical($GLOBALS['mapisession']->getSession(), $addrBook, $copyFrom, []);
3339
3340
					$filename = (!empty($messageProps[PR_SUBJECT])) ? $messageProps[PR_SUBJECT] : _('Untitled');
3341
					$filename .= '.ics';
3342
3343
					$props = [
3344
						PR_ATTACH_LONG_FILENAME => $filename,
3345
						PR_DISPLAY_NAME => $filename,
3346
						PR_ATTACH_METHOD => ATTACH_BY_VALUE,
3347
						PR_ATTACH_DATA_BIN => "",
3348
						PR_ATTACH_MIME_TAG => "application/octet-stream",
3349
						PR_ATTACHMENT_HIDDEN => false,
3350
						PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(),
3351
						PR_ATTACH_EXTENSION => pathinfo($filename, PATHINFO_EXTENSION),
3352
					];
3353
3354
					$attachment = mapi_message_createattach($message);
3355
					mapi_setprops($attachment, $props);
3356
3357
					// Stream the file to the PR_ATTACH_DATA_BIN property
3358
					$stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
3359
					mapi_stream_write($stream, $appointmentStream);
3360
3361
					// Commit the stream and save changes
3362
					mapi_stream_commit($stream);
3363
					mapi_savechanges($attachment);
3364
				}
3365
				else {
3366
					$filepath = $attachment_state->getAttachmentPath($tmpname);
3367
					if (is_file($filepath)) {
3368
						// Set contentId if attachment is inline
3369
						$cid = '';
3370
						if (isset($addedInlineAttachmentCidMapping[$tmpname])) {
3371
							$cid = $addedInlineAttachmentCidMapping[$tmpname];
3372
						}
3373
3374
						// If a .p7m file was manually uploaded by the user, we must change the mime type because
3375
						// otherwise mail applications will think the containing email is an encrypted email.
3376
						// That will make Outlook crash, and it will make grommunio Web show the original mail as encrypted
3377
						// without showing the attachment
3378
						$mimeType = $fileinfo["type"];
3379
						$smimeTags = ['multipart/signed', 'application/pkcs7-mime', 'application/x-pkcs7-mime'];
3380
						if (in_array($mimeType, $smimeTags)) {
3381
							$mimeType = "application/octet-stream";
3382
						}
3383
3384
						// Set attachment properties
3385
						$props = [
3386
							PR_ATTACH_LONG_FILENAME => $fileinfo["name"],
3387
							PR_DISPLAY_NAME => $fileinfo["name"],
3388
							PR_ATTACH_METHOD => ATTACH_BY_VALUE,
3389
							PR_ATTACH_DATA_BIN => "",
3390
							PR_ATTACH_MIME_TAG => $mimeType,
3391
							PR_ATTACHMENT_HIDDEN => !empty($cid) ? true : false,
3392
							PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(),
3393
							PR_ATTACH_EXTENSION => pathinfo((string) $fileinfo["name"], PATHINFO_EXTENSION),
3394
						];
3395
3396
						if (isset($fileinfo['sourcetype']) && $fileinfo['sourcetype'] === 'contactphoto') {
3397
							$props[PR_ATTACHMENT_HIDDEN] = true;
3398
							$props[PR_ATTACHMENT_CONTACTPHOTO] = true;
3399
						}
3400
3401
						if (!empty($cid)) {
3402
							$props[PR_ATTACH_CONTENT_ID] = $cid;
3403
						}
3404
3405
						// Create attachment and set props
3406
						$attachment = mapi_message_createattach($message);
3407
						mapi_setprops($attachment, $props);
3408
3409
						// Stream the file to the PR_ATTACH_DATA_BIN property
3410
						$stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
3411
						$handle = fopen($filepath, "r");
3412
						while (!feof($handle)) {
3413
							$contents = fread($handle, BLOCK_SIZE);
3414
							mapi_stream_write($stream, $contents);
3415
						}
3416
3417
						// Commit the stream and save changes
3418
						mapi_stream_commit($stream);
3419
						mapi_savechanges($attachment);
3420
						fclose($handle);
3421
						unlink($filepath);
3422
					}
3423
				}
3424
			}
3425
3426
			// Delete all the files in the state.
3427
			$attachment_state->clearAttachmentFiles($dialog_attachments);
3428
		}
3429
	}
3430
3431
	/**
3432
	 * Copy attachments from original message.
3433
	 *
3434
	 * @see Operations::saveMessage()
3435
	 *
3436
	 * @param object          $message                   MAPI Message Object
3437
	 * @param string          $attachments
3438
	 * @param MAPIMessage     $copyFromMessage           if set, copy the attachments from this message in addition to the uploaded attachments
3439
	 * @param bool            $copyInlineAttachmentsOnly if true then copy only inline attachments
3440
	 * @param AttachmentState $attachment_state          the state object in which the attachments are saved
3441
	 *                                                   between different requests
3442
	 */
3443
	public function copyAttachments($message, $attachments, $copyFromMessage, $copyInlineAttachmentsOnly, $attachment_state) {
3444
		$attachmentTable = mapi_message_getattachmenttable($copyFromMessage);
3445
		if ($attachmentTable && isset($attachments['dialog_attachments'])) {
3446
			$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]);
3447
			$deletedAttachments = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']);
3448
3449
			$plainText = $this->isPlainText($message);
3450
3451
			$properties = $GLOBALS['properties']->getMailProperties();
3452
			$blockStatus = mapi_getprops($copyFromMessage, [PR_BLOCK_STATUS]);
3453
			$blockStatus = Conversion::mapMAPI2XML($properties, $blockStatus);
3454
			$isSafeSender = false;
3455
3456
			// Here if message is HTML and block status is empty then and then call isSafeSender function
3457
			// to check that sender or sender's domain of original message was part of safe sender list.
3458
			if (!$plainText && empty($blockStatus)) {
3459
				$isSafeSender = $this->isSafeSender($copyFromMessage);
3460
			}
3461
3462
			$body = false;
3463
			foreach ($existingAttachments as $props) {
3464
				// check if this attachment is "deleted"
3465
3466
				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...
3467
					// skip attachment, remove reference from state as it no longer applies.
3468
					$attachment_state->removeDeletedAttachment($attachments['dialog_attachments'], $props[PR_ATTACH_NUM]);
3469
3470
					continue;
3471
				}
3472
3473
				$old = mapi_message_openattach($copyFromMessage, $props[PR_ATTACH_NUM]);
3474
				$isInlineAttachment = $attachment_state->isInlineAttachment($old);
3475
3476
				/*
3477
				 * If reply/reply all message, then copy only inline attachments.
3478
				 */
3479
				if ($copyInlineAttachmentsOnly) {
3480
					/*
3481
					 * if message is reply/reply all and format is plain text than ignore inline attachments
3482
					 * and normal attachments to copy from original mail.
3483
					 */
3484
					if ($plainText || !$isInlineAttachment) {
3485
						continue;
3486
					}
3487
				}
3488
				elseif ($plainText && $isInlineAttachment) {
3489
					/*
3490
					 * If message is forward and format of message is plain text then ignore only inline attachments from the
3491
					 * original mail.
3492
					 */
3493
					continue;
3494
				}
3495
3496
				/*
3497
				 * If the inline attachment is referenced with an content-id,
3498
				 * manually check if it's still referenced in the body otherwise remove it
3499
				 */
3500
				if ($isInlineAttachment) {
3501
					// Cache body, so we stream it once
3502
					if ($body === false) {
3503
						$body = streamProperty($message, PR_HTML);
3504
					}
3505
3506
					$contentID = $props[PR_ATTACH_CONTENT_ID];
3507
					if (!str_contains($body, (string) $contentID)) {
0 ignored issues
show
Bug introduced by
It seems like $body can also be of type false; however, parameter $haystack of str_contains() 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

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

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

4078
		$distlistMembers = $this->getMembersFromDistributionList($store, /** @scrutinizer ignore-type */ $distlist, $properties, $isRecursive);
Loading history...
4079
		$recipients = [];
4080
4081
		foreach ($distlistMembers as $member) {
4082
			$props = $this->convertDistlistMemberToRecipient($store, $member);
4083
			array_push($recipients, $props);
4084
		}
4085
4086
		return $recipients;
4087
	}
4088
4089
	/**
4090
	 * Function Which convert the shared/internal(local contact folder distlist)
4091
	 * folder's distlist members to recipient type.
4092
	 *
4093
	 * @param mapistore $store  MAPI store of the message
4094
	 * @param array     $member of distribution list contacts
4095
	 *
4096
	 * @return array members properties converted in to recipient
4097
	 */
4098
	public function convertDistlistMemberToRecipient($store, $member) {
4099
		$entryid = $member["props"]["entryid"];
4100
		$memberProps = $member["props"];
4101
		$props = [];
4102
4103
		$distlistType = $memberProps["distlist_type"];
4104
		$addressType = $memberProps["address_type"];
4105
4106
		$isGABDistlList = $distlistType == DL_DIST_AB && $addressType === "EX";
4107
		$isLocalDistlist = $distlistType == DL_DIST && $addressType === "MAPIPDL";
4108
4109
		$isGABContact = $memberProps["address_type"] === 'EX';
4110
		// If distlist_type is 0 then it means distlist member is external contact.
4111
		// For mare please read server/core/constants.php
4112
		$isLocalContact = !$isGABContact && $distlistType !== 0;
4113
4114
		/*
4115
		 * If distribution list belongs to the local contact folder then open that contact and
4116
		 * retrieve all properties which requires to prepare ideal recipient to send mail.
4117
		 */
4118
		if ($isLocalDistlist) {
4119
			try {
4120
				$distlist = $this->openMessage($store, hex2bin((string) $entryid));
4121
			}
4122
			catch (Exception) {
4123
				$distlist = $this->openMessage($GLOBALS["mapisession"]->getPublicMessageStore(), hex2bin((string) $entryid));
4124
			}
4125
4126
			$abProps = $this->getProps($distlist, $GLOBALS['properties']->getRecipientProperties());
4127
			$props = $abProps["props"];
4128
4129
			$props["entryid"] = $GLOBALS["entryid"]->wrapABEntryIdObj($abProps["entryid"], MAPI_DISTLIST);
4130
			$props["display_type"] = DT_DISTLIST;
4131
			$props["display_type_ex"] = DT_DISTLIST;
4132
			$props["address_type"] = $memberProps["address_type"];
4133
			$emailAddress = !empty($memberProps["email_address"]) ? $memberProps["email_address"] : "";
4134
			$props["smtp_address"] = $emailAddress;
4135
			$props["email_address"] = $emailAddress;
4136
		}
4137
		elseif ($isGABContact || $isGABDistlList) {
4138
			/*
4139
			 * If contact or distribution list belongs to GAB then open that contact and
4140
			 * retrieve all properties which requires to prepare ideal recipient to send mail.
4141
			 */
4142
			try {
4143
				$abentry = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), hex2bin((string) $entryid));
4144
				$abProps = $this->getProps($abentry, $GLOBALS['properties']->getRecipientProperties());
4145
				$props = $abProps["props"];
4146
				$props["entryid"] = $abProps["entryid"];
4147
			}
4148
			catch (Exception $e) {
4149
				// Throw MAPI_E_NOT_FOUND or MAPI_E_UNKNOWN_ENTRYID it may possible that contact is already
4150
				// deleted from server. so just create recipient
4151
				// with existing information of distlist member.
4152
				// recipient is not valid so sender get report mail for that
4153
				// particular recipient to inform that recipient is not exist.
4154
				if ($e->getCode() == MAPI_E_NOT_FOUND || $e->getCode() == MAPI_E_UNKNOWN_ENTRYID) {
4155
					$props["entryid"] = $memberProps["entryid"];
4156
					$props["display_type"] = DT_MAILUSER;
4157
					$props["display_type_ex"] = DT_MAILUSER;
4158
					$props["display_name"] = $memberProps["display_name"];
4159
					$props["smtp_address"] = $memberProps["email_address"];
4160
					$props["email_address"] = $memberProps["email_address"];
4161
					$props["address_type"] = !empty($memberProps["address_type"]) ? $memberProps["address_type"] : 'SMTP';
4162
				}
4163
				else {
4164
					throw $e;
4165
				}
4166
			}
4167
		}
4168
		else {
4169
			/*
4170
			 * If contact is belongs to local/shared folder then prepare ideal recipient to send mail
4171
			 * as per the contact type.
4172
			 */
4173
			$props["entryid"] = $isLocalContact ? $GLOBALS["entryid"]->wrapABEntryIdObj($entryid, MAPI_MAILUSER) : $memberProps["entryid"];
4174
			$props["display_type"] = DT_MAILUSER;
4175
			$props["display_type_ex"] = $isLocalContact ? DT_MAILUSER : DT_REMOTE_MAILUSER;
4176
			$props["display_name"] = $memberProps["display_name"];
4177
			$props["smtp_address"] = $memberProps["email_address"];
4178
			$props["email_address"] = $memberProps["email_address"];
4179
			$props["address_type"] = !empty($memberProps["address_type"]) ? $memberProps["address_type"] : 'SMTP';
4180
		}
4181
4182
		// Set object type property into each member of distribution list
4183
		$props["object_type"] = $this->getObjectTypeFromDistlistType($memberProps["distlist_type"]);
4184
4185
		return $props;
4186
	}
4187
4188
	/**
4189
	 * Parse reply-to value from PR_REPLY_RECIPIENT_ENTRIES property.
4190
	 *
4191
	 * @param string $flatEntryList the PR_REPLY_RECIPIENT_ENTRIES value
4192
	 *
4193
	 * @return array list of recipients in array structure
4194
	 */
4195
	public function readReplyRecipientEntry($flatEntryList) {
4196
		$addressbook = $GLOBALS["mapisession"]->getAddressbook();
4197
		$entryids = [];
4198
4199
		// Unpack number of entries, the byte count and the entries
4200
		$unpacked = unpack('V1cEntries/V1cbEntries/a*', $flatEntryList);
4201
4202
		// $unpacked consists now of the following fields:
4203
		//	'cEntries' => The number of entryids in our list
4204
		//	'cbEntries' => The total number of bytes inside 'abEntries'
4205
		//	'abEntries' => The list of Entryids
4206
		//
4207
		// Each 'abEntries' can be broken down into groups of 2 fields
4208
		//	'cb' => The length of the entryid
4209
		//	'entryid' => The entryid
4210
4211
		$position = 8; // sizeof(cEntries) + sizeof(cbEntries);
4212
4213
		for ($i = 0, $len = $unpacked['cEntries']; $i < $len; ++$i) {
4214
			// Obtain the size for the current entry
4215
			$size = unpack('a' . $position . '/V1cb/a*', $flatEntryList);
4216
4217
			// We have the size, now can obtain the bytes
4218
			$entryid = unpack('a' . $position . '/V1cb/a' . $size['cb'] . 'entryid/a*', $flatEntryList);
4219
4220
			// unpack() will remove the NULL characters, re-add
4221
			// them until we match the 'cb' length.
4222
			while ($entryid['cb'] > strlen((string) $entryid['entryid'])) {
4223
				$entryid['entryid'] .= chr(0x00);
4224
			}
4225
4226
			$entryids[] = $entryid['entryid'];
4227
4228
			// sizeof(cb) + strlen(entryid)
4229
			$position += 4 + $entryid['cb'];
4230
		}
4231
4232
		$recipients = [];
4233
		foreach ($entryids as $entryid) {
4234
			// Check if entryid extracted, since unpack errors can not be caught.
4235
			if (!$entryid) {
4236
				continue;
4237
			}
4238
4239
			// Handle malformed entryids
4240
			try {
4241
				$entry = mapi_ab_openentry($addressbook, $entryid);
4242
				$props = mapi_getprops($entry, [PR_ENTRYID, PR_SEARCH_KEY, PR_OBJECT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS]);
4243
4244
				// Put data in recipient array
4245
				$recipients[] = $this->composeRecipient(count($recipients), $props);
4246
			}
4247
			catch (MAPIException $e) {
4248
				try {
4249
					$oneoff = mapi_parseoneoff($entryid);
4250
				}
4251
				catch (MAPIException $ex) {
4252
					error_log(sprintf(
4253
						"readReplyRecipientEntry unable to open AB entry and mapi_parseoneoff failed: %s - %s",
4254
						get_mapi_error_name($ex->getCode()),
4255
						$ex->getDisplayMessage()
4256
					));
4257
4258
					continue;
4259
				}
4260
				if (!isset($oneoff['address'])) {
4261
					error_log(sprintf(
4262
						"readReplyRecipientEntry unable to open AB entry and oneoff address is not available: %s - %s ",
4263
						get_mapi_error_name($e->getCode()),
4264
						$e->getDisplayMessage()
4265
					));
4266
4267
					continue;
4268
				}
4269
4270
				$entryid = mapi_createoneoff($oneoff['name'] ?? '', $oneoff['type'] ?? 'SMTP', $oneoff['address']);
4271
				$props = [
4272
					PR_ENTRYID => $entryid,
4273
					PR_DISPLAY_NAME => !empty($oneoff['name']) ? $oneoff['name'] : $oneoff['address'],
4274
					PR_ADDRTYPE => $oneoff['type'] ?? 'SMTP',
4275
					PR_EMAIL_ADDRESS => $oneoff['address'],
4276
				];
4277
				$recipients[] = $this->composeRecipient(count($recipients), $props);
4278
			}
4279
		}
4280
4281
		return $recipients;
4282
	}
4283
4284
	private function composeRecipient($rowid, $props) {
4285
		return [
4286
			'rowid' => $rowid,
4287
			'props' => [
4288
				'entryid' => !empty($props[PR_ENTRYID]) ? bin2hex((string) $props[PR_ENTRYID]) : '',
4289
				'object_type' => $props[PR_OBJECT_TYPE] ?? MAPI_MAILUSER,
4290
				'search_key' => $props[PR_SEARCH_KEY] ?? '',
4291
				'display_name' => !empty($props[PR_DISPLAY_NAME]) ? $props[PR_DISPLAY_NAME] : $props[PR_EMAIL_ADDRESS],
4292
				'address_type' => $props[PR_ADDRTYPE] ?? 'SMTP',
4293
				'email_address' => $props[PR_EMAIL_ADDRESS] ?? '',
4294
				'smtp_address' => $props[PR_EMAIL_ADDRESS] ?? '',
4295
			],
4296
		];
4297
	}
4298
4299
	/**
4300
	 * Build full-page HTML from the TinyMCE HTML.
4301
	 *
4302
	 * This function basically takes the generated HTML from TinyMCE and embeds it in
4303
	 * a standalone HTML page (including header and CSS) to form.
4304
	 *
4305
	 * @param string $body  This is the HTML created by the TinyMCE
4306
	 * @param string $title Optional, this string is placed in the <title>
4307
	 *
4308
	 * @return string full HTML message
4309
	 */
4310
	public function generateBodyHTML($body, $title = "grommunio-web") {
4311
		$html = "<!DOCTYPE html>" .
4312
				"<html>\n" .
4313
				"<head>\n" .
4314
				"  <meta name=\"Generator\" content=\"grommunio-web v" . trim(file_get_contents('version')) . "\">\n" .
4315
				"  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" .
4316
				"  <title>" . htmlspecialchars($title) . "</title>\n";
4317
4318
		$html .= "</head>\n" .
4319
				"<body>\n" .
4320
				$body . "\n" .
4321
				"</body>\n" .
4322
				"</html>";
4323
4324
		return $html;
4325
	}
4326
4327
	/**
4328
	 * Calculate the total size for all items in the given folder.
4329
	 *
4330
	 * @param mapifolder $folder The folder for which the size must be calculated
0 ignored issues
show
Bug introduced by
The type mapifolder 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...
4331
	 *
4332
	 * @return number The folder size
4333
	 */
4334
	public function calcFolderMessageSize($folder) {
4335
		$folderProps = mapi_getprops($folder, [PR_MESSAGE_SIZE_EXTENDED]);
4336
4337
		return $folderProps[PR_MESSAGE_SIZE_EXTENDED] ?? 0;
4338
	}
4339
4340
	/**
4341
	 * Detect plaintext body type of message.
4342
	 *
4343
	 * @param mapimessage $message MAPI message resource to check
4344
	 *
4345
	 * @return bool TRUE if the message is a plaintext message, FALSE if otherwise
4346
	 */
4347
	public function isPlainText($message) {
4348
		$props = mapi_getprops($message, [PR_NATIVE_BODY_INFO]);
4349
		if (isset($props[PR_NATIVE_BODY_INFO]) && $props[PR_NATIVE_BODY_INFO] == 1) {
4350
			return true;
4351
		}
4352
4353
		return false;
4354
	}
4355
4356
	/**
4357
	 * Parse email recipient list and add all e-mail addresses to the recipient history.
4358
	 *
4359
	 * The recipient history is used for auto-suggestion when writing e-mails. This function
4360
	 * opens the recipient history property (PR_EC_RECIPIENT_HISTORY_JSON) and updates or appends
4361
	 * it with the passed email addresses.
4362
	 *
4363
	 * @param array $recipients list of recipients
4364
	 */
4365
	public function addRecipientsToRecipientHistory($recipients) {
4366
		if (empty($recipients) || !is_array($recipients)) {
4367
			return;
4368
		}
4369
4370
		$emailAddresses = [];
4371
		foreach ($recipients as $recipient) {
4372
			if (isset($recipient['props']) && is_array($recipient['props'])) {
4373
				$emailAddresses[] = $recipient['props'];
4374
			}
4375
		}
4376
4377
		if (empty($emailAddresses)) {
4378
			return;
4379
		}
4380
4381
		// Retrieve the recipient history
4382
		$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
4383
		$storeProps = mapi_getprops($store, [PR_EC_RECIPIENT_HISTORY_JSON]);
4384
		$recipient_history = [];
4385
4386
		if (isset($storeProps[PR_EC_RECIPIENT_HISTORY_JSON]) || propIsError(PR_EC_RECIPIENT_HISTORY_JSON, $storeProps) == MAPI_E_NOT_ENOUGH_MEMORY) {
4387
			$datastring = streamProperty($store, PR_EC_RECIPIENT_HISTORY_JSON);
4388
4389
			if (!empty($datastring)) {
4390
				$recipient_history = json_decode_data($datastring, true);
4391
			}
4392
		}
4393
4394
		if (!isset($recipient_history['recipients']) || !is_array($recipient_history['recipients'])) {
4395
			$recipient_history['recipients'] = [];
4396
		}
4397
4398
		$l_aNewHistoryItems = [];
4399
		foreach ($emailAddresses as $emailProps) {
4400
			$emailAddress = $this->resolveEmailAddressFromProps($emailProps);
4401
			if ($emailAddress === '') {
4402
				continue;
4403
			}
4404
4405
			$timestamp = time();
4406
			if ($this->updateRecipientHistoryEntry($recipient_history['recipients'], $emailProps, $emailAddress, $timestamp)) {
4407
				continue;
4408
			}
4409
4410
			if (!isset($l_aNewHistoryItems[$emailAddress])) {
4411
				$l_aNewHistoryItems[$emailAddress] = $this->buildRecipientHistoryEntry($emailProps, $timestamp);
4412
			}
4413
		}
4414
4415
		if (!empty($l_aNewHistoryItems)) {
4416
			foreach ($l_aNewHistoryItems as $l_aValue) {
4417
				$recipient_history['recipients'][] = $l_aValue;
4418
			}
4419
		}
4420
4421
		$l_sNewRecipientHistoryJSON = json_encode($recipient_history);
4422
4423
		$stream = mapi_openproperty($store, PR_EC_RECIPIENT_HISTORY_JSON, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
4424
		mapi_stream_setsize($stream, strlen($l_sNewRecipientHistoryJSON));
4425
		mapi_stream_write($stream, $l_sNewRecipientHistoryJSON);
4426
		mapi_stream_commit($stream);
4427
		mapi_savechanges($store);
4428
	}
4429
4430
	/**
4431
	 * Resolve the effective email address from a recipient property set.
4432
	 *
4433
	 * @param array $props raw properties extracted from the recipient row
4434
	 *
4435
	 * @return string trimmed email address or empty string when none available
4436
	 */
4437
	private function resolveEmailAddressFromProps(array $props) {
4438
		$addressType = $props['address_type'] ?? '';
4439
		if ($addressType === 'SMTP') {
4440
			$emailAddress = $props['smtp_address'] ?? '';
4441
			if ($emailAddress === '') {
4442
				$emailAddress = $props['email_address'] ?? '';
4443
			}
4444
		}
4445
		else {
4446
			$emailAddress = $props['email_address'] ?? '';
4447
			if ($emailAddress === '') {
4448
				$emailAddress = $props['smtp_address'] ?? '';
4449
			}
4450
		}
4451
4452
		return trim((string) $emailAddress);
4453
	}
4454
4455
	/**
4456
	 * Update an existing history entry when the address already exists.
4457
	 *
4458
	 * @param array  &$historyRecipients Reference to the recipient history array
4459
	 * @param array  $emailProps         current recipient properties
4460
	 * @param string $emailAddress       resolved email address
4461
	 * @param int    $timestamp          current timestamp used for counters
4462
	 *
4463
	 * @return bool TRUE when a matching history entry was updated, FALSE otherwise
4464
	 */
4465
	private function updateRecipientHistoryEntry(array &$historyRecipients, array $emailProps, $emailAddress, $timestamp) {
4466
		if (empty($historyRecipients)) {
4467
			return false;
4468
		}
4469
4470
		foreach ($historyRecipients as &$recipient) {
4471
			if (($emailProps['address_type'] ?? null) !== ($recipient['address_type'] ?? null)) {
4472
				continue;
4473
			}
4474
4475
			if (
4476
				$emailAddress !== ($recipient['email_address'] ?? '') &&
4477
				$emailAddress !== ($recipient['smtp_address'] ?? '')
4478
			) {
4479
				continue;
4480
			}
4481
4482
			$this->updateRecipientDisplayName($emailProps, $recipient);
4483
			$recipient['count'] = isset($recipient['count']) ? $recipient['count'] + 1 : 1;
4484
			$recipient['last_used'] = $timestamp;
4485
4486
			return true;
4487
		}
4488
		unset($recipient);
4489
4490
		return false;
4491
	}
4492
4493
	/**
4494
	 * Refresh the display name on a history entry when a better candidate is available.
4495
	 *
4496
	 * @param array $emailProps current recipient properties
4497
	 * @param array &$recipient Matched history entry to update
4498
	 */
4499
	private function updateRecipientDisplayName(array $emailProps, array &$recipient) {
4500
		$newDisplayName = trim((string) ($emailProps['display_name'] ?? ''));
4501
		if ($newDisplayName === '') {
4502
			return;
4503
		}
4504
4505
		$oldDisplayName = trim((string) ($recipient['display_name'] ?? ''));
4506
		$smtpAddress = $emailProps['smtp_address'] ?? '';
4507
4508
		if ($newDisplayName !== $smtpAddress || $oldDisplayName === '') {
4509
			$recipient['display_name'] = $newDisplayName;
4510
		}
4511
	}
4512
4513
	/**
4514
	 * Create a new history entry structure for a recipient address.
4515
	 *
4516
	 * @param array $emailProps recipient properties used for the entry
4517
	 * @param int   $timestamp  creation timestamp that seeds usage metrics
4518
	 *
4519
	 * @return array normalized recipient history entry payload
4520
	 */
4521
	private function buildRecipientHistoryEntry(array $emailProps, $timestamp) {
4522
		return [
4523
			'display_name' => $emailProps['display_name'] ?? '',
4524
			'smtp_address' => $emailProps['smtp_address'] ?? '',
4525
			'email_address' => $emailProps['email_address'] ?? '',
4526
			'address_type' => $emailProps['address_type'] ?? '',
4527
			'count' => 1,
4528
			'last_used' => $timestamp,
4529
			'object_type' => $emailProps['object_type'] ?? null,
4530
		];
4531
	}
4532
4533
	/**
4534
	 * Get the SMTP e-mail of an addressbook entry.
4535
	 *
4536
	 * @param string $entryid Addressbook entryid of object
4537
	 *
4538
	 * @return string SMTP e-mail address of that entry or FALSE on error
4539
	 */
4540
	public function getEmailAddressFromEntryID($entryid) {
4541
		try {
4542
			$mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $entryid);
4543
		}
4544
		catch (MAPIException $e) {
4545
			// if any invalid entryid is passed in this function then it should silently ignore it
4546
			// and continue with execution
4547
			if ($e->getCode() == MAPI_E_UNKNOWN_ENTRYID) {
4548
				$e->setHandled();
4549
4550
				return "";
4551
			}
4552
		}
4553
4554
		if (!isset($mailuser)) {
4555
			return "";
4556
		}
4557
4558
		$abprops = mapi_getprops($mailuser, [PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]);
4559
4560
		return $abprops[PR_SMTP_ADDRESS] ?? $abprops[PR_EMAIL_ADDRESS] ?? "";
4561
	}
4562
4563
	/**
4564
	 * Function which fetches all members of a distribution list recursively.
4565
	 *
4566
	 * @param resource $store        MAPI Message Store Object
4567
	 * @param resource $message      the distribution list message
4568
	 * @param array    $properties   array of properties to get properties of distlist
4569
	 * @param bool     $isRecursive  function will be called recursively if there is/are
4570
	 *                               distribution list inside the distlist to expand all the members,
4571
	 *                               pass true to expand distlist recursively, false to not expand
4572
	 * @param array    $listEntryIDs list of already expanded Distribution list from contacts folder,
4573
	 *                               This parameter is used for recursive call of the function
4574
	 *
4575
	 * @return object $items all members of a distlist
4576
	 */
4577
	public function getMembersFromDistributionList($store, $message, $properties, $isRecursive = false, $listEntryIDs = []) {
4578
		$items = [];
4579
4580
		$props = mapi_getprops($message, [$properties['oneoff_members'], $properties['members'], PR_ENTRYID]);
4581
4582
		// only continue when we have something to expand
4583
		if (!isset($props[$properties['oneoff_members']]) || !isset($props[$properties['members']])) {
4584
			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...
4585
		}
4586
4587
		if ($isRecursive) {
4588
			// when opening sub message we will not have entryid, so use entryid only when we have it
4589
			if (isset($props[PR_ENTRYID])) {
4590
				// for preventing recursion we need to store entryids, and check if the same distlist is going to be expanded again
4591
				if (in_array($props[PR_ENTRYID], $listEntryIDs)) {
4592
					// don't expand a distlist that is already expanded
4593
					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...
4594
				}
4595
4596
				$listEntryIDs[] = $props[PR_ENTRYID];
4597
			}
4598
		}
4599
4600
		$members = $props[$properties['members']];
4601
4602
		// parse oneoff members
4603
		$oneoffmembers = [];
4604
		foreach ($props[$properties['oneoff_members']] as $key => $item) {
4605
			$oneoffmembers[$key] = mapi_parseoneoff($item);
4606
		}
4607
4608
		foreach ($members as $key => $item) {
4609
			/*
4610
			 * PHP 5.5.0 and greater has made the unpack function incompatible with previous versions by changing:
4611
			 * - a = code now retains trailing NULL bytes.
4612
			 * - A = code now strips all trailing ASCII whitespace (spaces, tabs, newlines, carriage
4613
			 * returns, and NULL bytes).
4614
			 * for more http://php.net/manual/en/function.unpack.php
4615
			 */
4616
			if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
4617
				$parts = unpack('Vnull/A16guid/Ctype/a*entryid', (string) $item);
4618
			}
4619
			else {
4620
				$parts = unpack('Vnull/A16guid/Ctype/A*entryid', (string) $item);
4621
			}
4622
4623
			$memberItem = [];
4624
			$memberItem['props'] = [];
4625
			$memberItem['props']['distlist_type'] = $parts['type'];
4626
4627
			if ($parts['guid'] === hex2bin('812b1fa4bea310199d6e00dd010f5402')) {
4628
				// custom e-mail address (no user or contact)
4629
				$oneoff = mapi_parseoneoff($item);
4630
4631
				$memberItem['props']['display_name'] = $oneoff['name'];
4632
				$memberItem['props']['address_type'] = $oneoff['type'];
4633
				$memberItem['props']['email_address'] = $oneoff['address'];
4634
				$memberItem['props']['smtp_address'] = $oneoff['address'];
4635
				$memberItem['props']['entryid'] = bin2hex((string) $members[$key]);
4636
4637
				$items[] = $memberItem;
4638
			}
4639
			else {
4640
				if ($parts['type'] === DL_DIST && $isRecursive) {
4641
					// Expand distribution list to get distlist members inside the distributionlist.
4642
					$distlist = mapi_msgstore_openentry($store, $parts['entryid']);
4643
					$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

4643
					$items = array_merge($items, /** @scrutinizer ignore-type */ $this->getMembersFromDistributionList($store, $distlist, $properties, true, $listEntryIDs));
Loading history...
4644
				}
4645
				else {
4646
					$memberItem['props']['entryid'] = bin2hex((string) $parts['entryid']);
4647
					$memberItem['props']['display_name'] = $oneoffmembers[$key]['name'];
4648
					$memberItem['props']['address_type'] = $oneoffmembers[$key]['type'];
4649
					// distribution lists don't have valid email address so ignore that property
4650
4651
					if ($parts['type'] !== DL_DIST) {
4652
						$memberItem['props']['email_address'] = $oneoffmembers[$key]['address'];
4653
4654
						// internal members in distribution list don't have smtp address so add add that property
4655
						$memberProps = $this->convertDistlistMemberToRecipient($store, $memberItem);
0 ignored issues
show
Bug introduced by
$store of type resource is incompatible with the type mapistore expected by parameter $store of Operations::convertDistlistMemberToRecipient(). ( Ignorable by Annotation )

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

4655
						$memberProps = $this->convertDistlistMemberToRecipient(/** @scrutinizer ignore-type */ $store, $memberItem);
Loading history...
4656
						$memberItem['props']['smtp_address'] = $memberProps["smtp_address"] ?? $memberProps["email_address"];
4657
					}
4658
4659
					$items[] = $memberItem;
4660
				}
4661
			}
4662
		}
4663
4664
		return $items;
4665
	}
4666
4667
	/**
4668
	 * Convert inline image <img src="data:image/mimetype;.date> links in HTML email
4669
	 * to CID embedded images. Which are supported in major mail clients or
4670
	 * providers such as outlook.com or gmail.com.
4671
	 *
4672
	 * grommunio Web now extracts the base64 image, saves it as hidden attachment,
4673
	 * replace the img src tag with the 'cid' which corresponds with the attachments
4674
	 * cid.
4675
	 *
4676
	 * @param MAPIMessage $message the distribution list message
4677
	 */
4678
	public function convertInlineImage($message) {
4679
		$body = streamProperty($message, PR_HTML);
4680
		$imageIDs = [];
4681
4682
		// Only load the DOM if the HTML contains a img or data:text/plain due to a bug
4683
		// in Chrome on Windows in combination with TinyMCE.
4684
		if (str_contains($body, "img") || str_contains($body, "data:text/plain")) {
4685
			$doc = new DOMDocument();
4686
			$cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]);
4687
			$codepage = $cpprops[PR_INTERNET_CPID] ?? 1252;
4688
			$hackEncoding = '<meta http-equiv="Content-Type" content="text/html; charset=' . Conversion::getCodepageCharset($codepage) . '">';
4689
			// TinyMCE does not generate valid HTML, so we must suppress warnings.
4690
			@$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

4690
			/** @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...
4691
			$images = $doc->getElementsByTagName('img');
4692
			$saveChanges = false;
4693
4694
			foreach ($images as $image) {
4695
				$src = $image->getAttribute('src');
4696
4697
				if (!str_contains($src, "cid:") && (str_contains($src, "data:image") ||
4698
					str_contains($body, "data:text/plain"))) {
4699
					$saveChanges = true;
4700
4701
					// Extract mime type data:image/jpeg;
4702
					$firstOffset = strpos($src, '/') + 1;
4703
					$endOffset = strpos($src, ';');
4704
					$mimeType = substr($src, $firstOffset, $endOffset - $firstOffset);
4705
4706
					$dataPosition = strpos($src, ",");
4707
					// Extract encoded data
4708
					$rawImage = base64_decode(substr($src, $dataPosition + 1, strlen($src)));
4709
4710
					$uniqueId = uniqid();
4711
					$image->setAttribute('src', 'cid:' . $uniqueId);
4712
					// TinyMCE adds an extra inline image for some reason, remove it.
4713
					$image->setAttribute('data-mce-src', '');
4714
4715
					array_push($imageIDs, $uniqueId);
4716
4717
					// Create hidden attachment with CID
4718
					$inlineImage = mapi_message_createattach($message);
4719
					$props = [
4720
						PR_ATTACH_METHOD => ATTACH_BY_VALUE,
4721
						PR_ATTACH_CONTENT_ID => $uniqueId,
4722
						PR_ATTACHMENT_HIDDEN => true,
4723
						PR_ATTACH_FLAGS => 4,
4724
						PR_ATTACH_MIME_TAG => $mimeType !== 'plain' ? 'image/' . $mimeType : 'image/png',
4725
					];
4726
					mapi_setprops($inlineImage, $props);
4727
4728
					$stream = mapi_openproperty($inlineImage, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
4729
					mapi_stream_setsize($stream, strlen($rawImage));
4730
					mapi_stream_write($stream, $rawImage);
4731
					mapi_stream_commit($stream);
4732
					mapi_savechanges($inlineImage);
4733
				}
4734
				elseif (str_contains($src, "cid:")) {
4735
					// Check for the cid(there may be http: ) is in the image src. push the cid
4736
					// to $imageIDs array. which further used in clearDeletedInlineAttachments function.
4737
4738
					$firstOffset = strpos($src, ":") + 1;
4739
					$cid = substr($src, $firstOffset);
4740
					array_push($imageIDs, $cid);
4741
				}
4742
			}
4743
4744
			if ($saveChanges) {
4745
				// Write the <img src="cid:data"> changes to the HTML property
4746
				$body = $doc->saveHTML();
4747
				$stream = mapi_openproperty($message, PR_HTML, IID_IStream, 0, MAPI_MODIFY);
4748
				mapi_stream_setsize($stream, strlen($body));
4749
				mapi_stream_write($stream, $body);
4750
				mapi_stream_commit($stream);
4751
				mapi_savechanges($message);
4752
			}
4753
		}
4754
		$this->clearDeletedInlineAttachments($message, $imageIDs);
4755
	}
4756
4757
	/**
4758
	 * Delete the deleted inline image attachment from attachment store.
4759
	 *
4760
	 * @param MAPIMessage $message  the distribution list message
4761
	 * @param array       $imageIDs Array of existing inline image PR_ATTACH_CONTENT_ID
4762
	 */
4763
	public function clearDeletedInlineAttachments($message, $imageIDs = []) {
4764
		$attachmentTable = mapi_message_getattachmenttable($message);
4765
4766
		$restriction = [RES_AND, [
4767
			[RES_PROPERTY,
4768
				[
4769
					RELOP => RELOP_EQ,
4770
					ULPROPTAG => PR_ATTACHMENT_HIDDEN,
4771
					VALUE => [PR_ATTACHMENT_HIDDEN => true],
4772
				],
4773
			],
4774
			[RES_EXIST,
4775
				[
4776
					ULPROPTAG => PR_ATTACH_CONTENT_ID,
4777
				],
4778
			],
4779
		]];
4780
4781
		$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_CONTENT_ID, PR_ATTACH_NUM], $restriction);
4782
		foreach ($attachments as $attachment) {
4783
			$clearDeletedInlineAttach = array_search($attachment[PR_ATTACH_CONTENT_ID], $imageIDs) === false;
4784
			if ($clearDeletedInlineAttach) {
4785
				mapi_message_deleteattach($message, $attachment[PR_ATTACH_NUM]);
4786
			}
4787
		}
4788
	}
4789
4790
	/**
4791
	 * This function will fetch the user from mapi session and retrieve its LDAP image.
4792
	 * It will return the compressed image using php's GD library.
4793
	 *
4794
	 * @param string $userEntryId       The user entryid which is going to open
4795
	 * @param int    $compressedQuality The compression factor ranges from 0 (high) to 100 (low)
4796
	 *                                  Default value is set to 10 which is nearly
4797
	 *                                  extreme compressed image
4798
	 *
4799
	 * @return string A base64 encoded string (data url)
4800
	 */
4801
	public function getCompressedUserImage($userEntryId, $compressedQuality = 10) {
4802
		try {
4803
			$user = $GLOBALS['mapisession']->getUser($userEntryId);
4804
		}
4805
		catch (Exception $e) {
4806
			$msg = "Problem while getting a user from the addressbook. Error %s : %s.";
4807
			$formattedMsg = sprintf($msg, $e->getCode(), $e->getMessage());
4808
			error_log($formattedMsg);
4809
			Log::Write(LOGLEVEL_ERROR, "Operations:getCompressedUserImage() " . $formattedMsg);
4810
4811
			return "";
4812
		}
4813
4814
		$userImageProp = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]);
4815
		if (isset($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO])) {
4816
			return $this->compressedImage($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO], $compressedQuality);
4817
		}
4818
4819
		return "";
4820
	}
4821
4822
	/**
4823
	 * Function used to compressed the image.
4824
	 *
4825
	 * @param string $image the image which is going to compress
4826
	 * @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...
4827
	 * Default value is set to 10 which is nearly extreme compressed image
4828
	 * @param mixed $compressedQuality
4829
	 *
4830
	 * @return string A base64 encoded string (data url)
4831
	 */
4832
	public function compressedImage($image, $compressedQuality = 10) {
4833
		// Proceed only when GD library's functions and user image data are available.
4834
		if (function_exists('imagecreatefromstring')) {
4835
			try {
4836
				$image = imagecreatefromstring($image);
4837
			}
4838
			catch (Exception $e) {
4839
				$msg = "Problem while creating image from string. Error %s : %s.";
4840
				$formattedMsg = sprintf($msg, $e->getCode(), $e->getMessage());
4841
				error_log($formattedMsg);
4842
				Log::Write(LOGLEVEL_ERROR, "Operations:compressedImage() " . $formattedMsg);
4843
			}
4844
4845
			if ($image !== false) {
4846
				// We need to use buffer because imagejpeg will give output as image in browser or file.
4847
				ob_start();
4848
				imagejpeg($image, null, $compressedQuality);
4849
				$compressedImg = ob_get_contents();
4850
				ob_end_clean();
4851
4852
				// Free up memory space acquired by image.
4853
				imagedestroy($image);
4854
4855
				return strlen($compressedImg) > 0 ? "data:image/jpg;base64," . base64_encode($compressedImg) : "";
4856
			}
4857
		}
4858
4859
		return "";
4860
	}
4861
4862
	public function getPropertiesFromStoreRoot($store, $props) {
4863
		$root = mapi_msgstore_openentry($store);
4864
4865
		return mapi_getprops($root, $props);
4866
	}
4867
4868
	/**
4869
	 * Returns the encryption key for sodium functions.
4870
	 *
4871
	 * It will generate a new one if the user doesn't have an encryption key yet.
4872
	 * It will also save the key into EncryptionStore for this session if the key
4873
	 * wasn't there yet.
4874
	 *
4875
	 * @return string
4876
	 */
4877
	public function getFilesEncryptionKey() {
4878
		// fallback if FILES_ACCOUNTSTORE_V1_SECRET_KEY is defined globally
4879
		$key = defined('FILES_ACCOUNTSTORE_V1_SECRET_KEY') ? hex2bin((string) constant('FILES_ACCOUNTSTORE_V1_SECRET_KEY')) : null;
4880
		if ($key === null) {
4881
			$encryptionStore = EncryptionStore::getInstance();
4882
			$key = $encryptionStore->get('filesenckey');
4883
			if ($key === null) {
4884
				$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
4885
				$props = mapi_getprops($store, [PR_EC_WA_FILES_ENCRYPTION_KEY]);
4886
				if (isset($props[PR_EC_WA_FILES_ENCRYPTION_KEY])) {
4887
					$key = $props[PR_EC_WA_FILES_ENCRYPTION_KEY];
4888
				}
4889
				else {
4890
					$key = sodium_crypto_secretbox_keygen();
4891
					$encryptionStore->add('filesenckey', $key);
4892
					mapi_setprops($store, [PR_EC_WA_FILES_ENCRYPTION_KEY => $key]);
4893
					mapi_savechanges($store);
4894
				}
4895
			}
4896
		}
4897
4898
		return $key;
4899
	}
4900
}
4901