Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.43% covered (success)
96.43%
81 / 84
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
SMTPMailer
96.43% covered (success)
96.43%
81 / 84
88.89% covered (warning)
88.89%
8 / 9
27
0.00% covered (danger)
0.00%
0 / 1
 __destruct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 connect
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
5
 disconnect
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 authenticate
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
6.56
 send
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 sendMessage
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 getResponse
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 sendCommand
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 makeResponseCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
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 */
10namespace Framework\Email\Mailers;
11
12use Framework\Email\Mailer;
13use Framework\Email\Message;
14
15/**
16 * Class SMTPMailer.
17 *
18 * @see https://tools.ietf.org/html/rfc2821
19 *
20 * @package email
21 */
22class 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}