1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\Mailer\Transport;
16:
17: use Cake\Mailer\AbstractTransport;
18: use Cake\Mailer\Email;
19: use Cake\Network\Exception\SocketException;
20: use Cake\Network\Socket;
21: use Exception;
22:
23: 24: 25:
26: class SmtpTransport extends AbstractTransport
27: {
28: 29: 30: 31: 32:
33: protected $_defaultConfig = [
34: 'host' => 'localhost',
35: 'port' => 25,
36: 'timeout' => 30,
37: 'username' => null,
38: 'password' => null,
39: 'client' => null,
40: 'tls' => false,
41: 'keepAlive' => false
42: ];
43:
44: 45: 46: 47: 48:
49: protected $_socket;
50:
51: 52: 53: 54: 55:
56: protected $_content = [];
57:
58: 59: 60: 61: 62:
63: protected $_lastResponse = [];
64:
65: 66: 67: 68: 69: 70:
71: public function __destruct()
72: {
73: try {
74: $this->disconnect();
75: } catch (Exception $e) {
76:
77: }
78: }
79:
80: 81: 82: 83: 84: 85: 86:
87: public function __wakeup()
88: {
89: $this->_socket = null;
90: }
91:
92: 93: 94: 95: 96: 97: 98: 99:
100: public function connect()
101: {
102: if (!$this->connected()) {
103: $this->_connect();
104: $this->_auth();
105: }
106: }
107:
108: 109: 110: 111: 112:
113: public function connected()
114: {
115: return $this->_socket !== null && $this->_socket->connected;
116: }
117:
118: 119: 120: 121: 122: 123: 124: 125:
126: public function disconnect()
127: {
128: if ($this->connected()) {
129: $this->_disconnect();
130: }
131: }
132:
133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157:
158: public function getLastResponse()
159: {
160: return $this->_lastResponse;
161: }
162:
163: 164: 165: 166: 167: 168: 169:
170: public function send(Email $email)
171: {
172: if (!$this->connected()) {
173: $this->_connect();
174: $this->_auth();
175: } else {
176: $this->_smtpSend('RSET');
177: }
178:
179: $this->_sendRcpt($email);
180: $this->_sendData($email);
181:
182: if (!$this->_config['keepAlive']) {
183: $this->_disconnect();
184: }
185:
186: return $this->_content;
187: }
188:
189: 190: 191: 192: 193: 194:
195: protected function _bufferResponseLines(array $responseLines)
196: {
197: $response = [];
198: foreach ($responseLines as $responseLine) {
199: if (preg_match('/^(\d{3})(?:[ -]+(.*))?$/', $responseLine, $match)) {
200: $response[] = [
201: 'code' => $match[1],
202: 'message' => isset($match[2]) ? $match[2] : null
203: ];
204: }
205: }
206: $this->_lastResponse = array_merge($this->_lastResponse, $response);
207: }
208:
209: 210: 211: 212: 213: 214:
215: protected function _connect()
216: {
217: $this->_generateSocket();
218: if (!$this->_socket->connect()) {
219: throw new SocketException('Unable to connect to SMTP server.');
220: }
221: $this->_smtpSend(null, '220');
222:
223: $config = $this->_config;
224:
225: if (isset($config['client'])) {
226: $host = $config['client'];
227: } elseif ($httpHost = env('HTTP_HOST')) {
228: list($host) = explode(':', $httpHost);
229: } else {
230: $host = 'localhost';
231: }
232:
233: try {
234: $this->_smtpSend("EHLO {$host}", '250');
235: if ($config['tls']) {
236: $this->_smtpSend('STARTTLS', '220');
237: $this->_socket->enableCrypto('tls');
238: $this->_smtpSend("EHLO {$host}", '250');
239: }
240: } catch (SocketException $e) {
241: if ($config['tls']) {
242: throw new SocketException('SMTP server did not accept the connection or trying to connect to non TLS SMTP server using TLS.', null, $e);
243: }
244: try {
245: $this->_smtpSend("HELO {$host}", '250');
246: } catch (SocketException $e2) {
247: throw new SocketException('SMTP server did not accept the connection.', null, $e2);
248: }
249: }
250: }
251:
252: 253: 254: 255: 256: 257:
258: protected function _auth()
259: {
260: if (isset($this->_config['username'], $this->_config['password'])) {
261: $replyCode = (string)$this->_smtpSend('AUTH LOGIN', '334|500|502|504');
262: if ($replyCode === '334') {
263: try {
264: $this->_smtpSend(base64_encode($this->_config['username']), '334');
265: } catch (SocketException $e) {
266: throw new SocketException('SMTP server did not accept the username.', null, $e);
267: }
268: try {
269: $this->_smtpSend(base64_encode($this->_config['password']), '235');
270: } catch (SocketException $e) {
271: throw new SocketException('SMTP server did not accept the password.', null, $e);
272: }
273: } elseif ($replyCode === '504') {
274: throw new SocketException('SMTP authentication method not allowed, check if SMTP server requires TLS.');
275: } else {
276: throw new SocketException('AUTH command not recognized or not implemented, SMTP server may not require authentication.');
277: }
278: }
279: }
280:
281: 282: 283: 284: 285: 286:
287: protected function _prepareFromCmd($email)
288: {
289: return 'MAIL FROM:<' . $email . '>';
290: }
291:
292: 293: 294: 295: 296: 297:
298: protected function _prepareRcptCmd($email)
299: {
300: return 'RCPT TO:<' . $email . '>';
301: }
302:
303: 304: 305: 306: 307: 308:
309: protected function _prepareFromAddress($email)
310: {
311: $from = $email->getReturnPath();
312: if (empty($from)) {
313: $from = $email->getFrom();
314: }
315:
316: return $from;
317: }
318:
319: 320: 321: 322: 323: 324:
325: protected function _prepareRecipientAddresses($email)
326: {
327: $to = $email->getTo();
328: $cc = $email->getCc();
329: $bcc = $email->getBcc();
330:
331: return array_merge(array_keys($to), array_keys($cc), array_keys($bcc));
332: }
333:
334: 335: 336: 337: 338: 339:
340: protected function _prepareMessageHeaders($email)
341: {
342: return $email->getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'to', 'cc', 'subject', 'returnPath']);
343: }
344:
345: 346: 347: 348: 349: 350:
351: protected function _prepareMessage($email)
352: {
353: $lines = $email->message();
354: $messages = [];
355: foreach ($lines as $line) {
356: if (!empty($line) && ($line[0] === '.')) {
357: $messages[] = '.' . $line;
358: } else {
359: $messages[] = $line;
360: }
361: }
362:
363: return implode("\r\n", $messages);
364: }
365:
366: 367: 368: 369: 370: 371: 372:
373: protected function _sendRcpt($email)
374: {
375: $from = $this->_prepareFromAddress($email);
376: $this->_smtpSend($this->_prepareFromCmd(key($from)));
377:
378: $emails = $this->_prepareRecipientAddresses($email);
379: foreach ($emails as $mail) {
380: $this->_smtpSend($this->_prepareRcptCmd($mail));
381: }
382: }
383:
384: 385: 386: 387: 388: 389: 390:
391: protected function _sendData($email)
392: {
393: $this->_smtpSend('DATA', '354');
394:
395: $headers = $this->_headersToString($this->_prepareMessageHeaders($email));
396: $message = $this->_prepareMessage($email);
397:
398: $this->_smtpSend($headers . "\r\n\r\n" . $message . "\r\n\r\n\r\n.");
399: $this->_content = ['headers' => $headers, 'message' => $message];
400: }
401:
402: 403: 404: 405: 406: 407:
408: protected function _disconnect()
409: {
410: $this->_smtpSend('QUIT', false);
411: $this->_socket->disconnect();
412: }
413:
414: 415: 416: 417: 418: 419:
420: protected function _generateSocket()
421: {
422: $this->_socket = new Socket($this->_config);
423: }
424:
425: 426: 427: 428: 429: 430: 431: 432:
433: protected function _smtpSend($data, $checkCode = '250')
434: {
435: $this->_lastResponse = [];
436:
437: if ($data !== null) {
438: $this->_socket->write($data . "\r\n");
439: }
440:
441: $timeout = $this->_config['timeout'];
442:
443: while ($checkCode !== false) {
444: $response = '';
445: $startTime = time();
446: while (substr($response, -2) !== "\r\n" && ((time() - $startTime) < $timeout)) {
447: $bytes = $this->_socket->read();
448: if ($bytes === false || $bytes === null) {
449: break;
450: }
451: $response .= $bytes;
452: }
453: if (substr($response, -2) !== "\r\n") {
454: throw new SocketException('SMTP timeout.');
455: }
456: $responseLines = explode("\r\n", rtrim($response, "\r\n"));
457: $response = end($responseLines);
458:
459: $this->_bufferResponseLines($responseLines);
460:
461: if (preg_match('/^(' . $checkCode . ')(.)/', $response, $code)) {
462: if ($code[2] === '-') {
463: continue;
464: }
465:
466: return $code[1];
467: }
468: throw new SocketException(sprintf('SMTP Error: %s', $response));
469: }
470: }
471: }
472: