Issues (817)

includes/modules/class.resolvenamesmodule.php (5 issues)

1
<?php
2
3
/**
4
 * ResolveNames Module.
5
 */
6
class ResolveNamesModule extends Module {
7
	/**
8
	 * Constructor.
9
	 *
10
	 * @param mixed $id
11
	 * @param mixed $data
12
	 */
13
	public function __construct($id, $data) {
14
		parent::__construct($id, $data);
15
	}
16
17
	/**
18
	 * Executes all the actions in the $data variable.
19
	 */
20
	#[Override]
21
	public function execute() {
22
		foreach ($this->data as $actionType => $action) {
23
			if (isset($actionType)) {
24
				try {
25
					match ($actionType) {
26
						'checknames' => $this->checkNames($action),
0 ignored issues
show
Are you sure the usage of $this->checkNames($action) targeting ResolveNamesModule::checkNames() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
27
						default => $this->handleUnknownActionType($actionType),
0 ignored issues
show
Are you sure the usage of $this->handleUnknownActionType($actionType) targeting Module::handleUnknownActionType() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
28
					};
29
				}
30
				catch (MAPIException $e) {
31
					$this->processException($e, $actionType);
32
				}
33
			}
34
		}
35
	}
36
37
	/**
38
	 * Function which checks the names, sent by the client. This function is used
39
	 * when a user wants to sent an email and want to check the names filled in
40
	 * by the user in the to, cc and bcc field. This function uses the global
41
	 * user list to check if the names are correct.
42
	 *
43
	 * @param array $action the action data, sent by the client
44
	 */
45
	public function checkNames($action) {
46
		if (isset($action['resolverequests'])) {
47
			$data = [];
48
			$excludeLocalContacts = $action['exclude_local_contacts'] ?? false;
49
			$excludeGABGroups = $action['exclude_gab_groups'] ?? false;
50
			$resolveRequest = $action['resolverequests'];
51
			if (!is_array($resolveRequest)) {
52
				$resolveRequest = [$resolveRequest];
53
			}
54
55
			// open addressbook
56
			// When local contacts need to be excluded we have pass true as the first argument
57
			// so we will not have any Contact Providers set on the Addressbook resource.
58
			$ab = $GLOBALS['mapisession']->getAddressbook($excludeLocalContacts);
59
60
			$ab_dir = mapi_ab_openentry($ab);
61
			$resolveResponse = [];
62
63
			// check names
64
			foreach ($resolveRequest as $query) {
65
				if (is_array($query) && isset($query['id'])) {
66
					$responseEntry = [];
67
					$responseEntry['id'] = $query['id'];
68
69
					if (!empty($query['display_name']) || !empty($query['email_address'])) {
70
						$responseEntry['result'] = $this->searchAddressBook($ab, $ab_dir, $query, $excludeGABGroups);
71
						$resolveResponse[] = $responseEntry;
72
					}
73
				}
74
			}
75
76
			$data['resolveresponse'] = $resolveResponse;
77
78
			$this->addActionData('checknames', $data);
79
			$GLOBALS['bus']->addData($this->getResponseData());
80
		}
81
	}
82
83
	/**
84
	 * This function searches the addressbook specified for users and returns an array with data
85
	 * Please note that the returning array must be UTF8.
86
	 *
87
	 * @param resource $ab               The addressbook
88
	 * @param resource $ab_dir           The addressbook container
89
	 * @param string   $query            The search query, case is ignored
90
	 * @param bool     $excludeGABGroups flag to exclude groups from resolving
91
	 */
92
	public function searchAddressBook($ab, $ab_dir, $query, $excludeGABGroups) {
93
		// Prefer resolving the email_address. This allows the user
94
		// to resolve recipients with a display name that matches a EX
95
		// user with an alternative (external) email address.
96
		$searchstr = empty($query['email_address']) ? $query['display_name'] : $query['email_address'];
97
		// If the address_type is 'EX' then we are resolving something which must be found in
98
		// the GAB as an exact match. So add the flag EMS_AB_ADDRESS_LOOKUP to ensure we will not
99
		// get multiple results when multiple items have a partial match.
100
		$flags = $query['address_type'] === 'EX' ? EMS_AB_ADDRESS_LOOKUP : 0;
101
102
		try {
103
			// First, try an addressbook lookup
104
			$rows = mapi_ab_resolvename($ab, [[PR_DISPLAY_NAME => $searchstr]], $flags);
105
106
			// If the lookup succeeded but we should also check for exact hidden matches
107
			if (defined('ENABLE_RESOLVE_HIDDEN_USERS') && ENABLE_RESOLVE_HIDDEN_USERS && !($flags & EMS_AB_ADDRESS_LOOKUP)) {
108
				$hiddenRows = $this->resolveHiddenExactMatches($ab, $query);
0 ignored issues
show
$query of type string is incompatible with the type array expected by parameter $query of ResolveNamesModule::resolveHiddenExactMatches(). ( Ignorable by Annotation )

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

108
				$hiddenRows = $this->resolveHiddenExactMatches($ab, /** @scrutinizer ignore-type */ $query);
Loading history...
109
				if (!empty($hiddenRows)) {
110
					$rows = $this->mergeAndDeduplicateRows($rows, $hiddenRows);
111
				}
112
			}
113
114
			$this->searchContactsFolders($ab, $ab_dir, $searchstr, $rows);
115
		}
116
		catch (MAPIException $e) {
117
			if ($e->getCode() == MAPI_E_AMBIGUOUS_RECIP) {
118
				$ab_entryid = mapi_ab_getdefaultdir($ab);
119
				$ab_dir = mapi_ab_openentry($ab, $ab_entryid);
120
				// Ambiguous, show possibilities:
121
				$table = mapi_folder_getcontentstable($ab_dir, MAPI_DEFERRED_ERRORS);
122
				$restriction = $this->getAmbigiousContactRestriction($searchstr, $excludeGABGroups, PR_ACCOUNT);
123
124
				mapi_table_restrict($table, $restriction, TBL_BATCH);
125
				mapi_table_sort($table, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
126
127
				$rows = mapi_table_queryallrows($table, [PR_ACCOUNT, PR_ADDRTYPE, PR_DISPLAY_NAME, PR_ENTRYID, PR_SEARCH_KEY, PR_OBJECT_TYPE, PR_SMTP_ADDRESS, PR_DISPLAY_TYPE_EX, PR_EMAIL_ADDRESS, PR_OBJECT_TYPE, PR_DISPLAY_TYPE]);
128
129
				$rows = array_merge($rows, $this->getAmbigiousContactResolveResults($ab, $searchstr, $excludeGABGroups));
130
131
				// Also check for hidden users if enabled and this is an exact match
132
				if (defined('ENABLE_RESOLVE_HIDDEN_USERS') && ENABLE_RESOLVE_HIDDEN_USERS) {
133
					$hiddenRows = $this->resolveHiddenExactMatches($ab, $query);
134
					if (!empty($hiddenRows)) {
135
						$rows = $this->mergeAndDeduplicateRows($rows, $hiddenRows);
136
					}
137
				}
138
			}
139
			elseif ($e->getCode() == MAPI_E_NOT_FOUND) {
140
				$rows = [];
141
142
				if (defined('ENABLE_RESOLVE_HIDDEN_USERS') && ENABLE_RESOLVE_HIDDEN_USERS) {
143
					$rows = $this->resolveHiddenExactMatches($ab, $query);
144
				}
145
146
				if (empty($rows) && (($query['address_type'] ?? '') === 'SMTP')) {
147
					// If we still can't find anything, and we were searching for a SMTP user
148
					// we can generate a oneoff entry which contains the information of the user.
149
					if (!empty($query['email_address'])) {
150
						$rows[] = [
151
							PR_ACCOUNT => $query['email_address'], PR_ADDRTYPE => 'SMTP', PR_EMAIL_ADDRESS => $query['email_address'],
152
							PR_DISPLAY_NAME => $query['display_name'], PR_DISPLAY_TYPE_EX => DT_REMOTE_MAILUSER, PR_DISPLAY_TYPE => DT_MAILUSER,
153
							PR_SMTP_ADDRESS => $query['email_address'], PR_OBJECT_TYPE => MAPI_MAILUSER,
154
							PR_ENTRYID => mapi_createoneoff($query['display_name'], 'SMTP', $query['email_address']),
155
						];
156
					}
157
					// Check also the user's contacts folders
158
					else {
159
						$this->searchContactsFolders($ab, $ab_dir, $searchstr, $rows);
160
					}
161
				}
162
				elseif (empty($rows)) {
163
					$this->searchContactsFolders($ab, $ab_dir, $searchstr, $rows);
164
				}
165
			}
166
			else {
167
				// all other errors should be propagated to higher level exception handlers
168
				throw $e;
169
			}
170
		}
171
172
		$items = [];
173
		if ($rows) {
174
			foreach ($rows as $user_data) {
175
				$item = [];
176
177
				if (!isset($user_data[PR_ACCOUNT])) {
178
					$abitem = mapi_ab_openentry($ab, $user_data[PR_ENTRYID]);
179
					$user_data = mapi_getprops($abitem, [PR_ACCOUNT, PR_ADDRTYPE, PR_DISPLAY_NAME, PR_DISPLAY_TYPE_EX, PR_ENTRYID, PR_SEARCH_KEY, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_OBJECT_TYPE, PR_DISPLAY_TYPE]);
180
				}
181
182
				if ($excludeGABGroups && $user_data[PR_OBJECT_TYPE] === MAPI_DISTLIST) {
183
					// exclude groups from result
184
					continue;
185
				}
186
187
				$item = [];
188
				$item['object_type'] = $user_data[PR_OBJECT_TYPE] ?? MAPI_MAILUSER;
189
				$item['entryid'] = isset($user_data[PR_ENTRYID]) ? bin2hex($user_data[PR_ENTRYID]) : '';
190
				$item['display_name'] = $user_data[PR_DISPLAY_NAME] ?? '';
191
				$item['display_type'] = $user_data[PR_DISPLAY_TYPE] ?? DT_MAILUSER;
192
193
				// Test whether the GUID in the entryid is from the Contact Provider
194
				if ($GLOBALS['entryid']->hasContactProviderGUID($item['entryid'])) {
195
					// The properties for a Distribution List differs from the other objects
196
					if ($item['object_type'] == MAPI_DISTLIST) {
197
						$item['address_type'] = 'MAPIPDL';
198
						// The email_address is empty for DistList, using display name for resolving
199
						$item['email_address'] = $item['display_name'];
200
						$item['smtp_address'] ??= '';
201
					}
202
					else {
203
						$item['address_type'] = $user_data[PR_ADDRTYPE] ?? 'SMTP';
204
						if (isset($user_data['address_type']) && $user_data['address_type'] === 'EX') {
205
							$item['email_address'] = $user_data[PR_EMAIL_ADDRESS] ?? '';
206
						}
207
						else {
208
							// Fake being an EX account, since it's actually an SMTP addrtype the email address is in a different property.
209
							$item['smtp_address'] = $user_data[PR_EMAIL_ADDRESS] ?? '';
210
							// Keep the old scenario happy.
211
							$item['email_address'] = $user_data[PR_EMAIL_ADDRESS] ?? '';
212
						}
213
					}
214
				// It can be considered a GAB entry
215
				}
216
				else {
217
					$item['user_name'] = $user_data[PR_ACCOUNT] ?? $item['display_name'];
218
					$item['display_type_ex'] = $user_data[PR_DISPLAY_TYPE_EX] ?? MAPI_MAILUSER;
219
					$item['email_address'] = $user_data[PR_EMAIL_ADDRESS] ?? '';
220
					$item['smtp_address'] = $user_data[PR_SMTP_ADDRESS] ?? $item['email_address'];
221
					$item['address_type'] = $user_data[PR_ADDRTYPE] ?? 'SMTP';
222
				}
223
224
				if (isset($user_data[PR_SEARCH_KEY])) {
225
					$item['search_key'] = bin2hex($user_data[PR_SEARCH_KEY]);
226
				}
227
				else {
228
					$emailAddress = $item['smtp_address'] ?? $item['email_address'];
229
					$item['search_key'] = bin2hex(strtoupper($item['address_type'] . ':' . $emailAddress)) . '00';
230
				}
231
232
				array_push($items, $item);
233
			}
234
		}
235
236
		return $items;
237
	}
238
239
	/**
240
	 * Try to resolve a hidden user by matching the provided query exactly against
241
	 * common address book properties. Hidden entries are not returned in regular
242
	 * searches, but the EMS_AB_ADDRESS_LOOKUP flag allows us to perform an exact
243
	 * lookup that also returns hidden entries.
244
	 *
245
	 * @param resource $ab    The addressbook
246
	 * @param array    $query Resolve query sent by the client
247
	 *
248
	 * @return array
249
	 */
250
	protected function resolveHiddenExactMatches($ab, $query) {
251
		if (!defined('ENABLE_RESOLVE_HIDDEN_USERS') || !ENABLE_RESOLVE_HIDDEN_USERS) {
252
			return [];
253
		}
254
255
		$candidates = [];
256
257
		if (!empty($query['display_name'])) {
258
			$candidates[] = trim((string) $query['display_name']);
259
		}
260
261
		if (!empty($query['email_address'])) {
262
			$candidates[] = trim((string) $query['email_address']);
263
		}
264
265
		$candidates = array_values(array_unique(array_filter($candidates, 'strlen')));
266
		$addressType = strtoupper((string) ($query['address_type'] ?? ''));
267
268
		foreach ($candidates as $candidate) {
269
			$lookups = [PR_DISPLAY_NAME];
270
271
			if ($addressType === 'EX') {
272
				$lookups[] = PR_ACCOUNT;
273
			}
274
275
			if (str_contains($candidate, '@')) {
276
				$lookups[] = PR_EMAIL_ADDRESS;
277
				$lookups[] = PR_SMTP_ADDRESS;
278
279
				if ($addressType !== 'EX') {
280
					$lookups[] = PR_ACCOUNT;
281
				}
282
			}
283
284
			foreach (array_unique($lookups) as $property) {
285
				$rows = $this->resolveNameByProperty($ab, $property, $candidate);
286
				if (!empty($rows)) {
287
					return $rows;
288
				}
289
			}
290
		}
291
292
		return [];
293
	}
294
295
	/**
296
	 * Execute an exact resolve against the GAB for the provided property.
297
	 *
298
	 * @param resource $ab       The addressbook
299
	 * @param int      $property Property tag used for the lookup
300
	 * @param string   $value    Search value
301
	 *
302
	 * @return array
303
	 */
304
	protected function resolveNameByProperty($ab, $property, $value) {
305
		if ($value === '') {
306
			return [];
307
		}
308
309
		try {
310
			return mapi_ab_resolvename($ab, [[$property => $value]], EMS_AB_ADDRESS_LOOKUP);
311
		}
312
		catch (MAPIException $e) {
313
			if ($e->getCode() == MAPI_E_NOT_FOUND || $e->getCode() == MAPI_E_AMBIGUOUS_RECIP) {
314
				return [];
315
			}
316
317
			throw $e;
318
		}
319
	}
320
321
	/**
322
	 * Merge two arrays of rows and remove duplicates based on entryid.
323
	 * This prevents the same user from appearing multiple times when they're
324
	 * found through both normal and hidden user resolution.
325
	 *
326
	 * @param array $existingRows Existing rows from table/resolve queries
327
	 * @param array $newRows      New rows from hidden user resolution
328
	 *
329
	 * @return array Merged array with duplicates removed
330
	 */
331
	protected function mergeAndDeduplicateRows($existingRows, $newRows) {
332
		// Build a map of existing entryids
333
		$entryidMap = [];
334
		foreach ($existingRows as $row) {
335
			if (isset($row[PR_ENTRYID])) {
336
				$entryidMap[bin2hex($row[PR_ENTRYID])] = true;
337
			}
338
		}
339
340
		// Add new rows only if they don't already exist
341
		foreach ($newRows as $row) {
342
			if (isset($row[PR_ENTRYID])) {
343
				$entryidHex = bin2hex($row[PR_ENTRYID]);
344
				if (!isset($entryidMap[$entryidHex])) {
345
					$existingRows[] = $row;
346
					$entryidMap[$entryidHex] = true;
347
				}
348
			}
349
		}
350
351
		return $existingRows;
352
	}
353
354
	/**
355
	 * Used to find multiple entries from the contact folders in the Addressbook when resolving
356
	 * returned an ambiguous result. It will find the Contact folders in the Addressbook and
357
	 * apply a restriction to extract the entries.
358
	 *
359
	 * @param resource $ab               The addressbook
360
	 * @param string   $query            The search query, case is ignored
361
	 * @param bool     $excludeGABGroups flag to exclude groups from resolving
362
	 */
363
	public function getAmbigiousContactResolveResults($ab, $query, $excludeGABGroups) {
364
		/* We need to look for the Contact folders at the bottom of the following tree.
365
		*
366
		 * IAddrBook
367
		 *  - Root Container
368
		 *     - HIERARCHY TABLE
369
		 *        - Contacts Folders    (Contact Container)
370
		 *           - HIERARCHY TABLE         (Contact Container Hierarchy)
371
		 *              - Contact folder 1
372
		 *              - Contact folder 2
373
		 */
374
375
		$rows = [];
376
		$contactFolderRestriction = $this->getAmbigiousContactRestriction($query, $excludeGABGroups, PR_EMAIL_ADDRESS);
377
		// Open the AB Root Container by not supplying an entryid
378
		$abRootContainer = mapi_ab_openentry($ab);
379
380
		// Get the 'Contact Folders'
381
		$hierarchyTable = mapi_folder_gethierarchytable($abRootContainer, MAPI_DEFERRED_ERRORS);
382
		$abHierarchyRows = mapi_table_queryallrows($hierarchyTable, [PR_AB_PROVIDER_ID, PR_ENTRYID]);
383
384
		// Look for the 'Contacts Folders'
385
		for ($i = 0,$len = count($abHierarchyRows); $i < $len; ++$i) {
386
			// Check if the folder matches the Contact Provider GUID
387
			if ($abHierarchyRows[$i][PR_AB_PROVIDER_ID] == MUIDZCSAB) {
388
				$abContactContainerEntryid = $abHierarchyRows[$i][PR_ENTRYID];
389
				break;
390
			}
391
		}
392
393
		// Next go into the 'Contacts Folders' and look in the hierarchy table for the Contact folders.
394
		if ($abContactContainerEntryid) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $abContactContainerEntryid does not seem to be defined for all execution paths leading up to this point.
Loading history...
395
			// Get the rows from hierarchy table of the 'Contacts Folders'
396
			$abContactContainer = mapi_ab_openentry($ab, $abContactContainerEntryid);
397
			$abContactContainerHierarchyTable = mapi_folder_gethierarchytable($abContactContainer, MAPI_DEFERRED_ERRORS);
398
			$abContactContainerHierarchyRows = mapi_table_queryallrows($abContactContainerHierarchyTable, [PR_DISPLAY_NAME, PR_OBJECT_TYPE, PR_ENTRYID]);
399
400
			// Loop through all the contact folders found under the 'Contacts Folders' hierarchy
401
			for ($j = 0,$len = count($abContactContainerHierarchyRows); $j < $len; ++$j) {
402
				// Open, get contents table, restrict, sort and then merge the result in the list of $rows
403
				$abContactFolder = mapi_ab_openentry($ab, $abContactContainerHierarchyRows[$j][PR_ENTRYID]);
404
				$abContactFolderTable = mapi_folder_getcontentstable($abContactFolder, MAPI_DEFERRED_ERRORS);
405
406
				mapi_table_restrict($abContactFolderTable, $contactFolderRestriction, TBL_BATCH);
407
				mapi_table_sort($abContactFolderTable, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
408
409
				// Go go gadget, merge!
410
				$rows = array_merge($rows, mapi_table_queryallrows($abContactFolderTable, [PR_ACCOUNT, PR_DISPLAY_NAME, PR_ENTRYID, PR_OBJECT_TYPE, PR_SMTP_ADDRESS, PR_DISPLAY_TYPE_EX, PR_EMAIL_ADDRESS, PR_OBJECT_TYPE, PR_DISPLAY_TYPE]));
411
			}
412
		}
413
414
		return $rows;
415
	}
416
417
	/**
418
	 * Setup the restriction used for resolving in the Contact folders or GAB.
419
	 *
420
	 * @param string $query            The search query, case is ignored
421
	 * @param bool   $excludeGABGroups flag to exclude groups from resolving
422
	 * @param int    $content          the PROPTAG to search in
423
	 */
424
	public function getAmbigiousContactRestriction($query, $excludeGABGroups, $content) {
425
		// only return users from who the displayName or the username starts with $name
426
		// TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and $content.
427
		$resAnd = [
428
			[RES_OR,
429
				[
430
					[RES_CONTENT,
431
						[
432
							FUZZYLEVEL => FL_PREFIX | FL_IGNORECASE,
433
							ULPROPTAG => PR_DISPLAY_NAME,
434
							VALUE => $query,
435
						],
436
					],
437
					[RES_CONTENT,
438
						[
439
							FUZZYLEVEL => FL_PREFIX | FL_IGNORECASE,
440
							ULPROPTAG => $content,
441
							VALUE => $query,
442
						],
443
					],
444
				], // RES_OR
445
			],
446
		];
447
448
		// create restrictions based on excludeGABGroups flag
449
		if ($excludeGABGroups) {
450
			array_push($resAnd, [
451
				RES_PROPERTY,
452
				[
453
					RELOP => RELOP_EQ,
454
					ULPROPTAG => PR_OBJECT_TYPE,
455
					VALUE => MAPI_MAILUSER,
456
				],
457
			]);
458
		}
459
		else {
460
			array_push($resAnd, [RES_OR,
461
				[
462
					[RES_PROPERTY,
463
						[
464
							RELOP => RELOP_EQ,
465
							ULPROPTAG => PR_OBJECT_TYPE,
466
							VALUE => MAPI_MAILUSER,
467
						],
468
					],
469
					[RES_PROPERTY,
470
						[
471
							RELOP => RELOP_EQ,
472
							ULPROPTAG => PR_OBJECT_TYPE,
473
							VALUE => MAPI_DISTLIST,
474
						],
475
					],
476
				],
477
			]);
478
		}
479
480
		return [RES_AND, $resAnd];
481
	}
482
483
	/**
484
	 * Function does customization of exception based on module data.
485
	 * like, here it will generate display message based on actionType
486
	 * for particular exception.
487
	 *
488
	 * @param object     $e             Exception object
489
	 * @param string     $actionType    the action type, sent by the client
490
	 * @param MAPIobject $store         store object of message
0 ignored issues
show
The type MAPIobject 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...
491
	 * @param string     $parententryid parent entryid of the message
492
	 * @param string     $entryid       entryid of the message
493
	 * @param array      $action        the action data, sent by the client
494
	 */
495
	#[Override]
496
	public function handleException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) {
497
		if (is_null($e->displayMessage)) {
498
			switch ($actionType) {
499
				case 'checknames':
500
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
501
						$e->setDisplayMessage(_('You have insufficient privileges to perform this action.'));
502
					}
503
					else {
504
						$e->setDisplayMessage(_('Could not resolve user.'));
505
					}
506
					break;
507
			}
508
		}
509
510
		parent::handleException($e, $actionType, $store, $parententryid, $entryid, $action);
511
	}
512
513
	/**
514
	 * This function searches the private contact folders for users and returns an array with data.
515
	 * Please note that the returning array must be UTF8.
516
	 *
517
	 * @param resource $ab        The addressbook
518
	 * @param resource $ab_dir    The addressbook container
519
	 * @param string   $searchstr The search query, case is ignored
520
	 * @param array    $rows      Array of the found contacts
521
	 */
522
	public function searchContactsFolders($ab, $ab_dir, $searchstr, &$rows) {
523
		$abhtable = mapi_folder_gethierarchytable($ab_dir, MAPI_DEFERRED_ERRORS | CONVENIENT_DEPTH);
524
		$abcntfolders = mapi_table_queryallrows($abhtable, [PR_ENTRYID, PR_AB_PROVIDER_ID]);
525
		$restriction = [
526
			RES_CONTENT,
527
			[
528
				FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE,
529
				ULPROPTAG => PR_DISPLAY_NAME,
530
				VALUE => [PR_DISPLAY_NAME => $searchstr],
531
			],
532
		];
533
		// restriction on hierarchy table for PR_AB_PROVIDER_ID
534
		// seems not to work, just loop through
535
		foreach ($abcntfolders as $abcntfolder) {
536
			if ($abcntfolder[PR_AB_PROVIDER_ID] == ZARAFA_CONTACTS_GUID) {
537
				$abfldentry = mapi_ab_openentry($ab, $abcntfolder[PR_ENTRYID]);
538
				$abfldcontents = mapi_folder_getcontentstable($abfldentry);
539
				mapi_table_restrict($abfldcontents, $restriction);
540
				$r = mapi_table_queryallrows($abfldcontents, [PR_ACCOUNT, PR_ADDRTYPE, PR_DISPLAY_NAME, PR_ENTRYID,
541
					PR_SEARCH_KEY, PR_OBJECT_TYPE, PR_SMTP_ADDRESS, PR_DISPLAY_TYPE_EX, PR_EMAIL_ADDRESS,
542
					PR_OBJECT_TYPE, PR_DISPLAY_TYPE]);
543
				if (is_array($r) && !empty($r)) {
544
					$rows = array_merge($rows, $r);
545
				}
546
			}
547
		}
548
	}
549
}
550