PageRenderTime 135ms CodeModel.GetById 41ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fEmail.php

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

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