PageRenderTime 61ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fEmail.php

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

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