Issues (752)

includes/modules/class.resolvenamesmodule.php (4 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
			$this->searchContactsFolders($ab, $ab_dir, $searchstr, $rows);
106
		}
107
		catch (MAPIException $e) {
108
			if ($e->getCode() == MAPI_E_AMBIGUOUS_RECIP) {
109
				$ab_entryid = mapi_ab_getdefaultdir($ab);
110
				$ab_dir = mapi_ab_openentry($ab, $ab_entryid);
111
				// Ambiguous, show possibilities:
112
				$table = mapi_folder_getcontentstable($ab_dir, MAPI_DEFERRED_ERRORS);
113
				$restriction = $this->getAmbigiousContactRestriction($searchstr, $excludeGABGroups, PR_ACCOUNT);
114
115
				mapi_table_restrict($table, $restriction, TBL_BATCH);
116
				mapi_table_sort($table, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
117
118
				$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]);
119
120
				$rows = array_merge($rows, $this->getAmbigiousContactResolveResults($ab, $searchstr, $excludeGABGroups));
121
			}
122
			elseif ($e->getCode() == MAPI_E_NOT_FOUND) {
123
				$rows = [];
124
				if ($query['address_type'] === 'SMTP') {
125
					// If we still can't find anything, and we were searching for a SMTP user
126
					// we can generate a oneoff entry which contains the information of the user.
127
					if (!empty($query['email_address'])) {
128
						$rows[] = [
129
							PR_ACCOUNT => $query['email_address'], PR_ADDRTYPE => 'SMTP', PR_EMAIL_ADDRESS => $query['email_address'],
130
							PR_DISPLAY_NAME => $query['display_name'], PR_DISPLAY_TYPE_EX => DT_REMOTE_MAILUSER, PR_DISPLAY_TYPE => DT_MAILUSER,
131
							PR_SMTP_ADDRESS => $query['email_address'], PR_OBJECT_TYPE => MAPI_MAILUSER,
132
							PR_ENTRYID => mapi_createoneoff($query['display_name'], 'SMTP', $query['email_address']),
133
						];
134
					}
135
					// Check also the user's contacts folders
136
					else {
137
						$this->searchContactsFolders($ab, $ab_dir, $searchstr, $rows);
138
					}
139
				}
140
			}
141
			else {
142
				// all other errors should be propagated to higher level exception handlers
143
				throw $e;
144
			}
145
		}
146
147
		$items = [];
148
		if ($rows) {
149
			foreach ($rows as $user_data) {
150
				$item = [];
151
152
				if (!isset($user_data[PR_ACCOUNT])) {
153
					$abitem = mapi_ab_openentry($ab, $user_data[PR_ENTRYID]);
154
					$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]);
155
				}
156
157
				if ($excludeGABGroups && $user_data[PR_OBJECT_TYPE] === MAPI_DISTLIST) {
158
					// exclude groups from result
159
					continue;
160
				}
161
162
				$item = [];
163
				$item['object_type'] = $user_data[PR_OBJECT_TYPE] ?? MAPI_MAILUSER;
164
				$item['entryid'] = isset($user_data[PR_ENTRYID]) ? bin2hex($user_data[PR_ENTRYID]) : '';
165
				$item['display_name'] = $user_data[PR_DISPLAY_NAME] ?? '';
166
				$item['display_type'] = $user_data[PR_DISPLAY_TYPE] ?? DT_MAILUSER;
167
168
				// Test whether the GUID in the entryid is from the Contact Provider
169
				if ($GLOBALS['entryid']->hasContactProviderGUID($item['entryid'])) {
170
					// The properties for a Distribution List differs from the other objects
171
					if ($item['object_type'] == MAPI_DISTLIST) {
172
						$item['address_type'] = 'MAPIPDL';
173
						// The email_address is empty for DistList, using display name for resolving
174
						$item['email_address'] = $item['display_name'];
175
						$item['smtp_address'] ??= '';
176
					}
177
					else {
178
						$item['address_type'] = $user_data[PR_ADDRTYPE] ?? 'SMTP';
179
						if (isset($user_data['address_type']) && $user_data['address_type'] === 'EX') {
180
							$item['email_address'] = $user_data[PR_EMAIL_ADDRESS] ?? '';
181
						}
182
						else {
183
							// Fake being an EX account, since it's actually an SMTP addrtype the email address is in a different property.
184
							$item['smtp_address'] = $user_data[PR_EMAIL_ADDRESS] ?? '';
185
							// Keep the old scenario happy.
186
							$item['email_address'] = $user_data[PR_EMAIL_ADDRESS] ?? '';
187
						}
188
					}
189
				// It can be considered a GAB entry
190
				}
191
				else {
192
					$item['user_name'] = $user_data[PR_ACCOUNT] ?? $item['display_name'];
193
					$item['display_type_ex'] = $user_data[PR_DISPLAY_TYPE_EX] ?? MAPI_MAILUSER;
194
					$item['email_address'] = $user_data[PR_EMAIL_ADDRESS] ?? '';
195
					$item['smtp_address'] = $user_data[PR_SMTP_ADDRESS] ?? $item['email_address'];
196
					$item['address_type'] = $user_data[PR_ADDRTYPE] ?? 'SMTP';
197
				}
198
199
				if (isset($user_data[PR_SEARCH_KEY])) {
200
					$item['search_key'] = bin2hex($user_data[PR_SEARCH_KEY]);
201
				}
202
				else {
203
					$emailAddress = $item['smtp_address'] ?? $item['email_address'];
204
					$item['search_key'] = bin2hex(strtoupper($item['address_type'] . ':' . $emailAddress)) . '00';
205
				}
206
207
				array_push($items, $item);
208
			}
209
		}
210
211
		return $items;
212
	}
213
214
	/**
215
	 * Used to find multiple entries from the contact folders in the Addressbook when resolving
216
	 * returned an ambiguous result. It will find the Contact folders in the Addressbook and
217
	 * apply a restriction to extract the entries.
218
	 *
219
	 * @param resource $ab               The addressbook
220
	 * @param string   $query            The search query, case is ignored
221
	 * @param bool     $excludeGABGroups flag to exclude groups from resolving
222
	 */
223
	public function getAmbigiousContactResolveResults($ab, $query, $excludeGABGroups) {
224
		/* We need to look for the Contact folders at the bottom of the following tree.
225
		*
226
		 * IAddrBook
227
		 *  - Root Container
228
		 *     - HIERARCHY TABLE
229
		 *        - Contacts Folders    (Contact Container)
230
		 *           - HIERARCHY TABLE         (Contact Container Hierarchy)
231
		 *              - Contact folder 1
232
		 *              - Contact folder 2
233
		 */
234
235
		$rows = [];
236
		$contactFolderRestriction = $this->getAmbigiousContactRestriction($query, $excludeGABGroups, PR_EMAIL_ADDRESS);
237
		// Open the AB Root Container by not supplying an entryid
238
		$abRootContainer = mapi_ab_openentry($ab);
239
240
		// Get the 'Contact Folders'
241
		$hierarchyTable = mapi_folder_gethierarchytable($abRootContainer, MAPI_DEFERRED_ERRORS);
242
		$abHierarchyRows = mapi_table_queryallrows($hierarchyTable, [PR_AB_PROVIDER_ID, PR_ENTRYID]);
243
244
		// Look for the 'Contacts Folders'
245
		for ($i = 0,$len = count($abHierarchyRows); $i < $len; ++$i) {
246
			// Check if the folder matches the Contact Provider GUID
247
			if ($abHierarchyRows[$i][PR_AB_PROVIDER_ID] == MUIDZCSAB) {
248
				$abContactContainerEntryid = $abHierarchyRows[$i][PR_ENTRYID];
249
				break;
250
			}
251
		}
252
253
		// Next go into the 'Contacts Folders' and look in the hierarchy table for the Contact folders.
254
		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...
255
			// Get the rows from hierarchy table of the 'Contacts Folders'
256
			$abContactContainer = mapi_ab_openentry($ab, $abContactContainerEntryid);
257
			$abContactContainerHierarchyTable = mapi_folder_gethierarchytable($abContactContainer, MAPI_DEFERRED_ERRORS);
258
			$abContactContainerHierarchyRows = mapi_table_queryallrows($abContactContainerHierarchyTable, [PR_DISPLAY_NAME, PR_OBJECT_TYPE, PR_ENTRYID]);
259
260
			// Loop through all the contact folders found under the 'Contacts Folders' hierarchy
261
			for ($j = 0,$len = count($abContactContainerHierarchyRows); $j < $len; ++$j) {
262
				// Open, get contents table, restrict, sort and then merge the result in the list of $rows
263
				$abContactFolder = mapi_ab_openentry($ab, $abContactContainerHierarchyRows[$j][PR_ENTRYID]);
264
				$abContactFolderTable = mapi_folder_getcontentstable($abContactFolder, MAPI_DEFERRED_ERRORS);
265
266
				mapi_table_restrict($abContactFolderTable, $contactFolderRestriction, TBL_BATCH);
267
				mapi_table_sort($abContactFolderTable, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
268
269
				// Go go gadget, merge!
270
				$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]));
271
			}
272
		}
273
274
		return $rows;
275
	}
276
277
	/**
278
	 * Setup the restriction used for resolving in the Contact folders or GAB.
279
	 *
280
	 * @param string $query            The search query, case is ignored
281
	 * @param bool   $excludeGABGroups flag to exclude groups from resolving
282
	 * @param int    $content          the PROPTAG to search in
283
	 */
284
	public function getAmbigiousContactRestriction($query, $excludeGABGroups, $content) {
285
		// only return users from who the displayName or the username starts with $name
286
		// TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and $content.
287
		$resAnd = [
288
			[RES_OR,
289
				[
290
					[RES_CONTENT,
291
						[
292
							FUZZYLEVEL => FL_PREFIX | FL_IGNORECASE,
293
							ULPROPTAG => PR_DISPLAY_NAME,
294
							VALUE => $query,
295
						],
296
					],
297
					[RES_CONTENT,
298
						[
299
							FUZZYLEVEL => FL_PREFIX | FL_IGNORECASE,
300
							ULPROPTAG => $content,
301
							VALUE => $query,
302
						],
303
					],
304
				], // RES_OR
305
			],
306
		];
307
308
		// create restrictions based on excludeGABGroups flag
309
		if ($excludeGABGroups) {
310
			array_push($resAnd, [
311
				RES_PROPERTY,
312
				[
313
					RELOP => RELOP_EQ,
314
					ULPROPTAG => PR_OBJECT_TYPE,
315
					VALUE => MAPI_MAILUSER,
316
				],
317
			]);
318
		}
319
		else {
320
			array_push($resAnd, [RES_OR,
321
				[
322
					[RES_PROPERTY,
323
						[
324
							RELOP => RELOP_EQ,
325
							ULPROPTAG => PR_OBJECT_TYPE,
326
							VALUE => MAPI_MAILUSER,
327
						],
328
					],
329
					[RES_PROPERTY,
330
						[
331
							RELOP => RELOP_EQ,
332
							ULPROPTAG => PR_OBJECT_TYPE,
333
							VALUE => MAPI_DISTLIST,
334
						],
335
					],
336
				],
337
			]);
338
		}
339
340
		return [RES_AND, $resAnd];
341
	}
342
343
	/**
344
	 * Function does customization of exception based on module data.
345
	 * like, here it will generate display message based on actionType
346
	 * for particular exception.
347
	 *
348
	 * @param object     $e             Exception object
349
	 * @param string     $actionType    the action type, sent by the client
350
	 * @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...
351
	 * @param string     $parententryid parent entryid of the message
352
	 * @param string     $entryid       entryid of the message
353
	 * @param array      $action        the action data, sent by the client
354
	 */
355
	#[Override]
356
	public function handleException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) {
357
		if (is_null($e->displayMessage)) {
358
			switch ($actionType) {
359
				case 'checknames':
360
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
361
						$e->setDisplayMessage(_('You have insufficient privileges to perform this action.'));
362
					}
363
					else {
364
						$e->setDisplayMessage(_('Could not resolve user.'));
365
					}
366
					break;
367
			}
368
		}
369
370
		parent::handleException($e, $actionType, $store, $parententryid, $entryid, $action);
371
	}
372
373
	/**
374
	 * This function searches the private contact folders for users and returns an array with data.
375
	 * Please note that the returning array must be UTF8.
376
	 *
377
	 * @param resource $ab        The addressbook
378
	 * @param resource $ab_dir    The addressbook container
379
	 * @param string   $searchstr The search query, case is ignored
380
	 * @param array    $rows      Array of the found contacts
381
	 */
382
	public function searchContactsFolders($ab, $ab_dir, $searchstr, &$rows) {
383
		$abhtable = mapi_folder_gethierarchytable($ab_dir, MAPI_DEFERRED_ERRORS | CONVENIENT_DEPTH);
384
		$abcntfolders = mapi_table_queryallrows($abhtable, [PR_ENTRYID, PR_AB_PROVIDER_ID]);
385
		$restriction = [
386
			RES_CONTENT,
387
			[
388
				FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE,
389
				ULPROPTAG => PR_DISPLAY_NAME,
390
				VALUE => [PR_DISPLAY_NAME => $searchstr],
391
			],
392
		];
393
		// restriction on hierarchy table for PR_AB_PROVIDER_ID
394
		// seems not to work, just loop through
395
		foreach ($abcntfolders as $abcntfolder) {
396
			if ($abcntfolder[PR_AB_PROVIDER_ID] == ZARAFA_CONTACTS_GUID) {
397
				$abfldentry = mapi_ab_openentry($ab, $abcntfolder[PR_ENTRYID]);
398
				$abfldcontents = mapi_folder_getcontentstable($abfldentry);
399
				mapi_table_restrict($abfldcontents, $restriction);
400
				$r = mapi_table_queryallrows($abfldcontents, [PR_ACCOUNT, PR_ADDRTYPE, PR_DISPLAY_NAME, PR_ENTRYID,
401
					PR_SEARCH_KEY, PR_OBJECT_TYPE, PR_SMTP_ADDRESS, PR_DISPLAY_TYPE_EX, PR_EMAIL_ADDRESS,
402
					PR_OBJECT_TYPE, PR_DISPLAY_TYPE]);
403
				if (is_array($r) && !empty($r)) {
404
					$rows = array_merge($rows, $r);
405
				}
406
			}
407
		}
408
	}
409
}
410