SMTPClient::sendMail()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 3
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Kodus\Mail\SMTP;
4
5
use Psr\Log\LoggerAwareInterface;
6
use Psr\Log\LoggerInterface;
7
8
class SMTPClient implements LoggerAwareInterface
9
{
10
    /**
11
     * @var resource SMTP socket handle
12
     */
13
    protected $socket;
14
15
    /**
16
     * @var LoggerInterface|null
17
     */
18
    protected $logger;
19
20
    /**
21
     * @var string
22
     */
23
    protected $eol = "\r\n";
24
25
    /**
26
     * @var string last command issued to the SMTP server
27
     */
28
    protected $last_command;
29
30
    /**
31
     * @var string last result received from the SMTP server
32
     */
33
    protected $last_result;
34
35
    /**
36
     * @param resource $socket SMTP socket
37
     *
38
     * @throws UnexpectedCodeException on missing welcome message
39
     */
40 2
    public function __construct($socket)
41
    {
42 2
        $this->socket = $socket;
43
44 2
        $code = $this->readCode();
45
46 2
        if ($code !== '220') {
47
            throw new UnexpectedCodeException("220", $code, $this->last_command, $this->last_result);
48
        }
49 2
    }
50
51
    /**
52
     * Send the `QUIT` command and close the SMTP socket
53
     */
54 2
    public function __destruct()
55
    {
56 2
        $this->sendCommand("QUIT", "221");
57
58 2
        fclose($this->socket);
59 2
    }
60
61
    /**
62
     * @see http://www.php-fig.org/psr/psr-3/
63
     *
64
     * @param LoggerInterface|null $logger PSR-3 compliant Logger implementation
65
     *
66
     * @return void
67
     */
68 1
    public function setLogger(?LoggerInterface $logger = null): void
69
    {
70 1
        $this->logger = $logger;
71 1
    }
72
73
    /**
74
     * Send the `EHLO` command and checks the response
75
     *
76
     * @param string $client_domain
77
     */
78 2
    public function sendEHLO(string $client_domain)
79
    {
80 2
        $this->sendCommand("EHLO {$client_domain}", "250");
81 2
    }
82
83
    /**
84
     * Send the `STARTTLS` command and initialize stream socket encryption.
85
     *
86
     * @param int $crypto_method one of the STREAM_CRYPTO_METHOD_* constants (defined by PHP)
87
     *
88
     * @throws SMTPException on failure
89
     */
90
    public function sendSTARTTLS(int $crypto_method)
91
    {
92
        $this->sendCommand("STARTTLS", "220");
93
94
        if (! stream_socket_enable_crypto($this->socket, true, $crypto_method)) {
95
            throw new SMTPException("STARTTLS failed to enable crypto-method: {$crypto_method}");
96
        }
97
    }
98
99
    /**
100
     * Write an SMTP command (with EOL) to the SMTP socket, and read the status code.
101
     *
102
     * @param string      $command
103
     * @param string|null $expected_code optional expected response status-code
104
     *
105
     * @return string SMTP status code
106
     *
107
     * @throws UnexpectedCodeException
108
     */
109 2
    public function sendCommand(string $command, ?string $expected_code = null)
110
    {
111 2
        $this->last_command = $command;
112
113 2
        $this->log("S: {$command}");
114
115 2
        fwrite($this->socket, "{$command}{$this->eol}");
116
117 2
        $code = $this->readCode();
118
119 2
        if ($expected_code !== null && $code !== $expected_code) {
120
            throw new UnexpectedCodeException("250", $code, $this->last_command, $this->last_result);
121
        }
122
123 2
        return $code;
124
    }
125
126
    /**
127
     * @param string   $sender     sender e-mail address
128
     * @param string[] $recipients list of recipient e-mail addresses
129
     * @param callable $write function (resource $resouce) :
130
     */
131 2
    public function sendMail(string $sender, array $recipients, callable $write): void
132
    {
133 2
        $this->sendMailFromCommand($sender);
134 2
        $this->sendRecipientCommands($recipients);
135 2
        $this->sendDataCommands($write);
136 2
    }
137
138
    /**
139
     * Send the `MAIL FROM` command and check the response
140
     *
141
     * @param string $sender sender e-mail address
142
     */
143 2
    protected function sendMailFromCommand(string $sender)
144
    {
145 2
        $this->sendCommand("MAIL FROM:<{$sender}>", "250");
146 2
    }
147
148
    /**
149
     * Send a series of `RCPT TO` commands and check each response
150
     *
151
     * @param string[] $recipients list of recipient e-mail addresses
152
     */
153 2
    protected function sendRecipientCommands(array $recipients): void
154
    {
155 2
        foreach ($recipients as $recipient) {
156 2
            $this->sendCommand("RCPT TO:<{$recipient}>", "250");
157 2
        }
158 2
    }
159
160
    /**
161
     * Send the `DATA` command, expose the filtered stream to a callback for writing
162
     * the data, terminate the data-stream, and check the response.
163
     *
164
     * @param callable $write function (resource $resouce) : void
165
     *
166
     * @throws SMTPException
167
     */
168 2
    protected function sendDataCommands(callable $write): void
169
    {
170 2
        $this->sendCommand("DATA", "354");
171
172 2
        $filter = stream_filter_append($this->socket, SMTPDotStuffingFilter::FILTER_NAME, STREAM_FILTER_WRITE);
173
174 2
        $write($this->socket);
175
176 2
        stream_filter_remove($filter);
177
178 2
        $this->sendCommand("{$this->eol}.", "250");
179 2
    }
180
181
    /**
182
     * Read the SMTP status code
183
     *
184
     * @return string SMTP status code
185
     *
186
     * @throws SMTPException for unexpected response
187
     */
188 2
    protected function readCode(): string
189
    {
190 2
        while ($line = fgets($this->socket, 4096)) {
191 2
            $this->log("R: {$line}");
192
193 2
            $this->last_result = $line;
194
195 2
            if (preg_match('/^\d\d\d /', $line) === 1) {
196 2
                return substr($line, 0, 3);
197
            }
198 2
        }
199
200
        throw new SMTPException("unexpected response\nS: {$this->last_command}\nR: {$this->last_result}");
201
    }
202
203
    /**
204
     * @param string $message
205
     */
206 2
    protected function log(string $message): void
207
    {
208 2
        if ($this->logger) {
209 1
            $this->logger->debug($message);
210 1
        }
211 2
    }
212
}
213