xref: /plugin/smtp/vendor/txthinking/mailer/src/Mailer/SMTP.php (revision ee5c02056e482f4a07fb4b075ec45c4dfd3de0e8)
1<?php
2/***************************************************\
3 *
4 *  Mailer (https://github.com/txthinking/Mailer)
5 *
6 *  A lightweight PHP SMTP mail sender.
7 *  Implement RFC0821, RFC0822, RFC1869, RFC2045, RFC2821
8 *
9 *  Support html body, don't worry that the receiver's
10 *  mail client can't support html, because Mailer will
11 *  send both text/plain and text/html body, so if the
12 *  mail client can't support html, it will display the
13 *  text/plain body.
14 *
15 *  Create Date 2012-07-25.
16 *  Under the MIT license.
17 *
18 \***************************************************/
19
20namespace Tx\Mailer;
21
22use Psr\Log\LoggerInterface;
23use Tx\Mailer\Exceptions\CodeException;
24use Tx\Mailer\Exceptions\CryptoException;
25use Tx\Mailer\Exceptions\SMTPException;
26
27class SMTP
28{
29    /**
30     * smtp socket
31     */
32    protected $smtp;
33
34    /**
35     * smtp server
36     */
37    protected $host;
38
39    /**
40     * smtp server port
41     */
42    protected $port;
43
44    /**
45     * smtp secure ssl tls tlsv1.0 tlsv1.1 tlsv1.2
46     */
47    protected $secure;
48
49    /**
50     * EHLO message
51     */
52    protected $ehlo;
53
54    /**
55     * smtp username
56     */
57    protected $username;
58
59    /**
60     * smtp password
61     */
62    protected $password;
63
64    /**
65     * oauth access token
66     */
67    protected $oauthToken;
68
69    /**
70     * $this->CRLF
71     * @var string
72     */
73    protected $CRLF = "\r\n";
74
75    /**
76     * @var Message
77     */
78    protected $message;
79
80    /**
81     * @var LoggerInterface - Used to make things prettier than self::$logger
82     */
83    protected $logger;
84
85    /**
86     * Stack of all commands issued to SMTP
87     * @var array
88     */
89    protected $commandStack = array();
90
91    /**
92     * Stack of all results issued to SMTP
93     * @var array
94     */
95    protected $resultStack = array();
96
97    public function __construct(LoggerInterface $logger=null)
98    {
99        $this->logger = $logger;
100    }
101
102    /**
103     * set server and port
104     * @param string $host server
105     * @param int $port port
106     * @param string $secure ssl tls tlsv1.0 tlsv1.1 tlsv1.2
107     * @return $this
108     */
109    public function setServer($host, $port, $secure=null)
110    {
111        $this->host = $host;
112        $this->port = $port;
113        $this->secure = $secure;
114        if(!$this->ehlo) $this->ehlo = $host;
115        $this->logger && $this->logger->debug("Set: the server");
116        return $this;
117    }
118
119    /**
120     * auth login with server
121     * @param string $username
122     * @param string $password
123     * @return $this
124     */
125    public function setAuth($username, $password)
126    {
127        $this->username = $username;
128        $this->password = $password;
129        $this->logger && $this->logger->debug("Set: the auth login");
130        return $this;
131    }
132
133    /**
134     * auth oauthbearer with server
135     * @param string $accessToken
136     * @return $this
137     */
138    public function setOAuth($accessToken)
139    {
140        $this->oauthToken = $accessToken;
141        $this->logger && $this->logger->debug("Set: the auth oauthbearer");
142        return $this;
143    }
144
145    /**
146     * set the EHLO message
147     * @param $ehlo
148     * @return $this
149     */
150    public function setEhlo($ehlo)
151    {
152        $this->ehlo = $ehlo;
153        return $this;
154    }
155
156    /**
157     * Send the message
158     *
159     * @param Message $message
160     * @return bool
161     * @throws CodeException
162     * @throws CryptoException
163     * @throws SMTPException
164     */
165    public function send(Message $message)
166    {
167        $this->logger && $this->logger->debug('Set: a message will be sent');
168        $this->message = $message;
169        $this->connect()
170            ->ehlo();
171
172        if ($this->secure === 'tls' || $this->secure === 'tlsv1.0' || $this->secure === 'tlsv1.1' | $this->secure === 'tlsv1.2') {
173            $this->starttls()
174                ->ehlo();
175        }
176
177        if ($this->username !== null || $this->password !== null) {
178            $this->authLogin();
179        } elseif ($this->oauthToken !== null) {
180            $this->authOAuthBearer();
181        }
182        $this->mailFrom()
183            ->rcptTo()
184            ->data()
185            ->quit();
186        return fclose($this->smtp);
187    }
188
189    /**
190     * connect the server
191     * SUCCESS 220
192     * @return $this
193     * @throws CodeException
194     * @throws SMTPException
195     */
196    protected function connect()
197    {
198        $this->logger && $this->logger->debug("Connecting to {$this->host} at {$this->port}");
199        $host = ($this->secure == 'ssl') ? 'ssl://' . $this->host : $this->host;
200        $this->smtp = @fsockopen($host, $this->port);
201        //set block mode
202        //    stream_set_blocking($this->smtp, 1);
203        if (!$this->smtp){
204            throw new SMTPException("Could not open SMTP Port.");
205        }
206        $code = $this->getCode();
207        if ($code !== '220'){
208            throw new CodeException('220', $code, array_pop($this->resultStack));
209        }
210        return $this;
211    }
212
213    /**
214     * SMTP STARTTLS
215     * SUCCESS 220
216     * @return $this
217     * @throws CodeException
218     * @throws CryptoException
219     * @throws SMTPException
220     */
221    protected function starttls()
222    {
223        $in = "STARTTLS" . $this->CRLF;
224        $code = $this->pushStack($in);
225        if ($code !== '220'){
226            throw new CodeException('220', $code, array_pop($this->resultStack));
227        }
228
229        if ($this->secure !== 'tls' && version_compare(phpversion(), '5.6.0', '<')) {
230            throw new CryptoException('Crypto type expected PHP 5.6 or greater');
231        }
232
233        switch ($this->secure) {
234            case 'tlsv1.0':
235                $crypto_type = STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT;
236                break;
237            case 'tlsv1.1':
238                $crypto_type = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
239                break;
240            case 'tlsv1.2':
241                $crypto_type = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
242                break;
243            default:
244                $crypto_type = STREAM_CRYPTO_METHOD_TLS_CLIENT;
245                break;
246        }
247
248        if(!\stream_socket_enable_crypto($this->smtp, true, $crypto_type)) {
249            throw new CryptoException("Start TLS failed to enable crypto");
250        }
251        return $this;
252    }
253
254    /**
255     * SMTP EHLO
256     * SUCCESS 250
257     * @return $this
258     * @throws CodeException
259     * @throws SMTPException
260     */
261    protected function ehlo()
262    {
263        $in = "EHLO " . $this->ehlo . $this->CRLF;
264        $code = $this->pushStack($in);
265        if ($code !== '250'){
266            throw new CodeException('250', $code, array_pop($this->resultStack));
267        }
268        return $this;
269    }
270
271    /**
272     * SMTP AUTH LOGIN
273     * SUCCESS 334
274     * SUCCESS 334
275     * SUCCESS 235
276     * @return $this
277     * @throws CodeException
278     * @throws SMTPException
279     */
280    protected function authLogin()
281    {
282        $in = "AUTH LOGIN" . $this->CRLF;
283        $code = $this->pushStack($in);
284        if ($code !== '334'){
285            throw new CodeException('334', $code, array_pop($this->resultStack));
286        }
287        $in = base64_encode($this->username) . $this->CRLF;
288        $code = $this->pushStack($in);
289        if ($code !== '334'){
290            throw new CodeException('334', $code, array_pop($this->resultStack));
291        }
292        $in = base64_encode($this->password) . $this->CRLF;
293        $code = $this->pushStack($in);
294        if ($code !== '235'){
295            throw new CodeException('235', $code, array_pop($this->resultStack));
296        }
297        return $this;
298    }
299
300    /**
301     * SMTP AUTH OAUTHBEARER
302     * SUCCESS 235
303     * @return $this
304     * @throws CodeException
305     * @throws SMTPException
306     */
307    protected function authOAuthBearer()
308    {
309        $authStr = sprintf("n,a=%s,%shost=%s%sport=%s%sauth=Bearer %s%s%s",
310            $this->message->getFromEmail(),
311            chr(1),
312            $this->host,
313            chr(1),
314            $this->port,
315            chr(1),
316            $this->oauthToken,
317            chr(1),
318            chr(1)
319        );
320        $authStr = base64_encode($authStr);
321        $in = "AUTH OAUTHBEARER $authStr" . $this->CRLF;
322        $code = $this->pushStack($in);
323        if ($code !== '235'){
324            throw new CodeException('235', $code, array_pop($this->resultStack));
325        }
326        return $this;
327    }
328
329    /**
330     * SMTP AUTH XOAUTH2
331     * SUCCESS 235
332     * @return $this
333     * @throws CodeException
334     * @throws SMTPException
335     */
336    protected function authXOAuth2()
337    {
338        $authStr = sprintf("user=%s%sauth=Bearer %s%s%s",
339            $this->message->getFromEmail(),
340            chr(1),
341            $this->oauthToken,
342            chr(1),
343            chr(1)
344        );
345        $authStr = base64_encode($authStr);
346        $in = "AUTH XOAUTH2 $authStr" . $this->CRLF;
347        $code = $this->pushStack($in);
348        if ($code !== '235'){
349            throw new CodeException('235', $code, array_pop($this->resultStack));
350        }
351        return $this;
352    }
353
354    /**
355     * SMTP MAIL FROM
356     * SUCCESS 250
357     * @return $this
358     * @throws CodeException
359     * @throws SMTPException
360     */
361    protected function mailFrom()
362    {
363        $in = "MAIL FROM:<{$this->message->getFromEmail()}>" . $this->CRLF;
364        $code = $this->pushStack($in);
365        if ($code !== '250') {
366            throw new CodeException('250', $code, array_pop($this->resultStack));
367        }
368        return $this;
369    }
370
371    /**
372     * SMTP RCPT TO
373     * SUCCESS 250
374     * @return $this
375     * @throws CodeException
376     * @throws SMTPException
377     */
378    protected function rcptTo()
379    {
380        $to = array_merge(
381            $this->message->getTo(),
382            $this->message->getCc(),
383            $this->message->getBcc()
384        );
385        foreach ($to as $toEmail=>$_) {
386            $in = "RCPT TO:<" . $toEmail . ">" . $this->CRLF;
387            $code = $this->pushStack($in);
388            if ($code !== '250') {
389                throw new CodeException('250', $code, array_pop($this->resultStack));
390            }
391        }
392        return $this;
393    }
394
395    /**
396     * SMTP DATA
397     * SUCCESS 354
398     * SUCCESS 250
399     * @return $this
400     * @throws CodeException
401     * @throws SMTPException
402     */
403    protected function data()
404    {
405        $in = "DATA" . $this->CRLF;
406        $code = $this->pushStack($in);
407        if ($code !== '354') {
408            throw new CodeException('354', $code, array_pop($this->resultStack));
409        }
410        $in = $this->message->toString();
411        $code = $this->pushStack($in);
412        if ($code !== '250'){
413            throw new CodeException('250', $code, array_pop($this->resultStack));
414        }
415        return $this;
416    }
417
418    /**
419     * SMTP QUIT
420     * SUCCESS 221
421     * @return $this
422     * @throws CodeException
423     * @throws SMTPException
424     */
425    protected function quit()
426    {
427        $in = "QUIT" . $this->CRLF;
428        $code = $this->pushStack($in);
429        if ($code !== '221'){
430            throw new CodeException('221', $code, array_pop($this->resultStack));
431        }
432        return $this;
433    }
434
435    protected function pushStack($string)
436    {
437        $this->commandStack[] = $string;
438        fputs($this->smtp, $string, strlen($string));
439        $this->logger && $this->logger->debug('Sent: '. $string);
440        return $this->getCode();
441    }
442
443    /**
444     * get smtp response code
445     * once time has three digital and a space
446     * @return string
447     * @throws SMTPException
448     */
449    protected function getCode()
450    {
451        while ($str = fgets($this->smtp, 515)) {
452            $this->logger && $this->logger->debug("Got: ". $str);
453            $this->resultStack[] = $str;
454            if(substr($str,3,1) == " ") {
455                $code = substr($str,0,3);
456                return $code;
457            }
458        }
459        throw new SMTPException("SMTP Server did not respond with anything I recognized");
460    }
461
462}
463
464