EmailAddress   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 220
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 24
eloc 75
c 2
b 1
f 0
dl 0
loc 220
rs 10

11 Methods

Rating   Name   Duplication   Size   Complexity  
A isGmailWithPlusChar() 0 8 2
A getGmailAddressWithoutPlus() 0 3 1
A __construct() 0 4 1
A getDomain() 0 3 1
A getLocalPart() 0 3 1
A getUsername() 0 3 1
A getSanitizedGmailAddress() 0 7 1
A getComments() 0 3 1
A parseEmail() 0 15 2
C extractComments() 0 66 12
A getEmailAddress() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace EmailValidator;
6
7
class EmailAddress
8
{
9
    /**
10
     * @var string
11
     */
12
    private string $email;
13
14
    /**
15
     * @var string|null
16
     */
17
    private ?string $localPart = null;
18
19
    /**
20
     * @var string|null
21
     */
22
    private ?string $domain = null;
23
24
    /**
25
     * @var array<string>
26
     */
27
    private array $comments = [];
28
29
    public function __construct(string $email)
30
    {
31
        $this->email = $email;
32
        $this->parseEmail();
33
    }
34
35
    /**
36
     * Parses the email address into local part and domain
37
     *
38
     * This method handles:
39
     * - Multiple @ symbols in quoted strings
40
     * - Domain literals (IP addresses in square brackets)
41
     * - Comments in email addresses
42
     *
43
     * @return void
44
     */
45
    private function parseEmail(): void
46
    {
47
        $email = $this->email;
48
49
        // Extract comments while preserving their positions
50
        $email = $this->extractComments($email);
51
52
        // Split on the last @ symbol
53
        $atPos = strrpos($email, '@');
54
        if ($atPos === false) {
55
            return;
56
        }
57
58
        $this->localPart = substr($email, 0, $atPos);
59
        $this->domain = substr($email, $atPos + 1);
60
    }
61
62
    /**
63
     * Extracts comments from an email address while preserving their positions
64
     *
65
     * @param string $email The email address to process
66
     * @return string The email address with comments removed
67
     */
68
    private function extractComments(string $email): string
69
    {
70
        $result = '';
71
        $inComment = false;
72
        $commentDepth = 0;
73
        $currentComment = '';
74
        $escaped = false;
75
76
        for ($i = 0, $iMax = strlen($email); $i < $iMax; $i++) {
77
            $char = $email[$i];
78
79
            if ($escaped) {
80
                if ($inComment) {
81
                    $currentComment .= $char;
82
                } else {
83
                    $result .= $char;
84
                }
85
                $escaped = false;
86
                continue;
87
            }
88
89
            if ($char === '\\') {
90
                $escaped = true;
91
                if ($inComment) {
92
                    $currentComment .= $char;
93
                } else {
94
                    $result .= $char;
95
                }
96
                continue;
97
            }
98
99
            if ($char === '(') {
100
                if ($inComment) {
101
                    $commentDepth++;
102
                    $currentComment .= $char;
103
                } else {
104
                    $inComment = true;
105
                    $commentDepth = 1;
106
                }
107
                continue;
108
            }
109
110
            if ($char === ')') {
111
                if ($inComment) {
112
                    $commentDepth--;
113
                    if ($commentDepth === 0) {
114
                        $this->comments[] = $currentComment;
115
                        $currentComment = '';
116
                        $inComment = false;
117
                    } else {
118
                        $currentComment .= $char;
119
                    }
120
                } else {
121
                    $result .= $char;
122
                }
123
                continue;
124
            }
125
126
            if ($inComment) {
127
                $currentComment .= $char;
128
            } else {
129
                $result .= $char;
130
            }
131
        }
132
133
        return $result;
134
    }
135
136
    /**
137
     * Returns the domain name portion of the email address.
138
     *
139
     * @return string|null
140
     */
141
    public function getDomain(): ?string
142
    {
143
        return $this->domain;
144
    }
145
146
    /**
147
     * Returns the local part of the email address.
148
     *
149
     * @return string|null
150
     */
151
    public function getLocalPart(): ?string
152
    {
153
        return $this->localPart;
154
    }
155
156
    /**
157
     * Returns the complete email address.
158
     *
159
     * @return string
160
     */
161
    public function getEmailAddress(): string
162
    {
163
        return $this->email;
164
    }
165
166
    /**
167
     * Returns any comments found in the email address.
168
     *
169
     * @return array<string>
170
     */
171
    public function getComments(): array
172
    {
173
        return $this->comments;
174
    }
175
176
    /**
177
     * Returns the username portion of the email address.
178
     *
179
     * @since 1.1.0
180
     * @return string
181
     */
182
    private function getUsername(): string
183
    {
184
        return $this->localPart ?? '';
185
    }
186
187
    /**
188
     * Determines if a gmail account is using the "plus trick".
189
     *
190
     * @since 1.1.0
191
     * @return bool
192
     */
193
    public function isGmailWithPlusChar(): bool
194
    {
195
        $result = false;
196
        if (in_array($this->getDomain(), ['gmail.com', 'googlemail.com'])) {
197
            $result = strpos($this->getUsername(), '+') !== false;
198
        }
199
200
        return $result;
201
    }
202
203
    /**
204
     * Returns a gmail address without the "plus trick" portion of the email address.
205
     *
206
     * @since 1.1.0
207
     * @return string
208
     */
209
    public function getGmailAddressWithoutPlus(): string
210
    {
211
        return preg_replace('/^(.+?)(\+.+?)(@.+)/', '$1$3', $this->getEmailAddress());
212
    }
213
214
    /**
215
     * Returns a gmail address without the "plus trick" portion of the email address and all dots removed.
216
     *
217
     * @since 1.1.4
218
     * @return string
219
     */
220
    public function getSanitizedGmailAddress(): string
221
    {
222
        $email = new EmailAddress($this->getGmailAddressWithoutPlus());
223
        return sprintf(
224
            '%s@%s',
225
            str_replace('.', '', $email->getUsername()),
226
            $email->getDomain()
227
        );
228
    }
229
}
230