PageRenderTime 66ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/base/lib/flourishlib/fEmail.php

https://bitbucket.org/thanhtungnguyenphp/monitos
PHP | 1764 lines | 1290 code | 126 blank | 348 comment | 70 complexity | 1954aa3b4ec659e9c90da60ea2ac5e30 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Allows creating and sending a single email containing plaintext, HTML, attachments and S/MIME encryption
  4. *
  5. * Please note that this class uses the [http://php.net/function.mail mail()]
  6. * function by default. Developers that are sending multiple emails, or need
  7. * SMTP support, should use fSMTP with this class.
  8. *
  9. * This class is implemented to use the UTF-8 character encoding. Please see
  10. * http://flourishlib.com/docs/UTF-8 for more information.
  11. *
  12. * @copyright Copyright (c) 2008-2011 Will Bond, others
  13. * @author Will Bond [wb] <will@flourishlib.com>
  14. * @author Bill Bushee, iMarc LLC [bb-imarc] <bill@imarc.net>
  15. * @license http://flourishlib.com/license
  16. *
  17. * @package Flourish
  18. * @link http://flourishlib.com/fEmail
  19. *
  20. * @version 1.0.0b27
  21. * @changes 1.0.0b27 Fixed a bug with generating FQDNs on some Windows machines [wb, 2011-02-24]
  22. * @changes 1.0.0b26 Added ::addCustomerHeader() [wb, 2011-02-02]
  23. * @changes 1.0.0b25 Fixed a bug with finding the FQDN on non-Windows machines [wb, 2011-01-19]
  24. * @changes 1.0.0b24 Backwards Compatibility Break - the `$contents` parameter of ::addAttachment() is now first instead of third, ::addAttachment() will now accept fFile objects for the `$contents` parameter, added ::addRelatedFile() [wb, 2010-12-01]
  25. * @changes 1.0.0b23 Fixed a bug on Windows where emails starting with a `.` would have the `.` removed [wb, 2010-09-11]
  26. * @changes 1.0.0b22 Revamped the FQDN code and added ::getFQDN() [wb, 2010-09-07]
  27. * @changes 1.0.0b21 Added a check to prevent permissions warnings when getting the FQDN on Windows machines [wb, 2010-09-02]
  28. * @changes 1.0.0b20 Fixed ::send() to only remove the name of a recipient when dealing with the `mail()` function on Windows and to leave it when using fSMTP [wb, 2010-06-22]
  29. * @changes 1.0.0b19 Changed ::send() to return the message id for the email, fixed the email regexes to require [] around IPs [wb, 2010-05-05]
  30. * @changes 1.0.0b18 Fixed the name of the static method ::unindentExpand() [wb, 2010-04-28]
  31. * @changes 1.0.0b17 Added the static method ::unindentExpand() [wb, 2010-04-26]
  32. * @changes 1.0.0b16 Added support for sending emails via fSMTP [wb, 2010-04-20]
  33. * @changes 1.0.0b15 Added the `$unindent_expand_constants` parameter to ::setBody(), added ::loadBody() and ::loadHTMLBody(), fixed HTML emails with attachments [wb, 2010-03-14]
  34. * @changes 1.0.0b14 Changed ::send() to not double `.`s at the beginning of lines on Windows since it seemed to break things rather than fix them [wb, 2010-03-05]
  35. * @changes 1.0.0b13 Fixed the class to work when safe mode is turned on [wb, 2009-10-23]
  36. * @changes 1.0.0b12 Removed duplicate MIME-Version headers that were being included in S/MIME encrypted emails [wb, 2009-10-05]
  37. * @changes 1.0.0b11 Updated to use the new fValidationException API [wb, 2009-09-17]
  38. * @changes 1.0.0b10 Fixed a bug with sending both an HTML and a plaintext body [bb-imarc, 2009-06-18]
  39. * @changes 1.0.0b9 Fixed a bug where the MIME headers were not being set for all emails [wb, 2009-06-12]
  40. * @changes 1.0.0b8 Added the method ::clearRecipients() [wb, 2009-05-29]
  41. * @changes 1.0.0b7 Email names with UTF-8 characters are now properly encoded [wb, 2009-05-08]
  42. * @changes 1.0.0b6 Fixed a bug where <> quoted email addresses in validation messages were not showing [wb, 2009-03-27]
  43. * @changes 1.0.0b5 Updated for new fCore API [wb, 2009-02-16]
  44. * @changes 1.0.0b4 The recipient error message in ::validate() no longer contains a typo [wb, 2009-02-09]
  45. * @changes 1.0.0b3 Fixed a bug with missing content in the fValidationException thrown by ::validate() [wb, 2009-01-14]
  46. * @changes 1.0.0b2 Fixed a few bugs with sending S/MIME encrypted/signed emails [wb, 2009-01-10]
  47. * @changes 1.0.0b The initial implementation [wb, 2008-06-23]
  48. */
  49. class fEmail
  50. {
  51. // The following constants allow for nice looking callbacks to static methods
  52. const fixQmail = 'fEmail::fixQmail';
  53. const getFQDN = 'fEmail::getFQDN';
  54. const reset = 'fEmail::reset';
  55. const unindentExpand = 'fEmail::unindentExpand';
  56. /**
  57. * A regular expression to match an email address, exluding those with comments and folding whitespace
  58. *
  59. * The matches will be:
  60. *
  61. * - `[0]`: The whole email address
  62. * - `[1]`: The name before the `@`
  63. * - `[2]`: The domain/ip after the `@`
  64. *
  65. * @var string
  66. */
  67. const EMAIL_REGEX = '~^[ \t]*( # Allow leading whitespace
  68. (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+") # An "atom" or a quoted string
  69. (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))* # A . plus another "atom" or a quoted string, any number of times
  70. )@( # The @ symbol
  71. (?:[a-z0-9\\-]+\.)+[a-z]{2,}| # Domain name
  72. \[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\] # (or) IP addresses
  73. )[ \t]*$~ixD'; # Allow Trailing whitespace
  74. /**
  75. * A regular expression to match a `name <email>` string, exluding those with comments and folding whitespace
  76. *
  77. * The matches will be:
  78. *
  79. * - `[0]`: The whole name and email address
  80. * - `[1]`: The name
  81. * - `[2]`: The whole email address
  82. * - `[3]`: The email username before the `@`
  83. * - `[4]`: The email domain/ip after the `@`
  84. *
  85. * @var string
  86. */
  87. const NAME_EMAIL_REGEX = '~^[ \t]*( # Allow leading whitespace
  88. (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*) # An "atom" or a quoted string
  89. (?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*))*) # Another "atom" or a quoted string or a . followed by one of those, any number of times
  90. [ \t]*<[ \t]*(( # The < encapsulating the email address
  91. (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+") # An "atom" or a quoted string
  92. (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))* # A . plus another "atom" or a quoted string, any number of times
  93. )@( # The @ symbol
  94. (?:[a-z0-9\\-]+\.)+[a-z]{2,}| # Domain nam
  95. \[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\] # (or) IP addresses
  96. ))[ \t]*>[ \t]*$~ixD'; # Closing > and trailing whitespace
  97. /**
  98. * Flags if the class should convert `\r\n` to `\n` for qmail. This makes invalid email headers that may work.
  99. *
  100. * @var boolean
  101. */
  102. static private $convert_crlf = FALSE;
  103. /**
  104. * The local fully-qualified domain name
  105. */
  106. static private $fqdn;
  107. /**
  108. * Flags if the class should use [http://php.net/popen popen()] to send mail via sendmail
  109. *
  110. * @var boolean
  111. */
  112. static private $popen_sendmail = FALSE;
  113. /**
  114. * Composes text using fText if loaded
  115. *
  116. * @param string $message The message to compose
  117. * @param mixed $component A string or number to insert into the message
  118. * @param mixed ...
  119. * @return string The composed and possible translated message
  120. */
  121. static protected function compose($message)
  122. {
  123. $args = array_slice(func_get_args(), 1);
  124. if (class_exists('fText', FALSE)) {
  125. return call_user_func_array(
  126. array('fText', 'compose'),
  127. array($message, $args)
  128. );
  129. } else {
  130. return vsprintf($message, $args);
  131. }
  132. }
  133. /**
  134. * Sets the class to try and fix broken qmail implementations that add `\r` to `\r\n`
  135. *
  136. * Before trying to fix qmail with this method, please try using fSMTP
  137. * to connect to `localhost` and pass the fSMTP object to ::send().
  138. *
  139. * @return void
  140. */
  141. static public function fixQmail()
  142. {
  143. if (fCore::checkOS('windows')) {
  144. return;
  145. }
  146. $sendmail_command = ini_get('sendmail_path');
  147. if (!$sendmail_command) {
  148. self::$convert_crlf = TRUE;
  149. trigger_error(
  150. self::compose('The proper fix for sending through qmail is not possible since the sendmail path is not set'),
  151. E_USER_WARNING
  152. );
  153. trigger_error(
  154. self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
  155. E_USER_WARNING
  156. );
  157. }
  158. $sendmail_command_parts = explode(' ', $sendmail_command, 2);
  159. $sendmail_path = $sendmail_command_parts[0];
  160. $sendmail_dir = pathinfo($sendmail_path, PATHINFO_DIRNAME);
  161. $sendmail_params = (isset($sendmail_command_parts[1])) ? $sendmail_command_parts[1] : '';
  162. // Check to see if we can run sendmail via popen
  163. $executable = FALSE;
  164. $safe_mode = FALSE;
  165. if (!in_array(strtolower(ini_get('safe_mode')), array('0', '', 'off'))) {
  166. $safe_mode = TRUE;
  167. $exec_dirs = explode(';', ini_get('safe_mode_exec_dir'));
  168. foreach ($exec_dirs as $exec_dir) {
  169. if (stripos($sendmail_dir, $exec_dir) !== 0) {
  170. continue;
  171. }
  172. if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
  173. $executable = TRUE;
  174. }
  175. }
  176. } else {
  177. if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
  178. $executable = TRUE;
  179. }
  180. }
  181. if ($executable) {
  182. self::$popen_sendmail = TRUE;
  183. } else {
  184. self::$convert_crlf = TRUE;
  185. if ($safe_mode) {
  186. trigger_error(
  187. self::compose('The proper fix for sending through qmail is not possible since safe mode is turned on and the sendmail binary is not in one of the paths defined by the safe_mode_exec_dir ini setting'),
  188. E_USER_WARNING
  189. );
  190. trigger_error(
  191. self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
  192. E_USER_WARNING
  193. );
  194. } else {
  195. trigger_error(
  196. self::compose('The proper fix for sending through qmail is not possible since the sendmail binary could not be found or is not executable'),
  197. E_USER_WARNING
  198. );
  199. trigger_error(
  200. self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
  201. E_USER_WARNING
  202. );
  203. }
  204. }
  205. }
  206. /**
  207. * Returns the fully-qualified domain name of the server
  208. *
  209. * @internal
  210. *
  211. * @return string The fully-qualified domain name of the server
  212. */
  213. static public function getFQDN()
  214. {
  215. if (self::$fqdn !== NULL) {
  216. return self::$fqdn;
  217. }
  218. if (isset($_ENV['HOST'])) {
  219. self::$fqdn = $_ENV['HOST'];
  220. }
  221. if (strpos(self::$fqdn, '.') === FALSE && isset($_ENV['HOSTNAME'])) {
  222. self::$fqdn = $_ENV['HOSTNAME'];
  223. }
  224. if (strpos(self::$fqdn, '.') === FALSE) {
  225. self::$fqdn = php_uname('n');
  226. }
  227. if (strpos(self::$fqdn, '.') === FALSE) {
  228. $can_exec = !in_array('exec', array_map('trim', explode(',', ini_get('disable_functions')))) && !ini_get('safe_mode');
  229. if (fCore::checkOS('linux') && $can_exec) {
  230. self::$fqdn = trim(shell_exec('hostname --fqdn'));
  231. } elseif (fCore::checkOS('windows')) {
  232. $shell = new COM('WScript.Shell');
  233. $tcpip_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip';
  234. try {
  235. $domain = $shell->RegRead($tcpip_key . '\Parameters\NV Domain');
  236. } catch (com_exception $e) {
  237. try {
  238. $domain = $shell->RegRead($tcpip_key . '\Parameters\DhcpDomain');
  239. } catch (com_exception $e) {
  240. try {
  241. $adapters = $shell->RegRead($tcpip_key . '\Linkage\Route');
  242. foreach ($adapters as $adapter) {
  243. if ($adapter[0] != '{') { continue; }
  244. try {
  245. $domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\Domain');
  246. } catch (com_exception $e) {
  247. try {
  248. $domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\DhcpDomain');
  249. } catch (com_exception $e) { }
  250. }
  251. }
  252. } catch (com_exception $e) { }
  253. }
  254. }
  255. if (!empty($domain)) {
  256. self::$fqdn .= '.' . $domain;
  257. }
  258. } elseif (!fCore::checkOS('windows') && !ini_get('open_basedir') && file_exists('/etc/resolv.conf')) {
  259. $output = file_get_contents('/etc/resolv.conf');
  260. if (preg_match('#^domain ([a-z0-9_.-]+)#im', $output, $match)) {
  261. self::$fqdn .= '.' . $match[1];
  262. }
  263. }
  264. }
  265. return self::$fqdn;
  266. }
  267. /**
  268. * Resets the configuration of the class
  269. *
  270. * @internal
  271. *
  272. * @return void
  273. */
  274. static public function reset()
  275. {
  276. self::$convert_crlf = FALSE;
  277. self::$fqdn = NULL;
  278. self::$popen_sendmail = FALSE;
  279. }
  280. /**
  281. * Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
  282. *
  283. * @param mixed $value The value to check
  284. * @return boolean If the value is string-like
  285. */
  286. static protected function stringlike($value)
  287. {
  288. if ((!is_string($value) && !is_object($value) && !is_numeric($value)) || !strlen(trim($value))) {
  289. return FALSE;
  290. }
  291. return TRUE;
  292. }
  293. /**
  294. * Takes a block of text, unindents it and replaces {CONSTANT} tokens with the constant's value
  295. *
  296. * @param string $text The text to unindent and replace constants in
  297. * @return string The unindented text
  298. */
  299. static public function unindentExpand($text)
  300. {
  301. $text = preg_replace('#^[ \t]*\n|\n[ \t]*$#D', '', $text);
  302. if (preg_match('#^[ \t]+(?=\S)#m', $text, $match)) {
  303. $text = preg_replace('#^' . preg_quote($match[0]) . '#m', '', $text);
  304. }
  305. preg_match_all('#\{([a-z][a-z0-9_]*)\}#i', $text, $constants, PREG_SET_ORDER);
  306. foreach ($constants as $constant) {
  307. if (!defined($constant[1])) { continue; }
  308. $text = preg_replace('#' . preg_quote($constant[0], '#') . '#', constant($constant[1]), $text, 1);
  309. }
  310. return $text;
  311. }
  312. /**
  313. * The file contents to attach
  314. *
  315. * @var array
  316. */
  317. private $attachments = array();
  318. /**
  319. * The email address(es) to BCC to
  320. *
  321. * @var array
  322. */
  323. private $bcc_emails = array();
  324. /**
  325. * The email address to bounce to
  326. *
  327. * @var string
  328. */
  329. private $bounce_to_email = NULL;
  330. /**
  331. * The email address(es) to CC to
  332. *
  333. * @var array
  334. */
  335. private $cc_emails = array();
  336. /**
  337. * Custom headers
  338. *
  339. * @var array
  340. */
  341. private $custom_headers = array();
  342. /**
  343. * The email address being sent from
  344. *
  345. * @var string
  346. */
  347. private $from_email = NULL;
  348. /**
  349. * The HTML body of the email
  350. *
  351. * @var string
  352. */
  353. private $html_body = NULL;
  354. /**
  355. * The Message-ID header for the email
  356. *
  357. * @var string
  358. */
  359. private $message_id = NULL;
  360. /**
  361. * The plaintext body of the email
  362. *
  363. * @var string
  364. */
  365. private $plaintext_body = NULL;
  366. /**
  367. * The recipient's S/MIME PEM certificate filename, used for encryption of the message
  368. *
  369. * @var string
  370. */
  371. private $recipients_smime_cert_file = NULL;
  372. /**
  373. * The files to include as multipart/related
  374. *
  375. * @var array
  376. */
  377. private $related_files = array();
  378. /**
  379. * The email address to reply to
  380. *
  381. * @var string
  382. */
  383. private $reply_to_email = NULL;
  384. /**
  385. * The email address actually sending the email
  386. *
  387. * @var string
  388. */
  389. private $sender_email = NULL;
  390. /**
  391. * The senders's S/MIME PEM certificate filename, used for singing the message
  392. *
  393. * @var string
  394. */
  395. private $senders_smime_cert_file = NULL;
  396. /**
  397. * The senders's S/MIME private key filename, used for singing the message
  398. *
  399. * @var string
  400. */
  401. private $senders_smime_pk_file = NULL;
  402. /**
  403. * The senders's S/MIME private key password, used for singing the message
  404. *
  405. * @var string
  406. */
  407. private $senders_smime_pk_password = NULL;
  408. /**
  409. * If the message should be encrypted using the recipient's S/MIME certificate
  410. *
  411. * @var boolean
  412. */
  413. private $smime_encrypt = FALSE;
  414. /**
  415. * If the message should be signed using the senders's S/MIME private key
  416. *
  417. * @var boolean
  418. */
  419. private $smime_sign = FALSE;
  420. /**
  421. * The subject of the email
  422. *
  423. * @var string
  424. */
  425. private $subject = NULL;
  426. /**
  427. * The email address(es) to send to
  428. *
  429. * @var array
  430. */
  431. private $to_emails = array();
  432. /**
  433. * Initializes fEmail for creating message ids
  434. *
  435. * @return fEmail
  436. */
  437. public function __construct()
  438. {
  439. $this->message_id = '<' . fCryptography::randomString(10, 'hexadecimal') . '.' . time() . '@' . self::getFQDN() . '>';
  440. }
  441. /**
  442. * All requests that hit this method should be requests for callbacks
  443. *
  444. * @internal
  445. *
  446. * @param string $method The method to create a callback for
  447. * @return callback The callback for the method requested
  448. */
  449. public function __get($method)
  450. {
  451. return array($this, $method);
  452. }
  453. /**
  454. * Adds an attachment to the email
  455. *
  456. * If a duplicate filename is detected, it will be changed to be unique.
  457. *
  458. * @param string|fFile $contents The contents of the file
  459. * @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
  460. * @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
  461. * @return void
  462. */
  463. public function addAttachment($contents, $filename=NULL, $mime_type=NULL)
  464. {
  465. $this->extrapolateFileInfo($contents, $filename, $mime_type);
  466. while (isset($this->attachments[$filename])) {
  467. $this->generateNewFilename($filename);
  468. }
  469. $this->attachments[$filename] = array(
  470. 'mime-type' => $mime_type,
  471. 'contents' => $contents
  472. );
  473. }
  474. /**
  475. * Adds a “related” file to the email, returning the `Content-ID` for use in HTML
  476. *
  477. * The purpose of a related file is to be able to reference it in part of
  478. * the HTML body. Image `src` URLs can reference a related file by starting
  479. * the URL with `cid:` and then inserting the `Content-ID`.
  480. *
  481. * If a duplicate filename is detected, it will be changed to be unique.
  482. *
  483. * @param string|fFile $contents The contents of the file
  484. * @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
  485. * @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
  486. * @return string The fully-formed `cid:` URL for use in HTML `src` attributes
  487. */
  488. public function addRelatedFile($contents, $filename=NULL, $mime_type=NULL)
  489. {
  490. $this->extrapolateFileInfo($contents, $filename, $mime_type);
  491. while (isset($this->related_files[$filename])) {
  492. $this->generateNewFilename($filename);
  493. }
  494. $cid = count($this->related_files) . '.' . substr($this->message_id, 1, -1);
  495. $this->related_files[$filename] = array(
  496. 'mime-type' => $mime_type,
  497. 'contents' => $contents,
  498. 'content-id' => '<' . $cid . '>'
  499. );
  500. return 'cid:' . $cid;
  501. }
  502. /**
  503. * Adds a blind carbon copy (BCC) email recipient
  504. *
  505. * @param string $email The email address to BCC
  506. * @param string $name The recipient's name
  507. * @return void
  508. */
  509. public function addBCCRecipient($email, $name=NULL)
  510. {
  511. if (!$email) {
  512. return;
  513. }
  514. $this->bcc_emails[] = $this->combineNameEmail($name, $email);
  515. }
  516. /**
  517. * Adds a carbon copy (CC) email recipient
  518. *
  519. * @param string $email The email address to BCC
  520. * @param string $name The recipient's name
  521. * @return void
  522. */
  523. public function addCCRecipient($email, $name=NULL)
  524. {
  525. if (!$email) {
  526. return;
  527. }
  528. $this->cc_emails[] = $this->combineNameEmail($name, $email);
  529. }
  530. /**
  531. * Allows adding a custom header to the email
  532. *
  533. * If the method is called multiple times with the same name, the last
  534. * value will be used.
  535. *
  536. * Please note that this class will properly format the header, including
  537. * adding the `:` between the name and value and wrapping values that are
  538. * too long for a single line.
  539. *
  540. * @param string $name The name of the header
  541. * @param string $value The value of the header
  542. * @param array :$headers An associative array of `{name} => {value}`
  543. * @return void
  544. */
  545. public function addCustomHeader($name, $value=NULL)
  546. {
  547. if ($value === NULL && is_array($name)) {
  548. foreach ($name as $key => $value) {
  549. $this->addCustomHeader($key, $value);
  550. }
  551. return;
  552. }
  553. $lower_name = fUTF8::lower($name);
  554. $this->custom_headers[$lower_name] = array($name, $value);
  555. }
  556. /**
  557. * Adds an email recipient
  558. *
  559. * @param string $email The email address to send to
  560. * @param string $name The recipient's name
  561. * @return void
  562. */
  563. public function addRecipient($email, $name=NULL)
  564. {
  565. if (!$email) {
  566. return;
  567. }
  568. $this->to_emails[] = $this->combineNameEmail($name, $email);
  569. }
  570. /**
  571. * Takes a multi-address email header and builds it out using an array of emails
  572. *
  573. * @param string $header The header name without `': '`, the header is non-blank, `': '` will be added
  574. * @param array $emails The email addresses for the header
  575. * @return string The email header with a trailing `\r\n`
  576. */
  577. private function buildMultiAddressHeader($header, $emails)
  578. {
  579. $header .= ': ';
  580. $first = TRUE;
  581. $line = 1;
  582. foreach ($emails as $email) {
  583. if ($first) { $first = FALSE; } else { $header .= ', '; }
  584. // Try to stay within the recommended 78 character line limit
  585. $last_crlf_pos = (integer) strrpos($header, "\r\n");
  586. if (strlen($header . $email) - $last_crlf_pos > 78) {
  587. $header .= "\r\n ";
  588. $line++;
  589. }
  590. $header .= trim($email);
  591. }
  592. return $header . "\r\n";
  593. }
  594. /**
  595. * Removes all To, CC and BCC recipients from the email
  596. *
  597. * @return void
  598. */
  599. public function clearRecipients()
  600. {
  601. $this->to_emails = array();
  602. $this->cc_emails = array();
  603. $this->bcc_emails = array();
  604. }
  605. /**
  606. * Creates a 32-character boundary for a multipart message
  607. *
  608. * @return string A multipart boundary
  609. */
  610. private function createBoundary()
  611. {
  612. $chars = 'ancdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:-_';
  613. $last_index = strlen($chars) - 1;
  614. $output = '';
  615. for ($i = 0; $i < 28; $i++) {
  616. $output .= $chars[rand(0, $last_index)];
  617. }
  618. return $output;
  619. }
  620. /**
  621. * Turns a name and email into a `"name" <email>` string, or just `email` if no name is provided
  622. *
  623. * This method will remove newline characters from the name and email, and
  624. * will remove any backslash (`\`) and double quote (`"`) characters from
  625. * the name.
  626. *
  627. * @param string $name The name associated with the email address
  628. * @param string $email The email address
  629. * @return string The '"name" <email>' or 'email' string
  630. */
  631. private function combineNameEmail($name, $email)
  632. {
  633. // Strip lower ascii character since they aren't useful in email addresses
  634. $email = preg_replace('#[\x0-\x19]+#', '', $email);
  635. $name = preg_replace('#[\x0-\x19]+#', '', $name);
  636. if (!$name) {
  637. return $email;
  638. }
  639. // If the name contains any non-ascii bytes or stuff not allowed
  640. // in quoted strings we just make an encoded word out of it
  641. if (preg_replace('#[\x80-\xff\x5C\x22]#', '', $name) != $name) {
  642. // The longest header name that will contain email addresses is
  643. // "Bcc: ", which is 5 characters long
  644. $name = $this->makeEncodedWord($name, 5);
  645. } else {
  646. $name = '"' . $name . '"';
  647. }
  648. return $name . ' <' . $email . '>';
  649. }
  650. /**
  651. * Builds the body of the email
  652. *
  653. * @param string $boundary The boundary to use for the top level mime block
  654. * @return string The message body to be sent to the mail() function
  655. */
  656. private function createBody($boundary)
  657. {
  658. $boundary_stack = array($boundary);
  659. $mime_notice = self::compose(
  660. "This message has been formatted using MIME. It does not appear that your\r\nemail client supports MIME."
  661. );
  662. $body = '';
  663. if ($this->html_body || $this->attachments) {
  664. $body .= $mime_notice . "\r\n\r\n";
  665. }
  666. if ($this->html_body && $this->related_files && $this->attachments) {
  667. $body .= '--' . end($boundary_stack) . "\r\n";
  668. $boundary_stack[] = $this->createBoundary();
  669. $body .= 'Content-Type: multipart/related; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
  670. }
  671. if ($this->html_body && ($this->attachments || $this->related_files)) {
  672. $body .= '--' . end($boundary_stack) . "\r\n";
  673. $boundary_stack[] = $this->createBoundary();
  674. $body .= 'Content-Type: multipart/alternative; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
  675. }
  676. if ($this->html_body || $this->attachments) {
  677. $body .= '--' . end($boundary_stack) . "\r\n";
  678. $body .= "Content-Type: text/plain; charset=utf-8\r\n";
  679. $body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
  680. }
  681. $body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";
  682. if ($this->html_body) {
  683. $body .= '--' . end($boundary_stack) . "\r\n";
  684. $body .= "Content-Type: text/html; charset=utf-8\r\n";
  685. $body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
  686. $body .= $this->makeQuotedPrintable($this->html_body) . "\r\n";
  687. }
  688. if ($this->related_files) {
  689. $body .= '--' . end($boundary_stack) . "--\r\n";
  690. array_pop($boundary_stack);
  691. foreach ($this->related_files as $filename => $file_info) {
  692. $body .= '--' . end($boundary_stack) . "\r\n";
  693. $body .= 'Content-Type: ' . $file_info['mime-type'] . '; name="' . $filename . "\"\r\n";
  694. $body .= "Content-Transfer-Encoding: base64\r\n";
  695. $body .= 'Content-ID: ' . $file_info['content-id'] . "\r\n\r\n";
  696. $body .= $this->makeBase64($file_info['contents']) . "\r\n";
  697. }
  698. }
  699. if ($this->attachments) {
  700. if ($this->html_body) {
  701. $body .= '--' . end($boundary_stack) . "--\r\n";
  702. array_pop($boundary_stack);
  703. }
  704. foreach ($this->attachments as $filename => $file_info) {
  705. $body .= '--' . end($boundary_stack) . "\r\n";
  706. $body .= 'Content-Type: ' . $file_info['mime-type'] . "\r\n";
  707. $body .= "Content-Transfer-Encoding: base64\r\n";
  708. $body .= 'Content-Disposition: attachment; filename="' . $filename . "\";\r\n\r\n";
  709. $body .= $this->makeBase64($file_info['contents']) . "\r\n";
  710. }
  711. }
  712. if ($this->html_body || $this->attachments) {
  713. $body .= '--' . end($boundary_stack) . "--\r\n";
  714. array_pop($boundary_stack);
  715. }
  716. return $body;
  717. }
  718. /**
  719. * Builds the headers for the email
  720. *
  721. * @param string $boundary The boundary to use for the top level mime block
  722. * @param string $message_id The message id for the message
  723. * @return string The headers to be sent to the [http://php.net/function.mail mail()] function
  724. */
  725. private function createHeaders($boundary, $message_id)
  726. {
  727. $headers = '';
  728. if ($this->cc_emails) {
  729. $headers .= $this->buildMultiAddressHeader("Cc", $this->cc_emails);
  730. }
  731. if ($this->bcc_emails) {
  732. $headers .= $this->buildMultiAddressHeader("Bcc", $this->bcc_emails);
  733. }
  734. $headers .= "From: " . trim($this->from_email) . "\r\n";
  735. if ($this->reply_to_email) {
  736. $headers .= "Reply-To: " . trim($this->reply_to_email) . "\r\n";
  737. }
  738. if ($this->sender_email) {
  739. $headers .= "Sender: " . trim($this->sender_email) . "\r\n";
  740. }
  741. foreach ($this->custom_headers as $header_info) {
  742. $header_prefix = $header_info[0] . ': ';
  743. $headers .= $header_prefix . $this->makeEncodedWord($header_info[1], strlen($header_prefix)) . "\r\n";
  744. }
  745. $headers .= "Message-ID: " . $message_id . "\r\n";
  746. $headers .= "MIME-Version: 1.0\r\n";
  747. if (!$this->html_body && !$this->attachments) {
  748. $headers .= "Content-Type: text/plain; charset=utf-8\r\n";
  749. $headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
  750. } elseif ($this->html_body && !$this->attachments) {
  751. if ($this->related_files) {
  752. $headers .= 'Content-Type: multipart/related; boundary="' . $boundary . "\"\r\n";
  753. } else {
  754. $headers .= 'Content-Type: multipart/alternative; boundary="' . $boundary . "\"\r\n";
  755. }
  756. } elseif ($this->attachments) {
  757. $headers .= 'Content-Type: multipart/mixed; boundary="' . $boundary . "\"\r\n";
  758. }
  759. return $headers . "\r\n";
  760. }
  761. /**
  762. * Takes the body of the message and processes it with S/MIME
  763. *
  764. * @param string $to The recipients being sent to
  765. * @param string $subject The subject of the email
  766. * @param string $headers The headers for the message
  767. * @param string $body The message body
  768. * @return array `0` => The message headers, `1` => The message body
  769. */
  770. private function createSMIMEBody($to, $subject, $headers, $body)
  771. {
  772. if (!$this->smime_encrypt && !$this->smime_sign) {
  773. return array($headers, $body);
  774. }
  775. $plaintext_file = tempnam('', '__fEmail_');
  776. $ciphertext_file = tempnam('', '__fEmail_');
  777. $headers_array = array(
  778. 'To' => $to,
  779. 'Subject' => $subject
  780. );
  781. preg_match_all('#^([\w\-]+):\s+([^\n]+\n( [^\n]+\n)*)#im', $headers, $header_matches, PREG_SET_ORDER);
  782. foreach ($header_matches as $header_match) {
  783. $headers_array[$header_match[1]] = trim($header_match[2]);
  784. }
  785. $body_headers = "";
  786. if (isset($headers_array['Content-Type'])) {
  787. $body_headers .= 'Content-Type: ' . $headers_array['Content-Type'] . "\r\n";
  788. }
  789. if (isset($headers_array['Content-Transfer-Encoding'])) {
  790. $body_headers .= 'Content-Transfer-Encoding: ' . $headers_array['Content-Transfer-Encoding'] . "\r\n";
  791. }
  792. if ($body_headers) {
  793. $body = $body_headers . "\r\n" . $body;
  794. }
  795. file_put_contents($plaintext_file, $body);
  796. file_put_contents($ciphertext_file, '');
  797. // Set up the neccessary S/MIME resources
  798. if ($this->smime_sign) {
  799. $senders_smime_cert = file_get_contents($this->senders_smime_cert_file);
  800. $senders_private_key = openssl_pkey_get_private(
  801. file_get_contents($this->senders_smime_pk_file),
  802. $this->senders_smime_pk_password
  803. );
  804. if ($senders_private_key === FALSE) {
  805. throw new fValidationException(
  806. "The sender's S/MIME private key password specified does not appear to be valid for the private key"
  807. );
  808. }
  809. }
  810. if ($this->smime_encrypt) {
  811. $recipients_smime_cert = file_get_contents($this->recipients_smime_cert_file);
  812. }
  813. // If we are going to sign and encrypt, the best way is to sign, encrypt and then sign again
  814. if ($this->smime_encrypt && $this->smime_sign) {
  815. openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, array());
  816. openssl_pkcs7_encrypt($ciphertext_file, $plaintext_file, $recipients_smime_cert, array(), NULL, OPENSSL_CIPHER_RC2_128);
  817. openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
  818. } elseif ($this->smime_sign) {
  819. openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
  820. } elseif ($this->smime_encrypt) {
  821. openssl_pkcs7_encrypt($plaintext_file, $ciphertext_file, $recipients_smime_cert, $headers_array, NULL, OPENSSL_CIPHER_RC2_128);
  822. }
  823. // It seems that the contents of the ciphertext is not always \r\n line breaks
  824. $message = file_get_contents($ciphertext_file);
  825. $message = str_replace("\r\n", "\n", $message);
  826. $message = str_replace("\r", "\n", $message);
  827. $message = str_replace("\n", "\r\n", $message);
  828. list($new_headers, $new_body) = explode("\r\n\r\n", $message, 2);
  829. $new_headers = preg_replace('#^To:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
  830. $new_headers = preg_replace('#^Subject:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
  831. $new_headers = preg_replace("#^MIME-Version: 1.0\r?\n#mi", '', $new_headers, 1);
  832. $new_headers = preg_replace('#^Content-Type:\s+' . preg_quote($headers_array['Content-Type'], '#') . "\r?\n#mi", '', $new_headers);
  833. $new_headers = preg_replace('#^Content-Transfer-Encoding:\s+' . preg_quote($headers_array['Content-Transfer-Encoding'], '#') . "\r?\n#mi", '', $new_headers);
  834. unlink($plaintext_file);
  835. unlink($ciphertext_file);
  836. if ($this->smime_sign) {
  837. openssl_pkey_free($senders_private_key);
  838. }
  839. return array($new_headers, $new_body);
  840. }
  841. /**
  842. * Sets the email to be encrypted with S/MIME
  843. *
  844. * @param string $recipients_smime_cert_file The file path to the PEM-encoded S/MIME certificate for the recipient
  845. * @return void
  846. */
  847. public function encrypt($recipients_smime_cert_file)
  848. {
  849. if (!extension_loaded('openssl')) {
  850. throw new fEnvironmentException(
  851. 'S/MIME encryption was requested for an email, but the %s extension is not installed',
  852. 'openssl'
  853. );
  854. }
  855. if (!self::stringlike($recipients_smime_cert_file)) {
  856. throw new fProgrammerException(
  857. "The recipient's S/MIME certificate filename specified, %s, does not appear to be a valid filename",
  858. $recipients_smime_cert_file
  859. );
  860. }
  861. $this->smime_encrypt = TRUE;
  862. $this->recipients_smime_cert_file = $recipients_smime_cert_file;
  863. }
  864. /**
  865. * Extracts just the email addresses from an array of strings containing an
  866. * <email@address.com> or "Name" <email@address.com> combination.
  867. *
  868. * @param array $list The list of email or name/email to extract from
  869. * @return array The email addresses
  870. */
  871. private function extractEmails($list)
  872. {
  873. $output = array();
  874. foreach ($list as $email) {
  875. if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
  876. $output[] = $match[2];
  877. } else {
  878. preg_match(self::EMAIL_REGEX, $email, $match);
  879. $output[] = $match[0];
  880. }
  881. }
  882. return $output;
  883. }
  884. /**
  885. * Extracts the filename and mime-type from an fFile object
  886. *
  887. * @param string|fFile &$contents The file to extrapolate the info from
  888. * @param string &$filename The filename to use for the file
  889. * @param string &$mime_type The mime type of the file
  890. * @return void
  891. */
  892. private function extrapolateFileInfo(&$contents, &$filename, &$mime_type)
  893. {
  894. if ($contents instanceof fFile) {
  895. if ($filename === NULL) {
  896. $filename = $contents->getName();
  897. }
  898. if ($mime_type === NULL) {
  899. $mime_type = $contents->getMimeType();
  900. }
  901. $contents = $contents->read();
  902. } else {
  903. if (!self::stringlike($filename)) {
  904. throw new fProgrammerException(
  905. 'The filename specified, %s, does not appear to be a valid filename',
  906. $filename
  907. );
  908. }
  909. $filename = (string) $filename;
  910. if ($mime_type === NULL) {
  911. $mime_type = fFile::determineMimeType($filename, $contents);
  912. }
  913. }
  914. }
  915. /**
  916. * Generates a new filename in an attempt to create a unique name
  917. *
  918. * @param string $filename The filename to generate another name for
  919. * @return string The newly generated filename
  920. */
  921. private function generateNewFilename($filename)
  922. {
  923. $filename_info = fFilesystem::getPathInfo($filename);
  924. if (preg_match('#_copy(\d+)($|\.)#D', $filename_info['filename'], $match)) {
  925. $i = $match[1] + 1;
  926. } else {
  927. $i = 1;
  928. }
  929. $extension = ($filename_info['extension']) ? '.' . $filename_info['extension'] : '';
  930. return preg_replace('#_copy\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension;
  931. }
  932. /**
  933. * Loads the plaintext version of the email body from a file and applies replacements
  934. *
  935. * The should contain either ASCII or UTF-8 encoded text. Please see
  936. * http://flourishlib.com/docs/UTF-8 for more information.
  937. *
  938. * @throws fValidationException When no file was specified, the file does not exist or the path specified is not a file
  939. *
  940. * @param string|fFile $file The plaintext version of the email body
  941. * @param array $replacements The method will search the contents of the file for each key and replace it with the corresponding value
  942. * @return void
  943. */
  944. public function loadBody($file, $replacements=array())
  945. {
  946. if (!$file instanceof fFile) {
  947. $file = new fFile($file);
  948. }
  949. $plaintext = $file->read();
  950. if ($replacements) {
  951. $plaintext = strtr($plaintext, $replacements);
  952. }
  953. $this->plaintext_body = $plaintext;
  954. }
  955. /**
  956. * Loads the plaintext version of the email body from a file and applies replacements
  957. *
  958. * The should contain either ASCII or UTF-8 encoded text. Please see
  959. * http://flourishlib.com/docs/UTF-8 for more information.
  960. *
  961. * @throws fValidationException When no file was specified, the file does not exist or the path specified is not a file
  962. *
  963. * @param string|fFile $file The plaintext version of the email body
  964. * @param array $replacements The method will search the contents of the file for each key and replace it with the corresponding value
  965. * @return void
  966. */
  967. public function loadHTMLBody($file, $replacements=array())
  968. {
  969. if (!$file instanceof fFile) {
  970. $file = new fFile($file);
  971. }
  972. $html = $file->read();
  973. if ($replacements) {
  974. $html = strtr($html, $replacements);
  975. }
  976. $this->html_body = $html;
  977. }
  978. /**
  979. * Encodes a string to base64
  980. *
  981. * @param string $content The content to encode
  982. * @return string The encoded string
  983. */
  984. private function makeBase64($content)
  985. {
  986. return chunk_split(base64_encode($content));
  987. }
  988. /**
  989. * Encodes a string to UTF-8 encoded-word
  990. *
  991. * @param string $content The content to encode
  992. * @param integer $first_line_prefix_length The length of any prefix applied to the first line of the encoded word - this allows properly accounting for a header name
  993. * @return string The encoded string
  994. */
  995. private function makeEncodedWord($content, $first_line_prefix_length)
  996. {
  997. // Homogenize the line-endings to CRLF
  998. $content = str_replace("\r\n", "\n", $content);
  999. $content = str_replace("\r", "\n", $content);
  1000. $content = str_replace("\n", "\r\n", $content);
  1001. // Encoded word is not required if all characters are ascii
  1002. if (!preg_match('#[\x80-\xFF]#', $content)) {
  1003. return $content;
  1004. }
  1005. // A quick a dirty hex encoding
  1006. $content = rawurlencode($content);
  1007. $content = str_replace('=', '%3D', $content);
  1008. $content = str_replace('%', '=', $content);
  1009. // Decode characters that don't have to be coded
  1010. $decodings = array(
  1011. '=20' => '_', '=21' => '!', '=22' => '"', '=23' => '#',
  1012. '=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
  1013. '=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
  1014. '=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
  1015. '=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
  1016. '=40' => '@', '=5B' => '[', '=5C' => '\\', '=5D' => ']',
  1017. '=5E' => '^', '=60' => '`', '=7B' => '{', '=7C' => '|',
  1018. '=7D' => '}', '=7E' => '~', ' ' => '_'
  1019. );
  1020. $content = strtr($content, $decodings);
  1021. $length = strlen($content);
  1022. $prefix = '=?utf-8?Q?';
  1023. $suffix = '?=';
  1024. $prefix_length = 10;
  1025. $suffix_length = 2;
  1026. // This loop goes through and ensures we are wrapping by 75 chars
  1027. // including the encoded word delimiters
  1028. $output = $prefix;
  1029. $line_length = $prefix_length + $first_line_prefix_length;
  1030. for ($i=0; $i<$length; $i++) {
  1031. // Get info about the next character
  1032. $char_length = ($content[$i] == '=') ? 3 : 1;
  1033. $char = $content[$i];
  1034. if ($char_length == 3) {
  1035. $char .= $content[$i+1] . $content[$i+2];
  1036. }
  1037. // If we have too long a line, wrap it
  1038. if ($line_length + $suffix_length + $char_length > 75) {
  1039. $output .= $suffix . "\r\n " . $prefix;
  1040. $line_length = $prefix_length + 2;
  1041. }
  1042. // Add the character
  1043. $output .= $char;
  1044. // Figure out how much longer the line is
  1045. $line_length += $char_length;
  1046. // Skip characters if we have an encoded character
  1047. $i += $char_length-1;
  1048. }
  1049. if (substr($output, -2) != $suffix) {
  1050. $output .= $suffix;
  1051. }
  1052. return $output;
  1053. }
  1054. /**
  1055. * Encodes a string to quoted-printable, properly handles UTF-8
  1056. *
  1057. * @param string $content The content to encode
  1058. * @return string The encoded string
  1059. */
  1060. private function makeQuotedPrintable($content)
  1061. {
  1062. // Homogenize the line-endings to CRLF
  1063. $content = str_replace("\r\n", "\n", $content);
  1064. $content = str_replace("\r", "\n", $content);
  1065. $content = str_replace("\n", "\r\n", $content);
  1066. // A quick a dirty hex encoding
  1067. $content = rawurlencode($content);
  1068. $content = str_replace('=', '%3D', $content);
  1069. $content = str_replace('%', '=', $content);
  1070. // Decode characters that don't have to be coded
  1071. $decodings = array(
  1072. '=20' => ' ', '=21' => '!', '=22' => '"', '=23' => '#',
  1073. '=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
  1074. '=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
  1075. '=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
  1076. '=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
  1077. '=3F' => '?', '=40' => '@', '=5B' => '[', '=5C' => '\\',
  1078. '=5D' => ']', '=5E' => '^', '=5F' => '_', '=60' => '`',
  1079. '=7B' => '{', '=7C' => '|', '=7D' => '}', '=7E' => '~'
  1080. );
  1081. $content = strtr($content, $decodings);
  1082. $output = '';
  1083. $length = strlen($content);
  1084. // This loop goes through and ensures we are wrapping by 76 chars
  1085. $line_length = 0;
  1086. for ($i=0; $i<$length; $i++) {
  1087. // Get info about the next character
  1088. $char_length = ($content[$i] == '=') ? 3 : 1;
  1089. $char = $content[$i];
  1090. if ($char_length == 3) {
  1091. $char .= $content[$i+1] . $content[$i+2];
  1092. }
  1093. // Skip characters if we have an encoded character, this must be
  1094. // done before checking for whitespace at the beginning and end of
  1095. // lines or else characters in the content will be skipped
  1096. $i += $char_length-1;
  1097. // Spaces and tabs at the beginning and ending of lines have to be encoded
  1098. $begining_or_end = $line_length > 69 || $line_length == 0;
  1099. $tab_or_space = $char == ' ' || $char == "\t";
  1100. if ($begining_or_end && $tab_or_space) {
  1101. $char_length = 3;
  1102. $char = ($char == ' ') ? '=20' : '=09';
  1103. }
  1104. // If we have too long a line, wrap it
  1105. if ($char != "\r" && $char != "\n" && $line_length + $char_length > 75) {
  1106. $output .= "=\r\n";
  1107. $line_length = 0;
  1108. }
  1109. // Add the character
  1110. $output .= $char;
  1111. // Figure out how much longer the line is now
  1112. if ($char == "\r" || $char == "\n") {
  1113. $line_length = 0;
  1114. } else {
  1115. $line_length += $char_length;
  1116. }
  1117. }
  1118. return $output;
  1119. }
  1120. /**
  1121. * Sends the email
  1122. *
  1123. * The return value is the message id, which should be included as the
  1124. * `Message-ID` header of the email. While almost all SMTP servers will not
  1125. * modify this value, testing has indicated at least one (smtp.live.com
  1126. * for Windows Live Mail) does.
  1127. *
  1128. * @throws fValidationException When ::validate() throws an exception
  1129. *
  1130. * @param fSMTP $connection The SMTP connection to send the message over
  1131. * @return string The message id for the message - see method description for details
  1132. */
  1133. public function send($connection=NULL)
  1134. {
  1135. $this->validate();
  1136. // The mail() function on Windows doesn't support names in headers so
  1137. // we must strip them down to just the email address
  1138. if ($connection === NULL && fCore::checkOS('windows')) {
  1139. $vars = array('bcc_emails', 'bounce_to_email', 'cc_emails', 'from_email', 'reply_to_email', 'sender_email', 'to_emails');
  1140. foreach ($vars as $var) {
  1141. if (!is_array($this->$var)) {
  1142. if (preg_match(self::NAME_EMAIL_REGEX, $this->$var, $match)) {
  1143. $this->$var = $match[2];
  1144. }
  1145. } else {
  1146. $new_emails = array();
  1147. foreach ($this->$var as $email) {
  1148. if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
  1149. $email = $match[2];
  1150. }
  1151. $new_emails[] = $email;
  1152. }
  1153. $this->$var = $new_emails;
  1154. }
  1155. }
  1156. }
  1157. $to = substr(trim($this->buildMultiAddressHeader("To", $this->to_emails)), 4);
  1158. $top_level_boundary = $this->createBoundary();
  1159. $headers = $this->createHeaders($top_level_boundary, $this->message_id);
  1160. $subject = str_replace(array("\r", "\n"), '', $this->subject);
  1161. $subject = $this->makeEncodedWord($subject, 9);
  1162. $body = $this->createBody($top_level_boundary);
  1163. if ($this->smime_encrypt || $this->smime_sign) {
  1164. list($headers, $body) = $this->createSMIMEBody($to, $subject, $headers, $body);
  1165. }
  1166. // Remove extra line breaks
  1167. $headers = trim($headers);
  1168. $body = trim($body);
  1169. if ($connection) {
  1170. $to_emails = $this->extractEmails($this->to_emails);
  1171. $to_emails = array_merge($to_emails, $this->extractEmails($this->cc_emails));
  1172. $to_emails = array_merge($to_emails, $this->extractEmails($this->bcc_emails));
  1173. $from = $this->bounce_to_email ? $this->bounce_to_email : current($this->extractEmails(array($this->from_email)));
  1174. $connection->send($from, $to_emails, "To: " . $to . "\r\nSubject: " . $subject . "\r\n" . $headers, $body);
  1175. return $this->message_id;
  1176. }
  1177. // Sendmail when not in safe mode will allow you to set the envelope from address via the -f parameter
  1178. $parameters = NULL;
  1179. if (!fCore::checkOS('windows') && $this->bounce_to_email) {
  1180. preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
  1181. $parameters = '-f ' . $matches[0];
  1182. // Windows takes the Return-Path email from the sendmail_from ini setting
  1183. } elseif (fCore::checkOS('windows') && $this->bounce_to_email) {
  1184. $old_sendmail_from = ini_get('sendmail_from');
  1185. preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
  1186. ini_set('sendmail_from', $matches[0]);
  1187. }
  1188. // This is a gross qmail fix that is a last resort
  1189. if (self::$popen_sendmail || self::$convert_crlf) {
  1190. $to = str_replace("\r\n", "\n", $to);
  1191. $subject = str_replace("\r\n", "\n", $subject);
  1192. $body = str_replace("\r\n", "\n", $body);
  1193. $headers = str_replace("\r\n", "\n", $headers);
  1194. }
  1195. // If the user is using qmail and wants to try to fix the \r\r\n line break issue
  1196. if (self::$popen_sendmail) {
  1197. $sendmail_command = ini_get('sendmail_path');
  1198. if ($parameters) {
  1199. $sendmail_command .= ' ' . $parameters;
  1200. }
  1201. $sendmail_process = popen($sendmail_command, 'w');
  1202. fprintf($sendmail_process, "To: %s\n", $to);
  1203. fprintf($sendmail_process, "Subject: %s\n", $subject);
  1204. if ($headers) {
  1205. fprintf($sendmail_process, "%s\n", $headers);
  1206. }
  1207. fprintf($sendmail_process, "\n%s\n", $body);
  1208. $error = pclose($sendmail_process);
  1209. // This is the normal way to send mail
  1210. } else {
  1211. // On Windows, mail() sends directly to an SMTP server and will
  1212. // strip a leading . from the body
  1213. if (fCore::checkOS('windows')) {
  1214. $body = preg_replace('#^\.#', '..', $body);
  1215. }
  1216. if ($parameters) {
  1217. $error = !mail($to, $subject, $body, $headers, $parameters);
  1218. } else {
  1219. $error = !mail($to, $subject, $body, $headers);
  1220. }
  1221. }
  1222. if (fCore::checkOS('windows') && $this->bounce_to_email) {
  1223. ini_set('sendmail_from', $old_sendmail_from);
  1224. }
  1225. if ($error) {
  1226. throw new fConnectivityException(
  1227. 'An error occured while trying to send the email entitled %s',
  1228. $this->subject
  1229. );
  1230. }
  1231. return $this->message_id;
  1232. }
  1233. /**
  1234. * Sets the plaintext version of the email body
  1235. *
  1236. * This method accepts either ASCII or UTF-8 encoded text. Please see
  1237. * http://flourishlib.com/docs/UTF-8 for more information.
  1238. *
  1239. * @param string $plaintext The plaintext version of the email body
  1240. * @param boolean $unindent_expand_constants If this is `TRUE`, the body will be unindented as much as possible and {CONSTANT_NAME} will be replaced with the value of the constant
  1241. * @return void
  1242. */
  1243. public function setBody($plaintext, $unindent_expand_constants=FALSE)
  1244. {
  1245. if ($unindent_expand_constants) {
  1246. $plaintext = self::unindentExpand($plaintext);
  1247. }
  1248. $this->plaintext_body = $plaintext;
  1249. }
  1250. /**
  1251. * Adds the email address the email will be bounced to
  1252. *
  1253. * This email address will be set to the `Return-Path` header.
  1254. *
  1255. * @param string $email The email address to bounce to
  1256. * @return void
  1257. */
  1258. public function setBounceToEmail($email)
  1259. {
  1260. if (ini_get('safe_mode') && !fCore::checkOS('windows')) {
  1261. throw new fProgrammerException('It is not possible to set a Bounce-To Email address when safe mode is enabled on a non-Windows server');
  1262. }
  1263. if (!$email) {
  1264. return;
  1265. }
  1266. $this->bounce_to_email = $this->combineNameEmail('', $email);
  1267. }
  1268. /**
  1269. * Adds the `From:` email address to the email
  1270. *
  1271. * @param string $email The email address being sent from
  1272. * @param string $name The from email user's name - unfortunately on windows this is ignored
  1273. * @return void
  1274. */
  1275. public function setFromEmail($email, $name=NULL)
  1276. {
  1277. if (!$email) {
  1278. return;
  1279. }
  1280. $this->from_email = $this->combineNameEmail($name, $email);
  1281. }
  1282. /**
  1283. * Sets the HTML version of the email body
  1284. *
  1285. * This method accepts either ASCII or UTF-8 encoded text. Please see
  1286. * http://flourishlib.com/docs/UTF-8 for more information.
  1287. *
  1288. * @param string $html The HTML version of the email body
  1289. * @return void
  1290. */
  1291. public function setHTMLBody($html)
  1292. {
  1293. $this->html_body = $html;
  1294. }
  1295. /**
  1296. * Adds the `Reply-To:` email address to the email
  1297. *
  1298. * @param string $email The email address to reply to
  1299. * @param string $name The reply-to email user's name
  1300. * @return void
  1301. */
  1302. public function setReplyToEmail($email, $name=NULL)
  1303. {
  1304. if (!$email) {
  1305. return;
  1306. }
  1307. $this->reply_to_email = $this->combineNameEmail($name, $email);
  1308. }
  1309. /**
  1310. * Adds the `Sender:` email address to the email
  1311. *
  1312. * The `Sender:` header is used to indicate someone other than the `From:`
  1313. * address is actually submitting the message to the network.
  1314. *
  1315. * @param string $email The email address the message is actually being sent from
  1316. * @param string $name The sender email user's name
  1317. * @return void
  1318. */
  1319. public function setSenderEmail($email, $name=NULL)
  1320. {
  1321. if (!$email) {
  1322. return;
  1323. }
  1324. $this->sender_email = $this->combineNameEmail($name, $email);
  1325. }
  1326. /**
  1327. * Sets th…

Large files files are truncated, but you can click here to view the full file