PageRenderTime 51ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/email/driver/smtp.php

https://github.com/fuel/email
PHP | 350 lines | 209 code | 56 blank | 85 comment | 18 complexity | d6377c21dfe2c5f35ef979a07934b02b MD5 | raw file
  1. <?php
  2. /**
  3. * Fuel is a fast, lightweight, community driven PHP 5.4+ framework.
  4. *
  5. * @package Fuel
  6. * @version 1.9-dev
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2019 Fuel Development Team
  10. * @link https://fuelphp.com
  11. */
  12. namespace Email;
  13. class SmtpConnectionException extends \FuelException {}
  14. class SmtpCommandFailureException extends \EmailSendingFailedException {}
  15. class SmtpTimeoutException extends \EmailSendingFailedException {}
  16. class SmtpAuthenticationFailedException extends \FuelException {}
  17. class Email_Driver_Smtp extends \Email_Driver
  18. {
  19. /**
  20. * Class destructor
  21. */
  22. function __destruct()
  23. {
  24. // makes sure any open connections will be closed
  25. if ($this->smtp_connection)
  26. {
  27. $this->smtp_disconnect();
  28. }
  29. }
  30. /**
  31. * The SMTP connection
  32. */
  33. protected $smtp_connection = null;
  34. /**
  35. * Initiates the sending process.
  36. *
  37. * @return bool success boolean
  38. */
  39. protected function _send()
  40. {
  41. // send the email
  42. try
  43. {
  44. return $this->_send_email();
  45. }
  46. // something failed
  47. catch (\Exception $e)
  48. {
  49. // disconnect if needed
  50. if ($this->smtp_connection)
  51. {
  52. if ($e instanceOf SmtpTimeoutException)
  53. {
  54. // simply close the connection
  55. fclose($this->smtp_connection);
  56. $this->smtp_connection = null;
  57. }
  58. else
  59. {
  60. // proper close, with a QUIT
  61. $this->smtp_disconnect();
  62. }
  63. }
  64. // rethrow the exception
  65. throw $e;
  66. }
  67. }
  68. /**
  69. * Sends the actual email
  70. *
  71. * @return bool success boolean
  72. */
  73. protected function _send_email()
  74. {
  75. $message = $this->build_message(true);
  76. if(empty($this->config['smtp']['host']) or empty($this->config['smtp']['port']))
  77. {
  78. throw new \FuelException('Must supply a SMTP host and port, none given.');
  79. }
  80. // Use authentication?
  81. $authenticate = (empty($this->smtp_connection) and ! empty($this->config['smtp']['username']) and ! empty($this->config['smtp']['password']));
  82. // Connect
  83. $this->smtp_connect();
  84. // Authenticate when needed
  85. $authenticate and $this->smtp_authenticate();
  86. // Set return path
  87. $return_path = empty($this->config['return_path']) ? $this->config['from']['email'] : $this->config['return_path'];
  88. $this->smtp_send('MAIL FROM:<' . $return_path .'>', 250);
  89. foreach(array('to', 'cc', 'bcc') as $list)
  90. {
  91. foreach($this->{$list} as $recipient)
  92. {
  93. $this->smtp_send('RCPT TO:<'.$recipient['email'].'>', array(250, 251));
  94. }
  95. }
  96. // Prepare for data sending
  97. $this->smtp_send('DATA', 354);
  98. $lines = explode($this->config['newline'], $message['header'].preg_replace('/^\./m', '..$1', $message['body']));
  99. foreach($lines as $line)
  100. {
  101. if(substr($line, 0, 1) === '.')
  102. {
  103. $line = '.'.$line;
  104. }
  105. fputs($this->smtp_connection, $line.$this->config['newline']);
  106. }
  107. // Finish the message
  108. $this->smtp_send('.', 250);
  109. // Close the connection if we're not using pipelining
  110. $this->pipelining or $this->smtp_disconnect();
  111. return true;
  112. }
  113. /**
  114. * Connects to the given smtp and says hello to the other server.
  115. */
  116. protected function smtp_connect()
  117. {
  118. // re-use the existing connection
  119. if ( ! empty($this->smtp_connection))
  120. {
  121. return;
  122. }
  123. // add a transport if not given
  124. if (strpos($this->config['smtp']['host'], '://') === false)
  125. {
  126. $this->config['smtp']['host'] = 'tcp://'.$this->config['smtp']['host'];
  127. }
  128. $context = stream_context_create();
  129. if (is_array($this->config['smtp']['options']) and ! empty($this->config['smtp']['options']))
  130. {
  131. stream_context_set_option($context, $this->config['smtp']['options']);
  132. }
  133. $this->smtp_connection = stream_socket_client(
  134. $this->config['smtp']['host'].':'.$this->config['smtp']['port'],
  135. $error_number,
  136. $error_string,
  137. $this->config['smtp']['timeout'],
  138. STREAM_CLIENT_CONNECT,
  139. $context
  140. );
  141. if(empty($this->smtp_connection))
  142. {
  143. throw new SmtpConnectionException('Could not connect to SMTP: ('.$error_number.') '.$error_string);
  144. }
  145. // Clear the smtp response
  146. $this->smtp_get_response();
  147. // Just say hello!
  148. try
  149. {
  150. $this->smtp_send('EHLO'.' '.\Input::server('SERVER_NAME', 'localhost.local'), 250);
  151. }
  152. catch(SmtpCommandFailureException $e)
  153. {
  154. // Didn't work? Try HELO
  155. $this->smtp_send('HELO'.' '.\Input::server('SERVER_NAME', 'localhost.local'), 250);
  156. }
  157. // Enable TLS encryption if needed, and we're connecting using TCP
  158. if (\Arr::get($this->config, 'smtp.starttls', false) and strpos($this->config['smtp']['host'], 'tcp://') === 0)
  159. {
  160. try
  161. {
  162. $this->smtp_send('STARTTLS', 220);
  163. if ( ! stream_socket_enable_crypto($this->smtp_connection, true, STREAM_CRYPTO_METHOD_TLS_CLIENT))
  164. {
  165. throw new SmtpConnectionException('STARTTLS failed, Crypto client can not be enabled.');
  166. }
  167. }
  168. catch(SmtpCommandFailureException $e)
  169. {
  170. throw new SmtpConnectionException('STARTTLS failed, invalid return code received from server.');
  171. }
  172. // Say hello again, the service list might be updated (see RFC 3207 section 4.2)
  173. try
  174. {
  175. $this->smtp_send('EHLO'.' '.\Input::server('SERVER_NAME', 'localhost.local'), 250);
  176. }
  177. catch(SmtpCommandFailureException $e)
  178. {
  179. // Didn't work? Try HELO
  180. $this->smtp_send('HELO'.' '.\Input::server('SERVER_NAME', 'localhost.local'), 250);
  181. }
  182. }
  183. try
  184. {
  185. $this->smtp_send('HELP', 214);
  186. }
  187. catch(SmtpCommandFailureException $e)
  188. {
  189. // Let this pass as some servers don't support this.
  190. }
  191. }
  192. /**
  193. * Close SMTP connection
  194. */
  195. protected function smtp_disconnect()
  196. {
  197. $this->smtp_send('QUIT', false);
  198. fclose($this->smtp_connection);
  199. $this->smtp_connection = null;
  200. }
  201. /**
  202. * Performs authentication with the SMTP host
  203. */
  204. protected function smtp_authenticate()
  205. {
  206. // Encode login data
  207. $username = base64_encode($this->config['smtp']['username']);
  208. $password = base64_encode($this->config['smtp']['password']);
  209. try
  210. {
  211. // Prepare login
  212. $this->smtp_send('AUTH LOGIN', 334);
  213. // Send username
  214. $this->smtp_send($username, 334);
  215. // Send password
  216. $this->smtp_send($password, 235);
  217. }
  218. catch(SmtpCommandFailureException $e)
  219. {
  220. throw new SmtpAuthenticationFailedException('Failed authentication.');
  221. }
  222. }
  223. /**
  224. * Sends data to the SMTP host
  225. *
  226. * @param string $data The SMTP command
  227. * @param string|bool|string $expecting The expected response
  228. * @param bool $return_number Set to true to return the status number
  229. *
  230. * @throws \SmtpCommandFailureException When the command failed an expecting is not set to false.
  231. * @throws \SmtpTimeoutException SMTP connection timed out
  232. *
  233. * @return mixed Result or result number, false when expecting is false
  234. */
  235. protected function smtp_send($data, $expecting, $return_number = false)
  236. {
  237. ! is_array($expecting) and $expecting !== false and $expecting = array($expecting);
  238. stream_set_timeout($this->smtp_connection, $this->config['smtp']['timeout']);
  239. if ( ! fputs($this->smtp_connection, $data . $this->config['newline']))
  240. {
  241. if($expecting === false)
  242. {
  243. return false;
  244. }
  245. throw new SmtpCommandFailureException('Failed executing command: '. $data);
  246. }
  247. $info = stream_get_meta_data($this->smtp_connection);
  248. if($info['timed_out'])
  249. {
  250. throw new SmtpTimeoutException('SMTP connection timed out.');
  251. }
  252. // Get the reponse
  253. $response = $this->smtp_get_response();
  254. // Get the reponse number
  255. $number = (int) substr(trim($response), 0, 3);
  256. // Check against expected result
  257. if($expecting !== false and ! in_array($number, $expecting))
  258. {
  259. throw new SmtpCommandFailureException('Got an unexpected response from host on command: ['.$data.'] expecting: '.join(' or ', $expecting).' received: '.$response);
  260. }
  261. if($return_number)
  262. {
  263. return $number;
  264. }
  265. return $response;
  266. }
  267. /**
  268. * Get SMTP response
  269. *
  270. * @throws \SmtpTimeoutException
  271. *
  272. * @return string SMTP response
  273. */
  274. protected function smtp_get_response()
  275. {
  276. $data = '';
  277. // set the timeout.
  278. stream_set_timeout($this->smtp_connection, $this->config['smtp']['timeout']);
  279. while($str = fgets($this->smtp_connection, 512))
  280. {
  281. $info = stream_get_meta_data($this->smtp_connection);
  282. if($info['timed_out'])
  283. {
  284. throw new SmtpTimeoutException('SMTP connection timed out.');
  285. }
  286. $data .= $str;
  287. if (substr($str, 3, 1) === ' ')
  288. {
  289. break;
  290. }
  291. }
  292. return $data;
  293. }
  294. }