Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.43% |
81 / 84 |
|
88.89% |
8 / 9 |
CRAP | |
0.00% |
0 / 1 |
SMTPMailer | |
96.43% |
81 / 84 |
|
88.89% |
8 / 9 |
27 | |
0.00% |
0 / 1 |
__destruct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
connect | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
5 | |||
disconnect | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
authenticate | |
75.00% |
9 / 12 |
|
0.00% |
0 / 1 |
6.56 | |||
send | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
3 | |||
sendMessage | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
getResponse | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
sendCommand | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
makeResponseCode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework Email Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\Email\Mailers; |
11 | |
12 | use Framework\Email\Mailer; |
13 | use Framework\Email\Message; |
14 | |
15 | /** |
16 | * Class SMTPMailer. |
17 | * |
18 | * @see https://tools.ietf.org/html/rfc2821 |
19 | * |
20 | * @package email |
21 | */ |
22 | class SMTPMailer extends Mailer |
23 | { |
24 | /** |
25 | * @var false|resource $socket |
26 | */ |
27 | protected $socket = false; |
28 | |
29 | public function __destruct() |
30 | { |
31 | $this->disconnect(); |
32 | } |
33 | |
34 | protected function connect() : bool |
35 | { |
36 | if ($this->socket && ($this->getConfig('keep_alive') === true)) { |
37 | return $this->sendCommand('EHLO ' . $this->getConfig('hostname')) === 250; |
38 | } |
39 | $this->disconnect(); |
40 | $this->socket = @\stream_socket_client( |
41 | $this->getConfig('host') . ':' . $this->getConfig('port'), |
42 | $errorCode, |
43 | $errorMessage, |
44 | (float) $this->getConfig('connection_timeout'), |
45 | \STREAM_CLIENT_CONNECT, |
46 | \stream_context_create($this->getConfig('options')) |
47 | ); |
48 | if ($this->socket === false) { |
49 | $this->addLog('', 'Socket connection error ' . $errorCode . ': ' . $errorMessage); |
50 | return false; |
51 | } |
52 | $this->addLog('', $this->getResponse()); |
53 | $this->sendCommand('EHLO ' . $this->getConfig('hostname')); |
54 | if ($this->getConfig('tls')) { |
55 | $this->sendCommand('STARTTLS'); |
56 | \stream_socket_enable_crypto($this->socket, true, \STREAM_CRYPTO_METHOD_TLS_CLIENT); |
57 | $this->sendCommand('EHLO ' . $this->getConfig('hostname')); |
58 | } |
59 | return $this->authenticate(); |
60 | } |
61 | |
62 | protected function disconnect() : bool |
63 | { |
64 | if (\is_resource($this->socket)) { |
65 | $this->sendCommand('QUIT'); |
66 | $closed = \fclose($this->socket); |
67 | } |
68 | $this->socket = false; |
69 | return $closed ?? true; |
70 | } |
71 | |
72 | /** |
73 | * @see https://datatracker.ietf.org/doc/html/rfc2821#section-4.2.3 |
74 | * @see https://datatracker.ietf.org/doc/html/rfc4954#section-4.1 |
75 | * |
76 | * @return bool |
77 | */ |
78 | protected function authenticate() : bool |
79 | { |
80 | if (($this->getConfig('username') === null) && ($this->getConfig('password') === null)) { |
81 | return false; |
82 | } |
83 | $code = $this->sendCommand('AUTH LOGIN'); |
84 | if ($code === 503) { // Already authenticated |
85 | return true; |
86 | } |
87 | if ($code !== 334) { |
88 | return false; |
89 | } |
90 | $code = $this->sendCommand(\base64_encode($this->getConfig('username'))); |
91 | if ($code !== 334) { |
92 | return false; |
93 | } |
94 | $code = $this->sendCommand(\base64_encode($this->getConfig('password'))); |
95 | return $code === 235; |
96 | } |
97 | |
98 | /** |
99 | * Send an Email Message. |
100 | * |
101 | * @param Message $message |
102 | * |
103 | * @return bool |
104 | */ |
105 | public function send(Message $message) : bool |
106 | { |
107 | if (isset($this->debugCollector)) { |
108 | $start = \microtime(true); |
109 | $code = $this->sendMessage($message); |
110 | $end = \microtime(true); |
111 | $this->debugCollector->addData([ |
112 | 'start' => $start, |
113 | 'end' => $end, |
114 | 'code' => $code ?: 0, |
115 | 'from' => $message->getFromAddress() ?? $this->getConfig('username'), |
116 | 'length' => \strlen((string) $message), |
117 | 'recipients' => $message->getRecipients(), |
118 | 'headers' => $message->getHeaders(), |
119 | 'plain' => $message->getPlainMessage(), |
120 | 'html' => $message->getHtmlMessage(), |
121 | 'attachments' => $message->getAttachments(), |
122 | 'inlineAttachments' => $message->getInlineAttachments(), |
123 | ]); |
124 | return $code === 250; |
125 | } |
126 | return $this->sendMessage($message) === 250; |
127 | } |
128 | |
129 | protected function sendMessage(Message $message) : int | false |
130 | { |
131 | if ( ! $this->connect()) { |
132 | return false; |
133 | } |
134 | $message->setMailer($this); |
135 | $from = $message->getFromAddress() ?? $this->getConfig('username'); |
136 | $this->sendCommand('MAIL FROM: <' . $from . '>'); |
137 | foreach ($message->getRecipients() as $address) { |
138 | $this->sendCommand('RCPT TO: <' . $address . '>'); |
139 | } |
140 | $this->sendCommand('DATA'); |
141 | $code = $this->sendCommand( |
142 | $message . $this->getCrlf() . '.' |
143 | ); |
144 | if ($this->getConfig('keep_alive') !== true) { |
145 | $this->disconnect(); |
146 | } |
147 | return $code; |
148 | } |
149 | |
150 | /** |
151 | * Get Mail Server response. |
152 | * |
153 | * @return string |
154 | */ |
155 | protected function getResponse() : string |
156 | { |
157 | $response = ''; |
158 | // @phpstan-ignore-next-line |
159 | \stream_set_timeout($this->socket, $this->getConfig('response_timeout')); |
160 | // @phpstan-ignore-next-line |
161 | while (($line = \fgets($this->socket, 512)) !== false) { |
162 | $response .= \trim($line) . "\n"; |
163 | if (isset($line[3]) && $line[3] === ' ') { |
164 | break; |
165 | } |
166 | } |
167 | return \trim($response); |
168 | } |
169 | |
170 | /** |
171 | * Send command to mail server. |
172 | * |
173 | * @param string $command |
174 | * |
175 | * @return int Response code |
176 | */ |
177 | protected function sendCommand(string $command) : int |
178 | { |
179 | // @phpstan-ignore-next-line |
180 | \fwrite($this->socket, $command . $this->getCrlf()); |
181 | $response = $this->getResponse(); |
182 | $this->addLog($command, $response); |
183 | return $this->makeResponseCode($response); |
184 | } |
185 | |
186 | /** |
187 | * @see https://tools.ietf.org/html/rfc2821#section-4.2.3 |
188 | * @see https://en.wikipedia.org/wiki/List_of_SMTP_server_return_codes |
189 | * |
190 | * @param string $response |
191 | * |
192 | * @return int |
193 | */ |
194 | private function makeResponseCode(string $response) : int |
195 | { |
196 | return (int) \substr($response, 0, 3); |
197 | } |
198 | } |