PageRenderTime 232ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/symphony/lib/toolkit/class.smtp.php

https://github.com/nils-werner/symphony-2
PHP | 496 lines | 264 code | 62 blank | 170 comment | 49 complexity | ce2638706de6c29b79c6798da993d480 MD5 | raw file
  1. <?php
  2. /**
  3. * @package toolkit
  4. */
  5. /**
  6. * Exceptions to be thrown by the SMTP Client class
  7. */
  8. Class SMTPException extends Exception{}
  9. /**
  10. * A SMTP client class, for sending text/plain emails.
  11. * This class only supports the very basic SMTP functions.
  12. * Inspired by the SMTP class in the Zend library
  13. *
  14. * @author Huib Keemink <huib.keemink@creativedutchmen.com>
  15. * @version 0.1 - 20 okt 2010
  16. */
  17. Class SMTP{
  18. const TIMEOUT = 30;
  19. protected $_host;
  20. protected $_port;
  21. protected $_user = null;
  22. protected $_pass = null;
  23. protected $_transport = 'tcp';
  24. protected $_secure = false;
  25. protected $_header_fields = array();
  26. protected $_from = null;
  27. protected $_subject = null;
  28. protected $_to = array();
  29. protected $_ip = '127.0.0.1';
  30. protected $_connection = false;
  31. protected $_helo = false;
  32. protected $_mail = false;
  33. protected $_data = false;
  34. protected $_rcpt = false;
  35. protected $_auth = false;
  36. /**
  37. * Constructor.
  38. *
  39. * @param string $host
  40. * Host to connect to. Defaults to localhost (127.0.0.1)
  41. * @param integer $port
  42. * When ssl is used, defaults to 465
  43. * When no ssl is used, and ini_get returns no value, defaults to 25.
  44. * @param array $options
  45. * Currently supports 3 values:
  46. * $options['secure'] can be ssl, tls or null.
  47. * $options['username'] the username used to login to the server. Leave empty for no authentication.
  48. * $options['password'] the password used to login to the server. Leave empty for no authentication.
  49. * $options['local_ip'] the ip address used in the ehlo/helo commands. Only ip's are accepted.
  50. * @return void
  51. */
  52. public function __construct($host = '127.0.0.1', $port = null, $options = array()){
  53. if ($options['secure'] !== null) {
  54. switch (strtolower($options['secure'])) {
  55. case 'tls':
  56. $this->_secure = 'tls';
  57. break;
  58. case 'ssl':
  59. $this->_transport = 'ssl';
  60. $this->_secure = 'ssl';
  61. if ($port == null) {
  62. $port = 465;
  63. }
  64. break;
  65. case 'no':
  66. break;
  67. default:
  68. throw new SMTPException(__('Unsupported SSL type'));
  69. break;
  70. }
  71. }
  72. if (is_null($options['local_ip'])) {
  73. $this->_ip = gethostbyname(php_uname('n'));
  74. }
  75. else{
  76. $this->_ip = $options['local_ip'];
  77. }
  78. if ($port == null) {
  79. $port = 25;
  80. }
  81. if(($options['username'] !== null) && ($options['password'] !== null)){
  82. $this->_user = $options['username'];
  83. $this->_pass = $options['password'];
  84. }
  85. $this->_host = $host;
  86. $this->_port = $port;
  87. }
  88. /**
  89. * Checks to see if `$this->_connection` is a valid resource. Throws an
  90. * exception if there is no connection, otherwise returns true.
  91. *
  92. * @throws SMTPException
  93. * @return boolean
  94. */
  95. public function checkConnection() {
  96. if(!is_resource($this->_connection)){
  97. throw new SMTPException(__('No connection has been established to %s', array($this->_host)));
  98. }
  99. return true;
  100. }
  101. /**
  102. * The actual email sending.
  103. * The connection to the server (connecting, EHLO, AUTH, etc) is done here,
  104. * right before the actual email is sent. This is to make sure the connection does not time out.
  105. *
  106. * @param string $from
  107. * The from string. Should have the following format: email@domain.tld
  108. * @param string $to
  109. * The email address to send the email to.
  110. * @param string $subject
  111. * The subject to send the email to.
  112. * @param string $message
  113. * @return boolean
  114. */
  115. public function sendMail($from, $to, $subject, $message){
  116. $this->_connect($this->_host, $this->_port);
  117. $this->mail($from);
  118. if(!is_array($to)){
  119. $to = array($to);
  120. }
  121. foreach($to as $recipient){
  122. $this->rcpt($recipient);
  123. }
  124. $this->data($message);
  125. $this->rset();
  126. }
  127. /**
  128. * Sets a header to be sent in the email.
  129. *
  130. * @throws SMTPException
  131. * @param string $header
  132. * @param string $value
  133. * @return void
  134. */
  135. public function setHeader($header, $value){
  136. if(is_array($value)){
  137. throw new SMTPException(__('Header fields can only contain strings'));
  138. }
  139. $this->_header_fields[$header] = $value;
  140. }
  141. /**
  142. * Initiates the ehlo/helo requests.
  143. *
  144. * @throws SMTPException
  145. * @return void
  146. */
  147. public function helo(){
  148. if($this->_mail !== false){
  149. throw new SMTPException(__('Can not call HELO on existing session'));
  150. }
  151. //wait for the server to be ready
  152. $this->_expect(220,300);
  153. //send ehlo or ehlo request.
  154. try{
  155. $this->_ehlo();
  156. }
  157. catch(SMTPException $e){
  158. $this->_helo();
  159. }
  160. catch(Exception $e){
  161. throw $e;
  162. }
  163. $this->_helo = true;
  164. }
  165. /**
  166. * Calls the MAIL command on the server.
  167. *
  168. * @throws SMTPException
  169. * @param string $from
  170. * The email address to send the email from.
  171. * @return void
  172. */
  173. public function mail($from){
  174. if($this->_helo == false){
  175. throw new SMTPException(__('Must call EHLO (or HELO) before calling MAIL'));
  176. }
  177. else if($this->_mail !== false){
  178. throw new SMTPException(__('Only one call to MAIL may be made at a time.'));
  179. }
  180. $this->_send('MAIL FROM:<' . $from . '>');
  181. $this->_expect(250, 300);
  182. $this->_from = $from;
  183. $this->_mail = true;
  184. $this->_rcpt = false;
  185. $this->_data = false;
  186. }
  187. /**
  188. * Calls the RCPT command on the server. May be called multiple times for more than one recipient.
  189. *
  190. * @throws SMTPException
  191. * @param string $to
  192. * The address to send the email to.
  193. * @return void
  194. */
  195. public function rcpt($to){
  196. if($this->_mail == false){
  197. throw new SMTPException(__('Must call MAIL before calling RCPT'));
  198. }
  199. $this->_send('RCPT TO:<' . $to . '>');
  200. $this->_expect(array(250, 251), 300);
  201. $this->_rcpt = true;
  202. }
  203. /**
  204. * Calls the data command on the server.
  205. * Also includes header fields in the command.
  206. *
  207. * @throws SMTPException
  208. * @param string $data
  209. * @return void
  210. */
  211. public function data($data){
  212. if($this->_rcpt == false){
  213. throw new SMTPException(__('Must call RCPT before calling DATA'));
  214. }
  215. $this->_send('DATA');
  216. $this->_expect(354, 120);
  217. foreach($this->_header_fields as $name => $body){
  218. // Every header can contain an array. Will insert multiple header fields of that type with the contents of array.
  219. // Useful for multiple recipients, for instance.
  220. if(!is_array($body)){
  221. $body = array($body);
  222. }
  223. foreach($body as $val){
  224. $this->_send($name . ': ' . $val);
  225. }
  226. }
  227. // Send an empty newline. Solves bugs with Apple Mail
  228. $this->_send('');
  229. // Because the message can contain \n as a newline, replace all \r\n with \n and explode on \n.
  230. // The send() function will use the proper line ending (\r\n).
  231. $data = str_replace("\r\n", "\n", $data);
  232. $data_arr = explode("\n", $data);
  233. foreach($data_arr as $line){
  234. // Escape line if first character is a period (dot). http://tools.ietf.org/html/rfc2821#section-4.5.2
  235. if(strpos($line, '.') === 0){
  236. $line = '.' . $line;
  237. }
  238. $this->_send($line);
  239. }
  240. $this->_send('.');
  241. $this->_expect(250, 600);
  242. $this->_data = true;
  243. }
  244. /**
  245. * Resets the current session. This 'undoes' all rcpt, mail, etc calls.
  246. *
  247. * @return void
  248. */
  249. public function rset(){
  250. $this->_send('RSET');
  251. // MS ESMTP doesn't follow RFC, see [ZF-1377]
  252. $this->_expect(array(250, 220));
  253. $this->_mail = false;
  254. $this->_rcpt = false;
  255. $this->_data = false;
  256. }
  257. /**
  258. * Disconnects to the server.
  259. *
  260. * @return void
  261. */
  262. public function quit(){
  263. $this->_send('QUIT');
  264. $this->_expect(221, 300);
  265. $this->_connection = null;
  266. }
  267. /**
  268. * Authenticates to the server.
  269. * Currently supports the AUTH LOGIN command.
  270. * May be extended if more methods are needed.
  271. *
  272. * @throws SMTPException
  273. * @return void
  274. */
  275. protected function _auth(){
  276. if($this->_helo == false){
  277. throw new SMTPException(__('Must call EHLO (or HELO) before calling AUTH'));
  278. }
  279. else if($this->_auth !== false){
  280. throw new SMTPException(__('Can not call AUTH again.'));
  281. }
  282. $this->_send('AUTH LOGIN');
  283. $this->_expect(334);
  284. $this->_send(base64_encode($this->_user));
  285. $this->_expect(334);
  286. $this->_send(base64_encode($this->_pass));
  287. $this->_expect(235);
  288. $this->_auth = true;
  289. }
  290. /**
  291. * Calls the EHLO function.
  292. * This is the HELO function for more modern servers.
  293. *
  294. * @return void
  295. */
  296. protected function _ehlo(){
  297. $this->_send('EHLO [' . $this->_ip . ']');
  298. $this->_expect(array(250, 220), 300);
  299. }
  300. /**
  301. * Initiates the connection by calling the HELO function.
  302. * This function should only be used if the server does not support the HELO function.
  303. *
  304. * @return void
  305. */
  306. protected function _helo(){
  307. $this->_send('HELO [' . $this->_ip . ']');
  308. $this->_expect(array(250, 220), 300);
  309. }
  310. /**
  311. * Encrypts the current session with TLS.
  312. *
  313. * @return void
  314. */
  315. protected function _tls(){
  316. if ($this->_secure == 'tls') {
  317. $this->_send('STARTTLS');
  318. $this->_expect(220, 180);
  319. if (!stream_socket_enable_crypto($this->_connection, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
  320. throw new SMTPException(__('Unable to connect via TLS'));
  321. }
  322. $this->_ehlo();
  323. }
  324. }
  325. /**
  326. * Send a request to the host, appends the request with a line break.
  327. *
  328. * @param string $request
  329. * @return boolean|integer number of characters written.
  330. */
  331. protected function _send($request){
  332. $this->checkConnection();
  333. $result = fwrite($this->_connection, $request . "\r\n");
  334. if($result === false){
  335. throw new SMTPException(__('Could not send request: %s', array($request)));
  336. }
  337. return $result;
  338. }
  339. /**
  340. * Get a line from the stream.
  341. *
  342. * @param integer $timeout
  343. * Per-request timeout value if applicable. Defaults to null which
  344. * will not set a timeout.
  345. * @return string
  346. */
  347. protected function _receive($timeout = null) {
  348. $this->checkConnection();
  349. if ($timeout !== null) {
  350. stream_set_timeout($this->_connection, $timeout);
  351. }
  352. $response = fgets($this->_connection, 1024);
  353. $info = stream_get_meta_data($this->_connection);
  354. if (!empty($info['timed_out'])) {
  355. throw new SMTPException(__('%s has timed out', array($this->_host)));
  356. }
  357. else if ($response === false) {
  358. throw new SMTPException(__('Could not read from %s', array($this->_host)));
  359. }
  360. return $response;
  361. }
  362. /**
  363. * Parse server response for successful codes
  364. *
  365. * Read the response from the stream and check for expected return code.
  366. *
  367. * @throws SMTPException
  368. * @param string|array $code
  369. * One or more codes that indicate a successful response
  370. * @param integer $timeout
  371. * Per-request timeout value if applicable. Defaults to null which
  372. * will not set a timeout.
  373. * @return string
  374. * Last line of response string
  375. */
  376. protected function _expect($code, $timeout = null){
  377. $this->_response = array();
  378. $cmd = '';
  379. $more = '';
  380. $msg = '';
  381. $errMsg = '';
  382. if (!is_array($code)) {
  383. $code = array($code);
  384. }
  385. // Borrowed from the Zend Email Library
  386. do {
  387. $result = $this->_receive($timeout);
  388. list($cmd, $more, $msg) = preg_split('/([\s-]+)/', $result, 2, PREG_SPLIT_DELIM_CAPTURE);
  389. if ($errMsg !== '') {
  390. $errMsg .= ' ' . $msg;
  391. } elseif ($cmd === null || !in_array($cmd, $code)) {
  392. $errMsg = $msg;
  393. }
  394. } while (strpos($more, '-') === 0); // The '-' message prefix indicates an information string instead of a response string.
  395. if ($errMsg !== '') {
  396. throw new SMTPException($errMsg);
  397. }
  398. return $msg;
  399. }
  400. /**
  401. * Connect to the host, and perform basic functions like helo and auth.
  402. *
  403. * @throws SMTPException
  404. * @param string $host
  405. * @param integer $port
  406. * @return void
  407. */
  408. protected function _connect($host, $port){
  409. $errorNum = 0;
  410. $errorStr = '';
  411. $remoteAddr = $this->_transport . '://' . $host . ':' . $port;
  412. if(!is_resource($this->_connection)){
  413. $this->_connection = @stream_socket_client($remoteAddr, $errorNum, $errorStr, self::TIMEOUT);
  414. if($this->_connection === false){
  415. if($errorNum == 0){
  416. throw new SMTPException(__('Unable to open socket. Unknown error'));
  417. }
  418. else{
  419. throw new SMTPException(__('Unable to open socket. %s', array($errorStr)));
  420. }
  421. }
  422. if(@stream_set_timeout($this->_connection, self::TIMEOUT) === false){
  423. throw new SMTPException(__('Unable to set timeout.'));
  424. }
  425. $this->helo();
  426. if($this->_secure == 'tls'){
  427. $this->_tls();
  428. }
  429. if(($this->_user !== null) && ($this->_pass !== null)){
  430. $this->_auth();
  431. }
  432. }
  433. }
  434. }