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