Mail_RFC822::isValidInetAddress()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
nc 4
1
<?php
2
/**
3
 * SPDX-License-Identifier: BSD-3-Clause.
4
 *
5
 * RFC 822 Email address list validation Utility
6
 *
7
 * PHP version 5
8
 *
9
 * LICENSE:
10
 *
11
 * Copyright (c) 2001-2017, Chuck Hagenbuch & Richard Heyes
12
 * All rights reserved.
13
 *
14
 * Redistribution and use in source and binary forms, with or without
15
 * modification, are permitted provided that the following conditions
16
 * are met:
17
 *
18
 * 1. Redistributions of source code must retain the above copyright
19
 *    notice, this list of conditions and the following disclaimer.
20
 *
21
 * 2. Redistributions in binary form must reproduce the above copyright
22
 *    notice, this list of conditions and the following disclaimer in the
23
 *    documentation and/or other materials provided with the distribution.
24
 *
25
 * 3. Neither the name of the copyright holder nor the names of its
26
 *    contributors may be used to endorse or promote products derived from
27
 *    this software without specific prior written permission.
28
 *
29
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
30
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
31
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
32
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
33
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
34
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
35
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
36
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
37
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
38
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40
 *
41
 * @category    Mail
42
 *
43
 * @author      Richard Heyes <[email protected]>
44
 * @author      Chuck Hagenbuch <[email protected]
45
 * @copyright   2001-2017 Richard Heyes
46
 * @license     http://opensource.org/licenses/BSD-3-Clause New BSD License
47
 *
48
 * @version     CVS: $Id$
49
 *
50
 * @see        http://pear.php.net/package/Mail/
51
 */
52
53
/**
54
 * RFC 822 Email address list validation Utility.
55
 *
56
 * What is it?
57
 *
58
 * This class will take an address string, and parse it into it's constituent
59
 * parts, be that either addresses, groups, or combinations. Nested groups
60
 * are not supported. The structure it returns is pretty straight forward,
61
 * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use
62
 * print_r() to view the structure.
63
 *
64
 * How do I use it?
65
 *
66
 * $address_string = 'My Group: "Richard" <richard@localhost> (A comment), [email protected] (Ted Bloggs), Barney;';
67
 * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true)
68
 * print_r($structure);
69
 *
70
 * @author  Richard Heyes <[email protected]>
71
 * @author  Chuck Hagenbuch <[email protected]>
72
 *
73
 * @version $Revision$
74
 *
75
 * @license BSD
76
 */
77
class Mail_RFC822 {
78
	/**
79
	 * The address being parsed by the RFC822 object.
80
	 *
81
	 * @var string
82
	 */
83
	public $address = '';
84
85
	/**
86
	 * The default domain to use for unqualified addresses.
87
	 *
88
	 * @var string
89
	 */
90
	public $default_domain = 'localhost';
91
92
	/**
93
	 * Should we return a nested array showing groups, or flatten everything?
94
	 *
95
	 * @var bool
96
	 */
97
	public $nestGroups = true;
98
99
	/**
100
	 * Whether or not to validate atoms for non-ascii characters.
101
	 *
102
	 * @var bool
103
	 */
104
	public $validate = true;
105
106
	/**
107
	 * The array of raw addresses built up as we parse.
108
	 *
109
	 * @var array
110
	 */
111
	public $addresses = [];
112
113
	/**
114
	 * The final array of parsed address information that we build up.
115
	 *
116
	 * @var array
117
	 */
118
	public $structure = [];
119
120
	/**
121
	 * The current error message, if any.
122
	 *
123
	 * @var string
124
	 */
125
	public $error;
126
127
	/**
128
	 * An internal counter/pointer.
129
	 *
130
	 * @var int
131
	 */
132
	public $index;
133
134
	/**
135
	 * The number of groups that have been found in the address list.
136
	 *
137
	 * @var int
138
	 */
139
	public $num_groups = 0;
140
141
	/**
142
	 * A variable so that we can tell whether or not we're inside a
143
	 * Mail_RFC822 object.
144
	 *
145
	 * @var bool
146
	 */
147
	public $mailRFC822 = true;
148
149
	/**
150
	 * A limit after which processing stops.
151
	 *
152
	 * @var int
153
	 */
154
	public $limit;
155
156
	/**
157
	 * Sets up the object. The address must either be set here or when
158
	 * calling parseAddressList(). One or the other.
159
	 *
160
	 * @param string     $address        the address(es) to validate
161
	 * @param string     $default_domain Default domain/host etc. If not supplied, will be set to localhost.
162
	 * @param bool       $nest_groups    whether to return the structure with groups nested for easier viewing
163
	 * @param bool       $validate       Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
164
	 * @param null|mixed $limit
165
	 *
166
	 * @return object mail_RFC822 A new Mail_RFC822 object
167
	 */
168
	public function __construct($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) {
169
		if (isset($address)) {
170
			$this->address = $address;
171
		}
172
		if (isset($default_domain)) {
173
			$this->default_domain = $default_domain;
174
		}
175
		if (isset($nest_groups)) {
176
			$this->nestGroups = $nest_groups;
177
		}
178
		if (isset($validate)) {
179
			$this->validate = $validate;
180
		}
181
		if (isset($limit)) {
182
			$this->limit = $limit;
183
		}
184
	}
185
186
	/**
187
	 * Starts the whole process. The address must either be set here
188
	 * or when creating the object. One or the other.
189
	 *
190
	 * @param string     $address        the address(es) to validate
191
	 * @param string     $default_domain default domain/host etc
192
	 * @param bool       $nest_groups    whether to return the structure with groups nested for easier viewing
193
	 * @param bool       $validate       Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
194
	 * @param null|mixed $limit
195
	 *
196
	 * @return array a structured array of addresses
197
	 */
198
	public function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) {
199
		if (!isset($this) || !isset($this->mailRFC822)) {
200
			$obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit);
201
202
			return $obj->parseAddressList();
203
		}
204
205
		if (isset($address)) {
206
			$this->address = $address;
207
		}
208
		// grommunio-sync addition
209
		if (strlen(trim($this->address)) == 0) {
210
			return [];
211
		}
212
		if (isset($default_domain)) {
213
			$this->default_domain = $default_domain;
214
		}
215
		if (isset($nest_groups)) {
216
			$this->nestGroups = $nest_groups;
217
		}
218
		if (isset($validate)) {
219
			$this->validate = $validate;
220
		}
221
		if (isset($limit)) {
222
			$this->limit = $limit;
223
		}
224
225
		$this->structure = [];
226
		$this->addresses = [];
227
		$this->error = null;
228
		$this->index = null;
229
230
		// Unfold any long lines in $this->address.
231
		$this->address = preg_replace('/\r?\n/', "\r\n", $this->address);
232
		$this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address);
233
234
		while ($this->address = $this->_splitAddresses($this->address));
0 ignored issues
show
Documentation Bug introduced by
The property $address was declared of type string, but $this->_splitAddresses($this->address) is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
235
236
		if ($this->address === false || isset($this->error)) {
0 ignored issues
show
introduced by
The condition $this->address === false is always true.
Loading history...
237
			// require_once 'PEAR.php';
238
			return $this->raiseError($this->error);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->raiseError($this->error) returns the type false which is incompatible with the documented return type array.
Loading history...
239
		}
240
241
		// Validate each address individually.  If we encounter an invalid
242
		// address, stop iterating and return an error immediately.
243
		foreach ($this->addresses as $address) {
244
			$valid = $this->_validateAddress($address);
245
246
			if ($valid === false || isset($this->error)) {
247
				// require_once 'PEAR.php';
248
				return $this->raiseError($this->error);
249
			}
250
251
			if (!$this->nestGroups) {
252
				$this->structure = array_merge($this->structure, $valid);
253
			}
254
			else {
255
				$this->structure[] = $valid;
256
			}
257
		}
258
259
		return $this->structure;
260
	}
261
262
	/**
263
	 * Splits an address into separate addresses.
264
	 *
265
	 * @param string $address the addresses to split
266
	 *
267
	 * @return bool success or failure
268
	 */
269
	protected function _splitAddresses($address) {
270
		if (!empty($this->limit) && count($this->addresses) == $this->limit) {
271
			return '';
0 ignored issues
show
Bug Best Practice introduced by
The expression return '' returns the type string which is incompatible with the documented return type boolean.
Loading history...
272
		}
273
274
		if ($this->_isGroup($address) && !isset($this->error)) {
275
			$split_char = ';';
276
			$is_group = true;
277
		}
278
		elseif (!isset($this->error)) {
279
			$split_char = ',';
280
			$is_group = false;
281
		}
282
		elseif (isset($this->error)) {
283
			return false;
284
		}
285
286
		// Split the string based on the above ten or so lines.
287
		$parts = explode($split_char, $address);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $split_char does not seem to be defined for all execution paths leading up to this point.
Loading history...
288
		$string = $this->_splitCheck($parts, $split_char);
289
290
		// If a group...
291
		if ($is_group) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $is_group does not seem to be defined for all execution paths leading up to this point.
Loading history...
292
			// If $string does not contain a colon outside of
293
			// brackets/quotes etc then something's fubar.
294
295
			// First check there's a colon at all:
296
			if (strpos($string, ':') === false) {
0 ignored issues
show
Bug introduced by
It seems like $string 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

296
			if (strpos(/** @scrutinizer ignore-type */ $string, ':') === false) {
Loading history...
297
				$this->error = 'Invalid address: ' . $string;
0 ignored issues
show
Bug introduced by
Are you sure $string of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

297
				$this->error = 'Invalid address: ' . /** @scrutinizer ignore-type */ $string;
Loading history...
298
299
				return false;
300
			}
301
302
			// Now check it's outside of brackets/quotes:
303
			if (!$this->_splitCheck(explode(':', $string), ':')) {
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type false; however, parameter $string of explode() 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

303
			if (!$this->_splitCheck(explode(':', /** @scrutinizer ignore-type */ $string), ':')) {
Loading history...
304
				return false;
305
			}
306
307
			// We must have a group at this point, so increase the counter:
308
			++$this->num_groups;
309
		}
310
311
		// $string now contains the first full address/group.
312
		// Add to the addresses array.
313
		$this->addresses[] = [
314
			'address' => trim($string),
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type false; however, parameter $string of trim() 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

314
			'address' => trim(/** @scrutinizer ignore-type */ $string),
Loading history...
315
			'group' => $is_group,
316
		];
317
318
		// Remove the now stored address from the initial line, the +1
319
		// is to account for the explode character.
320
		$address = trim(substr($address, strlen($string) + 1));
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type false; however, parameter $string of strlen() 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

320
		$address = trim(substr($address, strlen(/** @scrutinizer ignore-type */ $string) + 1));
Loading history...
321
322
		// If the next char is a comma and this was a group, then
323
		// there are more addresses, otherwise, if there are any more
324
		// chars, then there is another address.
325
		if ($is_group && substr($address, 0, 1) == ',') {
326
			return trim(substr($address, 1));
0 ignored issues
show
Bug Best Practice introduced by
The expression return trim(substr($address, 1)) returns the type string which is incompatible with the documented return type boolean.
Loading history...
327
		}
328
		if (strlen($address) > 0) {
329
			return $address;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $address returns the type string which is incompatible with the documented return type boolean.
Loading history...
330
		}
331
332
		return '';
0 ignored issues
show
Bug Best Practice introduced by
The expression return '' returns the type string which is incompatible with the documented return type boolean.
Loading history...
333
		// If you got here then something's off
334
		return false;
0 ignored issues
show
Unused Code introduced by
return false is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
335
	}
336
337
	/**
338
	 * Checks for a group at the start of the string.
339
	 *
340
	 * @param string $address the address to check
341
	 *
342
	 * @return bool whether or not there is a group at the start of the string
343
	 */
344
	protected function _isGroup($address) {
345
		// First comma not in quotes, angles or escaped:
346
		$parts = explode(',', $address);
347
		$string = $this->_splitCheck($parts, ',');
348
349
		// Now we have the first address, we can reliably check for a
350
		// group by searching for a colon that's not escaped or in
351
		// quotes or angle brackets.
352
		if (count($parts = explode(':', $string)) > 1) {
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type false; however, parameter $string of explode() 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

352
		if (count($parts = explode(':', /** @scrutinizer ignore-type */ $string)) > 1) {
Loading history...
353
			$string2 = $this->_splitCheck($parts, ':');
354
355
			return $string2 !== $string;
356
		}
357
358
		return false;
359
	}
360
361
	/**
362
	 * A common function that will check an exploded string.
363
	 *
364
	 * @param array  $parts the exloded string
365
	 * @param string $char  the char that was exploded on
366
	 *
367
	 * @return mixed false if the string contains unclosed quotes/brackets, or the string on success
368
	 */
369
	protected function _splitCheck($parts, $char) {
370
		$string = $parts[0];
371
		$partsCount = count($parts);
372
373
		for ($i = 0; $i < $partsCount; ++$i) {
374
			if ($this->_hasUnclosedQuotes($string) ||
375
				$this->_hasUnclosedBrackets($string, '<>') ||
376
				$this->_hasUnclosedBrackets($string, '[]') ||
377
				$this->_hasUnclosedBrackets($string, '()') ||
378
				substr($string, -1) == '\\') {
379
				if (isset($parts[$i + 1])) {
380
					$string = $string . $char . $parts[$i + 1];
381
				}
382
				else {
383
					$this->error = 'Invalid address spec. Unclosed bracket or quotes';
384
385
					return false;
386
				}
387
			}
388
			else {
389
				$this->index = $i;
390
391
				break;
392
			}
393
		}
394
395
		return $string;
396
	}
397
398
	/**
399
	 * Checks if a string has unclosed quotes or not.
400
	 *
401
	 * @param string $string the string to check
402
	 *
403
	 * @return bool true if there are unclosed quotes inside the string,
404
	 *              false otherwise
405
	 */
406
	protected function _hasUnclosedQuotes($string) {
407
		$string = trim($string);
408
		$iMax = strlen($string);
409
		$in_quote = false;
410
		$i = $slashes = 0;
411
412
		for (; $i < $iMax; ++$i) {
413
			switch ($string[$i]) {
414
				case '\\':
415
					++$slashes;
416
417
					break;
418
419
				case '"':
420
					if ($slashes % 2 == 0) {
421
						$in_quote = !$in_quote;
0 ignored issues
show
introduced by
The condition $in_quote is always false.
Loading history...
422
					}
423
					// Fall through to default action below.
424
425
					// no break
426
				default:
427
					$slashes = 0;
428
429
					break;
430
			}
431
		}
432
433
		return $in_quote;
434
	}
435
436
	/**
437
	 * Checks if a string has an unclosed brackets or not. IMPORTANT:
438
	 * This function handles both angle brackets and square brackets;.
439
	 *
440
	 * @param string $string the string to check
441
	 * @param string $chars  the characters to check for
442
	 *
443
	 * @return bool true if there are unclosed brackets inside the string, false otherwise
444
	 */
445
	protected function _hasUnclosedBrackets($string, $chars) {
446
		$num_angle_start = substr_count($string, $chars[0]);
447
		$num_angle_end = substr_count($string, $chars[1]);
448
449
		$this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
450
		$this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
451
452
		if ($num_angle_start < $num_angle_end) {
453
			$this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
454
455
			return false;
456
		}
457
458
		return $num_angle_start > $num_angle_end;
459
	}
460
461
	/**
462
	 * Sub function that is used only by hasUnclosedBrackets().
463
	 *
464
	 * @param string $string the string to check
465
	 * @param int    &$num   The number of occurrences
466
	 * @param string $char   the character to count
467
	 *
468
	 * @return int the number of occurrences of $char in $string, adjusted for backslashes
469
	 */
470
	protected function _hasUnclosedBracketsSub($string, &$num, $char) {
471
		$parts = explode($char, $string);
472
		$partsCount = count($parts);
473
474
		for ($i = 0; $i < $partsCount; ++$i) {
475
			if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) {
476
				--$num;
477
			}
478
			if (isset($parts[$i + 1])) {
479
				$parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
480
			}
481
		}
482
483
		return $num;
484
	}
485
486
	/**
487
	 * Function to begin checking the address.
488
	 *
489
	 * @param string $address the address to validate
490
	 *
491
	 * @return mixed false on failure, or a structured array of address information on success
492
	 */
493
	protected function _validateAddress($address) {
494
		$is_group = false;
495
		$addresses = [];
496
497
		if ($address['group']) {
498
			$is_group = true;
499
500
			// Get the group part of the name
501
			$parts = explode(':', $address['address']);
502
			$groupname = $this->_splitCheck($parts, ':');
503
			$structure = [];
504
505
			// And validate the group part of the name.
506
			if (!$this->_validatePhrase($groupname)) {
0 ignored issues
show
Bug introduced by
It seems like $groupname can also be of type false; however, parameter $phrase of Mail_RFC822::_validatePhrase() 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

506
			if (!$this->_validatePhrase(/** @scrutinizer ignore-type */ $groupname)) {
Loading history...
507
				$this->error = 'Group name did not validate.';
508
509
				return false;
510
			}
511
			// Don't include groups if we are not nesting
512
			// them. This avoids returning invalid addresses.
513
			if ($this->nestGroups) {
514
				$structure = new stdClass();
515
				$structure->groupname = $groupname;
516
			}
517
518
			$address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));
0 ignored issues
show
Bug introduced by
Are you sure $groupname of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

518
			$address['address'] = ltrim(substr($address['address'], strlen(/** @scrutinizer ignore-type */ $groupname . ':')));
Loading history...
519
		}
520
521
		// If a group then split on comma and put into an array.
522
		// Otherwise, Just put the whole address in an array.
523
		if ($is_group) {
524
			while (strlen($address['address']) > 0) {
525
				$parts = explode(',', $address['address']);
526
				$addresses[] = $this->_splitCheck($parts, ',');
527
				$address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));
528
			}
529
		}
530
		else {
531
			$addresses[] = $address['address'];
532
		}
533
534
		// Trim the whitespace from all of the address strings.
535
		array_map('trim', $addresses);
536
537
		// Validate each mailbox.
538
		// Format could be one of: name <[email protected]>
539
		//                         [email protected]
540
		//                         geezer
541
		// ... or any other format valid by RFC 822.
542
		$addressesCount = count($addresses);
543
544
		for ($i = 0; $i < $addressesCount; ++$i) {
545
			if (!$this->validateMailbox($addresses[$i])) {
546
				if (empty($this->error)) {
547
					$this->error = 'Validation failed for: ' . $addresses[$i];
548
				}
549
550
				return false;
551
			}
552
		}
553
554
		// Nested format
555
		if ($this->nestGroups) {
556
			if ($is_group) {
557
				$structure->addresses = $addresses;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $structure does not seem to be defined for all execution paths leading up to this point.
Loading history...
558
			}
559
			else {
560
				$structure = $addresses[0];
561
			}
562
563
			// Flat format
564
		}
565
		else {
566
			if ($is_group) {
567
				$structure = array_merge($structure, $addresses);
568
			}
569
			else {
570
				$structure = $addresses;
571
			}
572
		}
573
574
		return $structure;
575
	}
576
577
	/**
578
	 * Function to validate a phrase.
579
	 *
580
	 * @param string $phrase the phrase to check
581
	 *
582
	 * @return bool success or failure
583
	 */
584
	protected function _validatePhrase($phrase) {
585
		// Splits on one or more Tab or space.
586
		$parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
587
588
		$phrase_parts = [];
589
		while (count($parts) > 0) {
590
			$phrase_parts[] = $this->_splitCheck($parts, ' ');
591
			for ($i = 0; $i < $this->index + 1; ++$i) {
592
				array_shift($parts);
593
			}
594
		}
595
596
		foreach ($phrase_parts as $part) {
597
			// If quoted string:
598
			if (substr($part, 0, 1) == '"') {
599
				if (!$this->_validateQuotedString($part)) {
600
					return false;
601
				}
602
603
				continue;
604
			}
605
606
			// Otherwise it's an atom:
607
			if (!$this->_validateAtom($part)) {
608
				return false;
609
			}
610
		}
611
612
		return true;
613
	}
614
615
	/**
616
	 * Function to validate an atom which from rfc822 is:
617
	 * atom = 1*<any CHAR except specials, SPACE and CTLs>.
618
	 *
619
	 * If validation ($this->validate) has been turned off, then
620
	 * validateAtom() doesn't actually check anything. This is so that you
621
	 * can split a list of addresses up before encoding personal names
622
	 * (umlauts, etc.), for example.
623
	 *
624
	 * @param string $atom the string to check
625
	 *
626
	 * @return bool success or failure
627
	 */
628
	protected function _validateAtom($atom) {
629
		if (!$this->validate) {
630
			// Validation has been turned off; assume the atom is okay.
631
			return true;
632
		}
633
634
		// Check for any char from ASCII 0 - ASCII 127
635
		if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
636
			return false;
637
		}
638
639
		// Check for specials:
640
		if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
641
			return false;
642
		}
643
644
		// Check for control characters (ASCII 0-31):
645
		if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
646
			return false;
647
		}
648
649
		return true;
650
	}
651
652
	/**
653
	 * Function to validate quoted string, which is:
654
	 * quoted-string = <"> *(qtext/quoted-pair) <">.
655
	 *
656
	 * @param string $qstring The string to check
657
	 *
658
	 * @return bool success or failure
659
	 */
660
	protected function _validateQuotedString($qstring) {
661
		// Leading and trailing "
662
		$qstring = substr($qstring, 1, -1);
663
664
		// Perform check, removing quoted characters first.
665
		return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));
666
	}
667
668
	/**
669
	 * Function to validate a mailbox, which is:
670
	 * mailbox =   addr-spec         ; simple address
671
	 *           / phrase route-addr ; name and route-addr.
672
	 *
673
	 * @param string &$mailbox The string to check
674
	 *
675
	 * @return bool success or failure
676
	 */
677
	public function validateMailbox(&$mailbox) {
678
		// A couple of defaults.
679
		$phrase = '';
680
		$comment = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $comment is dead and can be removed.
Loading history...
681
		$comments = [];
682
683
		// Catch any RFC822 comments and store them separately.
684
		$_mailbox = $mailbox;
685
		while (strlen(trim($_mailbox)) > 0) {
686
			$parts = explode('(', $_mailbox);
687
			$before_comment = $this->_splitCheck($parts, '(');
688
			if ($before_comment != $_mailbox) {
689
				// First char should be a (.
690
				$comment = substr(str_replace($before_comment, '', $_mailbox), 1);
0 ignored issues
show
Bug introduced by
It seems like $before_comment can also be of type false; however, parameter $search of str_replace() does only seem to accept string|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

690
				$comment = substr(str_replace(/** @scrutinizer ignore-type */ $before_comment, '', $_mailbox), 1);
Loading history...
691
				$parts = explode(')', $comment);
692
				$comment = $this->_splitCheck($parts, ')');
693
				$comments[] = $comment;
694
695
				// +2 is for the brackets
696
				$_mailbox = substr($_mailbox, strpos($_mailbox, '(' . $comment) + strlen($comment) + 2);
0 ignored issues
show
Bug introduced by
It seems like $comment can also be of type false; however, parameter $string of strlen() 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

696
				$_mailbox = substr($_mailbox, strpos($_mailbox, '(' . $comment) + strlen(/** @scrutinizer ignore-type */ $comment) + 2);
Loading history...
Bug introduced by
Are you sure $comment of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

696
				$_mailbox = substr($_mailbox, strpos($_mailbox, '(' . /** @scrutinizer ignore-type */ $comment) + strlen($comment) + 2);
Loading history...
697
			}
698
			else {
699
				break;
700
			}
701
		}
702
703
		foreach ($comments as $comment) {
704
			$mailbox = str_replace("({$comment})", '', $mailbox);
705
		}
706
707
		$mailbox = trim($mailbox);
708
709
		// Check for name + route-addr
710
		if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') {
711
			$parts = explode('<', $mailbox);
712
			$name = $this->_splitCheck($parts, '<');
713
714
			$phrase = trim($name);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type false; however, parameter $string of trim() 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

714
			$phrase = trim(/** @scrutinizer ignore-type */ $name);
Loading history...
715
			$route_addr = trim(substr($mailbox, strlen($name . '<'), -1));
0 ignored issues
show
Bug introduced by
Are you sure $name of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

715
			$route_addr = trim(substr($mailbox, strlen(/** @scrutinizer ignore-type */ $name . '<'), -1));
Loading history...
716
717
			// grommunio-sync fix for umlauts and other special chars
718
			if (substr($phrase, 0, 1) != '"' && substr($phrase, -1) != '"') {
719
				$phrase = '"' . $phrase . '"';
720
			}
721
722
			if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) {
723
				return false;
724
			}
725
726
			// Only got addr-spec
727
		}
728
		else {
729
			// First snip angle brackets if present.
730
			if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') {
731
				$addr_spec = substr($mailbox, 1, -1);
732
			}
733
			else {
734
				$addr_spec = $mailbox;
735
			}
736
737
			if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
738
				return false;
739
			}
740
		}
741
742
		// Construct the object that will be returned.
743
		$mbox = new stdClass();
744
745
		// Add the phrase (even if empty) and comments
746
		$mbox->personal = $phrase;
747
		$mbox->comment = isset($comments) ? $comments : [];
748
749
		if (isset($route_addr)) {
750
			$mbox->mailbox = $route_addr['local_part'];
751
			$mbox->host = $route_addr['domain'];
752
			$route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : '';
753
		}
754
		else {
755
			$mbox->mailbox = $addr_spec['local_part'];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $addr_spec does not seem to be defined for all execution paths leading up to this point.
Loading history...
756
			$mbox->host = $addr_spec['domain'];
757
		}
758
759
		$mailbox = $mbox;
760
761
		return true;
762
	}
763
764
	/**
765
	 * This function validates a route-addr which is:
766
	 * route-addr = "<" [route] addr-spec ">".
767
	 *
768
	 * Angle brackets have already been removed at the point of
769
	 * getting to this function.
770
	 *
771
	 * @param string $route_addr the string to check
772
	 *
773
	 * @return mixed false on failure, or an array containing validated address/route information on success
774
	 */
775
	protected function _validateRouteAddr($route_addr) {
776
		// Check for colon.
777
		if (strpos($route_addr, ':') !== false) {
778
			$parts = explode(':', $route_addr);
779
			$route = $this->_splitCheck($parts, ':');
780
		}
781
		else {
782
			$route = $route_addr;
783
		}
784
785
		// If $route is same as $route_addr then the colon was in
786
		// quotes or brackets or, of course, non existent.
787
		if ($route === $route_addr) {
788
			unset($route);
789
			$addr_spec = $route_addr;
790
			if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
791
				return false;
792
			}
793
		}
794
		else {
795
			// Validate route part.
796
			if (($route = $this->_validateRoute($route)) === false) {
0 ignored issues
show
Bug introduced by
It seems like $route can also be of type false; however, parameter $route of Mail_RFC822::_validateRoute() 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

796
			if (($route = $this->_validateRoute(/** @scrutinizer ignore-type */ $route)) === false) {
Loading history...
797
				return false;
798
			}
799
800
			$addr_spec = substr($route_addr, strlen($route . ':'));
801
802
			// Validate addr-spec part.
803
			if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
804
				return false;
805
			}
806
		}
807
808
		if (isset($route)) {
809
			$return['adl'] = $route;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$return was never initialized. Although not strictly required by PHP, it is generally a good practice to add $return = array(); before regardless.
Loading history...
810
		}
811
		else {
812
			$return['adl'] = '';
813
		}
814
815
		return array_merge($return, $addr_spec);
816
	}
817
818
	/**
819
	 * Function to validate a route, which is:
820
	 * route = 1#("@" domain) ":".
821
	 *
822
	 * @param string $route the string to check
823
	 *
824
	 * @return mixed false on failure, or the validated $route on success
825
	 */
826
	protected function _validateRoute($route) {
827
		// Split on comma.
828
		$domains = explode(',', trim($route));
829
830
		foreach ($domains as $domain) {
831
			$domain = str_replace('@', '', trim($domain));
832
			if (!$this->_validateDomain($domain)) {
833
				return false;
834
			}
835
		}
836
837
		return $route;
838
	}
839
840
	/**
841
	 * Function to validate a domain, though this is not quite what
842
	 * you expect of a strict internet domain.
843
	 *
844
	 * domain = sub-domain *("." sub-domain)
845
	 *
846
	 * @param string $domain the string to check
847
	 *
848
	 * @return mixed false on failure, or the validated domain on success
849
	 */
850
	protected function _validateDomain($domain) {
851
		// Note the different use of $subdomains and $sub_domains
852
		$subdomains = explode('.', $domain);
853
854
		while (count($subdomains) > 0) {
855
			$sub_domains[] = $this->_splitCheck($subdomains, '.');
856
			for ($i = 0; $i < $this->index + 1; ++$i) {
857
				array_shift($subdomains);
858
			}
859
		}
860
861
		foreach ($sub_domains as $sub_domain) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sub_domains does not seem to be defined for all execution paths leading up to this point.
Loading history...
862
			if (!$this->_validateSubdomain(trim($sub_domain))) {
863
				return false;
864
			}
865
		}
866
867
		// Managed to get here, so return input.
868
		return $domain;
869
	}
870
871
	/**
872
	 * Function to validate a subdomain:
873
	 *   subdomain = domain-ref / domain-literal.
874
	 *
875
	 * @param string $subdomain the string to check
876
	 *
877
	 * @return bool success or failure
878
	 */
879
	protected function _validateSubdomain($subdomain) {
880
		if (preg_match('|^\[(.*)]$|', $subdomain, $arr)) {
881
			if (!$this->_validateDliteral($arr[1])) {
882
				return false;
883
			}
884
		}
885
		else {
886
			if (!$this->_validateAtom($subdomain)) {
887
				return false;
888
			}
889
		}
890
891
		// Got here, so return successful.
892
		return true;
893
	}
894
895
	/**
896
	 * Function to validate a domain literal:
897
	 *   domain-literal =  "[" *(dtext / quoted-pair) "]".
898
	 *
899
	 * @param string $dliteral the string to check
900
	 *
901
	 * @return bool success or failure
902
	 */
903
	protected function _validateDliteral($dliteral) {
904
		return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && ((!isset($matches[1])) || $matches[1] != '\\');
905
	}
906
907
	/**
908
	 * Function to validate an addr-spec.
909
	 *
910
	 * addr-spec = local-part "@" domain
911
	 *
912
	 * @param string $addr_spec the string to check
913
	 *
914
	 * @return mixed false on failure, or the validated addr-spec on success
915
	 */
916
	protected function _validateAddrSpec($addr_spec) {
917
		$addr_spec = trim($addr_spec);
918
919
		// Split on @ sign if there is one.
920
		if (strpos($addr_spec, '@') !== false) {
921
			$parts = explode('@', $addr_spec);
922
			$local_part = $this->_splitCheck($parts, '@');
923
			$domain = substr($addr_spec, strlen($local_part . '@'));
0 ignored issues
show
Bug introduced by
Are you sure $local_part of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

923
			$domain = substr($addr_spec, strlen(/** @scrutinizer ignore-type */ $local_part . '@'));
Loading history...
924
925
			// No @ sign so assume the default domain.
926
		}
927
		else {
928
			$local_part = $addr_spec;
929
			$domain = $this->default_domain;
930
		}
931
932
		if (($local_part = $this->_validateLocalPart($local_part)) === false) {
0 ignored issues
show
Bug introduced by
It seems like $local_part can also be of type false; however, parameter $local_part of Mail_RFC822::_validateLocalPart() 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

932
		if (($local_part = $this->_validateLocalPart(/** @scrutinizer ignore-type */ $local_part)) === false) {
Loading history...
933
			return false;
934
		}
935
		if (($domain = $this->_validateDomain($domain)) === false) {
936
			return false;
937
		}
938
939
		// Got here so return successful.
940
		return ['local_part' => $local_part, 'domain' => $domain];
941
	}
942
943
	/**
944
	 * Function to validate the local part of an address:
945
	 *   local-part = word *("." word).
946
	 *
947
	 * @param string $local_part
948
	 *
949
	 * @return mixed false on failure, or the validated local part on success
950
	 */
951
	protected function _validateLocalPart($local_part) {
952
		$parts = explode('.', $local_part);
953
		$words = [];
954
955
		// Split the local_part into words.
956
		while (count($parts) > 0) {
957
			$words[] = $this->_splitCheck($parts, '.');
958
			for ($i = 0; $i < $this->index + 1; ++$i) {
959
				array_shift($parts);
960
			}
961
		}
962
963
		// Validate each word.
964
		foreach ($words as $word) {
965
			// word cannot be empty (#17317)
966
			if ($word === '') {
967
				return false;
968
			}
969
			// If this word contains an unquoted space, it is invalid. (6.2.4)
970
			if (strpos($word, ' ') && $word[0] !== '"') {
971
				return false;
972
			}
973
974
			if ($this->_validatePhrase(trim($word)) === false) {
975
				return false;
976
			}
977
		}
978
979
		// Managed to get here, so return the input.
980
		return $local_part;
981
	}
982
983
	/**
984
	 * Returns an approximate count of how many addresses are in the
985
	 * given string. This is APPROXIMATE as it only splits based on a
986
	 * comma which has no preceding backslash. Could be useful as
987
	 * large amounts of addresses will end up producing *large*
988
	 * structures when used with parseAddressList().
989
	 *
990
	 * @param string $data Addresses to count
991
	 *
992
	 * @return int Approximate count
993
	 */
994
	public function approximateCount($data) {
995
		return count(preg_split('/(?<!\\\\),/', $data));
996
	}
997
998
	/**
999
	 * This is a email validating function separate to the rest of the
1000
	 * class. It simply validates whether an email is of the common
1001
	 * internet form: <user>@<domain>. This can be sufficient for most
1002
	 * people. Optional stricter mode can be utilised which restricts
1003
	 * mailbox characters allowed to alphanumeric, full stop, hyphen
1004
	 * and underscore.
1005
	 *
1006
	 * @param string $data   Address to check
1007
	 * @param bool   $strict Optional stricter mode
1008
	 *
1009
	 * @return mixed False if it fails, an indexed array
1010
	 *               username/domain if it matches
1011
	 */
1012
	public function isValidInetAddress($data, $strict = false) {
1013
		$regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i';
1014
		if (preg_match($regex, trim($data), $matches)) {
1015
			return [$matches[1], $matches[2]];
1016
		}
1017
1018
		return false;
1019
	}
1020
1021
	/**
1022
	 * grommunio-sync helper for error logging
1023
	 * removing PEAR dependency.
1024
	 *
1025
	 * @param  string  debug message
0 ignored issues
show
Bug introduced by
The type debug 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...
1026
	 * @param mixed $message
1027
	 *
1028
	 * @return bool always false as there was an error
1029
	 */
1030
	public function raiseError($message) {
1031
		SLog::Write(LOGLEVEL_ERROR, "z_RFC822 error: " . $message);
1032
1033
		return false;
1034
	}
1035
}
1036