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