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