PageRenderTime 48ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/pkp/classes/mail/SMTPMailer.inc.php

https://github.com/lib-uoguelph-ca/ocs
PHP | 381 lines | 227 code | 54 blank | 100 comment | 71 complexity | d53eecc56f8bf139621c353338579c38 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /**
  3. * @file classes/mail/SMTPMailer.inc.php
  4. *
  5. * Copyright (c) 2000-2012 John Willinsky
  6. * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING.
  7. *
  8. * @class SMTPMailer
  9. * @ingroup mail
  10. *
  11. * @brief Class defining a simple SMTP mail client (reference RFCs 821 and 2821).
  12. *
  13. * TODO: TLS support
  14. */
  15. // $Id$
  16. import('mail.Mail');
  17. class SMTPMailer {
  18. /** @var $server string SMTP server hostname (default localhost) */
  19. var $server;
  20. /** @var $port string SMTP server port (default 25) */
  21. var $port;
  22. /** @var $auth string Authentication mechanism (optional) (PLAIN | LOGIN | CRAM-MD5 | DIGEST-MD5) */
  23. var $auth;
  24. /** @var $username string Username for authentication (optional) */
  25. var $username;
  26. /** @var $password string Password for authentication (optional) */
  27. var $password;
  28. /** @var $socket int SMTP socket */
  29. var $socket;
  30. /**
  31. * Constructor.
  32. */
  33. function SMTPMailer() {
  34. $this->server = Config::getVar('email', 'smtp_server');
  35. $this->port = Config::getVar('email', 'smtp_port');
  36. $this->auth = Config::getVar('email', 'smtp_auth');
  37. $this->username = Config::getVar('email', 'smtp_username');
  38. $this->password = Config::getVar('email', 'smtp_password');
  39. if (!$this->server)
  40. $this->server = 'localhost';
  41. if (!$this->port)
  42. $this->port = 25;
  43. }
  44. /**
  45. * Send mail.
  46. * @param $mail Mailer
  47. * @param $recipients string
  48. * @param $subject string
  49. * @param $body string
  50. * @param $headers string
  51. */
  52. function mail(&$mail, $recipients, $subject, $body, $headers = '') {
  53. // Establish connection
  54. if (!$this->connect())
  55. return false;
  56. if (!$this->receive('220'))
  57. return $this->disconnect('Did not receive expected 220');
  58. // Send HELO/EHLO command
  59. $serverHost = preg_replace("/:\d*$/", '', Request::getServerHost());
  60. if (!$this->send($this->auth ? 'EHLO' : 'HELO', $serverHost))
  61. return $this->disconnect('Could not send HELO/HELO');
  62. if (!$this->receive('250'))
  63. return $this->disconnect('Did not receive expected 250 (1)');
  64. if ($this->auth) {
  65. // Perform authentication
  66. if (!$this->authenticate())
  67. return $this->disconnect('Could not authenticate');
  68. }
  69. // Send MAIL command
  70. $sender = $mail->getEnvelopeSender();
  71. if (!isset($sender) || empty($sender)) {
  72. $from = $mail->getFrom();
  73. if (isset($from['email']) && !empty($from['email']))
  74. $sender = $from['email'];
  75. else
  76. $sender = get_current_user() . '@' . $serverHost;
  77. }
  78. if (!$this->send('MAIL', 'FROM:<' . $sender . '>'))
  79. return $this->disconnect('Could not send sender');
  80. if (!$this->receive('250'))
  81. return $this->disconnect('Did not receive expected 250 (2)');
  82. // Send RCPT command(s)
  83. $rcpt = array();
  84. if (($addrs = $mail->getRecipients()) !== null)
  85. $rcpt = array_merge($rcpt, $addrs);
  86. if (($addrs = $mail->getCcs()) !== null)
  87. $rcpt = array_merge($rcpt, $addrs);
  88. if (($addrs = $mail->getBccs()) !== null)
  89. $rcpt = array_merge($rcpt, $addrs);
  90. foreach ($rcpt as $addr) {
  91. if (!$this->send('RCPT', 'TO:<' . $addr['email'] .'>'))
  92. return $this->disconnect('Could not send recipients');
  93. if (!$this->receive(array('250', '251')))
  94. return $this->disconnect('Did not receive expected 250 or 251');
  95. }
  96. // Send headers and body
  97. if (!$this->send('DATA'))
  98. return $this->disconnect('Could not send DATA');
  99. if (!$this->receive('354'))
  100. return $this->disconnect('Did not receive expected 354');
  101. if (!$this->send('To:', empty($recipients) ? 'undisclosed-recipients:;' : $recipients))
  102. return $this->disconnect('Could not send recipients (2)');
  103. if (!$this->send('Subject:', $subject))
  104. return $this->disconnect('Could not send subject');
  105. $lines = explode(MAIL_EOL, $headers);
  106. for ($i = 0, $num = count($lines); $i < $num; $i++) {
  107. if (preg_match('/^bcc:/i', $lines[$i]))
  108. continue;
  109. if (!$this->send($lines[$i]))
  110. return $this->disconnect('Could not send headers');
  111. }
  112. if (!$this->send(''))
  113. return $this->disconnect('Could not send CR');
  114. $lines = explode(MAIL_EOL, $body);
  115. for ($i = 0, $num = count($lines); $i < $num; $i++) {
  116. if (substr($lines[$i], 0, 1) == '.')
  117. $lines[$i] = '.' . $lines[$i];
  118. if (!$this->send($lines[$i]))
  119. return $this->disconnect('Could not send body');
  120. }
  121. // Mark end of data
  122. if (!$this->send('.'))
  123. return $this->disconnect('Could not send EOT');
  124. if (!$this->receive('250'))
  125. return $this->disconnect('Did not receive expected 250 (3)');
  126. // Tear down connection
  127. return $this->disconnect();
  128. }
  129. /**
  130. * Connect to the SMTP server.
  131. * @return boolean
  132. */
  133. function connect() {
  134. $this->socket = fsockopen($this->server, $this->port, $errno, $errstr, 30);
  135. if (!$this->socket)
  136. return false;
  137. return true;
  138. }
  139. /**
  140. * Disconnect from the SMTP server, sending a QUIT first.
  141. * @param $success boolean
  142. * @return boolean
  143. */
  144. function disconnect($error = '') {
  145. if (!$this->send('QUIT') || !$this->receive('221') && empty($error)) {
  146. $error = 'Unable to disconnect from mail server';
  147. }
  148. fclose($this->socket);
  149. if (!empty($error)) {
  150. error_log('OJS SMTPMailer: ' . $error);
  151. return false;
  152. }
  153. return true;
  154. }
  155. /**
  156. * Send a command/data.
  157. * @param $command string
  158. * @param $data string
  159. * @return boolean
  160. */
  161. function send($command, $data = '') {
  162. $ret = @fwrite($this->socket, $command . (empty($data) ? '' : ' ' . $data) . "\r\n");
  163. if ($ret !== false)
  164. return true;
  165. return false;
  166. }
  167. /**
  168. * Receive a response.
  169. * @param $expected string/array expected response code(s)
  170. * @return boolean
  171. */
  172. function receive($expected) {
  173. return $this->receiveData($expected, $data);
  174. }
  175. /**
  176. * Receive a response and return the data payload.
  177. * @param $expected string/array expected response code
  178. * @param $data string buffer
  179. * @return boolean
  180. */
  181. function receiveData($expected, &$data) {
  182. do {
  183. $line = @fgets($this->socket);
  184. } while($line !== false && substr($line, 3, 1) != ' ');
  185. if ($line !== false) {
  186. $response = substr($line, 0, 3);
  187. $data = substr($line, 4);
  188. if ((is_array($expected) && in_array($response, $expected)) || ($response === $expected))
  189. return true;
  190. }
  191. return false;
  192. }
  193. /**
  194. * Authenticate using the specified mechanism.
  195. * @return boolean
  196. */
  197. function authenticate() {
  198. switch (strtoupper($this->auth)) {
  199. case 'PLAIN':
  200. return $this->authenticate_plain();
  201. case 'LOGIN':
  202. return $this->authenticate_login();
  203. case 'CRAM-MD5':
  204. return $this->authenticate_cram_md5();
  205. case 'DIGEST-MD5':
  206. return $this->authenticate_digest_md5();
  207. default:
  208. return true;
  209. }
  210. }
  211. /**
  212. * Authenticate using PLAIN.
  213. * @return boolean
  214. */
  215. function authenticate_plain() {
  216. $authString = $this->username . chr(0x00) . $this->username . chr(0x00) . $this->password;
  217. if (!$this->send('AUTH', 'PLAIN ' . base64_encode($authString)))
  218. return false;
  219. return $this->receive('235');
  220. }
  221. /**
  222. * Authenticate using LOGIN.
  223. * @return boolean
  224. */
  225. function authenticate_login() {
  226. if (!$this->send('AUTH', 'LOGIN'))
  227. return false;
  228. if (!$this->receive('334'))
  229. return false;
  230. if (!$this->send(base64_encode($this->username)))
  231. return false;
  232. if (!$this->receive('334'))
  233. return false;
  234. if (!$this->send(base64_encode($this->password)))
  235. return false;
  236. return $this->receive('235');
  237. }
  238. /**
  239. * Authenticate using CRAM-MD5 (see RFC 2195).
  240. * @return boolean
  241. */
  242. function authenticate_cram_md5() {
  243. if (!$this->send('AUTH', 'CRAM-MD5'))
  244. return false;
  245. if (!$this->receiveData('334', $digest))
  246. return false;
  247. $authString = $this->username . ' ' . $this->hmac_md5(base64_decode($digest), $this->password);
  248. if (!$this->send(base64_encode($authString)))
  249. return false;
  250. return $this->receive('235');
  251. }
  252. /**
  253. * Authenticate using DIGEST-MD5 (see RFC 2831).
  254. * @return boolean
  255. */
  256. function authenticate_digest_md5() {
  257. if (!$this->send('AUTH', 'DIGEST-MD5'))
  258. return false;
  259. if (!$this->receiveData('334', $data))
  260. return false;
  261. // FIXME Make parser smarter to handle "unusual" and error cases
  262. $challenge = array();
  263. $data = base64_decode($data);
  264. while(!empty($data)) {
  265. @list($key, $rest) = explode('=', $data, 2);
  266. if ($rest[0] != '"') {
  267. @list($value, $data) = explode(',', $rest, 2);
  268. } else {
  269. @list($value, $data) = explode('"', substr($rest, 1), 2);
  270. $data = substr($data, 1);
  271. }
  272. if (!empty($value))
  273. $challenge[$key] = $value;
  274. }
  275. $realms = explode(',', $challenge['realm']);
  276. if (empty($realms))
  277. $realm = $this->server;
  278. else
  279. $realm = $realms[0]; // FIXME Multiple realms
  280. $qop = 'auth';
  281. $nc = '00000001';
  282. $uri = 'smtp/' . $this->server;
  283. $cnonce = md5(uniqid(mt_rand(), true));
  284. $a1 = pack('H*', md5($this->username . ':' . $realm . ':' . $this->password)) . ':' . $challenge['nonce'] . ':' . $cnonce;
  285. // FIXME authorization ID not supported
  286. if (isset($authzid))
  287. $a1 .= ':' . $authzid;
  288. $a2 = 'AUTHENTICATE:' . $uri;
  289. // FIXME 'auth-int' and 'auth-conf' not supported
  290. if ($qop == 'auth-int' || $qop == 'auth-int')
  291. $a2 .= ':00000000000000000000000000000000';
  292. $response = md5(md5($a1) . ':' . ($challenge['nonce'] . ':' . $nc . ':' . $cnonce. ':' . $qop . ':' . md5($a2)));
  293. $authString = sprintf('charset=utf-8,username="%s",realm="%s",nonce="%s",nc=%s,cnonce="%s",digest-uri="%s",response=%s,qop=%s', $this->username, $realm, $challenge['nonce'], $nc, $cnonce, $uri, $response, $qop);
  294. if (!$this->send(base64_encode($authString)))
  295. return false;
  296. if (!$this->receive('334'))
  297. return false;
  298. if (!$this->send(''))
  299. return false;
  300. return $this->receive('235');
  301. }
  302. /**
  303. * Generic HMAC digest computation (see RFC 2104).
  304. * @param $hashfn string e.g., 'md5' or 'sha1'
  305. * @param $blocksize int
  306. * @param $data string
  307. * @param $key string
  308. * @return string (as hex)
  309. */
  310. function hmac($hashfn, $blocksize, $data, $key) {
  311. if (strlen($key) > $blocksize)
  312. $key = pack('H*', $hashfn($key));
  313. $key = str_pad($key, $blocksize, chr(0x00));
  314. $ipad = str_repeat(chr(0x36), $blocksize);
  315. $opad = str_repeat(chr(0x5C), $blocksize);
  316. $hmac = pack('H*', $hashfn(($key ^ $opad) . pack('H*', $hashfn(($key ^ $ipad) . $data))));
  317. return bin2hex($hmac);
  318. }
  319. /**
  320. * Compute HMAC-MD5 digest.
  321. * @return string (as hex)
  322. */
  323. function hmac_md5($data, $key = '') {
  324. return $this->hmac('md5', 64, $data, $key);
  325. }
  326. }
  327. ?>