Test Failed
Push — master ( 0b5a40...f1c8a7 )
by
unknown
14:39 queued 14s
created

Operations::deleteMessages()   F

Complexity

Conditions 26
Paths 152

Size

Total Lines 78
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 26
eloc 47
c 1
b 0
f 0
nc 152
nop 5
dl 0
loc 78
rs 3.7333

How to fix   Long Method    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
15
	class Operations {
16
		/**
17
		 * Gets the hierarchy list of all required stores.
18
		 *
19
		 * getHierarchyList builds an entire hierarchy list of all folders that should be shown in various places. Most importantly,
20
		 * it generates the list of folders to be show in the hierarchylistmodule (left-hand folder browser) on the client.
21
		 *
22
		 * It is also used to generate smaller hierarchy lists, for example for the 'create folder' dialog.
23
		 *
24
		 * 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
25
		 * the entryids and the parent_entryids of all the folders to build the tree.
26
		 *
27
		 * The return value is an associated array with the following keys:
28
		 * - store: array of stores
29
		 *
30
		 * Each store contains:
31
		 * - array("store_entryid" => entryid of store, name => name of store, subtree => entryid of viewable root, type => default|public|other, folder_type => "all")
32
		 * - folder: array of folders with each an array of properties (see Operations::setFolder() for properties)
33
		 *
34
		 * @param array  $properties   MAPI property mapping for folders
35
		 * @param int    $type         Which stores to fetch (HIERARCHY_GET_ALL | HIERARCHY_GET_DEFAULT | HIERARCHY_GET_ONE)
36
		 * @param object $store        Only when $type == HIERARCHY_GET_ONE
37
		 * @param array  $storeOptions Only when $type == HIERARCHY_GET_ONE, this overrides the  loading options which is normally
38
		 *                             obtained from the settings for loading the store (e.g. only load calendar).
39
		 * @param string $username     The username
40
		 *
41
		 * @return array Return structure
42
		 */
43
		public function getHierarchyList($properties, $type = HIERARCHY_GET_ALL, $store = null, $storeOptions = null, $username = null) {
44
			switch ($type) {
45
				case HIERARCHY_GET_ALL:
46
					$storelist = $GLOBALS["mapisession"]->getAllMessageStores();
47
					break;
48
49
				case HIERARCHY_GET_DEFAULT:
50
					$storelist = [$GLOBALS["mapisession"]->getDefaultMessageStore()];
51
					break;
52
53
				case HIERARCHY_GET_ONE:
54
					// Get single store and it's archive store as well
55
					$storelist = $GLOBALS["mapisession"]->getSingleMessageStores($store, $storeOptions, $username);
56
					break;
57
			}
58
59
			$data = [];
60
			$data["item"] = [];
61
62
			// Get the other store options
63
			if (isset($storeOptions)) {
64
				$otherUsers = $storeOptions;
65
			}
66
			else {
67
				$otherUsers = $GLOBALS["mapisession"]->retrieveOtherUsersFromSettings();
68
			}
69
70
			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...
71
				$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]);
72
73
				$inboxProps = [];
74
				$storeType = $msgstore_props[PR_MDB_PROVIDER];
75
76
				/*
77
				 * storetype is public and if public folder is disabled
78
				 * then continue in loop for next store.
79
				 */
80
				if ($storeType == ZARAFA_STORE_PUBLIC_GUID && ENABLE_PUBLIC_FOLDERS == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

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

331
									$this->invalidateResponseStore($storeData, $type, /** @scrutinizer ignore-type */ $folderEntryID);
Loading history...
332
333
									// Update the store properties to refer to the shared folder,
334
									// note that we don't care if we have access to the folder or not.
335
									$storeData["props"]["shared_folder_" . $sharedFolder["folder_type"]] = bin2hex($folderEntryID);
336
337
									// Indicate that we don't have access to the store,
338
									// so no more attempts to read properties or open entries.
339
									$store_access = false;
340
341
								// If you access according to the above check, go ahead and retrieve the MAPIFolder object
342
								}
343
								else {
344
									$folderEntryID = hex2bin($storeData["props"]["default_folder_" . $sharedFolder["folder_type"]]);
345
346
									// Update the store properties to refer to the shared folder,
347
									// note that we don't care if we have access to the folder or not.
348
									$storeData["props"]["shared_folder_" . $sharedFolder["folder_type"]] = bin2hex($folderEntryID);
349
350
									try {
351
										// load folder props
352
										$folder = mapi_msgstore_openentry($store, $folderEntryID);
353
									}
354
									catch (MAPIException $e) {
355
										// Add properties to the store response to indicate to the client
356
										// that the store could not be loaded.
357
										$this->invalidateResponseStore($storeData, $type, $folderEntryID);
358
359
										// Indicate that we don't have access to the store,
360
										// so no more attempts to read properties or open entries.
361
										$store_access = false;
362
363
										// We've handled the event
364
										$e->setHandled();
365
									}
366
								}
367
368
								// Check if a error handler already inserted a error folder,
369
								// or if we can insert the real folders here.
370
								if ($store_access === true) {
371
									// check if we need subfolders or not
372
									if ($openSubFolders === true) {
373
										// add folder data (with all subfolders recursively)
374
										// get parent folder's properties
375
										$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...
376
										$tempFolderProps = $this->setFolder($folderProps);
377
378
										array_push($storeData["folders"]["item"], $tempFolderProps);
379
380
										// get subfolders
381
										if ($tempFolderProps["props"]["has_subfolder"] != false) {
382
											$subfoldersData = [];
383
											$subfoldersData["folders"]["item"] = [];
384
											$this->getSubFolders($folder, $store, $properties, $subfoldersData);
385
386
											$storeData["folders"]["item"] = array_merge($storeData["folders"]["item"], $subfoldersData["folders"]["item"]);
387
										}
388
									}
389
									else {
390
										$folderProps = mapi_getprops($folder, $properties);
391
										$tempFolderProps = $this->setFolder($folderProps);
392
										// We don't load subfolders, this means the user isn't allowed
393
										// to create subfolders, as they should normally be hidden immediately.
394
										$tempFolderProps["props"]["access"] = ($tempFolderProps["props"]["access"] & ~MAPI_ACCESS_CREATE_HIERARCHY);
395
										// We don't load subfolders, so force the 'has_subfolder' property
396
										// to be false, so the UI will not consider loading subfolders.
397
										$tempFolderProps["props"]["has_subfolder"] = false;
398
										array_push($storeData["folders"]["item"], $tempFolderProps);
399
									}
400
								}
401
							}
402
						}
403
					}
404
					array_push($data["item"], $storeData);
405
				}
406
			}
407
408
			return $data;
409
		}
410
411
		/**
412
		 * Helper function to get the subfolders of a Personal Store.
413
		 *
414
		 * @param object $folder        mapi Folder Object
415
		 * @param object $store         Message Store Object
416
		 * @param array  $properties    MAPI property mappings for folders
417
		 * @param array  $storeData     Reference to an array. The folder properties are added to this array.
418
		 * @param mixed  $parentEntryid
419
		 */
420
		public function getSubFolders($folder, $store, $properties, &$storeData, $parentEntryid = false) {
421
			/**
422
			 * remove hidden folders, folders with PR_ATTR_HIDDEN property set
423
			 * should not be shown to the client.
424
			 */
425
			$restriction = [RES_OR, [
426
				[RES_PROPERTY,
427
					[
428
						RELOP => RELOP_EQ,
429
						ULPROPTAG => PR_ATTR_HIDDEN,
430
						VALUE => [PR_ATTR_HIDDEN => false],
431
					],
432
				],
433
				[RES_NOT,
434
					[
435
						[RES_EXIST,
436
							[
437
								ULPROPTAG => PR_ATTR_HIDDEN,
438
							],
439
						],
440
					],
441
				],
442
			]];
443
444
			$hierarchyTable = mapi_folder_gethierarchytable($folder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS);
445
			mapi_table_restrict($hierarchyTable, $restriction, TBL_BATCH);
446
447
			// Also request PR_DEPTH
448
			$columns = array_merge($properties, [PR_DEPTH]);
449
450
			mapi_table_setcolumns($hierarchyTable, $columns);
451
			$columns = null;
452
453
			// Load the hierarchy in bulks
454
			$rows = mapi_table_queryrows($hierarchyTable, $columns, 0, 0x7FFFFFFF);
455
456
			foreach ($rows as $subfolder) {
457
				if ($parentEntryid !== false && isset($subfolder[PR_DEPTH]) && $subfolder[PR_DEPTH] === 1) {
458
					$subfolder[PR_PARENT_ENTRYID] = $parentEntryid;
459
				}
460
				array_push($storeData["folders"]["item"], $this->setFolder($subfolder));
461
			}
462
		}
463
464
		/**
465
		 * Convert MAPI properties into useful XML properties for a folder.
466
		 *
467
		 * @param array $folderProps Properties of a folder
468
		 *
469
		 * @return array List of properties of a folder
470
		 *
471
		 * @todo The name of this function is misleading because it doesn't 'set' anything, it just reads some properties.
472
		 */
473
		public function setFolder($folderProps) {
474
			$props = [
475
				// Identification properties
476
				"entryid" => bin2hex($folderProps[PR_ENTRYID]),
477
				"parent_entryid" => bin2hex($folderProps[PR_PARENT_ENTRYID]),
478
				"store_entryid" => bin2hex($folderProps[PR_STORE_ENTRYID]),
479
				// Scalar properties
480
				"props" => [
481
					"display_name" => $folderProps[PR_DISPLAY_NAME],
482
					"object_type" => isset($folderProps[PR_OBJECT_TYPE]) ? $folderProps[PR_OBJECT_TYPE] : MAPI_FOLDER, // FIXME: Why isn't this always set?
483
					"content_count" => isset($folderProps[PR_CONTENT_COUNT]) ? $folderProps[PR_CONTENT_COUNT] : 0,
484
					"content_unread" => isset($folderProps[PR_CONTENT_UNREAD]) ? $folderProps[PR_CONTENT_UNREAD] : 0,
485
					"has_subfolder" => isset($folderProps[PR_SUBFOLDERS]) ? $folderProps[PR_SUBFOLDERS] : false,
486
					"container_class" => isset($folderProps[PR_CONTAINER_CLASS]) ? $folderProps[PR_CONTAINER_CLASS] : "IPF.Note",
487
					"access" => $folderProps[PR_ACCESS],
488
					"rights" => isset($folderProps[PR_RIGHTS]) ? $folderProps[PR_RIGHTS] : ecRightsNone,
489
					"assoc_content_count" => isset($folderProps[PR_ASSOC_CONTENT_COUNT]) ? $folderProps[PR_ASSOC_CONTENT_COUNT] : 0,
490
				],
491
			];
492
493
			$this->setExtendedFolderFlags($folderProps, $props);
494
495
			return $props;
496
		}
497
498
		/**
499
		 * Function is used to retrieve the favorites and search folders from
500
		 * respective favorites(IPM.Microsoft.WunderBar.Link) and search (IPM.Microsoft.WunderBar.SFInfo)
501
		 * link messages which belongs to associated contains table of IPM_COMMON_VIEWS folder.
502
		 *
503
		 * @param object $commonViewFolder MAPI Folder Object in which the favorites link messages lives
504
		 * @param array  $storeData        Reference to an array. The favorites folder properties are added to this array.
505
		 */
506
		public function getFavoritesFolders($commonViewFolder, &$storeData) {
507
			$table = mapi_folder_getcontentstable($commonViewFolder, MAPI_ASSOCIATED);
508
509
			$restriction = [RES_OR,
510
				[
511
					[RES_PROPERTY,
512
						[
513
							RELOP => RELOP_EQ,
514
							ULPROPTAG => PR_MESSAGE_CLASS,
515
							VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link"],
516
						],
517
					],
518
					[RES_PROPERTY,
519
						[
520
							RELOP => RELOP_EQ,
521
							ULPROPTAG => PR_MESSAGE_CLASS,
522
							VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo"],
523
						],
524
					],
525
				],
526
			];
527
528
			// Get hierarchy table from all FINDERS_ROOT folders of
529
			// all message stores.
530
			$stores = $GLOBALS["mapisession"]->getAllMessageStores();
531
			$finderHierarchyTables = [];
532
			foreach ($stores as $entryid => $store) {
533
				$props = mapi_getprops($store, [PR_DEFAULT_STORE, PR_FINDER_ENTRYID]);
534
				if (!$props[PR_DEFAULT_STORE]) {
535
					continue;
536
				}
537
538
				try {
539
					$finderFolder = mapi_msgstore_openentry($store, $props[PR_FINDER_ENTRYID]);
540
					$hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS);
541
					$finderHierarchyTables[$props[PR_FINDER_ENTRYID]] = $hierarchyTable;
542
				}
543
				catch (MAPIException $e) {
544
					$e->setHandled();
545
					$props = mapi_getprops($store, [PR_DISPLAY_NAME]);
546
					error_log(sprintf("Unable to open FINDER_ROOT for store \"%s\": %s (%s)",
547
						$props[PR_DISPLAY_NAME], mapi_strerror($e->getCode()),
548
						get_mapi_error_name($e->getCode())));
549
				}
550
			}
551
552
			$rows = mapi_table_queryallrows($table, $GLOBALS["properties"]->getFavoritesFolderProperties(), $restriction);
553
			$faultyLinkMsg = [];
554
			foreach ($rows as $row) {
555
				if (isset($row[PR_WLINK_TYPE]) && $row[PR_WLINK_TYPE] > wblSharedFolder) {
556
					continue;
557
				}
558
559
				try {
560
					if ($row[PR_MESSAGE_CLASS] === "IPM.Microsoft.WunderBar.Link") {
561
						// Find faulty link messages which does not linked to any message. if link message
562
						// does not contains store entryid in which actual message is located then it consider as
563
						// faulty link message.
564
						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...
565
							!isset($row[PR_WLINK_STORE_ENTRYID])) {
566
							array_push($faultyLinkMsg, $row[PR_ENTRYID]);
567
568
							continue;
569
						}
570
						$props = $this->getFavoriteLinkedFolderProps($row);
571
						if (empty($props)) {
572
							continue;
573
						}
574
					}
575
					elseif ($row[PR_MESSAGE_CLASS] === "IPM.Microsoft.WunderBar.SFInfo") {
576
						$props = $this->getFavoritesLinkedSearchFolderProps($row[PR_WB_SF_ID], $finderHierarchyTables);
577
						if (empty($props)) {
578
							continue;
579
						}
580
					}
581
				}
582
				catch (MAPIException $e) {
583
					continue;
584
				}
585
586
				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...
587
			}
588
589
			if (!empty($faultyLinkMsg)) {
590
				// remove faulty link messages from common view folder.
591
				mapi_folder_deletemessages($commonViewFolder, $faultyLinkMsg);
592
			}
593
		}
594
595
		/**
596
		 * Function which checks whether given linked Message is faulty or not.
597
		 * It will store faulty linked messages in given &$faultyLinkMsg array.
598
		 * Returns true if linked message of favorite item is faulty.
599
		 *
600
		 * @param array  &$faultyLinkMsg   reference in which faulty linked messages will be stored
601
		 * @param array  $allMessageStores Associative array with entryid -> mapistore of all open stores (private, public, delegate)
602
		 * @param object $linkedMessage    link message which belongs to associated contains table of IPM_COMMON_VIEWS folder
603
		 *
604
		 * @return true if linked message of favorite item is faulty or false
605
		 */
606
		public function checkFaultyFavoritesLinkedFolder(&$faultyLinkMsg, $allMessageStores, $linkedMessage) {
607
			// Find faulty link messages which does not linked to any message. if link message
608
			// does not contains store entryid in which actual message is located then it consider as
609
			// faulty link message.
610
			if (isset($linkedMessage[PR_WLINK_STORE_ENTRYID]) && empty($linkedMessage[PR_WLINK_STORE_ENTRYID])) {
611
				array_push($faultyLinkMsg, $linkedMessage[PR_ENTRYID]);
612
613
				return true;
614
			}
615
616
			// Check if store of a favorite Item does not exist in Hierarchy then
617
			// delete link message of that favorite item.
618
			// i.e. If a user is unhooked then remove its favorite items.
619
			$storeExist = array_key_exists($linkedMessage[PR_WLINK_STORE_ENTRYID], $allMessageStores);
620
			if (!$storeExist) {
621
				array_push($faultyLinkMsg, $linkedMessage[PR_ENTRYID]);
622
623
				return true;
624
			}
625
626
			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...
627
		}
628
629
		/**
630
		 * Function which get the favorites marked folders from favorites link message
631
		 * which belongs to associated contains table of IPM_COMMON_VIEWS folder.
632
		 *
633
		 * @param array $linkMessageProps properties of link message which belongs to
634
		 *                                associated contains table of IPM_COMMON_VIEWS folder
635
		 *
636
		 * @return array List of properties of a folder
637
		 */
638
		public function getFavoriteLinkedFolderProps($linkMessageProps) {
639
			// In webapp we use IPM_SUBTREE as root folder for the Hierarchy but OL is use IMsgStore as a
640
			// Root folder. OL never mark favorites to IPM_SUBTREE. So to make favorites compatible with OL
641
			// we need this check.
642
			// Here we check PR_WLINK_STORE_ENTRYID and PR_WLINK_ENTRYID is same. Which same only in one case
643
			// where some user has mark favorites to root(Inbox-<user name>) folder from OL. So here if condition
644
			// gets true we get the IPM_SUBTREE and send it to response as favorites folder to webapp.
645
			try {
646
				if ($GLOBALS['entryid']->compareEntryIds($linkMessageProps[PR_WLINK_STORE_ENTRYID], $linkMessageProps[PR_WLINK_ENTRYID])) {
647
					$storeObj = $GLOBALS["mapisession"]->openMessageStore($linkMessageProps[PR_WLINK_STORE_ENTRYID]);
648
					$subTreeEntryid = mapi_getprops($storeObj, [PR_IPM_SUBTREE_ENTRYID]);
649
					$folderObj = mapi_msgstore_openentry($storeObj, $subTreeEntryid[PR_IPM_SUBTREE_ENTRYID]);
650
				}
651
				else {
652
					$storeObj = $GLOBALS["mapisession"]->openMessageStore($linkMessageProps[PR_WLINK_STORE_ENTRYID]);
653
					if (!is_resource($storeObj)) {
654
						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...
655
					}
656
					$folderObj = mapi_msgstore_openentry($storeObj, $linkMessageProps[PR_WLINK_ENTRYID]);
657
				}
658
659
				return mapi_getprops($folderObj, $GLOBALS["properties"]->getFavoritesFolderProperties());
660
			}
661
			catch (Exception $e) {
662
				// in some cases error_log was causing an endless loop, so disable it for now
663
				// error_log($e);
664
			}
665
666
			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...
667
		}
668
669
		/**
670
		 * Function which retrieve the search folder from FINDERS_ROOT folder of all open
671
		 * message store.
672
		 *
673
		 * @param Binary $searchFolderId        contains a GUID that identifies the search folder.
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...
674
		 *                                      The value of this property MUST NOT change.
675
		 * @param array  $finderHierarchyTables hierarchy tables which belongs to FINDERS_ROOT
676
		 *                                      folder of message stores
677
		 *
678
		 * @return array list of search folder properties
679
		 */
680
		public function getFavoritesLinkedSearchFolderProps($searchFolderId, $finderHierarchyTables) {
681
			$restriction = [RES_EXIST,
682
				[
683
					ULPROPTAG => PR_EXTENDED_FOLDER_FLAGS,
684
				],
685
			];
686
687
			foreach ($finderHierarchyTables as $finderEntryid => $hierarchyTable) {
688
				$rows = mapi_table_queryallrows($hierarchyTable, $GLOBALS["properties"]->getFavoritesFolderProperties(), $restriction);
689
				foreach ($rows as $row) {
690
					$flags = unpack("H2ExtendedFlags-Id/H2ExtendedFlags-Cb/H8ExtendedFlags-Data/H2SearchFolderTag-Id/H2SearchFolderTag-Cb/H8SearchFolderTag-Data/H2SearchFolderId-Id/H2SearchFolderId-Cb/H32SearchFolderId-Data", $row[PR_EXTENDED_FOLDER_FLAGS]);
691
					if ($flags["SearchFolderId-Data"] === bin2hex($searchFolderId)) {
692
						return $row;
693
					}
694
				}
695
			}
696
		}
697
698
		/**
699
		 * Create link messages for default favorites(Inbox and Sent Items) folders in associated contains table of IPM_COMMON_VIEWS folder
700
		 * and remove all other link message from the same.
701
		 *
702
		 * @param string $commonViewFolderEntryid IPM_COMMON_VIEWS folder entryid
703
		 * @param object $store                   Message Store Object
704
		 * @param array  $storeData               the store data which use to create restriction
705
		 */
706
		public function setDefaultFavoritesFolder($commonViewFolderEntryid, $store, $storeData) {
707
			if ($GLOBALS["settings"]->get("zarafa/v1/contexts/hierarchy/show_default_favorites") !== false) {
708
				$commonViewFolder = mapi_msgstore_openentry($store, $commonViewFolderEntryid);
709
710
				$inboxFolderEntryid = hex2bin($storeData["props"]["default_folder_inbox"]);
711
				$sentFolderEntryid = hex2bin($storeData["props"]["default_folder_sent"]);
712
713
				$table = mapi_folder_getcontentstable($commonViewFolder, MAPI_ASSOCIATED);
714
715
				// Restriction for get all link message(IPM.Microsoft.WunderBar.Link)
716
				// and search link message (IPM.Microsoft.WunderBar.SFInfo) from
717
				// Associated contains table of IPM_COMMON_VIEWS folder.
718
				$findLinkMsgRestriction = [RES_OR,
719
					[
720
						[RES_PROPERTY,
721
							[
722
								RELOP => RELOP_EQ,
723
								ULPROPTAG => PR_MESSAGE_CLASS,
724
								VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link"],
725
							],
726
						],
727
						[RES_PROPERTY,
728
							[
729
								RELOP => RELOP_EQ,
730
								ULPROPTAG => PR_MESSAGE_CLASS,
731
								VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo"],
732
							],
733
						],
734
					],
735
				];
736
737
				// Restriction for find Inbox and/or Sent folder link message from
738
				// Associated contains table of IPM_COMMON_VIEWS folder.
739
				$findInboxOrSentLinkMessage = [RES_OR,
740
					[
741
						[RES_PROPERTY,
742
							[
743
								RELOP => RELOP_EQ,
744
								ULPROPTAG => PR_WLINK_ENTRYID,
745
								VALUE => [PR_WLINK_ENTRYID => $inboxFolderEntryid],
746
							],
747
						],
748
						[RES_PROPERTY,
749
							[
750
								RELOP => RELOP_EQ,
751
								ULPROPTAG => PR_WLINK_ENTRYID,
752
								VALUE => [PR_WLINK_ENTRYID => $sentFolderEntryid],
753
							],
754
						],
755
					],
756
				];
757
758
				// Restriction to get all link messages except Inbox and Sent folder's link messages from
759
				// Associated contains table of IPM_COMMON_VIEWS folder, if exist in it.
760
				$restriction = [RES_AND,
761
					[
762
						$findLinkMsgRestriction,
763
						[RES_NOT,
764
							[
765
								$findInboxOrSentLinkMessage,
766
							],
767
						],
768
					],
769
				];
770
771
				$rows = mapi_table_queryallrows($table, [PR_ENTRYID], $restriction);
772
				if (!empty($rows)) {
773
					$deleteMessages = [];
774
					foreach ($rows as $row) {
775
						array_push($deleteMessages, $row[PR_ENTRYID]);
776
					}
777
					mapi_folder_deletemessages($commonViewFolder, $deleteMessages);
778
				}
779
780
				// We need to remove all search folder from FIND_ROOT(search root folder)
781
				// when reset setting was triggered because on reset setting we remove all
782
				// link messages from common view folder which are linked with either
783
				// favorites or search folder.
784
				$finderFolderEntryid = hex2bin($storeData["props"]["finder_entryid"]);
785
				$finderFolder = mapi_msgstore_openentry($store, $finderFolderEntryid);
786
				$hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS);
787
				$folders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]);
788
				foreach ($folders as $folder) {
789
					try {
790
						mapi_folder_deletefolder($finderFolder, $folder[PR_ENTRYID]);
791
					}
792
					catch (MAPIException $e) {
793
						$msg = "Problem in deleting search folder while reset settings. MAPI Error %s.";
794
						$formattedMsg = sprintf($msg, get_mapi_error_name($e->getCode()));
795
						error_log($formattedMsg);
796
						Log::Write(LOGLEVEL_ERROR, "Operations:setDefaultFavoritesFolder() " . $formattedMsg);
797
					}
798
				}
799
				// Restriction used to find only Inbox and Sent folder's link messages from
800
				// Associated contains table of IPM_COMMON_VIEWS folder, if exist in it.
801
				$restriction = [RES_AND,
802
					[
803
						$findLinkMsgRestriction,
804
						$findInboxOrSentLinkMessage,
805
					],
806
				];
807
808
				$rows = mapi_table_queryallrows($table, [PR_WLINK_ENTRYID], $restriction);
809
810
				// If Inbox and Sent folder's link messages are not exist then create the
811
				// link message for those in associated contains table of IPM_COMMON_VIEWS folder.
812
				if (empty($rows)) {
813
					$defaultFavFoldersKeys = ["inbox", "sent"];
814
					foreach ($defaultFavFoldersKeys as $folderKey) {
815
						$folderObj = $GLOBALS["mapisession"]->openMessage(hex2bin($storeData["props"]["default_folder_" . $folderKey]));
816
						$props = mapi_getprops($folderObj, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]);
817
						$this->createFavoritesLink($commonViewFolder, $props);
818
					}
819
				}
820
				elseif (count($rows) < 2) {
821
					// If rows count is less than 2 it means associated contains table of IPM_COMMON_VIEWS folder
822
					// can have either Inbox or Sent folder link message in it. So we have to create link message
823
					// for Inbox or Sent folder which ever not exist in associated contains table of IPM_COMMON_VIEWS folder
824
					// to maintain default favorites folder.
825
					$row = $rows[0];
826
					$wlinkEntryid = $row[PR_WLINK_ENTRYID];
827
828
					$isInboxFolder = $GLOBALS['entryid']->compareEntryIds($wlinkEntryid, $inboxFolderEntryid);
829
830
					if (!$isInboxFolder) {
831
						$folderObj = $GLOBALS["mapisession"]->openMessage($inboxFolderEntryid);
832
					}
833
					else {
834
						$folderObj = $GLOBALS["mapisession"]->openMessage($sentFolderEntryid);
835
					}
836
837
					$props = mapi_getprops($folderObj, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]);
838
					$this->createFavoritesLink($commonViewFolder, $props);
839
				}
840
				$GLOBALS["settings"]->set("zarafa/v1/contexts/hierarchy/show_default_favorites", false, true);
841
			}
842
		}
843
844
		/**
845
		 * Create favorites link message (IPM.Microsoft.WunderBar.Link) or
846
		 * search link message ("IPM.Microsoft.WunderBar.SFInfo") in associated
847
		 * contains table of IPM_COMMON_VIEWS folder.
848
		 *
849
		 * @param object      $commonViewFolder MAPI Message Folder Object
850
		 * @param array       $folderProps      Properties of a folder
851
		 * @param bool|string $searchFolderId   search folder id which is used to identify the
852
		 *                                      linked search folder from search link message. by default it is false.
853
		 */
854
		public function createFavoritesLink($commonViewFolder, $folderProps, $searchFolderId = false) {
855
			if ($searchFolderId) {
856
				$props = [
857
					PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo",
858
					PR_WB_SF_ID => $searchFolderId,
859
					PR_WLINK_TYPE => wblSearchFolder,
860
				];
861
			}
862
			else {
863
				$defaultStoreEntryId = hex2bin($GLOBALS['mapisession']->getDefaultMessageStoreEntryId());
864
				$props = [
865
					PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link",
866
					PR_WLINK_ENTRYID => $folderProps[PR_ENTRYID],
867
					PR_WLINK_STORE_ENTRYID => $folderProps[PR_STORE_ENTRYID],
868
					PR_WLINK_TYPE => $GLOBALS['entryid']->compareEntryIds($defaultStoreEntryId, $folderProps[PR_STORE_ENTRYID]) ? wblNormalFolder : wblSharedFolder,
869
				];
870
			}
871
872
			$favoritesLinkMsg = mapi_folder_createmessage($commonViewFolder, MAPI_ASSOCIATED);
873
			mapi_setprops($favoritesLinkMsg, $props);
874
			mapi_savechanges($favoritesLinkMsg);
875
		}
876
877
		/**
878
		 * Convert MAPI properties into useful and human readable string for favorites folder.
879
		 *
880
		 * @param array $folderProps Properties of a folder
881
		 *
882
		 * @return array List of properties of a folder
883
		 */
884
		public function setFavoritesFolder($folderProps) {
885
			$props = $this->setFolder($folderProps);
886
			// Add and Make isFavorites to true, this allows the client to properly
887
			// indicate to the user that this is a favorites item/folder.
888
			$props["props"]["isFavorites"] = true;
889
			$props["props"]["folder_type"] = $folderProps[PR_FOLDER_TYPE];
890
891
			return $props;
892
		}
893
894
		/**
895
		 * Fetches extended flags for folder. If PR_EXTENDED_FLAGS is not set then we assume that client
896
		 * should handle which property to display.
897
		 *
898
		 * @param array $folderProps Properties of a folder
899
		 * @param array $props       properties in which flags should be set
900
		 */
901
		public function setExtendedFolderFlags($folderProps, &$props) {
902
			if (isset($folderProps[PR_EXTENDED_FOLDER_FLAGS])) {
903
				$flags = unpack("Cid/Cconst/Cflags", $folderProps[PR_EXTENDED_FOLDER_FLAGS]);
904
905
				// ID property is '1' this means 'Data' property contains extended flags
906
				if ($flags["id"] == 1) {
907
					$props["props"]["extended_flags"] = $flags["flags"];
908
				}
909
			}
910
		}
911
912
		/**
913
		 * Used to update the storeData with a folder and properties that will
914
		 * inform the user that the store could not be opened.
915
		 *
916
		 * @param array  &$storeData    The store data which will be updated
917
		 * @param string $folderType    The foldertype which was attempted to be loaded
918
		 * @param array  $folderEntryID The entryid of the which was attempted to be opened
919
		 */
920
		public function invalidateResponseStore(&$storeData, $folderType, $folderEntryID) {
921
			$folderName = "Folder";
922
			$containerClass = "IPF.Note";
923
924
			switch ($folderType) {
925
				case "all":
926
					$folderName = "IPM_SUBTREE";
927
					$containerClass = "IPF.Note";
928
					break;
929
930
				case "calendar":
931
					$folderName = _("Calendar");
932
					$containerClass = "IPF.Appointment";
933
					break;
934
935
				case "contact":
936
					$folderName = _("Contacts");
937
					$containerClass = "IPF.Contact";
938
					break;
939
940
				case "inbox":
941
					$folderName = _("Inbox");
942
					$containerClass = "IPF.Note";
943
					break;
944
945
				case "note":
946
					$folderName = _("Notes");
947
					$containerClass = "IPF.StickyNote";
948
					break;
949
950
				case "task":
951
					$folderName = _("Tasks");
952
					$containerClass = "IPF.Task";
953
					break;
954
			}
955
956
			// Insert a fake folder which will be shown to the user
957
			// to acknowledge that he has a shared store, but also
958
			// to indicate that he can't open it.
959
			$tempFolderProps = $this->setFolder([
960
				PR_ENTRYID => $folderEntryID,
961
				PR_PARENT_ENTRYID => hex2bin($storeData["props"]["subtree_entryid"]),
962
				PR_STORE_ENTRYID => hex2bin($storeData["store_entryid"]),
963
				PR_DISPLAY_NAME => $folderName,
964
				PR_OBJECT_TYPE => MAPI_FOLDER,
965
				PR_SUBFOLDERS => false,
966
				PR_CONTAINER_CLASS => $containerClass,
967
				PR_ACCESS => 0,
968
			]);
969
970
			// Mark the folder as unavailable, this allows the client to properly
971
			// indicate to the user that this is a fake entry.
972
			$tempFolderProps['props']['is_unavailable'] = true;
973
974
			array_push($storeData["folders"]["item"], $tempFolderProps);
975
976
			/* TRANSLATORS: This indicates that the opened folder belongs to a particular user,
977
			 * for example: 'Calendar of Holiday', in this case %1$s is 'Calendar' (the foldername)
978
			 * and %2$s is 'Holiday' (the username).
979
			 */
980
			$storeData["props"]["display_name"] = ($folderType === "all") ? $storeData["props"]["display_name"] : sprintf(_('%1$s of %2$s'), $folderName, $storeData["props"]["mailbox_owner_name"]);
981
			$storeData["props"]["subtree_entryid"] = $tempFolderProps["parent_entryid"];
982
			$storeData["props"]["folder_type"] = $folderType;
983
		}
984
985
		/**
986
		 * Used to update the storeData with a folder and properties that will function as a
987
		 * placeholder for the IPMSubtree that could not be opened.
988
		 *
989
		 * @param array &$storeData    The store data which will be updated
990
		 * @param array $folderEntryID The entryid of the which was attempted to be opened
991
		 */
992
		public function getDummyIPMSubtreeFolder(&$storeData, $folderEntryID) {
993
			// Insert a fake folder which will be shown to the user
994
			// to acknowledge that he has a shared store.
995
			$tempFolderProps = $this->setFolder([
996
				PR_ENTRYID => $folderEntryID,
997
				PR_PARENT_ENTRYID => hex2bin($storeData["props"]["subtree_entryid"]),
998
				PR_STORE_ENTRYID => hex2bin($storeData["store_entryid"]),
999
				PR_DISPLAY_NAME => "IPM_SUBTREE",
1000
				PR_OBJECT_TYPE => MAPI_FOLDER,
1001
				PR_SUBFOLDERS => true,
1002
				PR_CONTAINER_CLASS => "IPF.Note",
1003
				PR_ACCESS => 0,
1004
			]);
1005
1006
			array_push($storeData["folders"]["item"], $tempFolderProps);
1007
			$storeData["props"]["subtree_entryid"] = $tempFolderProps["parent_entryid"];
1008
		}
1009
1010
		/**
1011
		 * Create a MAPI folder.
1012
		 *
1013
		 * This function simply creates a MAPI folder at a specific location with a specific folder
1014
		 * type.
1015
		 *
1016
		 * @param object $store         MAPI Message Store Object in which the folder lives
1017
		 * @param string $parententryid The parent entryid in which the new folder should be created
1018
		 * @param string $name          The name of the new folder
1019
		 * @param string $type          The type of the folder (PR_CONTAINER_CLASS, so value should be 'IPM.Appointment', etc)
1020
		 * @param array  $folderProps   reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of new folder
1021
		 *
1022
		 * @return bool true if action succeeded, false if not
1023
		 */
1024
		public function createFolder($store, $parententryid, $name, $type, &$folderProps) {
1025
			$result = false;
1026
			$folder = mapi_msgstore_openentry($store, $parententryid);
1027
1028
			if ($folder) {
1029
				/**
1030
				 * @TODO: If parent folder has any sub-folder with the same name than this will return
1031
				 * MAPI_E_COLLISION error, so show this error to client and don't close the dialog.
1032
				 */
1033
				$new_folder = mapi_folder_createfolder($folder, $name);
1034
1035
				if ($new_folder) {
1036
					mapi_setprops($new_folder, [PR_CONTAINER_CLASS => $type]);
1037
					$result = true;
1038
1039
					$folderProps = mapi_getprops($new_folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1040
				}
1041
			}
1042
1043
			return $result;
1044
		}
1045
1046
		/**
1047
		 * Rename a folder.
1048
		 *
1049
		 * This function renames the specified folder. However, a conflict situation can arise
1050
		 * if the specified folder name already exists. In this case, the folder name is postfixed with
1051
		 * an ever-higher integer to create a unique folder name.
1052
		 *
1053
		 * @param object $store       MAPI Message Store Object
1054
		 * @param string $entryid     The entryid of the folder to rename
1055
		 * @param string $name        The new name of the folder
1056
		 * @param array  $folderProps reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID
1057
		 *
1058
		 * @return bool true if action succeeded, false if not
1059
		 */
1060
		public function renameFolder($store, $entryid, $name, &$folderProps) {
1061
			$folder = mapi_msgstore_openentry($store, $entryid);
1062
			if (!$folder)
1063
				return false;
1064
			$result = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
1065
			$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]);
1066
			/*
1067
			 * If parent folder has any sub-folder with the same name than this will return
1068
			 * MAPI_E_COLLISION error while renaming folder, so show this error to client,
1069
			 * and revert changes in view.
1070
			 */
1071
			try {
1072
				mapi_setprops($folder, [PR_DISPLAY_NAME => $name]);
1073
				mapi_savechanges($folder);
1074
				$result = true;
1075
			}
1076
			catch (MAPIException $e) {
1077
				if ($e->getCode() == MAPI_E_COLLISION) {
1078
					/*
1079
					 * revert folder name to original one
1080
					 * There is a bug in php-mapi that updates folder name in hierarchy table with null value
1081
					 * so we need to revert those change by again setting the old folder name
1082
					 * (ZCP-11586)
1083
					 */
1084
					mapi_setprops($folder, [PR_DISPLAY_NAME => $folderProps[PR_DISPLAY_NAME]]);
1085
					mapi_savechanges($folder);
1086
				}
1087
1088
				// rethrow exception so we will send error to client
1089
				throw $e;
1090
			}
1091
			return $result;
1092
		}
1093
1094
		/**
1095
		 * Check if a folder is 'special'.
1096
		 *
1097
		 * All default MAPI folders such as 'inbox', 'outbox', etc have special permissions; you can not rename them for example. This
1098
		 * function returns TRUE if the specified folder is 'special'.
1099
		 *
1100
		 * @param object $store   MAPI Message Store Object
1101
		 * @param string $entryid The entryid of the folder
1102
		 *
1103
		 * @return bool true if folder is a special folder, false if not
1104
		 */
1105
		public function isSpecialFolder($store, $entryid) {
1106
			$msgstore_props = mapi_getprops($store, [PR_MDB_PROVIDER]);
1107
1108
			// "special" folders don't exists in public store
1109
			if ($msgstore_props[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
1110
				return false;
1111
			}
1112
1113
			// Check for the Special folders which are provided on the store
1114
			$msgstore_props = mapi_getprops($store, [
1115
				PR_IPM_SUBTREE_ENTRYID,
1116
				PR_IPM_OUTBOX_ENTRYID,
1117
				PR_IPM_SENTMAIL_ENTRYID,
1118
				PR_IPM_WASTEBASKET_ENTRYID,
1119
				PR_IPM_PUBLIC_FOLDERS_ENTRYID,
1120
				PR_IPM_FAVORITES_ENTRYID,
1121
			]);
1122
1123
			if (array_search($entryid, $msgstore_props)) {
1124
				return true;
1125
			}
1126
1127
			// Check for the Special folders which are provided on the root folder
1128
			$root = mapi_msgstore_openentry($store, null);
1129
			$rootProps = mapi_getprops($root, [
1130
				PR_IPM_APPOINTMENT_ENTRYID,
1131
				PR_IPM_CONTACT_ENTRYID,
1132
				PR_IPM_DRAFTS_ENTRYID,
1133
				PR_IPM_JOURNAL_ENTRYID,
1134
				PR_IPM_NOTE_ENTRYID,
1135
				PR_IPM_TASK_ENTRYID,
1136
				PR_ADDITIONAL_REN_ENTRYIDS,
1137
			]);
1138
1139
			if (array_search($entryid, $rootProps)) {
1140
				return true;
1141
			}
1142
1143
			// The PR_ADDITIONAL_REN_ENTRYIDS are a bit special
1144
			if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS]) && is_array($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
1145
				if (array_search($entryid, $rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
1146
					return true;
1147
				}
1148
			}
1149
1150
			// Check if the given folder is the inbox, note that we are unsure
1151
			// if we have permissions on that folder, so we need a try catch.
1152
			try {
1153
				$inbox = mapi_msgstore_getreceivefolder($store);
1154
				$props = mapi_getprops($inbox, [PR_ENTRYID]);
1155
1156
				if ($props[PR_ENTRYID] == $entryid) {
1157
					return true;
1158
				}
1159
			}
1160
			catch (MAPIException $e) {
1161
				if ($e->getCode() !== MAPI_E_NO_ACCESS) {
1162
					throw $e;
1163
				}
1164
			}
1165
1166
			return false;
1167
		}
1168
1169
		/**
1170
		 * Delete a folder.
1171
		 *
1172
		 * Deleting a folder normally just moves the folder to the wastebasket, which is what this function does. However,
1173
		 * if the folder was already in the wastebasket, then the folder is really deleted.
1174
		 *
1175
		 * @param object $store         MAPI Message Store Object
1176
		 * @param string $parententryid The parent in which the folder should be deleted
1177
		 * @param string $entryid       The entryid of the folder which will be deleted
1178
		 * @param array  $folderProps   reference to an array which will be filled with PR_ENTRYID, PR_STORE_ENTRYID of the deleted object
1179
		 * @param bool   $softDelete    flag for indicating that folder should be soft deleted which can be recovered from
1180
		 *                              restore deleted items
1181
		 * @param bool   $hardDelete    flag for indicating that folder should be hard deleted from system and can not be
1182
		 *                              recovered from restore soft deleted items
1183
		 *
1184
		 * @return bool true if action succeeded, false if not
1185
		 *
1186
		 * @todo subfolders of folders in the wastebasket should also be hard-deleted
1187
		 */
1188
		public function deleteFolder($store, $parententryid, $entryid, &$folderProps, $softDelete = false, $hardDelete = false) {
1189
			$result = false;
1190
			$msgprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID]);
1191
			$folder = mapi_msgstore_openentry($store, $parententryid);
1192
1193
			if ($folder && !$this->isSpecialFolder($store, $entryid)) {
1194
				if ($hardDelete === true) {
1195
					// hard delete the message if requested
1196
					// beware that folder can not be recovered after this and will be deleted from system entirely
1197
					if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS | DELETE_HARD_DELETE)) {
1198
						$result = true;
1199
1200
						// if exists, also delete settings made for this folder (client don't need an update for this)
1201
						$GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid));
1202
					}
1203
				}
1204
				else {
1205
					if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID])) {
1206
						// TODO: check if not only $parententryid=wastebasket, but also the parents of that parent...
1207
						// if folder is already in wastebasket or softDelete is requested then delete the message
1208
						if ($msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete === true) {
1209
							if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS)) {
1210
								$result = true;
1211
1212
								// if exists, also delete settings made for this folder (client don't need an update for this)
1213
								$GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid));
1214
							}
1215
						}
1216
						else {
1217
							// move the folder to wastebasket
1218
							$wastebasket = mapi_msgstore_openentry($store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID]);
1219
1220
							$deleted_folder = mapi_msgstore_openentry($store, $entryid);
1221
							$props = mapi_getprops($deleted_folder, [PR_DISPLAY_NAME]);
1222
1223
							try {
1224
								/*
1225
								 * To decrease overload of checking for conflicting folder names on modification of every folder
1226
								 * we should first try to copy folder and if it returns MAPI_E_COLLISION then
1227
								 * only we should check for the conflicting folder names and generate a new name
1228
								 * and copy folder with the generated name.
1229
								 */
1230
								mapi_folder_copyfolder($folder, $entryid, $wastebasket, $props[PR_DISPLAY_NAME], FOLDER_MOVE);
1231
								$folderProps = mapi_getprops($deleted_folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1232
								$result = true;
1233
							}
1234
							catch (MAPIException $e) {
1235
								if ($e->getCode() == MAPI_E_COLLISION) {
1236
									$foldername = $this->checkFolderNameConflict($store, $wastebasket, $props[PR_DISPLAY_NAME]);
1237
1238
									mapi_folder_copyfolder($folder, $entryid, $wastebasket, $foldername, FOLDER_MOVE);
1239
									$folderProps = mapi_getprops($deleted_folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1240
									$result = true;
1241
								}
1242
								else {
1243
									// all other errors should be propagated to higher level exception handlers
1244
									throw $e;
1245
								}
1246
							}
1247
						}
1248
					}
1249
					else {
1250
						if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS)) {
1251
							$result = true;
1252
1253
							// if exists, also delete settings made for this folder (client don't need an update for this)
1254
							$GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid));
1255
						}
1256
					}
1257
				}
1258
			}
1259
1260
			return $result;
1261
		}
1262
1263
		/**
1264
		 * Empty folder.
1265
		 *
1266
		 * Removes all items from a folder. This is a real delete, not a move.
1267
		 *
1268
		 * @param object $store           MAPI Message Store Object
1269
		 * @param string $entryid         The entryid of the folder which will be emptied
1270
		 * @param array  $folderProps     reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of the emptied folder
1271
		 * @param bool   $hardDelete      flag to indicate if messages will be hard deleted and can not be recoved using restore soft deleted items
1272
		 * @param bool   $emptySubFolders true to remove all messages with child folders of selected folder else false will
1273
		 *                                remove only message of selected folder
1274
		 *
1275
		 * @return bool true if action succeeded, false if not
1276
		 */
1277
		public function emptyFolder($store, $entryid, &$folderProps, $hardDelete = false, $emptySubFolders = true) {
1278
			$result = false;
1279
			$folder = mapi_msgstore_openentry($store, $entryid);
1280
1281
			if ($folder) {
1282
				$flag = DEL_ASSOCIATED;
1283
1284
				if ($hardDelete) {
1285
					$flag |= DELETE_HARD_DELETE;
1286
				}
1287
1288
				if ($emptySubFolders) {
1289
					$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...
1290
				}
1291
				else {
1292
					// Delete all items of selected folder without
1293
					// removing child folder and it's content.
1294
					// FIXME: it is effecting performance because mapi_folder_emptyfolder function not provide facility to
1295
					// remove only selected folder items without touching child folder and it's items.
1296
					// for more check KC-1268
1297
					$table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS);
1298
					$rows = mapi_table_queryallrows($table, [PR_ENTRYID]);
1299
					$messages = [];
1300
					foreach ($rows as $row) {
1301
						array_push($messages, $row[PR_ENTRYID]);
1302
					}
1303
					$result = mapi_folder_deletemessages($folder, $messages, $flag);
1304
				}
1305
1306
				$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1307
				$result = true;
1308
			}
1309
1310
			return $result;
1311
		}
1312
1313
		/**
1314
		 * Copy or move a folder.
1315
		 *
1316
		 * @param object $store               MAPI Message Store Object
1317
		 * @param string $parentfolderentryid The parent entryid of the folder which will be copied or moved
1318
		 * @param string $sourcefolderentryid The entryid of the folder which will be copied or moved
1319
		 * @param string $destfolderentryid   The entryid of the folder which the folder will be copied or moved to
1320
		 * @param bool   $moveFolder          true - move folder, false - copy folder
1321
		 * @param array  $folderProps         reference to an array which will be filled with entryids
1322
		 * @param mixed  $deststore
1323
		 *
1324
		 * @return bool true if action succeeded, false if not
1325
		 */
1326
		public function copyFolder($store, $parentfolderentryid, $sourcefolderentryid, $destfolderentryid, $deststore, $moveFolder, &$folderProps) {
1327
			$result = false;
1328
			$sourceparentfolder = mapi_msgstore_openentry($store, $parentfolderentryid);
1329
			$destfolder = mapi_msgstore_openentry($deststore, $destfolderentryid);
1330
			if (!$this->isSpecialFolder($store, $sourcefolderentryid) && $sourceparentfolder && $destfolder && $deststore) {
1331
				$folder = mapi_msgstore_openentry($store, $sourcefolderentryid);
1332
				$props = mapi_getprops($folder, [PR_DISPLAY_NAME]);
1333
1334
				try {
1335
					/*
1336
					  * To decrease overload of checking for conflicting folder names on modification of every folder
1337
					  * we should first try to copy/move folder and if it returns MAPI_E_COLLISION then
1338
					  * only we should check for the conflicting folder names and generate a new name
1339
					  * and copy/move folder with the generated name.
1340
					  */
1341
					if ($moveFolder) {
1342
						mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $props[PR_DISPLAY_NAME], FOLDER_MOVE);
1343
						$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1344
						// In some cases PR_PARENT_ENTRYID is not available in mapi_getprops, add it manually
1345
						$folderProps[PR_PARENT_ENTRYID] = $destfolderentryid;
1346
						$result = true;
1347
					}
1348
					else {
1349
						mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $props[PR_DISPLAY_NAME], COPY_SUBFOLDERS);
1350
						$result = true;
1351
					}
1352
				}
1353
				catch (MAPIException $e) {
1354
					if ($e->getCode() == MAPI_E_COLLISION) {
1355
						$foldername = $this->checkFolderNameConflict($deststore, $destfolder, $props[PR_DISPLAY_NAME]);
1356
						if ($moveFolder) {
1357
							mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $foldername, FOLDER_MOVE);
1358
							$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1359
							$result = true;
1360
						}
1361
						else {
1362
							mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $foldername, COPY_SUBFOLDERS);
1363
							$result = true;
1364
						}
1365
					}
1366
					else {
1367
						// all other errors should be propagated to higher level exception handlers
1368
						throw $e;
1369
					}
1370
				}
1371
			}
1372
1373
			return $result;
1374
		}
1375
1376
		/**
1377
		 * Read MAPI table.
1378
		 *
1379
		 * This function performs various operations to open, setup, and read all rows from a MAPI table.
1380
		 *
1381
		 * The output from this function is an XML array structure which can be sent directly to XML serialisation.
1382
		 *
1383
		 * @param object $store       MAPI Message Store Object
1384
		 * @param string $entryid     The entryid of the folder to read the table from
1385
		 * @param array  $properties  The set of properties which will be read
1386
		 * @param array  $sort        The set properties which the table will be sort on (formatted as a MAPI sort order)
1387
		 * @param int    $start       Starting row at which to start reading rows
1388
		 * @param int    $rowcount    Number of rows which should be read
1389
		 * @param array  $restriction Table restriction to apply to the table (formatted as MAPI restriction)
1390
		 * @param array  $folderProps reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of the folder
1391
		 *
1392
		 * @return array XML array structure with row data
1393
		 */
1394
		public function getTable($store, $entryid, $properties, $sort, $start, $rowcount = false, $restriction = false, $getHierarchy = false, $flags = MAPI_DEFERRED_ERRORS) {
1395
			$data = [];
1396
			$folder = mapi_msgstore_openentry($store, $entryid);
1397
1398
			if ($folder) {
1399
				$table = $getHierarchy ? mapi_folder_gethierarchytable($folder, $flags) : mapi_folder_getcontentstable($folder, $flags);
1400
1401
				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...
1402
					$rowcount = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50);
1403
				}
1404
1405
				if (is_array($restriction)) {
1406
					mapi_table_restrict($table, $restriction, TBL_BATCH);
1407
				}
1408
1409
				if (is_array($sort) && !empty($sort)) {
1410
					/*
1411
					 * If the sort array contains the PR_SUBJECT column we should change this to
1412
					 * PR_NORMALIZED_SUBJECT to make sure that when sorting on subjects: "sweet" and
1413
					 * "RE: sweet", the first one is displayed before the latter one. If the subject
1414
					 * is used for sorting the PR_MESSAGE_DELIVERY_TIME must be added as well as
1415
					 * Outlook behaves the same way in this case.
1416
					 */
1417
					if (isset($sort[PR_SUBJECT])) {
1418
						$sortReplace = [];
1419
						foreach ($sort as $key => $value) {
1420
							if ($key == PR_SUBJECT) {
1421
								$sortReplace[PR_NORMALIZED_SUBJECT] = $value;
1422
								$sortReplace[PR_MESSAGE_DELIVERY_TIME] = TABLE_SORT_DESCEND;
1423
							}
1424
							else {
1425
								$sortReplace[$key] = $value;
1426
							}
1427
						}
1428
						$sort = $sortReplace;
1429
					}
1430
1431
					mapi_table_sort($table, $sort, TBL_BATCH);
1432
				}
1433
1434
				$data["item"] = [];
1435
1436
				$rows = mapi_table_queryrows($table, $properties, $start, $rowcount);
1437
				foreach ($rows as $row) {
1438
					$itemData = Conversion::mapMAPI2XML($properties, $row);
1439
1440
					// For ZARAFA type users the email_address properties are filled with the username
1441
					// Here we will copy that property to the *_username property for consistency with
1442
					// the getMessageProps() function
1443
					// We will not retrieve the real email address (like the getMessageProps function does)
1444
					// for all items because that would be a performance decrease!
1445
					if (isset($itemData['props']["sent_representing_email_address"])) {
1446
						$itemData['props']["sent_representing_username"] = $itemData['props']["sent_representing_email_address"];
1447
					}
1448
					if (isset($itemData['props']["sender_email_address"])) {
1449
						$itemData['props']["sender_username"] = $itemData['props']["sender_email_address"];
1450
					}
1451
					if (isset($itemData['props']["received_by_email_address"])) {
1452
						$itemData['props']["received_by_username"] = $itemData['props']["received_by_email_address"];
1453
					}
1454
1455
					array_push($data["item"], $itemData);
1456
				}
1457
1458
				// Update the page information
1459
				$data["page"] = [];
1460
				$data["page"]["start"] = $start;
1461
				$data["page"]["rowcount"] = $rowcount;
1462
				$data["page"]["totalrowcount"] = mapi_table_getrowcount($table);
1463
			}
1464
1465
			return $data;
1466
		}
1467
1468
		/**
1469
		 * Returns TRUE of the MAPI message only has inline attachments.
1470
		 *
1471
		 * @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...
1472
		 *
1473
		 * @return bool TRUE if the item contains only inline attachments, FALSE otherwise
1474
		 *
1475
		 * @deprecated This function is not used, because it is much too slow to run on all messages in your inbox
1476
		 */
1477
		public function hasOnlyInlineAttachments($message) {
1478
			$attachmentTable = mapi_message_getattachmenttable($message);
1479
			if ($attachmentTable) {
1480
				$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACHMENT_HIDDEN]);
1481
				foreach ($attachments as $attachmentRow) {
1482
					if (!isset($attachmentRow[PR_ATTACHMENT_HIDDEN]) || !$attachmentRow[PR_ATTACHMENT_HIDDEN]) {
1483
						return false;
1484
					}
1485
				}
1486
			}
1487
1488
			return true;
1489
		}
1490
1491
		/**
1492
		 * Read message properties.
1493
		 *
1494
		 * Reads a message and returns the data as an XML array structure with all data from the message that is needed
1495
		 * to show a message (for example in the preview pane)
1496
		 *
1497
		 * @param object $store      MAPI Message Store Object
1498
		 * @param object $message    The MAPI Message Object
1499
		 * @param array  $properties Mapping of properties that should be read
1500
		 * @param bool   $html2text  true - body will be converted from html to text, false - html body will be returned
1501
		 *
1502
		 * @return array item properties
1503
		 *
1504
		 * @todo Function name is misleading as it doesn't just get message properties
1505
		 */
1506
		public function getMessageProps($store, $message, $properties, $html2text = false) {
1507
			$props = [];
1508
1509
			if ($message) {
0 ignored issues
show
introduced by
$message is of type object, thus it always evaluated to true.
Loading history...
1510
				$itemprops = mapi_getprops($message, $properties);
1511
1512
				/* If necessary stream the property, if it's > 8KB */
1513
				if (isset($itemprops[PR_TRANSPORT_MESSAGE_HEADERS]) || propIsError(PR_TRANSPORT_MESSAGE_HEADERS, $itemprops) == MAPI_E_NOT_ENOUGH_MEMORY) {
1514
					$itemprops[PR_TRANSPORT_MESSAGE_HEADERS] = mapi_openproperty($message, PR_TRANSPORT_MESSAGE_HEADERS);
1515
				}
1516
1517
				$props = Conversion::mapMAPI2XML($properties, $itemprops);
1518
1519
				// Get actual SMTP address for sent_representing_email_address and received_by_email_address
1520
				$smtpprops = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_RECEIVED_BY_ENTRYID, PR_SENDER_ENTRYID]);
1521
1522
				if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID])) {
1523
					try {
1524
						$user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(true), $smtpprops[PR_SENT_REPRESENTING_ENTRYID]);
1525
						if (isset($user)) {
1526
							$user_props = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]);
1527
							if (isset($user_props[PR_EMS_AB_THUMBNAIL_PHOTO])) {
1528
								$props["props"]['thumbnail_photo'] = "data:image/jpeg;base64," . base64_encode($user_props[PR_EMS_AB_THUMBNAIL_PHOTO]);
1529
							}
1530
						}
1531
					}
1532
					catch (MAPIException $e) {
1533
						// do nothing
1534
					}
1535
				}
1536
1537
				/*
1538
				 * Check that we have PR_SENT_REPRESENTING_ENTRYID for the item, and also
1539
				 * Check that we have sent_representing_email_address property there in the message,
1540
				 * but for contacts we are not using sent_representing_* properties so we are not
1541
				 * getting it from the message. So basically this will be used for mail items only
1542
				 */
1543
				if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $props["props"]["sent_representing_email_address"])) {
1544
					$props["props"]["sent_representing_username"] = $props["props"]["sent_representing_email_address"];
1545
					$sentRepresentingSearchKey = isset($props['props']['sent_representing_search_key']) ? hex2bin($props['props']['sent_representing_search_key']) : false;
1546
					$props["props"]["sent_representing_email_address"] = $this->getEmailAddress($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $sentRepresentingSearchKey);
1547
				}
1548
1549
				if (isset($smtpprops[PR_SENDER_ENTRYID], $props["props"]["sender_email_address"])) {
1550
					$props["props"]["sender_username"] = $props["props"]["sender_email_address"];
1551
					$senderSearchKey = isset($props['props']['sender_search_key']) ? hex2bin($props['props']['sender_search_key']) : false;
1552
					$props["props"]["sender_email_address"] = $this->getEmailAddress($smtpprops[PR_SENDER_ENTRYID], $senderSearchKey);
1553
				}
1554
1555
				if (isset($smtpprops[PR_RECEIVED_BY_ENTRYID], $props["props"]["received_by_email_address"])) {
1556
					$props["props"]["received_by_username"] = $props["props"]["received_by_email_address"];
1557
					$receivedSearchKey = isset($props['props']['received_by_search_key']) ? hex2bin($props['props']['received_by_search_key']) : false;
1558
					$props["props"]["received_by_email_address"] = $this->getEmailAddress($smtpprops[PR_RECEIVED_BY_ENTRYID], $receivedSearchKey);
1559
				}
1560
1561
				// Get body content
1562
				// TODO: Move retrieving the body to a separate function.
1563
				$plaintext = $this->isPlainText($message);
1564
				$tmpProps = mapi_getprops($message, [PR_BODY, PR_HTML]);
1565
1566
				if (empty($tmpProps[PR_HTML])) {
1567
					$tmpProps = mapi_getprops($message, [PR_BODY, PR_RTF_COMPRESSED]);
1568
					if (isset($tmpProps[PR_RTF_COMPRESSED])) {
1569
						$tmpProps[PR_HTML] = mapi_decompressrtf($tmpProps[PR_RTF_COMPRESSED]);
1570
					}
1571
				}
1572
1573
				$htmlcontent = '';
1574
				$plaincontent = '';
1575
				if (!$plaintext && isset($tmpProps[PR_HTML])) {
1576
					$cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]);
1577
					$codepage = isset($cpprops[PR_INTERNET_CPID]) ? $cpprops[PR_INTERNET_CPID] : 65001;
1578
					$htmlcontent = Conversion::convertCodepageStringToUtf8($codepage, $tmpProps[PR_HTML]);
1579
					if (!empty($htmlcontent)) {
1580
						if ($html2text) {
1581
							$htmlcontent = '';
1582
						}
1583
						else {
1584
							$props["props"]["isHTML"] = true;
1585
						}
1586
					}
1587
1588
					$htmlcontent = trim($htmlcontent, "\0");
1589
				}
1590
1591
				if (isset($tmpProps[PR_BODY])) {
1592
					// only open property if it exists
1593
					$plaincontent = mapi_message_openproperty($message, PR_BODY);
1594
					$plaincontent = trim($plaincontent, "\0");
1595
				}
1596
				else {
1597
					if ($html2text && isset($tmpProps[PR_HTML])) {
1598
						$plaincontent = strip_tags($tmpProps[PR_HTML]);
1599
					}
1600
				}
1601
1602
				if (!empty($htmlcontent)) {
1603
					$props["props"]["html_body"] = $htmlcontent;
1604
					$props["props"]["isHTML"] = true;
1605
				}
1606
				else {
1607
					$props["props"]["isHTML"] = false;
1608
				}
1609
				$props["props"]["body"] = $plaincontent;
1610
1611
				// Get reply-to information, otherwise consider the sender to be the reply-to person.
1612
				$props['reply-to'] = ['item' => []];
1613
				$messageprops = mapi_getprops($message, [PR_REPLY_RECIPIENT_ENTRIES]);
1614
				if (isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES])) {
1615
					$props['reply-to']['item'] = $this->readReplyRecipientEntry($messageprops[PR_REPLY_RECIPIENT_ENTRIES]);
1616
				}
1617
				if (!isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES]) || count($props['reply-to']['item']) === 0) {
1618
					if (isset($props['props']['sent_representing_email_address']) && !empty($props['props']['sent_representing_email_address'])) {
1619
						$props['reply-to']['item'][] = [
1620
							'rowid' => 0,
1621
							'props' => [
1622
								'entryid' => $props['props']['sent_representing_entryid'],
1623
								'display_name' => $props['props']['sent_representing_name'],
1624
								'smtp_address' => $props['props']['sent_representing_email_address'],
1625
								'address_type' => $props['props']['sent_representing_address_type'],
1626
								'object_type' => MAPI_MAILUSER,
1627
								'search_key' => isset($props['props']['sent_representing_search_key']) ? $props['props']['sent_representing_search_key'] : '',
1628
							],
1629
						];
1630
					}
1631
					elseif (!empty($props['props']['sender_email_address'])) {
1632
						$props['reply-to']['item'][] = [
1633
							'rowid' => 0,
1634
							'props' => [
1635
								'entryid' => $props['props']['sender_entryid'],
1636
								'display_name' => $props['props']['sender_name'],
1637
								'smtp_address' => $props['props']['sender_email_address'],
1638
								'address_type' => $props['props']['sender_address_type'],
1639
								'object_type' => MAPI_MAILUSER,
1640
								'search_key' => $props['props']['sender_search_key'],
1641
							],
1642
						];
1643
					}
1644
				}
1645
1646
				// Get recipients
1647
				$recipients = $GLOBALS["operations"]->getRecipientsInfo($message);
1648
				if (!empty($recipients)) {
1649
					$props["recipients"] = [
1650
						"item" => $recipients,
1651
					];
1652
				}
1653
1654
				// Get attachments
1655
				$attachments = $GLOBALS["operations"]->getAttachmentsInfo($message);
1656
				if (!empty($attachments)) {
1657
					$props["attachments"] = [
1658
						"item" => $attachments,
1659
					];
1660
					$cid_found = false;
1661
					foreach ($attachments as $attachement) {
1662
						if (isset($attachement["props"]["cid"])) {
1663
							$cid_found = true;
1664
						}
1665
					}
1666
					if ($cid_found == true && isset($htmlcontent)) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

Loading history...
1667
						preg_match_all('/src="cid:(.*)"/Uims', $htmlcontent, $matches);
1668
						if (count($matches) > 0) {
1669
							$search = [];
1670
							$replace = [];
1671
							foreach ($matches[1] as $match) {
1672
								$idx = -1;
1673
								foreach ($attachments as $key => $attachement) {
1674
									if (isset($attachement["props"]["cid"]) &&
1675
										strcasecmp($match, $attachement["props"]["cid"]) == 0) {
1676
										$idx = $key;
1677
										$num = $attachement["props"]["attach_num"];
1678
									}
1679
								}
1680
								if ($idx == -1) {
1681
									continue;
1682
								}
1683
								$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...
1684
								if (empty($attach)) {
1685
									continue;
1686
								}
1687
								$attachprop = mapi_getprops($attach, [PR_ATTACH_DATA_BIN, PR_ATTACH_MIME_TAG]);
1688
								if (empty($attachprop) || !isset($attachprop[PR_ATTACH_DATA_BIN])) {
1689
									continue;
1690
								}
1691
								if (!isset($attachprop[PR_ATTACH_MIME_TAG])) {
1692
									$mime_tag = "text/plain";
1693
								}
1694
								else {
1695
									$mime_tag = $attachprop[PR_ATTACH_MIME_TAG];
1696
								}
1697
								$search[] = "src=\"cid:{$match}\"";
1698
								$replace[] = "src=\"data:{$mime_tag};base64," . base64_encode($attachprop[PR_ATTACH_DATA_BIN]) . "\"";
1699
								unset($props["attachments"]["item"][$idx]);
1700
							}
1701
							$props["attachments"]["item"] = array_values($props["attachments"]["item"]);
1702
							$htmlcontent = str_replace($search, $replace, $htmlcontent);
1703
							$props["props"]["html_body"] = $htmlcontent;
1704
						}
1705
					}
1706
				}
1707
1708
				// for distlists, we need to get members data
1709
				if (isset($props["props"]["oneoff_members"], $props["props"]["members"])) {
1710
					// remove non-client props
1711
					unset($props["props"]["members"], $props["props"]["oneoff_members"]);
1712
1713
					// get members
1714
					$members = $GLOBALS["operations"]->getMembersFromDistributionList($store, $message, $properties);
1715
					if (!empty($members)) {
1716
						$props["members"] = [
1717
							"item" => $members,
1718
						];
1719
					}
1720
				}
1721
			}
1722
1723
			return $props;
1724
		}
1725
1726
		/**
1727
		 * Get the email address either from entryid or search key. Function is helpful
1728
		 * to retrieve the email address of already deleted contact which is use as a
1729
		 * recipient in message.
1730
		 *
1731
		 * @param string      $entryId   the entryId of an item/recipient
1732
		 * @param bool|string $searchKey then search key of an item/recipient
1733
		 *
1734
		 * @return string email address if found else return empty string
1735
		 */
1736
		public function getEmailAddress($entryId, $searchKey = false) {
1737
			$emailAddress = $this->getEmailAddressFromEntryID($entryId);
1738
			if (empty($emailAddress) && $searchKey !== false) {
1739
				$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

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

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

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

2719
				$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...
2720
2721
				// FIXME: currently message is deleted from original store and new message is created
2722
				// in current user's store, but message should be moved
2723
2724
				// delete message from it's original location
2725
				if (!empty($oldEntryId) && !empty($oldParentEntryId)) {
2726
					$folder = mapi_msgstore_openentry($origStore, $oldParentEntryId);
2727
					mapi_folder_deletemessages($folder, [$oldEntryId], DELETE_HARD_DELETE);
2728
				}
2729
			}
2730
			else {
2731
				// 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.
2732
				$outbox = mapi_msgstore_openentry($store, $storeprops[PR_IPM_OUTBOX_ENTRYID]);
2733
2734
				// Open the old and the new message
2735
				$newmessage = mapi_folder_createmessage($outbox);
2736
				$oldEntryId = $entryid;
2737
2738
				// Remember the new entryid
2739
				$newprops = mapi_getprops($newmessage, [PR_ENTRYID]);
2740
				$entryid = $newprops[PR_ENTRYID];
2741
2742
				if (!empty($oldEntryId)) {
2743
					$message = mapi_msgstore_openentry($store, $oldEntryId);
2744
					// Copy the entire message
2745
					mapi_copyto($message, [], [], $newmessage);
2746
					$tmpProps = mapi_getprops($message);
2747
					$oldParentEntryId = $tmpProps[PR_PARENT_ENTRYID];
2748
					if ($storeprops[PR_IPM_OUTBOX_ENTRYID] == $oldParentEntryId) {
2749
						$folder = $outbox;
2750
					}
2751
					else {
2752
						$folder = mapi_msgstore_openentry($store, $oldParentEntryId);
2753
					}
2754
2755
					// Copy message_class for S/MIME plugin
2756
					if (isset($tmpProps[PR_MESSAGE_CLASS])) {
2757
						$props[PR_MESSAGE_CLASS] = $tmpProps[PR_MESSAGE_CLASS];
2758
					}
2759
					// Delete the old message
2760
					mapi_folder_deletemessages($folder, [$oldEntryId]);
2761
				}
2762
2763
				// save changes to new message created in outbox
2764
				mapi_savechanges($newmessage);
2765
2766
				$reprProps = mapi_getprops($newmessage, [PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ENTRYID]);
2767
				if (isset($reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], $reprProps[PR_SENDER_EMAIL_ADDRESS], $reprProps[PR_SENT_REPRESENTING_ENTRYID]) &&
2768
					strcasecmp($reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], $reprProps[PR_SENDER_EMAIL_ADDRESS]) != 0) {
2769
					$ab = $GLOBALS['mapisession']->getAddressbook();
2770
					$abitem = mapi_ab_openentry($ab, $reprProps[PR_SENT_REPRESENTING_ENTRYID]);
2771
					$abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2772
2773
					$props[PR_SENT_REPRESENTING_NAME] = $abitemprops[PR_DISPLAY_NAME];
2774
					$props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS];
2775
					$props[PR_SENT_REPRESENTING_ADDRTYPE] = "EX";
2776
					$props[PR_SENT_REPRESENTING_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY];
2777
				}
2778
				// Save the new message properties
2779
				$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

2779
				$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...
2780
			}
2781
2782
			if (!$message)
0 ignored issues
show
introduced by
$message is of type mapimessage, thus it always evaluated to true.
Loading history...
2783
				return false;
2784
			// Allowing to hook in just before the data sent away to be sent to the client
2785
			$GLOBALS['PluginManager']->triggerHook('server.core.operations.submitmessage', [
2786
				'moduleObject' => $this,
2787
				'store' => $store,
2788
				'entryid' => $entryid,
2789
				'message' => &$message,
2790
			]);
2791
			// Submit the message (send)
2792
			try {
2793
				mapi_message_submitmessage($message);
2794
			}
2795
			catch (MAPIException $e) {
2796
				$username = $GLOBALS["mapisession"]->getUserName();
2797
				$errorName = get_mapi_error_name($e->getCode());
2798
				error_log(sprintf(
2799
					'Unable to submit message for %s, MAPI error: %s. ' .
2800
					'SMTP server may be down or it refused the message or the message' .
2801
					' is too large to submit or user does not have the permission ...',
2802
					$username,
2803
					$errorName
2804
				));
2805
2806
				return $errorName;
2807
			}
2808
2809
			$tmp_props = mapi_getprops($message, [PR_PARENT_ENTRYID]);
2810
			$messageProps[PR_PARENT_ENTRYID] = $tmp_props[PR_PARENT_ENTRYID];
2811
2812
			$this->addRecipientsToRecipientHistory($this->getRecipientsInfo($message));
2813
			return false;
2814
		}
2815
2816
		/**
2817
		 * Delete messages.
2818
		 *
2819
		 * This function does what is needed when a user presses 'delete' on a MAPI message. This means that:
2820
		 *
2821
		 * - Items in the own store are moved to the wastebasket
2822
		 * - Items in the wastebasket are deleted
2823
		 * - Items in other users stores are moved to our own wastebasket
2824
		 * - Items in the public store are deleted
2825
		 *
2826
		 * @param mapistore $store         MAPI Message Store Object
2827
		 * @param string    $parententryid parent entryid of the messages to be deleted
2828
		 * @param array     $entryids      a list of entryids which will be deleted
2829
		 * @param bool      $softDelete    flag for soft-deleteing (when user presses Shift+Del)
2830
		 * @param bool      $unread        message is unread
2831
		 *
2832
		 * @return bool true if action succeeded, false if not
2833
		 */
2834
		public function deleteMessages($store, $parententryid, $entryids,
2835
		    $softDelete = false, $unread = false)
2836
		{
2837
			$result = false;
2838
			if (!is_array($entryids)) {
0 ignored issues
show
introduced by
The condition is_array($entryids) is always true.
Loading history...
2839
				$entryids = [$entryids];
2840
			}
2841
2842
			$folder = mapi_msgstore_openentry($store, $parententryid);
2843
			$flags = $unread ? GX_DELMSG_NOTIFY_UNREAD : 0;
2844
			$msgprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_OUTBOX_ENTRYID]);
2845
2846
			switch ($msgprops[PR_MDB_PROVIDER]) {
2847
			case ZARAFA_STORE_DELEGATE_GUID:
2848
				$softDelete = $softDelete || defined('ENABLE_DEFAULT_SOFT_DELETE') ? ENABLE_DEFAULT_SOFT_DELETE : false;
2849
				// with a store from an other user we need our own waste basket...
2850
				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...
2851
					// except when it is the waste basket itself
2852
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2853
					break;
2854
				}
2855
				$defaultstore = $GLOBALS["mapisession"]->getDefaultMessageStore();
2856
				$msgprops = mapi_getprops($defaultstore, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER]);
2857
2858
				if (!isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) ||
2859
				    $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid) {
2860
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2861
					break;
2862
				}
2863
				try {
2864
					$result = $this->copyMessages($store, $parententryid, $defaultstore, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true);
2865
				}
2866
				catch (MAPIException $e) {
2867
					$e->setHandled();
2868
					// if moving fails, try normal delete
2869
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2870
				}
2871
				break;
2872
2873
			case ZARAFA_STORE_ARCHIVER_GUID:
2874
			case ZARAFA_STORE_PUBLIC_GUID:
2875
				// always delete in public store and archive store
2876
				$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2877
				break;
2878
2879
			case ZARAFA_SERVICE_GUID:
2880
				// delete message when in your own waste basket, else move it to the waste basket
2881
				if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) && $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

Loading history...
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $msgprops[... || $softDelete == true, Probably Intended Meaning: IssetNode && ($msgprops[...|| $softDelete == true)
Loading history...
2882
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2883
					break;
2884
				}
2885
				try {
2886
					// if the message is deleting from outbox then first delete the
2887
					// message from an outgoing queue.
2888
					if (function_exists("mapi_msgstore_abortsubmit") && isset($msgprops[PR_IPM_OUTBOX_ENTRYID]) && $msgprops[PR_IPM_OUTBOX_ENTRYID] === $parententryid) {
2889
						foreach ($entryids as $entryid) {
2890
							$message = mapi_msgstore_openentry($store, $entryid);
2891
							$messageProps = mapi_getprops($message, [PR_DEFERRED_SEND_TIME]);
2892
							if (isset($messageProps[PR_DEFERRED_SEND_TIME])) {
2893
								mapi_msgstore_abortsubmit($store, $entryid);
2894
							}
2895
						}
2896
					}
2897
					$result = $this->copyMessages($store, $parententryid, $store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true);
2898
				}
2899
				catch (MAPIException $e) {
2900
					if ($e->getCode() === MAPI_E_NOT_IN_QUEUE || $e->getCode() === MAPI_E_UNABLE_TO_ABORT) {
2901
						throw $e;
2902
					}
2903
2904
					$e->setHandled();
2905
					// if moving fails, try normal delete
2906
					$result = mapi_folder_deletemessages($folder, $entryids, $flags);
2907
				}
2908
				break;
2909
			}
2910
2911
			return $result;
2912
		}
2913
2914
		/**
2915
		 * Copy or move messages.
2916
		 *
2917
		 * @param object $store         MAPI Message Store Object
2918
		 * @param string $parententryid parent entryid of the messages
2919
		 * @param string $destentryid   destination folder
2920
		 * @param array  $entryids      a list of entryids which will be copied or moved
2921
		 * @param array  $ignoreProps   a list of proptags which should not be copied over
2922
		 *                              to the new message
2923
		 * @param bool   $moveMessages  true - move messages, false - copy messages
2924
		 * @param array  $props         a list of proptags which should set in new messages
2925
		 * @param mixed  $destStore
2926
		 *
2927
		 * @return bool true if action succeeded, false if not
2928
		 */
2929
		public function copyMessages($store, $parententryid, $destStore, $destentryid, $entryids, $ignoreProps, $moveMessages, $props = []) {
2930
			$sourcefolder = mapi_msgstore_openentry($store, $parententryid);
2931
			$destfolder = mapi_msgstore_openentry($destStore, $destentryid);
2932
2933
			if (!$sourcefolder || !$destfolder) {
2934
				error_log("Could not open source or destination folder. Aborting.");
2935
2936
				return false;
2937
			}
2938
2939
			if (!is_array($entryids)) {
0 ignored issues
show
introduced by
The condition is_array($entryids) is always true.
Loading history...
2940
				$entryids = [$entryids];
2941
			}
2942
2943
			/*
2944
			 * If there are no properties to ignore as well as set then we can use mapi_folder_copymessages instead
2945
			 * of mapi_copyto. mapi_folder_copymessages is much faster then copyto since it executes
2946
			 * the copying on the server instead of in client.
2947
			 */
2948
			if (empty($ignoreProps) && empty($props)) {
2949
				try {
2950
					mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0);
2951
				}
2952
				catch (MAPIException $e) {
2953
					error_log(sprintf("mapi_folder_copymessages failed with code: 0x%08X. Wait 250ms and try again", mapi_last_hresult()));
2954
					// wait 250ms before trying again
2955
					usleep(250000);
2956
2957
					try {
2958
						mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0);
2959
					}
2960
					catch (MAPIException $e) {
2961
						error_log(sprintf("2nd attempt of mapi_folder_copymessages also failed with code: 0x%08X. Abort.", mapi_last_hresult()));
2962
2963
						return false;
2964
					}
2965
				}
2966
			}
2967
			else {
2968
				foreach ($entryids as $entryid) {
2969
					$oldmessage = mapi_msgstore_openentry($store, $entryid);
2970
					$newmessage = mapi_folder_createmessage($destfolder);
2971
2972
					mapi_copyto($oldmessage, [], $ignoreProps, $newmessage, 0);
2973
					if (!empty($props)) {
2974
						mapi_setprops($newmessage, $props);
2975
					}
2976
					mapi_savechanges($newmessage);
2977
				}
2978
				if ($moveMessages) {
2979
					// while moving message we actually copy that particular message into
2980
					// destination folder, and remove it from source folder. so we must have
2981
					// to hard delete the message.
2982
					mapi_folder_deletemessages($sourcefolder, $entryids, DELETE_HARD_DELETE);
2983
				}
2984
			}
2985
2986
			return true;
2987
		}
2988
2989
		/**
2990
		 * Set message read flag.
2991
		 *
2992
		 * @param object $store        MAPI Message Store Object
2993
		 * @param string $entryid      entryid of the message
2994
		 * @param array  $flags        Array of options, may contain "unread" and "noreceipt". The absence of these flags indicate the opposite ("read" and "sendreceipt").
2995
		 * @param array  $messageProps reference to an array which will be filled with PR_ENTRYID, PR_STORE_ENTRYID and PR_PARENT_ENTRYID of the message
2996
		 * @param array  $props        properties of the message
2997
		 * @param mixed  $msg_action
2998
		 *
2999
		 * @return bool true if action succeeded, false if not
3000
		 */
3001
		public function setMessageFlag($store, $entryid, $flags, $msg_action = false, &$props = false) {
3002
			$message = $this->openMessage($store, $entryid);
3003
3004
			if ($message) {
0 ignored issues
show
introduced by
$message is of type object, thus it always evaluated to true.
Loading history...
3005
				/**
3006
				 * convert flags of PR_MESSAGE_FLAGS property to flags that is
3007
				 * used in mapi_message_setreadflag.
3008
				 */
3009
				$flag = MAPI_DEFERRED_ERRORS;		// set unread flag, read receipt will be sent
3010
3011
				if (($flags & MSGFLAG_RN_PENDING) && isset($msg_action['send_read_receipt']) && $msg_action['send_read_receipt'] == false) {
3012
					$flag |= SUPPRESS_RECEIPT;
3013
				}
3014
				else {
3015
					if (!($flags & MSGFLAG_READ)) {
3016
						$flag |= CLEAR_READ_FLAG;
3017
					}
3018
				}
3019
3020
				mapi_message_setreadflag($message, $flag);
3021
3022
				if (is_array($props)) {
3023
					$props = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]);
3024
				}
3025
			}
3026
3027
			return true;
3028
		}
3029
3030
		/**
3031
		 * Create a unique folder name based on a provided new folder name.
3032
		 *
3033
		 * checkFolderNameConflict() checks if a folder name conflict is caused by the given $foldername.
3034
		 * This function is used for copying of moving a folder to another folder. It returns
3035
		 * a unique foldername.
3036
		 *
3037
		 * @param object $store      MAPI Message Store Object
3038
		 * @param object $folder     MAPI Folder Object
3039
		 * @param string $foldername the folder name
3040
		 *
3041
		 * @return string correct foldername
3042
		 */
3043
		public function checkFolderNameConflict($store, $folder, $foldername) {
3044
			$folderNames = [];
3045
3046
			$hierarchyTable = mapi_folder_gethierarchytable($folder, MAPI_DEFERRED_ERRORS);
3047
			mapi_table_sort($hierarchyTable, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
3048
3049
			$subfolders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]);
3050
3051
			if (is_array($subfolders)) {
3052
				foreach ($subfolders as $subfolder) {
3053
					$folderObject = mapi_msgstore_openentry($store, $subfolder[PR_ENTRYID]);
3054
					$folderProps = mapi_getprops($folderObject, [PR_DISPLAY_NAME]);
3055
3056
					array_push($folderNames, strtolower($folderProps[PR_DISPLAY_NAME]));
3057
				}
3058
			}
3059
3060
			if (array_search(strtolower($foldername), $folderNames) !== false) {
3061
				$i = 2;
3062
				while (array_search(strtolower($foldername)." ($i)", $folderNames) !== false)
3063
					++$i;
3064
				$foldername .= " ($i)";
3065
			}
3066
3067
			return $foldername;
3068
		}
3069
3070
		/**
3071
		 * Set the recipients of a MAPI message.
3072
		 *
3073
		 * @param object $message    MAPI Message Object
3074
		 * @param array  $recipients XML array structure of recipients
3075
		 * @param bool   $send       true if we are going to send this message else false
3076
		 */
3077
		public function setRecipients($message, $recipients, $send = false) {
3078
			if (empty($recipients)) {
3079
				// no recipients are sent from client
3080
				return;
3081
			}
3082
3083
			$newRecipients = [];
3084
			$removeRecipients = [];
3085
			$modifyRecipients = [];
3086
3087
			if (isset($recipients['add']) && !empty($recipients['add'])) {
3088
				$newRecipients = $this->createRecipientList($recipients['add'], 'add', false, $send);
3089
			}
3090
3091
			if (isset($recipients['remove']) && !empty($recipients['remove'])) {
3092
				$removeRecipients = $this->createRecipientList($recipients['remove'], 'remove');
3093
			}
3094
3095
			if (isset($recipients['modify']) && !empty($recipients['modify'])) {
3096
				$modifyRecipients = $this->createRecipientList($recipients['modify'], 'modify', false, $send);
3097
			}
3098
3099
			if (!empty($removeRecipients)) {
3100
				mapi_message_modifyrecipients($message, MODRECIP_REMOVE, $removeRecipients);
3101
			}
3102
3103
			if (!empty($modifyRecipients)) {
3104
				mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $modifyRecipients);
3105
			}
3106
3107
			if (!empty($newRecipients)) {
3108
				mapi_message_modifyrecipients($message, MODRECIP_ADD, $newRecipients);
3109
			}
3110
		}
3111
3112
		/**
3113
		 * Copy recipients from original message.
3114
		 *
3115
		 * If we are sending mail from a delegator's folder, we need to copy all recipients from the original message
3116
		 *
3117
		 * @param object      $message         MAPI Message Object
3118
		 * @param MAPIMessage $copyFromMessage If set we copy all recipients from this message
3119
		 */
3120
		public function copyRecipients($message, $copyFromMessage = false) {
3121
			$recipienttable = mapi_message_getrecipienttable($copyFromMessage);
3122
			$messageRecipients = mapi_table_queryallrows($recipienttable, $GLOBALS["properties"]->getRecipientProperties());
3123
			if (!empty($messageRecipients)) {
3124
				mapi_message_modifyrecipients($message, MODRECIP_ADD, $messageRecipients);
3125
			}
3126
		}
3127
3128
		/**
3129
		 * Set attachments in a MAPI message.
3130
		 *
3131
		 * This function reads any attachments that have been previously uploaded and copies them into
3132
		 * the passed MAPI message resource. For a description of the dialog_attachments variable and
3133
		 * generally how attachments work when uploading, see Operations::saveMessage()
3134
		 *
3135
		 * @see Operations::saveMessage()
3136
		 *
3137
		 * @param object          $message          MAPI Message Object
3138
		 * @param array           $attachments      XML array structure of attachments
3139
		 * @param AttachmentState $attachment_state the state object in which the attachments are saved
3140
		 *                                          between different requests
3141
		 */
3142
		public function setAttachments($message, $attachments, $attachment_state) {
3143
			// Check if attachments should be deleted. This is set in the "upload_attachment.php" file
3144
			if (isset($attachments['dialog_attachments'])) {
3145
				$deleted = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']);
3146
				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...
3147
					foreach ($deleted as $attach_num) {
3148
						try {
3149
							mapi_message_deleteattach($message, (int) $attach_num);
3150
						}
3151
						catch (Exception $e) {
3152
							continue;
3153
						}
3154
					}
3155
					$attachment_state->clearDeletedAttachments($attachments['dialog_attachments']);
3156
				}
3157
			}
3158
3159
			$addedInlineAttachmentCidMapping = [];
3160
			if (is_array($attachments) && !empty($attachments)) {
3161
				// Set contentId to saved attachments.
3162
				if (isset($attachments['add']) && is_array($attachments['add']) && !empty($attachments['add'])) {
3163
					foreach ($attachments['add'] as $key => $attach) {
3164
						if ($attach && isset($attach['inline']) && $attach['inline']) {
3165
							$addedInlineAttachmentCidMapping[$attach['attach_num']] = $attach['cid'];
3166
							$msgattachment = mapi_message_openattach($message, $attach['attach_num']);
3167
							if ($msgattachment) {
3168
								$props = [PR_ATTACH_CONTENT_ID => $attach['cid'], PR_ATTACHMENT_HIDDEN => true];
3169
								mapi_setprops($msgattachment, $props);
3170
								mapi_savechanges($msgattachment);
3171
							}
3172
						}
3173
					}
3174
				}
3175
3176
				// Delete saved inline images if removed from body.
3177
				if (isset($attachments['remove']) && is_array($attachments['remove']) && !empty($attachments['remove'])) {
3178
					foreach ($attachments['remove'] as $key => $attach) {
3179
						if ($attach && isset($attach['inline']) && $attach['inline']) {
3180
							$msgattachment = mapi_message_openattach($message, $attach['attach_num']);
3181
							if ($msgattachment) {
3182
								mapi_message_deleteattach($message, $attach['attach_num']);
3183
								mapi_savechanges($message);
3184
							}
3185
						}
3186
					}
3187
				}
3188
			}
3189
3190
			if ($attachments['dialog_attachments']) {
3191
				$dialog_attachments = $attachments['dialog_attachments'];
3192
			}
3193
			else {
3194
				return;
3195
			}
3196
3197
			$files = $attachment_state->getAttachmentFiles($dialog_attachments);
3198
			if ($files) {
3199
				// Loop through the uploaded attachments
3200
				foreach ($files as $tmpname => $fileinfo) {
3201
					if ($fileinfo['sourcetype'] === 'embedded') {
3202
						// open message which needs to be embedded
3203
						$copyFromStore = $GLOBALS['mapisession']->openMessageStore(hex2bin($fileinfo['store_entryid']));
3204
						$copyFrom = mapi_msgstore_openentry($copyFromStore, hex2bin($fileinfo['entryid']));
3205
3206
						$msgProps = mapi_getprops($copyFrom, [PR_SUBJECT]);
3207
3208
						// get message and copy it to attachment table as embedded attachment
3209
						$props = [];
3210
						$props[PR_EC_WA_ATTACHMENT_ID] = $fileinfo['attach_id'];
3211
						$props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG;
3212
						$props[PR_DISPLAY_NAME] = !empty($msgProps[PR_SUBJECT]) ? $msgProps[PR_SUBJECT] : _('Untitled');
3213
3214
						// Create new attachment.
3215
						$attachment = mapi_message_createattach($message);
3216
						mapi_setprops($attachment, $props);
3217
3218
						$imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
3219
3220
						// Copy the properties from the source message to the attachment
3221
						mapi_copyto($copyFrom, [], [], $imessage, 0); // includes attachments and recipients
3222
3223
						// save changes in the embedded message and the final attachment
3224
						mapi_savechanges($imessage);
3225
						mapi_savechanges($attachment);
3226
					}
3227
					elseif ($fileinfo['sourcetype'] === 'icsfile') {
3228
						$messageStore = $GLOBALS['mapisession']->openMessageStore(hex2bin($fileinfo['store_entryid']));
3229
						$copyFrom = mapi_msgstore_openentry($messageStore, hex2bin($fileinfo['entryid']));
3230
3231
						// Get addressbook for current session
3232
						$addrBook = $GLOBALS['mapisession']->getAddressbook();
3233
3234
						// get message properties.
3235
						$messageProps = mapi_getprops($copyFrom, [PR_SUBJECT]);
3236
3237
						// Read the appointment as RFC2445-formatted ics stream.
3238
						$appointmentStream = mapi_mapitoical($GLOBALS['mapisession']->getSession(), $addrBook, $copyFrom, []);
3239
3240
						$filename = (!empty($messageProps[PR_SUBJECT])) ? $messageProps[PR_SUBJECT] : _('Untitled');
3241
						$filename .= '.ics';
3242
3243
						$props = [
3244
							PR_ATTACH_LONG_FILENAME => $filename,
3245
							PR_DISPLAY_NAME => $filename,
3246
							PR_ATTACH_METHOD => ATTACH_BY_VALUE,
3247
							PR_ATTACH_DATA_BIN => "",
3248
							PR_ATTACH_MIME_TAG => "application/octet-stream",
3249
							PR_ATTACHMENT_HIDDEN => false,
3250
							PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(),
3251
							PR_ATTACH_EXTENSION => pathinfo($filename, PATHINFO_EXTENSION),
3252
						];
3253
3254
						$attachment = mapi_message_createattach($message);
3255
						mapi_setprops($attachment, $props);
3256
3257
						// Stream the file to the PR_ATTACH_DATA_BIN property
3258
						$stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
3259
						mapi_stream_write($stream, $appointmentStream);
3260
3261
						// Commit the stream and save changes
3262
						mapi_stream_commit($stream);
3263
						mapi_savechanges($attachment);
3264
					}
3265
					else {
3266
						$filepath = $attachment_state->getAttachmentPath($tmpname);
3267
						if (is_file($filepath)) {
3268
							// Set contentId if attachment is inline
3269
							$cid = '';
3270
							if (isset($addedInlineAttachmentCidMapping[$tmpname])) {
3271
								$cid = $addedInlineAttachmentCidMapping[$tmpname];
3272
							}
3273
3274
							// If a .p7m file was manually uploaded by the user, we must change the mime type because
3275
							// otherwise mail applications will think the containing email is an encrypted email.
3276
							// That will make Outlook crash, and it will make grommunio Web show the original mail as encrypted
3277
							// without showing the attachment
3278
							$mimeType = $fileinfo["type"];
3279
							$smimeTags = ['multipart/signed', 'application/pkcs7-mime', 'application/x-pkcs7-mime'];
3280
							if (in_array($mimeType, $smimeTags)) {
3281
								$mimeType = "application/octet-stream";
3282
							}
3283
3284
							// Set attachment properties
3285
							$props = [
3286
								PR_ATTACH_LONG_FILENAME => $fileinfo["name"],
3287
								PR_DISPLAY_NAME => $fileinfo["name"],
3288
								PR_ATTACH_METHOD => ATTACH_BY_VALUE,
3289
								PR_ATTACH_DATA_BIN => "",
3290
								PR_ATTACH_MIME_TAG => $mimeType,
3291
								PR_ATTACHMENT_HIDDEN => !empty($cid) ? true : false,
3292
								PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(),
3293
								PR_ATTACH_EXTENSION => pathinfo($fileinfo["name"], PATHINFO_EXTENSION),
3294
							];
3295
3296
							if (isset($fileinfo['sourcetype']) && $fileinfo['sourcetype'] === 'contactphoto') {
3297
								$props[PR_ATTACHMENT_HIDDEN] = true;
3298
								$props[PR_ATTACHMENT_CONTACTPHOTO] = true;
3299
							}
3300
3301
							if (!empty($cid)) {
3302
								$props[PR_ATTACH_CONTENT_ID] = $cid;
3303
							}
3304
3305
							// Create attachment and set props
3306
							$attachment = mapi_message_createattach($message);
3307
							mapi_setprops($attachment, $props);
3308
3309
							// Stream the file to the PR_ATTACH_DATA_BIN property
3310
							$stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
3311
							$handle = fopen($filepath, "r");
3312
							while (!feof($handle)) {
3313
								$contents = fread($handle, BLOCK_SIZE);
3314
								mapi_stream_write($stream, $contents);
3315
							}
3316
3317
							// Commit the stream and save changes
3318
							mapi_stream_commit($stream);
3319
							mapi_savechanges($attachment);
3320
							fclose($handle);
3321
							unlink($filepath);
3322
						}
3323
					}
3324
				}
3325
3326
				// Delete all the files in the state.
3327
				$attachment_state->clearAttachmentFiles($dialog_attachments);
3328
			}
3329
		}
3330
3331
		/**
3332
		 * Copy attachments from original message.
3333
		 *
3334
		 * @see Operations::saveMessage()
3335
		 *
3336
		 * @param object          $message                   MAPI Message Object
3337
		 * @param string          $dialog_attachments        For a description of the dialog_attachments variable and generally how attachments work when uploading, see Operations::saveMessage()
3338
		 * @param MAPIMessage     $copyFromMessage           if set, copy the attachments from this message in addition to the uploaded attachments
3339
		 * @param bool            $copyInlineAttachmentsOnly if true then copy only inline attachments
3340
		 * @param AttachmentState $attachment_state          the state object in which the attachments are saved
3341
		 *                                                   between different requests
3342
		 * @param mixed           $attachments
3343
		 */
3344
		public function copyAttachments($message, $attachments, $copyFromMessage, $copyInlineAttachmentsOnly, $attachment_state) {
3345
			$attachmentTable = mapi_message_getattachmenttable($copyFromMessage);
3346
			if ($attachmentTable && isset($attachments['dialog_attachments'])) {
3347
				$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]);
3348
				$deletedAttachments = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']);
3349
3350
				$plainText = $this->isPlainText($message);
3351
3352
				$properties = $GLOBALS['properties']->getMailProperties();
3353
				$blockStatus = mapi_getprops($copyFromMessage, [PR_BLOCK_STATUS]);
3354
				$blockStatus = Conversion::mapMAPI2XML($properties, $blockStatus);
3355
				$isSafeSender = false;
3356
3357
				// Here if message is HTML and block status is empty then and then call isSafeSender function
3358
				// to check that sender or sender's domain of original message was part of safe sender list.
3359
				if (!$plainText && empty($blockStatus)) {
3360
					$isSafeSender = $this->isSafeSender($copyFromMessage);
3361
				}
3362
3363
				$body = false;
3364
				foreach ($existingAttachments as $props) {
3365
					// check if this attachment is "deleted"
3366
3367
					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...
3368
						// skip attachment, remove reference from state as it no longer applies.
3369
						$attachment_state->removeDeletedAttachment($attachments['dialog_attachments'], $props[PR_ATTACH_NUM]);
3370
3371
						continue;
3372
					}
3373
3374
					$old = mapi_message_openattach($copyFromMessage, $props[PR_ATTACH_NUM]);
3375
					$isInlineAttachment = $attachment_state->isInlineAttachment($old);
3376
3377
					/*
3378
					 * If reply/reply all message, then copy only inline attachments.
3379
					 */
3380
					if ($copyInlineAttachmentsOnly) {
3381
						/*
3382
						 * if message is reply/reply all and format is plain text than ignore inline attachments
3383
						 * and normal attachments to copy from original mail.
3384
						 */
3385
						if ($plainText || !$isInlineAttachment) {
3386
							continue;
3387
						}
3388
					}
3389
					elseif ($plainText && $isInlineAttachment) {
3390
						/*
3391
						 * If message is forward and format of message is plain text then ignore only inline attachments from the
3392
						 * original mail.
3393
						 */
3394
						continue;
3395
					}
3396
3397
					/*
3398
					 * If the inline attachment is referenced with an content-id,
3399
					 * manually check if it's still referenced in the body otherwise remove it
3400
					 */
3401
					if ($isInlineAttachment) {
3402
						// Cache body, so we stream it once
3403
						if ($body === false) {
3404
							$body = streamProperty($message, PR_HTML);
3405
						}
3406
3407
						$contentID = $props[PR_ATTACH_CONTENT_ID];
3408
						if (strpos($body, $contentID) === false) {
0 ignored issues
show
Bug introduced by
It seems like $body can also be of type false; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

4478
						$items = array_merge($items, /** @scrutinizer ignore-type */ $this->getMembersFromDistributionList($store, $distlist, $properties, true, $listEntryIDs));
Loading history...
4479
					}
4480
					else {
4481
						$memberItem['props']['entryid'] = bin2hex($parts['entryid']);
4482
						$memberItem['props']['display_name'] = $oneoffmembers[$key]['name'];
4483
						$memberItem['props']['address_type'] = $oneoffmembers[$key]['type'];
4484
						// distribution lists don't have valid email address so ignore that property
4485
4486
						if ($parts['type'] !== DL_DIST) {
4487
							$memberItem['props']['email_address'] = $oneoffmembers[$key]['address'];
4488
4489
							// internal members in distribution list don't have smtp address so add add that property
4490
							$memberProps = $this->convertDistlistMemberToRecipient($store, $memberItem);
4491
							$memberItem['props']['smtp_address'] = isset($memberProps["smtp_address"]) ? $memberProps["smtp_address"] : $memberProps["email_address"];
4492
						}
4493
4494
						$items[] = $memberItem;
4495
					}
4496
				}
4497
			}
4498
4499
			return $items;
4500
		}
4501
4502
		/**
4503
		 * Convert inline image <img src="data:image/mimetype;.date> links in HTML email
4504
		 * to CID embedded images. Which are supported in major mail clients or
4505
		 * providers such as outlook.com or gmail.com.
4506
		 *
4507
		 * grommunio Web now extracts the base64 image, saves it as hidden attachment,
4508
		 * replace the img src tag with the 'cid' which corresponds with the attachments
4509
		 * cid.
4510
		 *
4511
		 * @param MAPIMessage $message the distribution list message
4512
		 */
4513
		public function convertInlineImage($message) {
4514
			$body = streamProperty($message, PR_HTML);
4515
			$imageIDs = [];
4516
4517
			// Only load the DOM if the HTML contains a img or data:text/plain due to a bug
4518
			// in Chrome on Windows in combination with TinyMCE.
4519
			if (strpos($body, "img") !== false || strpos($body, "data:text/plain") !== false) {
4520
				$doc = new DOMDocument();
4521
				$cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]);
4522
				$codepage = isset($cpprops[PR_INTERNET_CPID]) ? $cpprops[PR_INTERNET_CPID] : 1252;
4523
				$hackEncoding = '<meta http-equiv="Content-Type" content="text/html; charset=' . Conversion::getCodepageCharset($codepage) . '">';
4524
				// TinyMCE does not generate valid HTML, so we must suppress warnings.
4525
				@$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

4525
				/** @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...
4526
				$images = $doc->getElementsByTagName('img');
4527
				$saveChanges = false;
4528
4529
				foreach ($images as $image) {
4530
					$src = $image->getAttribute('src');
4531
4532
					if (strpos($src, "cid:") === false && (strpos($src, "data:image") !== false ||
4533
						strpos($body, "data:text/plain") !== false)) {
4534
						$saveChanges = true;
4535
4536
						// Extract mime type data:image/jpeg;
4537
						$firstOffset = strpos($src, '/') + 1;
4538
						$endOffset = strpos($src, ';');
4539
						$mimeType = substr($src, $firstOffset, $endOffset - $firstOffset);
4540
4541
						$dataPosition = strpos($src, ",");
4542
						// Extract encoded data
4543
						$rawImage = base64_decode(substr($src, $dataPosition + 1, strlen($src)));
4544
4545
						$uniqueId = uniqid();
4546
						$image->setAttribute('src', 'cid:' . $uniqueId);
4547
						// TinyMCE adds an extra inline image for some reason, remove it.
4548
						$image->setAttribute('data-mce-src', '');
4549
4550
						array_push($imageIDs, $uniqueId);
4551
4552
						// Create hidden attachment with CID
4553
						$inlineImage = mapi_message_createattach($message);
4554
						$props = [
4555
							PR_ATTACH_METHOD => ATTACH_BY_VALUE,
4556
							PR_ATTACH_CONTENT_ID => $uniqueId,
4557
							PR_ATTACHMENT_HIDDEN => true,
4558
							PR_ATTACH_FLAGS => 4,
4559
							PR_ATTACH_MIME_TAG => $mimeType !== 'plain' ? 'image/' . $mimeType : 'image/png',
4560
						];
4561
						mapi_setprops($inlineImage, $props);
4562
4563
						$stream = mapi_openproperty($inlineImage, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
4564
						mapi_stream_setsize($stream, strlen($rawImage));
4565
						mapi_stream_write($stream, $rawImage);
4566
						mapi_stream_commit($stream);
4567
						mapi_savechanges($inlineImage);
4568
					}
4569
					elseif (strstr($src, "cid:") !== false) {
4570
						// Check for the cid(there may be http: ) is in the image src. push the cid
4571
						// to $imageIDs array. which further used in clearDeletedInlineAttachments function.
4572
4573
						$firstOffset = strpos($src, ":") + 1;
4574
						$cid = substr($src, $firstOffset);
4575
						array_push($imageIDs, $cid);
4576
					}
4577
				}
4578
4579
				if ($saveChanges) {
4580
					// Write the <img src="cid:data"> changes to the HTML property
4581
					$body = $doc->saveHTML();
4582
					$stream = mapi_openproperty($message, PR_HTML, IID_IStream, 0, MAPI_MODIFY);
4583
					mapi_stream_setsize($stream, strlen($body));
4584
					mapi_stream_write($stream, $body);
4585
					mapi_stream_commit($stream);
4586
					mapi_savechanges($message);
4587
				}
4588
			}
4589
			$this->clearDeletedInlineAttachments($message, $imageIDs);
4590
		}
4591
4592
		/**
4593
		 * Delete the deleted inline image attachment from attachment store.
4594
		 *
4595
		 * @param MAPIMessage $message  the distribution list message
4596
		 * @param array       $imageIDs Array of existing inline image PR_ATTACH_CONTENT_ID
4597
		 */
4598
		public function clearDeletedInlineAttachments($message, $imageIDs = []) {
4599
			$attachmentTable = mapi_message_getattachmenttable($message);
4600
4601
			$restriction = [RES_AND, [
4602
				[RES_PROPERTY,
4603
					[
4604
						RELOP => RELOP_EQ,
4605
						ULPROPTAG => PR_ATTACHMENT_HIDDEN,
4606
						VALUE => [PR_ATTACHMENT_HIDDEN => true],
4607
					],
4608
				],
4609
				[RES_EXIST,
4610
					[
4611
						ULPROPTAG => PR_ATTACH_CONTENT_ID,
4612
					],
4613
				],
4614
			]];
4615
4616
			$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_CONTENT_ID, PR_ATTACH_NUM], $restriction);
4617
			foreach ($attachments as $attachment) {
4618
				$clearDeletedInlineAttach = array_search($attachment[PR_ATTACH_CONTENT_ID], $imageIDs) === false;
4619
				if ($clearDeletedInlineAttach) {
4620
					mapi_message_deleteattach($message, $attachment[PR_ATTACH_NUM]);
4621
				}
4622
			}
4623
		}
4624
4625
		/**
4626
		 * This function will fetch the user from mapi session and retrieve its LDAP image.
4627
		 * It will return the compressed image using php's GD library.
4628
		 *
4629
		 * @param string $userEntryid the user entryid which is going to open
4630
		 * @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...
4631
		 * Default value is set to 10 which is nearly extreme compressed image
4632
		 * @param mixed $userEntryId
4633
		 * @param mixed $compressedQuality
4634
		 *
4635
		 * @return string A base64 encoded string (data url)
4636
		 */
4637
		public function getCompressedUserImage($userEntryId, $compressedQuality = 10) {
4638
			try {
4639
				$user = $GLOBALS['mapisession']->getUser($userEntryId);
4640
			}
4641
			catch (Exception $e) {
4642
				$msg = "Problem while getting a user from the addressbook. Error %s : %s.";
4643
				$formattedMsg = sprintf($msg, $e->getCode(), $e->getMessage());
4644
				error_log($formattedMsg);
4645
				Log::Write(LOGLEVEL_ERROR, "Operations:getCompressedUserImage() " . $formattedMsg);
4646
4647
				return "";
4648
			}
4649
4650
			$userImageProp = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]);
4651
			if (isset($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO])) {
4652
				return $this->compressedImage($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO], $compressedQuality);
4653
			}
4654
4655
			return "";
4656
		}
4657
4658
		/**
4659
		 * Function used to compressed the image.
4660
		 *
4661
		 * @param string $image the image which is going to compress
4662
		 * @param int compressedQuality The compression factor range from 0 (high) to 100 (low)
4663
		 * Default value is set to 10 which is nearly extreme compressed image
4664
		 * @param mixed $compressedQuality
4665
		 *
4666
		 * @return string A base64 encoded string (data url)
4667
		 */
4668
		public function compressedImage($image, $compressedQuality = 10) {
4669
			// Proceed only when GD library's functions and user image data are available.
4670
			if (function_exists('imagecreatefromstring')) {
4671
				try {
4672
					$image = imagecreatefromstring($image);
4673
				}
4674
				catch (Exception $e) {
4675
					$msg = "Problem while creating image from string. Error %s : %s.";
4676
					$formattedMsg = sprintf($msg, $e->getCode(), $e->getMessage());
4677
					error_log($formattedMsg);
4678
					Log::Write(LOGLEVEL_ERROR, "Operations:compressedImage() " . $formattedMsg);
4679
				}
4680
4681
				if ($image !== false) {
4682
					// We need to use buffer because imagejpeg will give output as image in browser or file.
4683
					ob_start();
4684
					imagejpeg($image, null, $compressedQuality);
4685
					$compressedImg = ob_get_contents();
4686
					ob_end_clean();
4687
4688
					// Free up memory space acquired by image.
4689
					imagedestroy($image);
4690
4691
					return strlen($compressedImg) > 0 ? "data:image/jpg;base64," . base64_encode($compressedImg) : "";
4692
				}
4693
			}
4694
4695
			return "";
4696
		}
4697
4698
		public function getPropertiesFromStoreRoot($store, $props) {
4699
			$root = mapi_msgstore_openentry($store, null);
4700
4701
			return mapi_getprops($root, $props);
4702
		}
4703
4704
		/**
4705
		 * Returns the encryption key for sodium functions.
4706
		 *
4707
		 * It will generate a new one if the user doesn't have an encryption key yet.
4708
		 * It will also save the key into EncryptionStore for this session if the key
4709
		 * wasn't there yet.
4710
		 *
4711
		 * @return string
4712
		 */
4713
		public function getFilesEncryptionKey() {
4714
			// fallback if FILES_ACCOUNTSTORE_V1_SECRET_KEY is defined globally
4715
			$key = defined('FILES_ACCOUNTSTORE_V1_SECRET_KEY') ? hex2bin(constant('FILES_ACCOUNTSTORE_V1_SECRET_KEY')) : null;
4716
			if ($key === null) {
4717
				$encryptionStore = \EncryptionStore::getInstance();
4718
				$key = $encryptionStore->get('filesenckey');
4719
				if ($key === null) {
4720
					$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
4721
					$props = mapi_getprops($store, [PR_EC_WA_FILES_ENCRYPTION_KEY]);
4722
					if (isset($props[PR_EC_WA_FILES_ENCRYPTION_KEY])) {
4723
						$key = $props[PR_EC_WA_FILES_ENCRYPTION_KEY];
4724
					}
4725
					else {
4726
						$key = sodium_crypto_secretbox_keygen();
4727
						$encryptionStore->add('filesenckey', $key);
4728
						mapi_setprops($store, [PR_EC_WA_FILES_ENCRYPTION_KEY => $key]);
4729
						mapi_savechanges($store);
4730
					}
4731
				}
4732
			}
4733
4734
			return $key;
4735
		}
4736
	}
4737