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
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     * $this->CRLF
66     * @var string
67     */
68    protected $CRLF = "\r\n";
69
70    /**
71     * @var Message
72     */
73    protected $message;
74
75    /**
76     * @var LoggerInterface - Used to make things prettier than self::$logger
77     */
78    protected $logger;
79
80    /**
81     * Stack of all commands issued to SMTP
82     * @var array
83     */
84    protected $commandStack = array();
85
86    /**
87     * Stack of all results issued to SMTP
88     * @var array
89     */
90    protected $resultStack = array();
91
92    public function __construct(LoggerInterface $logger=null)
93    {
94        $this->logger = $logger;
95    }
96
97    /**
98     * set server and port
99     * @param string $host server
100     * @param int $port port
101     * @param string $secure ssl tls
102     * @return $this
103     */
104    public function setServer($host, $port, $secure=null)
105    {
106        $this->host = $host;
107        $this->port = $port;
108        $this->secure = $secure;
109        if(!$this->ehlo) $this->ehlo = $host;
110        $this->logger && $this->logger->debug("Set: the server");
111        return $this;
112    }
113
114    /**
115     * auth with server
116     * @param string $username
117     * @param string $password
118     * @return $this
119     */
120    public function setAuth($username, $password){
121        $this->username = $username;
122        $this->password = $password;
123        $this->logger && $this->logger->debug("Set: the auth");
124        return $this;
125    }
126
127    /**
128     * set the EHLO message
129     * @param $ehlo
130     * @return $this
131     */
132    public function setEhlo($ehlo){
133        $this->ehlo = $ehlo;
134        return $this;
135    }
136
137    /**
138     * Send the message
139     *
140     * @param Message $message
141     * @return $this
142     * @throws CodeException
143     * @throws CryptoException
144     * @throws SMTPException
145     */
146    public function send(Message $message){
147        $this->logger && $this->logger->debug('Set: a message will be sent');
148        $this->message = $message;
149        $this->connect()
150            ->ehlo();
151
152        if ($this->secure === 'tls'){
153            $this->starttls()
154                ->ehlo();
155        }
156        $this->authLogin()
157            ->mailFrom()
158            ->rcptTo()
159            ->data()
160            ->quit();
161        return fclose($this->smtp);
162    }
163
164    /**
165     * connect the server
166     * SUCCESS 220
167     * @return $this
168     * @throws CodeException
169     * @throws SMTPException
170     */
171    protected function connect(){
172        $this->logger && $this->logger->debug("Connecting to {$this->host} at {$this->port}");
173        $host = ($this->secure == 'ssl') ? 'ssl://' . $this->host : $this->host;
174        $this->smtp = @fsockopen($host, $this->port);
175        //set block mode
176        //    stream_set_blocking($this->smtp, 1);
177        if (!$this->smtp){
178            throw new SMTPException("Could not open SMTP Port.");
179        }
180        $code = $this->getCode();
181        if ($code !== '220'){
182            throw new CodeException('220', $code, array_pop($this->resultStack));
183        }
184        return $this;
185    }
186
187    /**
188     * SMTP STARTTLS
189     * SUCCESS 220
190     * @return $this
191     * @throws CodeException
192     * @throws CryptoException
193     * @throws SMTPException
194     */
195    protected function starttls(){
196        $in = "STARTTLS" . $this->CRLF;
197        $code = $this->pushStack($in);
198        if ($code !== '220'){
199            throw new CodeException('220', $code, array_pop($this->resultStack));
200        }
201        if(!stream_socket_enable_crypto($this->smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
202            throw new CryptoException("Start TLS failed to enable crypto");
203        }
204        return $this;
205    }
206
207    /**
208     * SMTP EHLO
209     * SUCCESS 250
210     * @return $this
211     * @throws CodeException
212     * @throws SMTPException
213     */
214    protected function ehlo(){
215        $in = "EHLO " . $this->ehlo . $this->CRLF;
216        $code = $this->pushStack($in);
217        if ($code !== '250'){
218            throw new CodeException('250', $code, array_pop($this->resultStack));
219        }
220        return $this;
221    }
222
223    /**
224     * SMTP AUTH LOGIN
225     * SUCCESS 334
226     * SUCCESS 334
227     * SUCCESS 235
228     * @return $this
229     * @throws CodeException
230     * @throws SMTPException
231     */
232    protected function authLogin()
233    {
234        if ($this->username === null && $this->password === null) {
235            // Unless the user has specifically set a username/password
236            // Do not try to authorize.
237            return $this;
238        }
239
240        $in = "AUTH LOGIN" . $this->CRLF;
241        $code = $this->pushStack($in);
242        if ($code !== '334'){
243            throw new CodeException('334', $code, array_pop($this->resultStack));
244        }
245        $in = base64_encode($this->username) . $this->CRLF;
246        $code = $this->pushStack($in);
247        if ($code !== '334'){
248            throw new CodeException('334', $code, array_pop($this->resultStack));
249        }
250        $in = base64_encode($this->password) . $this->CRLF;
251        $code = $this->pushStack($in);
252        if ($code !== '235'){
253            throw new CodeException('235', $code, array_pop($this->resultStack));
254        }
255        return $this;
256    }
257
258    /**
259     * SMTP MAIL FROM
260     * SUCCESS 250
261     * @return $this
262     * @throws CodeException
263     * @throws SMTPException
264     */
265    protected function mailFrom(){
266        $in = "MAIL FROM:<{$this->message->getFromEmail()}>" . $this->CRLF;
267        $code = $this->pushStack($in);
268        if ($code !== '250') {
269            throw new CodeException('250', $code, array_pop($this->resultStack));
270        }
271        return $this;
272    }
273
274    /**
275     * SMTP RCPT TO
276     * SUCCESS 250
277     * @return $this
278     * @throws CodeException
279     * @throws SMTPException
280     */
281    protected function rcptTo(){
282        foreach ($this->message->getTo() as $toEmail) {
283            $in = "RCPT TO:<" . $toEmail . ">" . $this->CRLF;
284            $code = $this->pushStack($in);
285            if ($code !== '250') {
286                throw new CodeException('250', $code, array_pop($this->resultStack));
287            }
288        }
289        return $this;
290    }
291
292    /**
293     * SMTP DATA
294     * SUCCESS 354
295     * SUCCESS 250
296     * @return $this
297     * @throws CodeException
298     * @throws SMTPException
299     */
300    protected function data(){
301        $in = "DATA" . $this->CRLF;
302        $code = $this->pushStack($in);
303        if ($code !== '354') {
304            throw new CodeException('354', $code, array_pop($this->resultStack));
305        }
306        $in = $this->message->toString();
307        $code = $this->pushStack($in);
308        if ($code !== '250'){
309            throw new CodeException('250', $code, array_pop($this->resultStack));
310        }
311        return $this;
312    }
313
314    /**
315     * SMTP QUIT
316     * SUCCESS 221
317     * @return $this
318     * @throws CodeException
319     * @throws SMTPException
320     */
321    protected function quit(){
322        $in = "QUIT" . $this->CRLF;
323        $code = $this->pushStack($in);
324        if ($code !== '221'){
325            throw new CodeException('221', $code, array_pop($this->resultStack));
326        }
327        return $this;
328    }
329
330    protected function pushStack($string)
331    {
332        $this->commandStack[] = $string;
333        fputs($this->smtp, $string, strlen($string));
334        $this->logger && $this->logger->debug('Sent: '. $string);
335        return $this->getCode();
336    }
337
338    /**
339     * get smtp response code
340     * once time has three digital and a space
341     * @return string
342     * @throws SMTPException
343     */
344    protected function getCode() {
345        while ($str = fgets($this->smtp, 515)) {
346            $this->logger && $this->logger->debug("Got: ". $str);
347            $this->resultStack[] = $str;
348            if(substr($str,3,1) == " ") {
349                $code = substr($str,0,3);
350                return $code;
351            }
352        }
353        throw new SMTPException("SMTP Server did not respond with anything I recognized");
354    }
355
356}
357
358