PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/trunk/squirrelmail/class/deliver/Deliver_SMTP.class.php

#
PHP | 541 lines | 376 code | 41 blank | 124 comment | 84 complexity | c122d694ac242232862b271b3ad42e1e MD5 | raw file
Possible License(s): AGPL-1.0, GPL-2.0
  1. <?php
  2. /**
  3. * Deliver_SMTP.class.php
  4. *
  5. * SMTP delivery backend for the Deliver class.
  6. *
  7. * @copyright 1999-2012 The SquirrelMail Project Team
  8. * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  9. * @version $Id: Deliver_SMTP.class.php 14249 2012-01-02 02:09:17Z pdontthink $
  10. * @package squirrelmail
  11. */
  12. /** @ignore */
  13. if (!defined('SM_PATH')) define('SM_PATH','../../');
  14. /** This of course depends upon Deliver */
  15. include_once(SM_PATH . 'class/deliver/Deliver.class.php');
  16. /**
  17. * Deliver messages using SMTP
  18. * @package squirrelmail
  19. */
  20. class Deliver_SMTP extends Deliver {
  21. /**
  22. * Array keys are uppercased ehlo keywords
  23. * array key values are ehlo params. If ehlo-param contains space, it is splitted into array.
  24. * @var array ehlo
  25. * @since 1.5.1
  26. */
  27. var $ehlo = array();
  28. /**
  29. * @var string domain
  30. * @since 1.5.1
  31. */
  32. var $domain = '';
  33. /**
  34. * SMTP STARTTLS rfc: "Both the client and the server MUST know if there
  35. * is a TLS session active."
  36. * Variable should be set to true, when encryption is turned on.
  37. * @var boolean
  38. * @since 1.5.1
  39. */
  40. var $tls_enabled = false;
  41. /**
  42. * Private var
  43. * var stream $stream
  44. * @todo don't pass stream resource in class method arguments.
  45. */
  46. //var $stream = false;
  47. /** @var string delivery error message */
  48. var $dlv_msg = '';
  49. /** @var integer delivery error number from server */
  50. var $dlv_ret_nr = '';
  51. /** @var string delivery error message from server */
  52. var $dlv_server_msg = '';
  53. function preWriteToStream(&$s) {
  54. if ($s) {
  55. if ($s{0} == '.') $s = '.' . $s;
  56. $s = str_replace("\n.","\n..",$s);
  57. }
  58. }
  59. function initStream($message, $domain, $length=0, $host='', $port='', $user='', $pass='', $authpop=false, $pop_host='') {
  60. global $use_smtp_tls,$smtp_auth_mech;
  61. if ($authpop) {
  62. $this->authPop($pop_host, '', $user, $pass);
  63. }
  64. $rfc822_header = $message->rfc822_header;
  65. $from = $rfc822_header->from[0];
  66. $to = $rfc822_header->to;
  67. $cc = $rfc822_header->cc;
  68. $bcc = $rfc822_header->bcc;
  69. $content_type = $rfc822_header->content_type;
  70. // MAIL FROM: <from address> MUST be empty in cae of MDN (RFC2298)
  71. if ($content_type->type0 == 'multipart' &&
  72. $content_type->type1 == 'report' &&
  73. isset($content_type->properties['report-type']) &&
  74. $content_type->properties['report-type']=='disposition-notification') {
  75. // reinitialize the from object because otherwise the from header somehow
  76. // is affected. This $from var is used for smtp command MAIL FROM which
  77. // is not the same as what we put in the rfc822 header.
  78. $from = new AddressStructure();
  79. $from->host = '';
  80. $from->mailbox = '';
  81. }
  82. if ($use_smtp_tls == 1) {
  83. if ((check_php_version(4,3)) && (extension_loaded('openssl'))) {
  84. $stream = @fsockopen('tls://' . $host, $port, $errorNumber, $errorString);
  85. $this->tls_enabled = true;
  86. } else {
  87. /**
  88. * don't connect to server when user asks for smtps and
  89. * PHP does not support it.
  90. */
  91. $errorNumber = '';
  92. $errorString = _("Secure SMTP (TLS) is enabled in SquirrelMail configuration, but used PHP version does not support it.");
  93. }
  94. } else {
  95. $stream = @fsockopen($host, $port, $errorNumber, $errorString);
  96. }
  97. if (!$stream) {
  98. // reset tls state var to default value, if connection fails
  99. $this->tls_enabled = false;
  100. // set error messages
  101. $this->dlv_msg = $errorString;
  102. $this->dlv_ret_nr = $errorNumber;
  103. $this->dlv_server_msg = _("Can't open SMTP stream.");
  104. return(0);
  105. }
  106. // get server greeting
  107. $tmp = fgets($stream, 1024);
  108. if ($this->errorCheck($tmp, $stream)) {
  109. return(0);
  110. }
  111. /*
  112. * If $_SERVER['HTTP_HOST'] is set, use that in our HELO to the SMTP
  113. * server. This should fix the DNS issues some people have had
  114. */
  115. if (sqgetGlobalVar('HTTP_HOST', $HTTP_HOST, SQ_SERVER)) { // HTTP_HOST is set
  116. // optionally trim off port number
  117. if($p = strrpos($HTTP_HOST, ':')) {
  118. $HTTP_HOST = substr($HTTP_HOST, 0, $p);
  119. }
  120. $helohost = $HTTP_HOST;
  121. } else { // For some reason, HTTP_HOST is not set - revert to old behavior
  122. $helohost = $domain;
  123. }
  124. // if the host is an IPv4 address, enclose it in brackets
  125. //
  126. if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $helohost))
  127. $helohost = '[' . $helohost . ']';
  128. /* Lets introduce ourselves */
  129. fputs($stream, "EHLO $helohost\r\n");
  130. // Read ehlo response
  131. $tmp = $this->parse_ehlo_response($stream);
  132. if ($this->errorCheck($tmp,$stream)) {
  133. // fall back to HELO if EHLO is not supported (error 5xx)
  134. if ($this->dlv_ret_nr{0} == '5') {
  135. fputs($stream, "HELO $helohost\r\n");
  136. $tmp = fgets($stream,1024);
  137. if ($this->errorCheck($tmp,$stream)) {
  138. return(0);
  139. }
  140. } else {
  141. return(0);
  142. }
  143. }
  144. /**
  145. * Implementing SMTP STARTTLS (rfc2487) in php 5.1.0+
  146. * http://www.php.net/stream-socket-enable-crypto
  147. */
  148. if ($use_smtp_tls === 2) {
  149. if (function_exists('stream_socket_enable_crypto')) {
  150. // don't try starting tls, when client thinks that it is already active
  151. if ($this->tls_enabled) {
  152. $this->dlv_msg = _("TLS session is already activated.");
  153. return 0;
  154. } elseif (!array_key_exists('STARTTLS',$this->ehlo)) {
  155. // check for starttls in ehlo response
  156. $this->dlv_msg = _("SMTP STARTTLS is enabled in SquirrelMail configuration, but used SMTP server does not support it");
  157. return 0;
  158. }
  159. // issue starttls command
  160. fputs($stream, "STARTTLS\r\n");
  161. // get response
  162. $tmp = fgets($stream,1024);
  163. if ($this->errorCheck($tmp,$stream)) {
  164. return 0;
  165. }
  166. // start crypto on connection. suppress function errors.
  167. if (@stream_socket_enable_crypto($stream,true,STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
  168. // starttls was successful (rfc2487 5.2 Result of the STARTTLS Command)
  169. // get new EHLO response
  170. fputs($stream, "EHLO $helohost\r\n");
  171. // Read ehlo response
  172. $tmp = $this->parse_ehlo_response($stream);
  173. if ($this->errorCheck($tmp,$stream)) {
  174. // don't revert to helo here. server must support ESMTP
  175. return 0;
  176. }
  177. // set information about started tls
  178. $this->tls_enabled = true;
  179. } else {
  180. /**
  181. * stream_socket_enable_crypto() call failed.
  182. */
  183. $this->dlv_msg = _("Unable to start TLS.");
  184. return 0;
  185. // Bug: can't get error message. See comments in sqimap_create_stream().
  186. }
  187. } else {
  188. // php install does not support stream_socket_enable_crypto() function
  189. $this->dlv_msg = _("SMTP STARTTLS is enabled in SquirrelMail configuration, but used PHP version does not support functions that allow to enable encryption on open socket.");
  190. return 0;
  191. }
  192. }
  193. // FIXME: check ehlo response before using authentication
  194. // Try authentication by a plugin
  195. //
  196. // NOTE: there is another hook in functions/auth.php called "smtp_auth"
  197. // that allows a plugin to specify a different set of login credentials
  198. // (so is slightly mis-named, but is too old to change), so be careful
  199. // that you do not confuse your hook names.
  200. //
  201. $smtp_auth_args = array(
  202. 'auth_mech' => $smtp_auth_mech,
  203. 'user' => $user,
  204. 'pass' => $pass,
  205. 'host' => $host,
  206. 'port' => $port,
  207. 'stream' => $stream,
  208. );
  209. if (boolean_hook_function('smtp_authenticate', $smtp_auth_args, 1)) {
  210. // authentication succeeded
  211. } else if (( $smtp_auth_mech == 'cram-md5') or ( $smtp_auth_mech == 'digest-md5' )) {
  212. // Doing some form of non-plain auth
  213. if ($smtp_auth_mech == 'cram-md5') {
  214. fputs($stream, "AUTH CRAM-MD5\r\n");
  215. } elseif ($smtp_auth_mech == 'digest-md5') {
  216. fputs($stream, "AUTH DIGEST-MD5\r\n");
  217. }
  218. $tmp = fgets($stream,1024);
  219. if ($this->errorCheck($tmp,$stream)) {
  220. return(0);
  221. }
  222. // At this point, $tmp should hold "334 <challenge string>"
  223. $chall = substr($tmp,4);
  224. // Depending on mechanism, generate response string
  225. if ($smtp_auth_mech == 'cram-md5') {
  226. $response = cram_md5_response($user,$pass,$chall);
  227. } elseif ($smtp_auth_mech == 'digest-md5') {
  228. $response = digest_md5_response($user,$pass,$chall,'smtp',$host);
  229. }
  230. fputs($stream, $response);
  231. // Let's see what the server had to say about that
  232. $tmp = fgets($stream,1024);
  233. if ($this->errorCheck($tmp,$stream)) {
  234. return(0);
  235. }
  236. // CRAM-MD5 is done at this point. If DIGEST-MD5, there's a bit more to go
  237. if ($smtp_auth_mech == 'digest-md5') {
  238. // $tmp contains rspauth, but I don't store that yet. (No need yet)
  239. fputs($stream,"\r\n");
  240. $tmp = fgets($stream,1024);
  241. if ($this->errorCheck($tmp,$stream)) {
  242. return(0);
  243. }
  244. }
  245. // CRAM-MD5 and DIGEST-MD5 code ends here
  246. } elseif ($smtp_auth_mech == 'none') {
  247. // No auth at all, just send helo and then send the mail
  248. // We already said hi earlier, nothing more is needed.
  249. } elseif ($smtp_auth_mech == 'login') {
  250. // The LOGIN method
  251. fputs($stream, "AUTH LOGIN\r\n");
  252. $tmp = fgets($stream, 1024);
  253. if ($this->errorCheck($tmp, $stream)) {
  254. return(0);
  255. }
  256. fputs($stream, base64_encode ($user) . "\r\n");
  257. $tmp = fgets($stream, 1024);
  258. if ($this->errorCheck($tmp, $stream)) {
  259. return(0);
  260. }
  261. fputs($stream, base64_encode($pass) . "\r\n");
  262. $tmp = fgets($stream, 1024);
  263. if ($this->errorCheck($tmp, $stream)) {
  264. return(0);
  265. }
  266. } elseif ($smtp_auth_mech == "plain") {
  267. /* SASL Plain */
  268. $auth = base64_encode("$user\0$user\0$pass");
  269. $query = "AUTH PLAIN\r\n";
  270. fputs($stream, $query);
  271. $read=fgets($stream, 1024);
  272. if (substr($read,0,3) == '334') { // OK so far..
  273. fputs($stream, "$auth\r\n");
  274. $read = fgets($stream, 1024);
  275. }
  276. $results=explode(" ",$read,3);
  277. $response=$results[1];
  278. $message=$results[2];
  279. } else {
  280. /* Right here, they've reached an unsupported auth mechanism.
  281. This is the ugliest hack I've ever done, but it'll do till I can fix
  282. things up better tomorrow. So tired... */
  283. if ($this->errorCheck("535 Unable to use this auth type",$stream)) {
  284. return(0);
  285. }
  286. }
  287. /* Ok, who is sending the message? */
  288. $fromaddress = (strlen($from->mailbox) && $from->host) ?
  289. $from->mailbox.'@'.$from->host : '';
  290. fputs($stream, 'MAIL FROM:<'.$fromaddress.">\r\n");
  291. $tmp = fgets($stream, 1024);
  292. if ($this->errorCheck($tmp, $stream)) {
  293. return(0);
  294. }
  295. /* send who the recipients are */
  296. for ($i = 0, $cnt = count($to); $i < $cnt; $i++) {
  297. if (!$to[$i]->host) $to[$i]->host = $domain;
  298. if (strlen($to[$i]->mailbox)) {
  299. fputs($stream, 'RCPT TO:<'.$to[$i]->mailbox.'@'.$to[$i]->host.">\r\n");
  300. $tmp = fgets($stream, 1024);
  301. if ($this->errorCheck($tmp, $stream)) {
  302. return(0);
  303. }
  304. }
  305. }
  306. for ($i = 0, $cnt = count($cc); $i < $cnt; $i++) {
  307. if (!$cc[$i]->host) $cc[$i]->host = $domain;
  308. if (strlen($cc[$i]->mailbox)) {
  309. fputs($stream, 'RCPT TO:<'.$cc[$i]->mailbox.'@'.$cc[$i]->host.">\r\n");
  310. $tmp = fgets($stream, 1024);
  311. if ($this->errorCheck($tmp, $stream)) {
  312. return(0);
  313. }
  314. }
  315. }
  316. for ($i = 0, $cnt = count($bcc); $i < $cnt; $i++) {
  317. if (!$bcc[$i]->host) $bcc[$i]->host = $domain;
  318. if (strlen($bcc[$i]->mailbox)) {
  319. fputs($stream, 'RCPT TO:<'.$bcc[$i]->mailbox.'@'.$bcc[$i]->host.">\r\n");
  320. $tmp = fgets($stream, 1024);
  321. if ($this->errorCheck($tmp, $stream)) {
  322. return(0);
  323. }
  324. }
  325. }
  326. /* Lets start sending the actual message */
  327. fputs($stream, "DATA\r\n");
  328. $tmp = fgets($stream, 1024);
  329. if ($this->errorCheck($tmp, $stream)) {
  330. return(0);
  331. }
  332. return $stream;
  333. }
  334. function finalizeStream($stream) {
  335. fputs($stream, "\r\n.\r\n"); /* end the DATA part */
  336. $tmp = fgets($stream, 1024);
  337. $this->errorCheck($tmp, $stream);
  338. if ($this->dlv_ret_nr != 250) {
  339. return(0);
  340. }
  341. fputs($stream, "QUIT\r\n"); /* log off */
  342. fclose($stream);
  343. return true;
  344. }
  345. /* check if an SMTP reply is an error and set an error message) */
  346. function errorCheck($line, $smtpConnection) {
  347. $err_num = substr($line, 0, 3);
  348. $this->dlv_ret_nr = $err_num;
  349. $server_msg = substr($line, 4);
  350. while(substr($line, 0, 4) == ($err_num.'-')) {
  351. $line = fgets($smtpConnection, 1024);
  352. $server_msg .= substr($line, 4);
  353. }
  354. if ( ((int) $err_num{0}) < 4) {
  355. return false;
  356. }
  357. switch ($err_num) {
  358. case '421': $message = _("Service not available, closing channel");
  359. break;
  360. case '432': $message = _("A password transition is needed");
  361. break;
  362. case '450': $message = _("Requested mail action not taken: mailbox unavailable");
  363. break;
  364. case '451': $message = _("Requested action aborted: error in processing");
  365. break;
  366. case '452': $message = _("Requested action not taken: insufficient system storage");
  367. break;
  368. case '454': $message = _("Temporary authentication failure");
  369. break;
  370. case '500': $message = _("Syntax error; command not recognized");
  371. break;
  372. case '501': $message = _("Syntax error in parameters or arguments");
  373. break;
  374. case '502': $message = _("Command not implemented");
  375. break;
  376. case '503': $message = _("Bad sequence of commands");
  377. break;
  378. case '504': $message = _("Command parameter not implemented");
  379. break;
  380. case '530': $message = _("Authentication required");
  381. break;
  382. case '534': $message = _("Authentication mechanism is too weak");
  383. break;
  384. case '535': $message = _("Authentication failed");
  385. break;
  386. case '538': $message = _("Encryption required for requested authentication mechanism");
  387. break;
  388. case '550': $message = _("Requested action not taken: mailbox unavailable");
  389. break;
  390. case '551': $message = _("User not local; please try forwarding");
  391. break;
  392. case '552': $message = _("Requested mail action aborted: exceeding storage allocation");
  393. break;
  394. case '553': $message = _("Requested action not taken: mailbox name not allowed");
  395. break;
  396. case '554': $message = _("Transaction failed");
  397. break;
  398. default: $message = _("Unknown response");
  399. break;
  400. }
  401. $this->dlv_msg = $message;
  402. $this->dlv_server_msg = $server_msg;
  403. return true;
  404. }
  405. function authPop($pop_server='', $pop_port='', $user, $pass) {
  406. if (!$pop_port) {
  407. $pop_port = 110;
  408. }
  409. if (!$pop_server) {
  410. $pop_server = 'localhost';
  411. }
  412. $popConnection = @fsockopen($pop_server, $pop_port, $err_no, $err_str);
  413. if (!$popConnection) {
  414. error_log("Error connecting to POP Server ($pop_server:$pop_port)"
  415. . " $err_no : $err_str");
  416. return false;
  417. } else {
  418. $tmp = fgets($popConnection, 1024); /* banner */
  419. if (substr($tmp, 0, 3) != '+OK') {
  420. return false;
  421. }
  422. fputs($popConnection, "USER $user\r\n");
  423. $tmp = fgets($popConnection, 1024);
  424. if (substr($tmp, 0, 3) != '+OK') {
  425. return false;
  426. }
  427. fputs($popConnection, 'PASS ' . $pass . "\r\n");
  428. $tmp = fgets($popConnection, 1024);
  429. if (substr($tmp, 0, 3) != '+OK') {
  430. return false;
  431. }
  432. fputs($popConnection, "QUIT\r\n"); /* log off */
  433. fclose($popConnection);
  434. }
  435. return true;
  436. }
  437. /**
  438. * Parses ESMTP EHLO response (rfc1869)
  439. *
  440. * Reads SMTP response to EHLO command and fills class variables
  441. * (ehlo array and domain string). Returns last line.
  442. * @param stream $stream smtp connection stream.
  443. * @return string last ehlo line
  444. * @since 1.5.1
  445. */
  446. function parse_ehlo_response($stream) {
  447. // don't cache ehlo information
  448. $this->ehlo=array();
  449. $ret = '';
  450. $firstline = true;
  451. /**
  452. * ehlo mailclient.example.org
  453. * 250-mail.example.org
  454. * 250-PIPELINING
  455. * 250-SIZE 52428800
  456. * 250-DATAZ
  457. * 250-STARTTLS
  458. * 250-AUTH LOGIN PLAIN
  459. * 250 8BITMIME
  460. */
  461. while ($line=fgets($stream, 1024)){
  462. // match[1] = first symbol after 250
  463. // match[2] = domain or ehlo-keyword
  464. // match[3] = greeting or ehlo-param
  465. // match space after keyword in ehlo-keyword CR LF
  466. if (preg_match("/^250(-|\s)(\S*)\s+(\S.*)\r\n/",$line,$match)||
  467. preg_match("/^250(-|\s)(\S*)\s*\r\n/",$line,$match)) {
  468. if ($firstline) {
  469. // first ehlo line (250[-\ ]domain SP greeting)
  470. $this->domain = $match[2];
  471. $firstline=false;
  472. } elseif (!isset($match[3])) {
  473. // simple one word extension
  474. $this->ehlo[strtoupper($match[2])]='';
  475. } elseif (!preg_match("/\s/",trim($match[3]))) {
  476. // extension with one option
  477. // yes, I know about ctype extension. no, i don't want to depend on it
  478. $this->ehlo[strtoupper($match[2])]=trim($match[3]);
  479. } else {
  480. // ehlo-param with spaces
  481. $this->ehlo[strtoupper($match[2])]=explode(' ',trim($match[3]));
  482. }
  483. if ($match[1]==' ') {
  484. // stop while cycle, if we reach last 250 line
  485. $ret = $line;
  486. break;
  487. }
  488. } else {
  489. // this is not 250 response
  490. $ret = $line;
  491. break;
  492. }
  493. }
  494. return $ret;
  495. }
  496. }