Passed
Push — master ( ab1b16...12794c )
by
unknown
14:44 queued 14s
created

ResolveNamesModule::handleException()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 6
dl 0
loc 15
rs 9.9666
c 0
b 0
f 0
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
	public function execute() {
21
		foreach ($this->data as $actionType => $action) {
22
			if (isset($actionType)) {
23
				try {
24
					switch ($actionType) {
25
						case 'checknames':
26
							$this->checkNames($action);
27
							break;
28
29
						default:
30
							$this->handleUnknownActionType($actionType);
31
					}
32
				}
33
				catch (MAPIException $e) {
34
					$this->processException($e, $actionType);
35
				}
36
			}
37
		}
38
	}
39
40
	/**
41
	 * Function which checks the names, sent by the client. This function is used
42
	 * when a user wants to sent an email and want to check the names filled in
43
	 * by the user in the to, cc and bcc field. This function uses the global
44
	 * user list to check if the names are correct.
45
	 *
46
	 * @param array $action the action data, sent by the client
47
	 */
48
	public function checkNames($action) {
49
		if (isset($action['resolverequests'])) {
50
			$data = [];
51
			$excludeLocalContacts = $action['exclude_local_contacts'] ?? false;
52
			$excludeGABGroups = $action['exclude_gab_groups'] ?? false;
53
			$resolveRequest = $action['resolverequests'];
54
			if (!is_array($resolveRequest)) {
55
				$resolveRequest = [$resolveRequest];
56
			}
57
58
			// open addressbook
59
			// When local contacts need to be excluded we have pass true as the first argument
60
			// so we will not have any Contact Providers set on the Addressbook resource.
61
			$ab = $GLOBALS['mapisession']->getAddressbook($excludeLocalContacts);
62
63
			$ab_dir = mapi_ab_openentry($ab);
64
			$resolveResponse = [];
65
66
			// check names
67
			foreach ($resolveRequest as $query) {
68
				if (is_array($query) && isset($query['id'])) {
69
					$responseEntry = [];
70
					$responseEntry['id'] = $query['id'];
71
72
					if (!empty($query['display_name']) || !empty($query['email_address'])) {
73
						$responseEntry['result'] = $this->searchAddressBook($ab, $ab_dir, $query, $excludeGABGroups);
74
						$resolveResponse[] = $responseEntry;
75
					}
76
				}
77
			}
78
79
			$data['resolveresponse'] = $resolveResponse;
80
81
			$this->addActionData('checknames', $data);
82
			$GLOBALS['bus']->addData($this->getResponseData());
83
		}
84
	}
85
86
	/**
87
	 * This function searches the addressbook specified for users and returns an array with data
88
	 * Please note that the returning array must be UTF8.
89
	 *
90
	 * @param resource $ab               The addressbook
91
	 * @param resource $ab_dir           The addressbook container
92
	 * @param string   $query            The search query, case is ignored
93
	 * @param bool     $excludeGABGroups flag to exclude groups from resolving
94
	 */
95
	public function searchAddressBook($ab, $ab_dir, $query, $excludeGABGroups) {
96
		// Prefer resolving the email_address. This allows the user
97
		// to resolve recipients with a display name that matches a EX
98
		// user with an alternative (external) email address.
99
		$searchstr = empty($query['email_address']) ? $query['display_name'] : $query['email_address'];
100
		// If the address_type is 'EX' then we are resolving something which must be found in
101
		// the GAB as an exact match. So add the flag EMS_AB_ADDRESS_LOOKUP to ensure we will not
102
		// get multiple results when multiple items have a partial match.
103
		$flags = $query['address_type'] === 'EX' ? EMS_AB_ADDRESS_LOOKUP : 0;
104
105
		try {
106
			// First, try an addressbook lookup
107
			$rows = mapi_ab_resolvename($ab, [[PR_DISPLAY_NAME => $searchstr]], $flags);
108
			$this->searchContactsFolders($ab, $ab_dir, $searchstr, $rows);
109
		}
110
		catch (MAPIException $e) {
111
			if ($e->getCode() == MAPI_E_AMBIGUOUS_RECIP) {
112
				$ab_entryid = mapi_ab_getdefaultdir($ab);
113
				$ab_dir = mapi_ab_openentry($ab, $ab_entryid);
114
				// Ambiguous, show possibilities:
115
				$table = mapi_folder_getcontentstable($ab_dir, MAPI_DEFERRED_ERRORS);
116
				$restriction = $this->getAmbigiousContactRestriction($searchstr, $excludeGABGroups, PR_ACCOUNT);
117
118
				mapi_table_restrict($table, $restriction, TBL_BATCH);
119
				mapi_table_sort($table, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
120
121
				$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]);
122
123
				$rows = array_merge($rows, $this->getAmbigiousContactResolveResults($ab, $searchstr, $excludeGABGroups));
124
			}
125
			elseif ($e->getCode() == MAPI_E_NOT_FOUND) {
126
				$rows = [];
127
				if ($query['address_type'] === 'SMTP') {
128
					// If we still can't find anything, and we were searching for a SMTP user
129
					// we can generate a oneoff entry which contains the information of the user.
130
					if (!empty($query['email_address'])) {
131
						$rows[] = [
132
							PR_ACCOUNT => $query['email_address'], PR_ADDRTYPE => 'SMTP', PR_EMAIL_ADDRESS => $query['email_address'],
133
							PR_DISPLAY_NAME => $query['display_name'], PR_DISPLAY_TYPE_EX => DT_REMOTE_MAILUSER, PR_DISPLAY_TYPE => DT_MAILUSER,
134
							PR_SMTP_ADDRESS => $query['email_address'], PR_OBJECT_TYPE => MAPI_MAILUSER,
135
							PR_ENTRYID => mapi_createoneoff($query['display_name'], 'SMTP', $query['email_address']),
136
						];
137
					}
138
					// Check also the user's contacts folders
139
					else {
140
						$this->searchContactsFolders($ab, $ab_dir, $searchstr, $rows);
141
					}
142
				}
143
			}
144
			else {
145
				// all other errors should be propagated to higher level exception handlers
146
				throw $e;
147
			}
148
		}
149
150
		$items = [];
151
		if ($rows) {
152
			foreach ($rows as $user_data) {
153
				$item = [];
154
155
				if (!isset($user_data[PR_ACCOUNT])) {
156
					$abitem = mapi_ab_openentry($ab, $user_data[PR_ENTRYID]);
157
					$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]);
158
				}
159
160
				if ($excludeGABGroups && $user_data[PR_OBJECT_TYPE] === MAPI_DISTLIST) {
161
					// exclude groups from result
162
					continue;
163
				}
164
165
				$item = [];
166
				$item['object_type'] = isset($user_data[PR_OBJECT_TYPE]) ? $user_data[PR_OBJECT_TYPE] : MAPI_MAILUSER;
167
				$item['entryid'] = isset($user_data[PR_ENTRYID]) ? bin2hex($user_data[PR_ENTRYID]) : '';
168
				$item['display_name'] = isset($user_data[PR_DISPLAY_NAME]) ? $user_data[PR_DISPLAY_NAME] : '';
169
				$item['display_type'] = isset($user_data[PR_DISPLAY_TYPE]) ? $user_data[PR_DISPLAY_TYPE] : DT_MAILUSER;
170
171
				// Test whether the GUID in the entryid is from the Contact Provider
172
				if ($GLOBALS['entryid']->hasContactProviderGUID($item['entryid'])) {
173
					// The properties for a Distribution List differs from the other objects
174
					if ($item['object_type'] == MAPI_DISTLIST) {
175
						$item['address_type'] = 'MAPIPDL';
176
						// The email_address is empty for DistList, using display name for resolving
177
						$item['email_address'] = $item['display_name'];
178
						$item['smtp_address'] = isset($item['smtp_address']) ? $item['smtp_address'] : '';
179
					}
180
					else {
181
						$item['address_type'] = 'EX';
182
						if (isset($user_data['address_type']) && $user_data['address_type'] === 'EX') {
183
							$item['email_address'] = isset($user_data[PR_EMAIL_ADDRESS]) ? $user_data[PR_EMAIL_ADDRESS] : '';
184
						}
185
						else {
186
							// Fake being an EX account, since it's actually an SMTP addrtype the email address is in a different property.
187
							$item['smtp_address'] = isset($user_data[PR_EMAIL_ADDRESS]) ? $user_data[PR_EMAIL_ADDRESS] : '';
188
							// Keep the old scenario happy.
189
							$item['email_address'] = isset($user_data[PR_EMAIL_ADDRESS]) ? $user_data[PR_EMAIL_ADDRESS] : '';
190
						}
191
					}
192
				// It can be considered a GAB entry
193
				}
194
				else {
195
					$item['user_name'] = isset($user_data[PR_ACCOUNT]) ? $user_data[PR_ACCOUNT] : $item['display_name'];
196
					$item['display_type_ex'] = isset($user_data[PR_DISPLAY_TYPE_EX]) ? $user_data[PR_DISPLAY_TYPE_EX] : MAPI_MAILUSER;
197
					$item['email_address'] = isset($user_data[PR_EMAIL_ADDRESS]) ? $user_data[PR_EMAIL_ADDRESS] : '';
198
					$item['smtp_address'] = isset($user_data[PR_SMTP_ADDRESS]) ? $user_data[PR_SMTP_ADDRESS] : $item['email_address'];
199
					$item['address_type'] = isset($user_data[PR_ADDRTYPE]) ? $user_data[PR_ADDRTYPE] : 'SMTP';
200
				}
201
202
				if (isset($user_data[PR_SEARCH_KEY])) {
203
					$item['search_key'] = bin2hex($user_data[PR_SEARCH_KEY]);
204
				}
205
				else {
206
					$emailAddress = isset($item['smtp_address']) ? $item['smtp_address'] : $item['email_address'];
207
					$item['search_key'] = bin2hex(strtoupper($item['address_type'] . ':' . $emailAddress)) . '00';
208
				}
209
210
				array_push($items, $item);
211
			}
212
		}
213
214
		return $items;
215
	}
216
217
	/**
218
	 * Used to find multiple entries from the contact folders in the Addressbook when resolving
219
	 * returned an ambiguous result. It will find the Contact folders in the Addressbook and
220
	 * apply a restriction to extract the entries.
221
	 *
222
	 * @param resource $ab               The addressbook
223
	 * @param string   $query            The search query, case is ignored
224
	 * @param bool     $excludeGABGroups flag to exclude groups from resolving
225
	 */
226
	public function getAmbigiousContactResolveResults($ab, $query, $excludeGABGroups) {
227
		/* We need to look for the Contact folders at the bottom of the following tree.
228
		*
229
		 * IAddrBook
230
		 *  - Root Container
231
		 *     - HIERARCHY TABLE
232
		 *        - Contacts Folders    (Contact Container)
233
		 *           - HIERARCHY TABLE         (Contact Container Hierarchy)
234
		 *              - Contact folder 1
235
		 *              - Contact folder 2
236
		 */
237
238
		$rows = [];
239
		$contactFolderRestriction = $this->getAmbigiousContactRestriction($query, $excludeGABGroups, PR_EMAIL_ADDRESS);
240
		// Open the AB Root Container by not supplying an entryid
241
		$abRootContainer = mapi_ab_openentry($ab);
242
243
		// Get the 'Contact Folders'
244
		$hierarchyTable = mapi_folder_gethierarchytable($abRootContainer, MAPI_DEFERRED_ERRORS);
245
		$abHierarchyRows = mapi_table_queryallrows($hierarchyTable, [PR_AB_PROVIDER_ID, PR_ENTRYID]);
246
247
		// Look for the 'Contacts Folders'
248
		for ($i = 0,$len = count($abHierarchyRows); $i < $len; ++$i) {
249
			// Check if the folder matches the Contact Provider GUID
250
			if ($abHierarchyRows[$i][PR_AB_PROVIDER_ID] == MUIDZCSAB) {
251
				$abContactContainerEntryid = $abHierarchyRows[$i][PR_ENTRYID];
252
				break;
253
			}
254
		}
255
256
		// Next go into the 'Contacts Folders' and look in the hierarchy table for the Contact folders.
257
		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...
258
			// Get the rows from hierarchy table of the 'Contacts Folders'
259
			$abContactContainer = mapi_ab_openentry($ab, $abContactContainerEntryid);
260
			$abContactContainerHierarchyTable = mapi_folder_gethierarchytable($abContactContainer, MAPI_DEFERRED_ERRORS);
261
			$abContactContainerHierarchyRows = mapi_table_queryallrows($abContactContainerHierarchyTable, [PR_DISPLAY_NAME, PR_OBJECT_TYPE, PR_ENTRYID]);
262
263
			// Loop through all the contact folders found under the 'Contacts Folders' hierarchy
264
			for ($j = 0,$len = count($abContactContainerHierarchyRows); $j < $len; ++$j) {
265
				// Open, get contents table, restrict, sort and then merge the result in the list of $rows
266
				$abContactFolder = mapi_ab_openentry($ab, $abContactContainerHierarchyRows[$j][PR_ENTRYID]);
267
				$abContactFolderTable = mapi_folder_getcontentstable($abContactFolder, MAPI_DEFERRED_ERRORS);
268
269
				mapi_table_restrict($abContactFolderTable, $contactFolderRestriction, TBL_BATCH);
270
				mapi_table_sort($abContactFolderTable, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
271
272
				// Go go gadget, merge!
273
				$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]));
274
			}
275
		}
276
277
		return $rows;
278
	}
279
280
	/**
281
	 * Setup the restriction used for resolving in the Contact folders or GAB.
282
	 *
283
	 * @param string $query            The search query, case is ignored
284
	 * @param bool   $excludeGABGroups flag to exclude groups from resolving
285
	 * @param int    $content          the PROPTAG to search in
286
	 */
287
	public function getAmbigiousContactRestriction($query, $excludeGABGroups, $content) {
288
		// only return users from who the displayName or the username starts with $name
289
		// TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and $content.
290
		$resAnd = [
291
			[RES_OR,
292
				[
293
					[RES_CONTENT,
294
						[
295
							FUZZYLEVEL => FL_PREFIX | FL_IGNORECASE,
296
							ULPROPTAG => PR_DISPLAY_NAME,
297
							VALUE => $query,
298
						],
299
					],
300
					[RES_CONTENT,
301
						[
302
							FUZZYLEVEL => FL_PREFIX | FL_IGNORECASE,
303
							ULPROPTAG => $content,
304
							VALUE => $query,
305
						],
306
					],
307
				], // RES_OR
308
			],
309
		];
310
311
		// create restrictions based on excludeGABGroups flag
312
		if ($excludeGABGroups) {
313
			array_push($resAnd, [
314
				RES_PROPERTY,
315
				[
316
					RELOP => RELOP_EQ,
317
					ULPROPTAG => PR_OBJECT_TYPE,
318
					VALUE => MAPI_MAILUSER,
319
				],
320
			]);
321
		}
322
		else {
323
			array_push($resAnd, [RES_OR,
324
				[
325
					[RES_PROPERTY,
326
						[
327
							RELOP => RELOP_EQ,
328
							ULPROPTAG => PR_OBJECT_TYPE,
329
							VALUE => MAPI_MAILUSER,
330
						],
331
					],
332
					[RES_PROPERTY,
333
						[
334
							RELOP => RELOP_EQ,
335
							ULPROPTAG => PR_OBJECT_TYPE,
336
							VALUE => MAPI_DISTLIST,
337
						],
338
					],
339
				],
340
			]);
341
		}
342
343
		return [RES_AND, $resAnd];
344
	}
345
346
	/**
347
	 * Function does customization of exception based on module data.
348
	 * like, here it will generate display message based on actionType
349
	 * for particular exception.
350
	 *
351
	 * @param object     $e             Exception object
352
	 * @param string     $actionType    the action type, sent by the client
353
	 * @param MAPIobject $store         store object of message
0 ignored issues
show
Bug introduced by
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...
354
	 * @param string     $parententryid parent entryid of the message
355
	 * @param string     $entryid       entryid of the message
356
	 * @param array      $action        the action data, sent by the client
357
	 */
358
	public function handleException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) {
359
		if (is_null($e->displayMessage)) {
360
			switch ($actionType) {
361
				case 'checknames':
362
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
363
						$e->setDisplayMessage(_('You have insufficient privileges to perform this action.'));
364
					}
365
					else {
366
						$e->setDisplayMessage(_('Could not resolve user.'));
367
					}
368
					break;
369
			}
370
		}
371
372
		parent::handleException($e, $actionType, $store, $parententryid, $entryid, $action);
373
	}
374
375
	/**
376
	 * This function searches the private contact folders for users and returns an array with data.
377
	 * Please note that the returning array must be UTF8.
378
	 *
379
	 * @param resource $ab        The addressbook
380
	 * @param resource $ab_dir    The addressbook container
381
	 * @param string   $searchstr The search query, case is ignored
382
	 * @param array    $rows      Array of the found contacts
383
	 */
384
	public function searchContactsFolders($ab, $ab_dir, $searchstr, &$rows) {
385
		$abhtable = mapi_folder_gethierarchytable($ab_dir, MAPI_DEFERRED_ERRORS | CONVENIENT_DEPTH);
386
		$abcntfolders = mapi_table_queryallrows($abhtable, [PR_ENTRYID, PR_AB_PROVIDER_ID]);
387
		$restriction = [
388
			RES_CONTENT,
389
			[
390
				FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE,
391
				ULPROPTAG => PR_DISPLAY_NAME,
392
				VALUE => [PR_DISPLAY_NAME => $searchstr],
393
			],
394
		];
395
		// restriction on hierarchy table for PR_AB_PROVIDER_ID
396
		// seems not to work, just loop through
397
		foreach ($abcntfolders as $abcntfolder) {
398
			if ($abcntfolder[PR_AB_PROVIDER_ID] == ZARAFA_CONTACTS_GUID) {
399
				$abfldentry = mapi_ab_openentry($ab, $abcntfolder[PR_ENTRYID]);
400
				$abfldcontents = mapi_folder_getcontentstable($abfldentry);
401
				mapi_table_restrict($abfldcontents, $restriction);
402
				$r = mapi_table_queryallrows($abfldcontents, [PR_ACCOUNT, PR_ADDRTYPE, PR_DISPLAY_NAME, PR_ENTRYID,
403
					PR_SEARCH_KEY, PR_OBJECT_TYPE, PR_SMTP_ADDRESS, PR_DISPLAY_TYPE_EX, PR_EMAIL_ADDRESS,
404
					PR_OBJECT_TYPE, PR_DISPLAY_TYPE]);
405
				if (is_array($r) && !empty($r)) {
406
					$rows = array_merge($rows, $r);
407
				}
408
			}
409
		}
410
	}
411
}
412