Mail_RFC822   F
last analyzed

Complexity

Total Complexity 136

Size/Duplication

Total Lines 958
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 327
c 1
b 0
f 0
dl 0
loc 958
rs 2
wmc 136

23 Methods

Rating   Name   Duplication   Size   Complexity  
B _validateLocalPart() 0 30 8
B _validateRouteAddr() 0 41 7
A __construct() 0 15 6
C _validateAddress() 0 82 12
A raiseError() 0 4 1
A _validateSubdomain() 0 14 4
B _validatePhrase() 0 29 7
A _validateDliteral() 0 2 3
A _isGroup() 0 15 2
A isValidInetAddress() 0 7 3
F parseAddressList() 0 62 16
A _hasUnclosedBrackets() 0 14 2
A approximateCount() 0 2 1
C _splitAddresses() 0 67 13
A _hasUnclosedBracketsSub() 0 14 5
A _hasUnclosedQuotes() 0 28 5
A _validateQuotedString() 0 6 1
B _splitCheck() 0 27 8
A _validateAtom() 0 22 5
A _validateAddrSpec() 0 25 4
A _validateRoute() 0 12 3
A _validateDomain() 0 19 5
C validateMailbox() 0 85 15

How to fix   Complexity   

Complex Class

Complex classes like Mail_RFC822 often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Mail_RFC822, and based on these observations, apply Extract Interface, too.

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

298
				$this->error = 'Invalid address: ' . /** @scrutinizer ignore-type */ $string;
Loading history...
299
300
				return false;
301
			}
302
303
			// Now check it's outside of brackets/quotes:
304
			if (!$this->_splitCheck(explode(':', (string) $string), ':')) {
305
				return false;
306
			}
307
308
			// We must have a group at this point, so increase the counter:
309
			++$this->num_groups;
310
		}
311
312
		// $string now contains the first full address/group.
313
		// Add to the addresses array.
314
		$this->addresses[] = [
315
			'address' => trim((string) $string),
316
			'group' => $is_group,
317
		];
318
319
		// Remove the now stored address from the initial line, the +1
320
		// is to account for the explode character.
321
		$address = trim(substr($address, strlen((string) $string) + 1));
322
323
		// If the next char is a comma and this was a group, then
324
		// there are more addresses, otherwise, if there are any more
325
		// chars, then there is another address.
326
		if ($is_group && str_starts_with($address, ',')) {
327
			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...
328
		}
329
		if (strlen($address) > 0) {
330
			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...
331
		}
332
333
		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...
334
335
		// If you got here then something's off
336
		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...
337
	}
338
339
	/**
340
	 * Checks for a group at the start of the string.
341
	 *
342
	 * @param string $address the address to check
343
	 *
344
	 * @return bool whether or not there is a group at the start of the string
345
	 */
346
	protected function _isGroup($address) {
347
		// First comma not in quotes, angles or escaped:
348
		$parts = explode(',', $address);
349
		$string = $this->_splitCheck($parts, ',');
350
351
		// Now we have the first address, we can reliably check for a
352
		// group by searching for a colon that's not escaped or in
353
		// quotes or angle brackets.
354
		if (count($parts = explode(':', (string) $string)) > 1) {
355
			$string2 = $this->_splitCheck($parts, ':');
356
357
			return $string2 !== $string;
358
		}
359
360
		return false;
361
	}
362
363
	/**
364
	 * A common function that will check an exploded string.
365
	 *
366
	 * @param array  $parts the exloded string
367
	 * @param string $char  the char that was exploded on
368
	 *
369
	 * @return mixed false if the string contains unclosed quotes/brackets, or the string on success
370
	 */
371
	protected function _splitCheck($parts, $char) {
372
		$string = $parts[0];
373
		$partsCount = count($parts);
374
375
		for ($i = 0; $i < $partsCount; ++$i) {
376
			if ($this->_hasUnclosedQuotes($string) ||
377
				$this->_hasUnclosedBrackets($string, '<>') ||
378
				$this->_hasUnclosedBrackets($string, '[]') ||
379
				$this->_hasUnclosedBrackets($string, '()') ||
380
				str_ends_with((string) $string, '\\')) {
381
				if (isset($parts[$i + 1])) {
382
					$string = $string . $char . $parts[$i + 1];
383
				}
384
				else {
385
					$this->error = 'Invalid address spec. Unclosed bracket or quotes';
386
387
					return false;
388
				}
389
			}
390
			else {
391
				$this->index = $i;
392
393
				break;
394
			}
395
		}
396
397
		return $string;
398
	}
399
400
	/**
401
	 * Checks if a string has unclosed quotes or not.
402
	 *
403
	 * @param string $string the string to check
404
	 *
405
	 * @return bool true if there are unclosed quotes inside the string,
406
	 *              false otherwise
407
	 */
408
	protected function _hasUnclosedQuotes($string) {
409
		$string = trim($string);
410
		$iMax = strlen($string);
411
		$in_quote = false;
412
		$i = $slashes = 0;
413
414
		for (; $i < $iMax; ++$i) {
415
			switch ($string[$i]) {
416
				case '\\':
417
					++$slashes;
418
419
					break;
420
421
				case '"':
422
					if ($slashes % 2 == 0) {
423
						$in_quote = !$in_quote;
0 ignored issues
show
introduced by
The condition $in_quote is always false.
Loading history...
424
					}
425
					// Fall through to default action below.
426
427
					// no break
428
				default:
429
					$slashes = 0;
430
431
					break;
432
			}
433
		}
434
435
		return $in_quote;
436
	}
437
438
	/**
439
	 * Checks if a string has an unclosed brackets or not. IMPORTANT:
440
	 * This function handles both angle brackets and square brackets;.
441
	 *
442
	 * @param string $string the string to check
443
	 * @param string $chars  the characters to check for
444
	 *
445
	 * @return bool true if there are unclosed brackets inside the string, false otherwise
446
	 */
447
	protected function _hasUnclosedBrackets($string, $chars) {
448
		$num_angle_start = substr_count($string, $chars[0]);
449
		$num_angle_end = substr_count($string, $chars[1]);
450
451
		$this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
452
		$this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
453
454
		if ($num_angle_start < $num_angle_end) {
455
			$this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
456
457
			return false;
458
		}
459
460
		return $num_angle_start > $num_angle_end;
461
	}
462
463
	/**
464
	 * Sub function that is used only by hasUnclosedBrackets().
465
	 *
466
	 * @param string $string the string to check
467
	 * @param int    &$num   The number of occurrences
468
	 * @param string $char   the character to count
469
	 *
470
	 * @return int the number of occurrences of $char in $string, adjusted for backslashes
471
	 */
472
	protected function _hasUnclosedBracketsSub($string, &$num, $char) {
473
		$parts = explode($char, $string);
474
		$partsCount = count($parts);
475
476
		for ($i = 0; $i < $partsCount; ++$i) {
477
			if (str_ends_with($parts[$i], '\\') || $this->_hasUnclosedQuotes($parts[$i])) {
478
				--$num;
479
			}
480
			if (isset($parts[$i + 1])) {
481
				$parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
482
			}
483
		}
484
485
		return $num;
486
	}
487
488
	/**
489
	 * Function to begin checking the address.
490
	 *
491
	 * @param string $address the address to validate
492
	 *
493
	 * @return mixed false on failure, or a structured array of address information on success
494
	 */
495
	protected function _validateAddress($address) {
496
		$is_group = false;
497
		$addresses = [];
498
499
		if ($address['group']) {
500
			$is_group = true;
501
502
			// Get the group part of the name
503
			$parts = explode(':', (string) $address['address']);
504
			$groupname = $this->_splitCheck($parts, ':');
505
			$structure = [];
506
507
			// And validate the group part of the name.
508
			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

508
			if (!$this->_validatePhrase(/** @scrutinizer ignore-type */ $groupname)) {
Loading history...
509
				$this->error = 'Group name did not validate.';
510
511
				return false;
512
			}
513
			// Don't include groups if we are not nesting
514
			// them. This avoids returning invalid addresses.
515
			if ($this->nestGroups) {
516
				$structure = new stdClass();
517
				$structure->groupname = $groupname;
518
			}
519
520
			$address['address'] = ltrim(substr((string) $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

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

692
				$comment = substr(str_replace(/** @scrutinizer ignore-type */ $before_comment, '', $_mailbox), 1);
Loading history...
693
				$parts = explode(')', $comment);
694
				$comment = $this->_splitCheck($parts, ')');
695
				$comments[] = $comment;
696
697
				// +2 is for the brackets
698
				$_mailbox = substr($_mailbox, strpos($_mailbox, '(' . $comment) + strlen((string) $comment) + 2);
0 ignored issues
show
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

698
				$_mailbox = substr($_mailbox, strpos($_mailbox, '(' . /** @scrutinizer ignore-type */ $comment) + strlen((string) $comment) + 2);
Loading history...
699
			}
700
			else {
701
				break;
702
			}
703
		}
704
705
		foreach ($comments as $comment) {
706
			$mailbox = str_replace("({$comment})", '', $mailbox);
707
		}
708
709
		$mailbox = trim($mailbox);
710
711
		// Check for name + route-addr
712
		if (str_ends_with($mailbox, '>') && !str_starts_with($mailbox, '<')) {
713
			$parts = explode('<', $mailbox);
714
			$name = $this->_splitCheck($parts, '<');
715
716
			$phrase = trim((string) $name);
717
			$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

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

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

925
			$domain = substr($addr_spec, strlen(/** @scrutinizer ignore-type */ $local_part . '@'));
Loading history...
926
927
		// No @ sign so assume the default domain.
928
		}
929
		else {
930
			$local_part = $addr_spec;
931
			$domain = $this->default_domain;
932
		}
933
934
		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

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