functions.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. <?php
  2. /**
  3. * All helpder functions for this plugin can be found here
  4. */
  5. /**
  6. * Sends out a full HTML mail
  7. *
  8. * @param array $options In the format:
  9. * to => STR|ARR of recipients in RFC-2822 format (http://www.faqs.org/rfcs/rfc2822.html)
  10. * from => STR of senden in RFC-2822 format (http://www.faqs.org/rfcs/rfc2822.html)
  11. * subject => STR with the subject of the message
  12. * body => STR with the message body
  13. * plaintext_message STR with the plaintext version of the message
  14. * html_message => STR with the HTML version of the message
  15. * cc => NULL|STR|ARR of CC recipients in RFC-2822 format (http://www.faqs.org/rfcs/rfc2822.html)
  16. * bcc => NULL|STR|ARR of BCC recipients in RFC-2822 format (http://www.faqs.org/rfcs/rfc2822.html)
  17. * date => NULL|UNIX timestamp with the date the message was created
  18. * attachments => NULL|ARR of array(array('mimetype', 'filename', 'content'))
  19. *
  20. * @return bool
  21. */
  22. function html_email_handler_send_email(array $options = null) {
  23. static $limit_subject;
  24. $site = elgg_get_site_entity();
  25. // make site email
  26. $site_from = html_email_handler_make_rfc822_address($site);
  27. // get sendmail options
  28. $sendmail_options = html_email_handler_get_sendmail_options();
  29. if (!isset($limit_subject)) {
  30. $limit_subject = false;
  31. if (elgg_get_plugin_setting("limit_subject", "html_email_handler") == "yes") {
  32. $limit_subject = true;
  33. }
  34. }
  35. // set default options
  36. $default_options = array(
  37. "to" => array(),
  38. "from" => $site_from,
  39. "subject" => "",
  40. "html_message" => "",
  41. "plaintext_message" => "",
  42. "cc" => array(),
  43. "bcc" => array(),
  44. "date" => null,
  45. );
  46. // merge options
  47. $options = array_merge($default_options, $options);
  48. // redo to/from for notifications
  49. $notification = elgg_extract('notification', $options);
  50. if (!empty($notification) && ($notification instanceof \Elgg\Notifications\Notification)) {
  51. $recipient = $notification->getRecipient();
  52. $sender = $notification->getSender();
  53. $options['to'] = html_email_handler_make_rfc822_address($recipient);
  54. if (!isset($options['recipient'])) {
  55. $options['recipient'] = $recipient;
  56. }
  57. if (!($sender instanceof \ElggUser) && $sender->email) {
  58. $options['from'] = html_email_handler_make_rfc822_address($sender);
  59. } else {
  60. $options['from'] = $site_from;
  61. }
  62. }
  63. // check options
  64. if (!empty($options["to"]) && !is_array($options["to"])) {
  65. $options["to"] = array($options["to"]);
  66. }
  67. if (!empty($options["cc"]) && !is_array($options["cc"])) {
  68. $options["cc"] = array($options["cc"]);
  69. }
  70. if (!empty($options["bcc"]) && !is_array($options["bcc"])) {
  71. $options["bcc"] = array($options["bcc"]);
  72. }
  73. if (empty($options['html_message']) && empty($options['plaintext_message'])) {
  74. $options['html_message'] = html_email_handler_make_html_body($options);
  75. $options['plaintext_message'] = $options['body'];
  76. }
  77. // can we send a message
  78. if (empty($options["to"]) || (empty($options["html_message"]) && empty($options["plaintext_message"]))) {
  79. return false;
  80. }
  81. // start preparing
  82. // Facyla : better without spaces and special chars
  83. //$boundary = uniqid($site->name);
  84. $boundary = uniqid(elgg_get_friendly_title($site->name));
  85. $headers = $options['headers'];
  86. // start building headers
  87. if (!empty($options["from"])) {
  88. $headers['From'] = $options['from'];
  89. } else {
  90. $headers['From'] = $site_from;
  91. }
  92. // check CC mail
  93. if (!empty($options["cc"])) {
  94. $headers['Cc'] = implode(', ', $options['cc']);
  95. }
  96. // check BCC mail
  97. if (!empty($options["bcc"])) {
  98. $headers['Bcc'] = implode(', ', $options['bcc']);
  99. }
  100. // add a date header
  101. if (!empty($options["date"])) {
  102. $headers['Date'] = date('r', $options['date']);
  103. }
  104. $headers['X-Mailer'] = ' PHP/' . phpversion();
  105. $headers['MIME-Version'] = '1.0';
  106. // Facyla : try to add attchments if set
  107. $attachments = "";
  108. // Allow to add single or multiple attachments
  109. if (!empty($options["attachments"])) {
  110. $attachment_counter = 0;
  111. foreach ($options["attachments"] as $attachment) {
  112. // Alternatively fetch content based on an absolute path to a file on server:
  113. if (empty($attachment["content"]) && !empty($attachment["filepath"])) {
  114. $attachment["content"] = chunk_split(base64_encode(file_get_contents($attachment["filepath"])));
  115. }
  116. // Cannot attach an empty file in any case..
  117. if (empty($attachment["content"])) {
  118. continue;
  119. }
  120. // Count valid attachments
  121. $attachment_counter++;
  122. // Use defaults for other less critical settings
  123. if (empty($attachment["mimetype"])) {
  124. $attachment["mimetype"] = "application/octet-stream";
  125. }
  126. if (empty($attachment["filename"])) {
  127. $attachment["filename"] = "file_" . $attachment_counter;
  128. }
  129. $attachments .= "Content-Type: {" . $attachment["mimetype"] . "};" . PHP_EOL . " name=\"" . $attachment["filename"] . "\"" . PHP_EOL;
  130. $attachments .= "Content-Disposition: attachment;" . PHP_EOL . " filename=\"" . $attachment["filename"] . "\"" . PHP_EOL;
  131. $attachments .= "Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL;
  132. $attachments .= $attachment["content"] . PHP_EOL . PHP_EOL;
  133. $attachments .= "--mixed--" . $boundary . PHP_EOL;
  134. }
  135. }
  136. // Use attachments headers for real only if they are valid
  137. if (!empty($attachments)) {
  138. $headers['Content-Type'] = "multipart/mixed; boundary=\"mixed--{$boundary}\"";
  139. } else {
  140. $headers['Content-Type'] = "multipart/alternative; boundary=\"{$boundary}\"";
  141. }
  142. $header_eol = "\r\n";
  143. if (elgg_get_config('broken_mta')) {
  144. // Allow non-RFC 2822 mail headers to support some broken MTAs
  145. $header_eol = "\n";
  146. }
  147. // stringify headers
  148. $headers_string = '';
  149. foreach ($headers as $key => $value) {
  150. $headers_string .= "$key: $value{$header_eol}";
  151. }
  152. // start building the message
  153. $message = "";
  154. // TEXT part of message
  155. $plaintext_message = elgg_extract("plaintext_message", $options);
  156. if (!empty($plaintext_message)) {
  157. // normalize URL's in the text
  158. $plaintext_message = html_email_handler_normalize_urls($plaintext_message);
  159. // add boundry / content type
  160. $message .= "--" . $boundary . PHP_EOL;
  161. $message .= "Content-Type: text/plain; charset=\"utf-8\"" . PHP_EOL;
  162. $message .= "Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL;
  163. // add content
  164. $message .= chunk_split(base64_encode($plaintext_message)) . PHP_EOL . PHP_EOL;
  165. }
  166. // HTML part of message
  167. $html_message = elgg_extract("html_message", $options);
  168. if (!empty($html_message)) {
  169. $html_boundary = $boundary;
  170. // normalize URL's in the text
  171. $html_message = html_email_handler_normalize_urls($html_message);
  172. $html_message = html_email_handler_base64_encode_images($html_message);
  173. $image_attachments = html_email_handler_attach_images($html_message);
  174. if (is_array($image_attachments)) {
  175. $html_boundary .= "-alt";
  176. $html_message = elgg_extract("text", $image_attachments);
  177. $message .= "--" . $boundary . PHP_EOL;
  178. $message .= "Content-Type: multipart/related; boundary=\"$html_boundary\"" . PHP_EOL . PHP_EOL;
  179. }
  180. // add boundry / content type
  181. $message .= "--" . $html_boundary . PHP_EOL;
  182. $message .= "Content-Type: text/html; charset=\"utf-8\"" . PHP_EOL;
  183. $message .= "Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL;
  184. // add content
  185. $message .= chunk_split(base64_encode($html_message)) . PHP_EOL;
  186. if (is_array($image_attachments)) {
  187. $images = elgg_extract("images", $image_attachments);
  188. foreach ($images as $image_info) {
  189. $message .= "--" . $html_boundary . PHP_EOL;
  190. $message .= "Content-Type: " . elgg_extract("content_type", $image_info) . "; charset=\"utf-8\"" . PHP_EOL;
  191. $message .= "Content-Disposition: inline; filename=\"" . elgg_extract("name", $image_info) . "\"" . PHP_EOL;
  192. $message .= "Content-ID: <" . elgg_extract("uid", $image_info) . ">" . PHP_EOL;
  193. $message .= "Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL;
  194. // add content
  195. $message .= chunk_split(elgg_extract("data", $image_info)) . PHP_EOL;
  196. }
  197. $message .= "--" . $html_boundary . "--" . PHP_EOL;
  198. }
  199. }
  200. // Final boundry
  201. $message .= "--" . $boundary . "--" . PHP_EOL;
  202. // Facyla : FILE part of message
  203. if (!empty($attachments)) {
  204. // Build strings that will be added before TEXT/HTML message
  205. $before_message = "--mixed--" . $boundary . PHP_EOL;
  206. $before_message .= "Content-Type: multipart/alternative; boundary=\"" . $boundary . "\"" . PHP_EOL . PHP_EOL;
  207. // Build strings that will be added after TEXT/HTML message
  208. $after_message = PHP_EOL;
  209. $after_message .= "--mixed--" . $boundary . PHP_EOL;
  210. $after_message .= $attachments;
  211. // Wrap TEXT/HTML message into mixed message content
  212. $message = $before_message . PHP_EOL . $message . PHP_EOL . $after_message;
  213. }
  214. // convert to to correct format
  215. $to = implode(", ", $options["to"]);
  216. // encode subject to handle special chars
  217. $subject = $options["subject"];
  218. $subject = html_entity_decode($subject, ENT_QUOTES, 'UTF-8'); // Decode any html entities
  219. if ($limit_subject) {
  220. $subject = elgg_get_excerpt($subject, 175);
  221. }
  222. $subject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
  223. return mail($to, $subject, $message, $headers_string, $sendmail_options);
  224. }
  225. /**
  226. * This function converts CSS to inline style, the CSS needs to be found in a <style> element
  227. *
  228. * @param string $html_text the html text to be converted
  229. *
  230. * @return false|string
  231. */
  232. function html_email_handler_css_inliner($html_text) {
  233. $result = false;
  234. if (!empty($html_text) && defined("XML_DOCUMENT_NODE")) {
  235. $css = "";
  236. // set custom error handling
  237. libxml_use_internal_errors(true);
  238. $dom = new DOMDocument();
  239. $dom->loadHTML($html_text);
  240. $styles = $dom->getElementsByTagName("style");
  241. if (!empty($styles)) {
  242. $style_count = $styles->length;
  243. for ($i = 0; $i < $style_count; $i++) {
  244. $css .= $styles->item($i)->nodeValue;
  245. }
  246. }
  247. // clear error log
  248. libxml_clear_errors();
  249. $emo = new Pelago\Emogrifier($html_text, $css);
  250. $result = $emo->emogrify();
  251. }
  252. return $result;
  253. }
  254. /**
  255. * Make the HTML body from a $options array
  256. *
  257. * @param array $options the options
  258. * @param string $body the message body
  259. *
  260. * @return string
  261. */
  262. function html_email_handler_make_html_body($options = "", $body = "") {
  263. global $CONFIG;
  264. if (!is_array($options)) {
  265. elgg_deprecated_notice("html_email_handler_make_html_body now takes an array as param, please update your code", "1.9");
  266. $options = array(
  267. "subject" => $options,
  268. "body" => $body
  269. );
  270. }
  271. $defaults = array(
  272. "subject" => "",
  273. "body" => "",
  274. "language" => get_current_language()
  275. );
  276. $options = array_merge($defaults, $options);
  277. $options['body'] = parse_urls($options['body']);
  278. // in some cases when pagesetup isn't done yet this can cause problems
  279. // so manualy set is to done
  280. $unset = false;
  281. if (!isset($CONFIG->pagesetupdone)) {
  282. $unset = true;
  283. $CONFIG->pagesetupdone = true;
  284. }
  285. // generate HTML mail body
  286. $result = elgg_view("html_email_handler/notification/body", $options);
  287. // do we need to restore pagesetup
  288. if ($unset) {
  289. unset($CONFIG->pagesetupdone);
  290. }
  291. if (defined("XML_DOCUMENT_NODE")) {
  292. if ($transform = html_email_handler_css_inliner($result)) {
  293. $result = $transform;
  294. }
  295. }
  296. return $result;
  297. }
  298. /**
  299. * Get the plugin settings for sendmail
  300. *
  301. * @return string
  302. */
  303. function html_email_handler_get_sendmail_options() {
  304. static $result;
  305. if (!isset($result)) {
  306. $result = "";
  307. $setting = elgg_get_plugin_setting("sendmail_options", "html_email_handler");
  308. if (!empty($setting)) {
  309. $result = $setting;
  310. }
  311. }
  312. return $result;
  313. }
  314. /**
  315. * This function build an RFC822 compliant address
  316. *
  317. * This function requires the option 'entity'
  318. *
  319. * @param ElggEntity $entity entity to use as the basis for the address
  320. * @param bool $use_fallback provides a fallback email if none defined
  321. *
  322. * @return string the correctly formatted address
  323. */
  324. function html_email_handler_make_rfc822_address(ElggEntity $entity, $use_fallback = true) {
  325. // get the email address of the entity
  326. $email = $entity->email;
  327. if (empty($email) && $use_fallback) {
  328. // no email found, fallback to site email
  329. $site = elgg_get_site_entity();
  330. $email = $site->email;
  331. if (empty($email)) {
  332. // no site email, default to noreply
  333. $email = "noreply@" . $site->getDomain();
  334. }
  335. }
  336. // build the RFC822 format
  337. if (!empty($entity->name)) {
  338. $name = $entity->name;
  339. if (strstr($name, ",")) {
  340. $name = '"' . $name . '"'; // Protect the name with quotations if it contains a comma
  341. }
  342. $name = "=?UTF-8?B?" . base64_encode($name) . "?="; // Encode the name. If may content non ASCII chars.
  343. $email = $name . " <" . $email . ">";
  344. }
  345. return $email;
  346. }
  347. /**
  348. * Normalize all URL's in the text to full URL's
  349. *
  350. * @param string $text the text to check for URL's
  351. *
  352. * @return string
  353. */
  354. function html_email_handler_normalize_urls($text) {
  355. static $pattern = '/\s(?:href|src)=([\'"]\S+[\'"])/i';
  356. if (empty($text)) {
  357. return $text;
  358. }
  359. // find all matches
  360. $matches = array();
  361. preg_match_all($pattern, $text, $matches);
  362. if (empty($matches) || !isset($matches[1])) {
  363. return $text;
  364. }
  365. // go through all the matches
  366. $urls = $matches[1];
  367. $urls = array_unique($urls);
  368. foreach ($urls as $url) {
  369. // remove wrapping quotes from the url
  370. $real_url = substr($url, 1, -1);
  371. // normalize url
  372. $new_url = elgg_normalize_url($real_url);
  373. // make the correct replacement string
  374. $replacement = str_replace($real_url, $new_url, $url);
  375. // replace the url in the content
  376. $text = str_replace($url, $replacement, $text);
  377. }
  378. return $text;
  379. }
  380. /**
  381. * Convert images to inline images
  382. *
  383. * This can be enabled with a plugin setting (default: off)
  384. *
  385. * @param string $text the text of the message to embed the images from
  386. *
  387. * @return string
  388. */
  389. function html_email_handler_base64_encode_images($text) {
  390. static $plugin_setting;
  391. if (empty($text)) {
  392. return $text;
  393. }
  394. if (!isset($plugin_setting)) {
  395. $plugin_setting = false;
  396. if (elgg_get_plugin_setting("embed_images", "html_email_handler", "no") === "base64") {
  397. $plugin_setting = true;
  398. }
  399. }
  400. if (!$plugin_setting) {
  401. return $text;
  402. }
  403. $image_urls = html_email_handler_find_images($text);
  404. if (empty($image_urls)) {
  405. return $text;
  406. }
  407. foreach ($image_urls as $url) {
  408. // remove wrapping quotes from the url
  409. $image_url = substr($url, 1, -1);
  410. // get the image contents
  411. $contents = html_email_handler_get_image($image_url);
  412. if (empty($contents)) {
  413. continue;
  414. }
  415. // build inline image
  416. $replacement = str_replace($image_url, "data:" . $contents, $url);
  417. // replace in text
  418. $text = str_replace($url, $replacement, $text);
  419. }
  420. return $text;
  421. }
  422. /**
  423. * Get the contents of an image url for embedding
  424. *
  425. * @param string $image_url the URL of the image
  426. *
  427. * @return false|string
  428. */
  429. function html_email_handler_get_image($image_url) {
  430. static $proxy_host;
  431. static $proxy_port;
  432. static $session_cookie;
  433. static $cache_dir;
  434. if (empty($image_url)) {
  435. return false;
  436. }
  437. $image_url = htmlspecialchars_decode($image_url);
  438. $image_url = elgg_normalize_url($image_url);
  439. // check cache
  440. if (!isset($cache_dir)) {
  441. $cache_dir = elgg_get_config("dataroot") . "html_email_handler/image_cache/";
  442. if (!is_dir($cache_dir)) {
  443. mkdir($cache_dir, "0755", true);
  444. }
  445. }
  446. $cache_file = md5($image_url);
  447. if (file_exists($cache_dir . $cache_file)) {
  448. return file_get_contents($cache_dir . $cache_file);
  449. }
  450. // build cURL options
  451. $ch = curl_init($image_url);
  452. curl_setopt($ch, CURLOPT_HEADER, false);
  453. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  454. curl_setopt($ch, CURLOPT_TIMEOUT, 5);
  455. // set proxy settings
  456. if (!isset($proxy_host)) {
  457. $proxy_host = false;
  458. $setting = elgg_get_plugin_setting("proxy_host", "html_email_handler");
  459. if (!empty($setting)) {
  460. $proxy_host = $setting;
  461. }
  462. }
  463. if ($proxy_host) {
  464. curl_setopt($ch, CURLOPT_PROXY, $proxy_host);
  465. }
  466. if (!isset($proxy_port)) {
  467. $proxy_port = false;
  468. $setting = (int) elgg_get_plugin_setting("proxy_port", "html_email_handler");
  469. if ($setting > 0) {
  470. $proxy_port = $setting;
  471. }
  472. }
  473. if ($proxy_port) {
  474. curl_setopt($ch, CURLOPT_PROXYPORT, $proxy_port);
  475. }
  476. // check if local url, so we can send Elgg cookies
  477. if (strpos($image_url, elgg_get_site_url()) !== false) {
  478. if (!isset($session_cookie)) {
  479. $session_cookie = false;
  480. $cookie_settings = elgg_get_config("cookie");
  481. if (!empty($cookie_settings)) {
  482. $cookie_name = elgg_extract("name", $cookie_settings["session"]);
  483. $session_cookie = $cookie_name . "=" . session_id();
  484. }
  485. }
  486. if ($session_cookie) {
  487. curl_setopt($ch, CURLOPT_COOKIE, $session_cookie);
  488. }
  489. }
  490. // get the image
  491. $contents = curl_exec($ch);
  492. $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
  493. $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
  494. curl_close($ch);
  495. if (empty($contents) || ($http_code !== 200)) {
  496. return false;
  497. }
  498. // build a valid uri
  499. // https://en.wikipedia.org/wiki/Data_URI_scheme
  500. $base64_result = $content_type . ";charset=UTF-8;base64," . base64_encode($contents);
  501. // write to cache
  502. file_put_contents($cache_dir . $cache_file, $base64_result);
  503. // return result
  504. return $base64_result;
  505. }
  506. /**
  507. * Find img src's in text
  508. *
  509. * @param string $text the text to search though
  510. *
  511. * @return false|array
  512. */
  513. function html_email_handler_find_images($text) {
  514. static $pattern = '/\ssrc=([\'"]\S+[\'"])/i';
  515. if (empty($text)) {
  516. return false;
  517. }
  518. // find all matches
  519. $matches = array();
  520. preg_match_all($pattern, $text, $matches);
  521. if (empty($matches) || !isset($matches[1])) {
  522. return false;
  523. }
  524. // return all the found image urls
  525. return array_unique($matches[1]);
  526. }
  527. /**
  528. * Get information needed for attaching the images to the e-mail
  529. *
  530. * @param string $text the html text to search images in
  531. *
  532. * @return string|array
  533. */
  534. function html_email_handler_attach_images($text) {
  535. static $plugin_setting;
  536. if (empty($text)) {
  537. return $text;
  538. }
  539. // get plugin setting for replacement
  540. if (!isset($plugin_setting)) {
  541. $plugin_setting = false;
  542. if (elgg_get_plugin_setting("embed_images", "html_email_handler", "no") === "attach") {
  543. $plugin_setting = true;
  544. }
  545. }
  546. // check plugin setting
  547. if (!$plugin_setting) {
  548. return $text;
  549. }
  550. // get images
  551. $image_urls = html_email_handler_find_images($text);
  552. if (empty($image_urls)) {
  553. return $text;
  554. }
  555. $result = array(
  556. "images" => array()
  557. );
  558. foreach ($image_urls as $url) {
  559. // remove wrapping quotes from the url
  560. $image_url = substr($url, 1, -1);
  561. // get the image contents
  562. $contents = html_email_handler_get_image($image_url);
  563. if (empty($contents)) {
  564. continue;
  565. }
  566. // make different parts of the result
  567. list($content_type, $data) = explode(";charset=UTF-8;base64,", $contents);
  568. // Unique ID
  569. $uid = uniqid();
  570. $result["images"][] = array(
  571. "uid" => $uid,
  572. "content_type" => $content_type,
  573. "data" => $data,
  574. "name" => basename($image_url)
  575. );
  576. // replace url in the text with uid
  577. $replacement = str_replace($image_url, "cid:" . $uid, $url);
  578. $text = str_replace($url, $replacement, $text);
  579. }
  580. // return new text
  581. $result["text"] = $text;
  582. // return result
  583. return $result;
  584. }