Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.80% covered (success)
87.80%
36 / 41
70.00% covered (success)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Address
87.80% covered (success)
87.80%
36 / 41
70.00% covered (success)
70.00%
7 / 10
29.42
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
13
 renderName
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getAddress
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAddress
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 render
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 needsQuoting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Mime\Part\Header;
16
17/**
18 * RFC 5322 address - a single mailbox (optional display name + addr-spec).
19 * Not a full RFC 5322 grammar: no groups, no obs-* forms - malformed or
20 * unusual input degrades to a bare address rather than throwing.
21 *
22 * @category   Pop
23 * @package    Pop\Mime
24 * @author     Nick Sagona, III <dev@noladev.com>
25 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    3.0.0
28 */
29final class Address
30{
31
32    /**
33     * Display name, already RFC 2047-decoded
34     * @var ?string
35     */
36    protected ?string $name = null;
37
38    /**
39     * The addr-spec (e.g. "john@example.com"), kept as an opaque string
40     * @var string
41     */
42    protected string $address;
43
44    /**
45     * @param string  $address
46     * @param ?string $name
47     */
48    public function __construct(string $address, ?string $name = null)
49    {
50        $this->address = $address;
51        $this->name    = $name;
52    }
53
54    /**
55     * Parse a single address (no comma-splitting - see AddressList for
56     * multiple addresses). Never throws - malformed input (unbalanced or
57     * absent angle brackets) degrades to treating the whole string as a
58     * bare address.
59     *
60     * @param  string $text
61     * @return Address
62     */
63    public static function parse(string $text): Address
64    {
65        $trimmed = trim($text);
66        $tokens  = (new Lexer($trimmed))->tokenize();
67
68        $openIndex  = null;
69        $closeIndex = null;
70        foreach ($tokens as $i => $token) {
71            if (($token->type === Lexer::DELIMITER) && ($token->value === '<') && ($openIndex === null)) {
72                $openIndex = $i;
73            } else if (($token->type === Lexer::DELIMITER) && ($token->value === '>') &&
74                ($openIndex !== null) && ($closeIndex === null)) {
75                $closeIndex = $i;
76            }
77        }
78
79        if (($openIndex === null) || ($closeIndex === null)) {
80            return new self($trimmed);
81        }
82
83        foreach (array_slice($tokens, $closeIndex + 1) as $token) {
84            if ($token->type !== Lexer::FWS) {
85                return new self($trimmed);
86            }
87        }
88
89        $name       = self::renderName(array_slice($tokens, 0, $openIndex));
90        $openEnd    = $tokens[$openIndex]->end;
91        $closeToken = $tokens[$closeIndex];
92        $address    = trim(substr($trimmed, $openEnd, $closeToken->start - $openEnd));
93
94        return new self($address, $name);
95    }
96
97    /**
98     * @param  Token[] $tokens
99     * @return ?string
100     */
101    protected static function renderName(array $tokens): ?string
102    {
103        $value = '';
104        foreach ($tokens as $token) {
105            $value .= ($token->type === Lexer::FWS) ? ' ' : $token->value;
106        }
107        $value = trim($value);
108
109        return ($value !== '') ? EncodedWord::decode($value) : null;
110    }
111
112    /**
113     * @return ?string
114     */
115    public function getName(): ?string
116    {
117        return $this->name;
118    }
119
120    /**
121     * @param  ?string $name
122     * @return Address
123     */
124    public function setName(?string $name): Address
125    {
126        $this->name = $name;
127        return $this;
128    }
129
130    /**
131     * @return string
132     */
133    public function getAddress(): string
134    {
135        return $this->address;
136    }
137
138    /**
139     * @param  string $address
140     * @return Address
141     */
142    public function setAddress(string $address): Address
143    {
144        $this->address = $address;
145        return $this;
146    }
147
148    /**
149     * @return string
150     */
151    public function render(): string
152    {
153        if ($this->name === null) {
154            return $this->address;
155        }
156
157        $encoded = EncodedWord::encode($this->name);
158
159        if (($encoded === $this->name) && self::needsQuoting($this->name)) {
160            $encoded = '"' . addcslashes($encoded, '"\\') . '"';
161        }
162
163        return $encoded . ' <' . $this->address . '>';
164    }
165
166    /**
167     * @param  string $name
168     * @return bool
169     */
170    protected static function needsQuoting(string $name): bool
171    {
172        return (bool)preg_match('/[,;:<>()\[\]@\\\\"]/', $name);
173    }
174
175    /**
176     * @return string
177     */
178    public function __toString(): string
179    {
180        return $this->render();
181    }
182
183}