mimemail.inc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. <?php
  2. /**
  3. * @file
  4. * Common mail functions for sending e-mail. Originally written by Gerhard.
  5. *
  6. * Allie Micka <allie at pajunas dot com>
  7. */
  8. /**
  9. * Attempts to RFC822-compliant headers for the mail message or its MIME parts.
  10. *
  11. * @todo Could use some enhancement and stress testing.
  12. *
  13. * @param array $headers
  14. * An array of headers.
  15. *
  16. * @return string
  17. * A string containing the headers.
  18. */
  19. function mimemail_rfc_headers($headers) {
  20. $header = '';
  21. $crlf = variable_get('mimemail_crlf', MAIL_LINE_ENDINGS);
  22. foreach ($headers as $key => $value) {
  23. $key = trim($key);
  24. // Collapse spaces and get rid of newline characters.
  25. $value = preg_replace('/(\s+|\n|\r|^\s|\s$)/', ' ', $value);
  26. // Fold headers if they're too long.
  27. if (drupal_strlen($value) > 60) {
  28. // If there's a semicolon, use that to separate.
  29. if (count($array = preg_split('/;\s*/', $value)) > 1) {
  30. $value = trim(join(";$crlf ", $array));
  31. }
  32. else {
  33. $value = wordwrap($value, 50, "$crlf ", FALSE);
  34. }
  35. }
  36. $header .= "$key: $value$crlf";
  37. }
  38. return trim($header);
  39. }
  40. /**
  41. * Gives useful defaults for standard email headers.
  42. *
  43. * @param array $headers
  44. * Message headers.
  45. * @param string $from
  46. * The address of the sender.
  47. *
  48. * @return array
  49. * Overwrited headers.
  50. */
  51. function mimemail_headers($headers, $from = NULL) {
  52. $default_from = variable_get('site_mail', ini_get('sendmail_from'));
  53. // Overwrite standard headers.
  54. if ($from) {
  55. if (!isset($headers['From']) || $headers['From'] == $default_from) {
  56. $headers['From'] = $from;
  57. }
  58. if (!isset($headers['Sender']) || $headers['Sender'] == $default_from) {
  59. $headers['Sender'] = $from;
  60. }
  61. // This may not work. The MTA may rewrite the Return-Path.
  62. if (!isset($headers['Return-Path']) || $headers['Return-Path'] == $default_from) {
  63. preg_match('/[a-z\d\-\.\+_]+@(?:[a-z\d\-]+\.)+[a-z\d]{2,4}/i', $from, $matches);
  64. $headers['Return-Path'] = "<$matches[0]>";
  65. }
  66. }
  67. // Convert From header if it is an array.
  68. if (is_array($headers['From'])) {
  69. $headers['From'] = mimemail_address($headers['From']);
  70. }
  71. // Run all headers through mime_header_encode() to convert non-ascii
  72. // characters to an rfc compliant string, similar to drupal_mail().
  73. foreach ($headers as $key => $value) {
  74. $headers[$key] = mime_header_encode($value);
  75. }
  76. return $headers;
  77. }
  78. /**
  79. * Extracts links to local images from HTML documents.
  80. *
  81. * @param string $html
  82. * A string containing the HTML source of the message.
  83. *
  84. * @return array
  85. * An array containing the document body and the extracted files like the following.
  86. * array(
  87. * array(
  88. * 'name' => document name
  89. * 'content' => html text, local image urls replaced by Content-IDs,
  90. * 'Content-Type' => 'text/html; charset=utf-8')
  91. * array(
  92. * 'name' => file name,
  93. * 'file' => reference to local file,
  94. * 'Content-ID' => generated Content-ID,
  95. * 'Content-Type' => derived using mime_content_type if available, educated guess otherwise
  96. * )
  97. * )
  98. */
  99. function mimemail_extract_files($html) {
  100. $pattern = '/(<link[^>]+href=[\'"]?|<object[^>]+codebase=[\'"]?|@import |[\s]src=[\'"]?)([^\'>"]+)([\'"]?)/mis';
  101. $content = preg_replace_callback($pattern, '_mimemail_replace_files', $html);
  102. $encoding = '8Bit';
  103. $body = explode("\n", $content);
  104. foreach ($body as $line) {
  105. if (drupal_strlen($line) > 998) {
  106. $encoding = 'base64';
  107. break;
  108. }
  109. }
  110. if ($encoding == 'base64') {
  111. $content = rtrim(chunk_split(base64_encode($content)));
  112. }
  113. $document = array(array(
  114. 'Content-Type' => "text/html; charset=utf-8",
  115. 'Content-Transfer-Encoding' => $encoding,
  116. 'content' => $content,
  117. ));
  118. $files = _mimemail_file();
  119. return array_merge($document, $files);
  120. }
  121. /**
  122. * Callback function for preg_replace_callback().
  123. */
  124. function _mimemail_replace_files($matches) {
  125. return stripslashes($matches[1]) . _mimemail_file($matches[2]) . stripslashes($matches[3]);
  126. }
  127. /**
  128. * Helper function to extract local files.
  129. *
  130. * @param string $url
  131. * (optional) The URI or the absolute URL to the file.
  132. * @param string $content
  133. * (optional) The actual file content.
  134. * @param string $name
  135. * (optional) The file name.
  136. * @param string $type
  137. * (optional) The file type.
  138. * @param string $disposition
  139. * (optional) The content disposition. Defaults to inline.
  140. *
  141. * @return
  142. * The Content-ID and/or an array of the files on success or the URL on failure.
  143. */
  144. function _mimemail_file($url = NULL, $content = NULL, $name = '', $type = '', $disposition = 'inline') {
  145. static $files = array();
  146. static $ids = array();
  147. if ($url) {
  148. $image = preg_match('!\.(png|gif|jpg|jpeg)$!i', $url);
  149. $linkonly = variable_get('mimemail_linkonly', 0);
  150. // The file exists on the server as-is. Allows for non-web-accessible files.
  151. if (@is_file($url) && $image && !$linkonly) {
  152. $file = $url;
  153. }
  154. else {
  155. $url = _mimemail_url($url, 'TRUE');
  156. // The $url is absolute, we're done here.
  157. $scheme = file_uri_scheme($url);
  158. if ($scheme == 'http' || $scheme == 'https' || preg_match('!mailto:!', $url)) {
  159. return $url;
  160. }
  161. // The $url is a non-local URI that needs to be converted to a URL.
  162. else {
  163. $file = (drupal_realpath($url)) ? drupal_realpath($url) : file_create_url($url);
  164. }
  165. }
  166. }
  167. // We have the actual content.
  168. elseif ($content) {
  169. $file = $content;
  170. }
  171. if (isset($file) && (@is_file($file) || $content)) {
  172. if (!$name) {
  173. $name = (@is_file($file)) ? basename($file) : 'attachment.dat';
  174. }
  175. if (!$type) {
  176. $type = ($name) ? file_get_mimetype($name) : file_get_mimetype($file);
  177. }
  178. $id = md5($file) .'@'. $_SERVER['HTTP_HOST'];
  179. // Prevent duplicate items.
  180. if (isset($ids[$id])) {
  181. return 'cid:'. $ids[$id];
  182. }
  183. $new_file = array(
  184. 'name' => $name,
  185. 'file' => $file,
  186. 'Content-ID' => $id,
  187. 'Content-Disposition' => $disposition,
  188. 'Content-Type' => $type,
  189. );
  190. $files[] = $new_file;
  191. $ids[$id] = $id;
  192. return 'cid:' . $id;
  193. }
  194. // The $file does not exist and no $content, return the $url if possible.
  195. elseif ($url) {
  196. return $url;
  197. }
  198. $ret = $files;
  199. $files = array();
  200. $ids = array();
  201. return $ret;
  202. }
  203. /**
  204. * Build a multipart body.
  205. *
  206. * @param array $parts
  207. * An associative array containing the parts to be included:
  208. * - name: A string containing the name of the attachment.
  209. * - content: A string containing textual content.
  210. * - file: A string containing file content.
  211. * - Content-Type: A string containing the content type of either file or content. Mandatory
  212. * for content, optional for file. If not present, it will be derived from file the file if
  213. * mime_content_type is available. If not, application/octet-stream is used.
  214. * - Content-Disposition: (optional) A string containing the disposition. Defaults to inline.
  215. * - Content-Transfer-Encoding: (optional) Base64 is assumed for files, 8bit for other content.
  216. * - Content-ID: (optional) for in-mail references to attachements.
  217. * Name is mandatory, one of content and file is required, they are mutually exclusive.
  218. * @param string $content_type
  219. * (optional) A string containing the content-type for the combined message. Defaults to
  220. * multipart/mixed.
  221. *
  222. * @return array
  223. * An associative array containing the following elements:
  224. * - body: A string containing the MIME-encoded multipart body of a mail.
  225. * - headers: An array that includes some headers for the mail to be sent.
  226. */
  227. function mimemail_multipart_body($parts, $content_type = 'multipart/mixed; charset=utf-8', $sub_part = FALSE) {
  228. $boundary = md5(uniqid($_SERVER['REQUEST_TIME'], TRUE));
  229. $body = '';
  230. $headers = array(
  231. 'Content-Type' => "$content_type; boundary=\"$boundary\"",
  232. );
  233. if (!$sub_part) {
  234. $headers['MIME-Version'] = '1.0';
  235. $body = "This is a multi-part message in MIME format.\n";
  236. }
  237. foreach ($parts as $part) {
  238. $part_headers = array();
  239. if (isset($part['Content-ID'])) {
  240. $part_headers['Content-ID'] = '<' . $part['Content-ID'] . '>';
  241. }
  242. if (isset($part['Content-Type'])) {
  243. $part_headers['Content-Type'] = $part['Content-Type'];
  244. }
  245. if (isset($part['Content-Disposition'])) {
  246. $part_headers['Content-Disposition'] = $part['Content-Disposition'];
  247. }
  248. elseif (strpos($part['Content-Type'], 'multipart/alternative') === FALSE) {
  249. $part_headers['Content-Disposition'] = 'inline';
  250. }
  251. if (isset($part['Content-Transfer-Encoding'])) {
  252. $part_headers['Content-Transfer-Encoding'] = $part['Content-Transfer-Encoding'];
  253. }
  254. // Mail content provided as a string.
  255. if (isset($part['content']) && $part['content']) {
  256. if (!isset($part['Content-Transfer-Encoding'])) {
  257. $part_headers['Content-Transfer-Encoding'] = '8bit';
  258. }
  259. $part_body = $part['content'];
  260. if (isset($part['name'])) {
  261. $part_headers['Content-Type'] .= '; name="' . $part['name'] . '"';
  262. $part_headers['Content-Disposition'] .= '; filename="' . $part['name'] . '"';
  263. }
  264. // Mail content references in a filename.
  265. }
  266. else {
  267. if (!isset($part['Content-Transfer-Encoding'])) {
  268. $part_headers['Content-Transfer-Encoding'] = 'base64';
  269. }
  270. if (!isset($part['Content-Type'])) {
  271. $part['Content-Type'] = file_get_mimetype($part['file']);
  272. }
  273. if (isset($part['name'])) {
  274. $part_headers['Content-Type'] .= '; name="' . $part['name'] . '"';
  275. $part_headers['Content-Disposition'] .= '; filename="' . $part['name'] . '"';
  276. }
  277. if (isset($part['file'])) {
  278. $file = (is_file($part['file'])) ? file_get_contents($part['file']) : $part['file'];
  279. $part_body = chunk_split(base64_encode($file), 76, variable_get('mimemail_crlf', "\n"));
  280. }
  281. }
  282. $body .= "\n--$boundary\n";
  283. $body .= mimemail_rfc_headers($part_headers) . "\n\n";
  284. $body .= isset($part_body) ? $part_body : '';
  285. }
  286. $body .= "\n--$boundary--\n";
  287. return array('headers' => $headers, 'body' => $body);
  288. }
  289. /**
  290. * Callback for preg_replace_callback().
  291. */
  292. function _mimemail_expand_links($matches) {
  293. return $matches[1] . _mimemail_url($matches[2]);
  294. }
  295. /**
  296. * Generate a multipart message body with a text alternative for some HTML text.
  297. *
  298. * @param string $body
  299. * The HTML message body.
  300. * @param string $subject
  301. * The message subject.
  302. * @param boolean $plain
  303. * (optional) Whether the recipient prefers plaintext-only messages. Defaults to FALSE.
  304. * @param string $plaintext
  305. * (optional) The plaintext message body.
  306. * @param array $attachments
  307. * (optional) The files to be attached to the message.
  308. *
  309. * @return array
  310. * An associative array containing the following elements:
  311. * - body: A string containing the MIME-encoded multipart body of a mail.
  312. * - headers: An array that includes some headers for the mail to be sent.
  313. *
  314. * The first mime part is a multipart/alternative containing mime-encoded sub-parts for
  315. * HTML and plaintext. Each subsequent part is the required image or attachment.
  316. */
  317. function mimemail_html_body($body, $subject, $plain = FALSE, $plaintext = NULL, $attachments = array()) {
  318. if (empty($plaintext)) {
  319. // @todo Remove once filter_xss() can handle direct descendant selectors in inline CSS.
  320. // @see http://drupal.org/node/1116930
  321. // @see http://drupal.org/node/370903
  322. // Pull out the message body.
  323. preg_match('|<body.*?</body>|mis', $body, $matches);
  324. $plaintext = drupal_html_to_text($matches[0]);
  325. }
  326. if ($plain) {
  327. // Plain mail without attachment.
  328. if (empty($attachments)) {
  329. $content_type = 'text/plain';
  330. return array(
  331. 'body' => $plaintext,
  332. 'headers' => array('Content-Type' => 'text/plain; charset=utf-8'),
  333. );
  334. }
  335. // Plain mail with attachement.
  336. else {
  337. $content_type = 'multipart/mixed';
  338. $parts = array(array(
  339. 'content' => $plaintext,
  340. 'Content-Type' => 'text/plain; charset=utf-8',
  341. ));
  342. }
  343. }
  344. else {
  345. $content_type = 'multipart/mixed';
  346. $plaintext_part = array('Content-Type' => 'text/plain; charset=utf-8', 'content' => $plaintext);
  347. // Expand all local links.
  348. $pattern = '/(<a[^>]+href=")([^"]*)/mi';
  349. $body = preg_replace_callback($pattern, '_mimemail_expand_links', $body);
  350. $mime_parts = mimemail_extract_files($body);
  351. $content = array($plaintext_part, array_shift($mime_parts));
  352. $content = mimemail_multipart_body($content, 'multipart/alternative', TRUE);
  353. $parts = array(array('Content-Type' => $content['headers']['Content-Type'], 'content' => $content['body']));
  354. if ($mime_parts) {
  355. $parts = array_merge($parts, $mime_parts);
  356. $content = mimemail_multipart_body($parts, 'multipart/related; type="multipart/alternative"', TRUE);
  357. $parts = array(array('Content-Type' => $content['headers']['Content-Type'], 'content' => $content['body']));
  358. }
  359. }
  360. if (is_array($attachments) && !empty($attachments)) {
  361. foreach ($attachments as $a) {
  362. $a = (object) $a;
  363. $path = isset($a->uri) ? $a->uri : (isset($a->filepath) ? $a->filepath : NULL);
  364. $content = isset($a->filecontent) ? $a->filecontent : NULL;
  365. $name = isset($a->filename) ? $a->filename : NULL;
  366. $type = isset($a->filemime) ? $a->filemime : NULL;
  367. _mimemail_file($path, $content, $name, $type, 'attachment');
  368. $parts = array_merge($parts, _mimemail_file());
  369. }
  370. }
  371. return mimemail_multipart_body($parts, $content_type);
  372. }
  373. /**
  374. * Helper function to format URLs.
  375. *
  376. * @param string $url
  377. * The file path.
  378. *
  379. * @return string
  380. * A processed URL.
  381. */
  382. function _mimemail_url($url, $embed_file = NULL) {
  383. global $base_url;
  384. $url = urldecode($url);
  385. // If the URL is absolute or a mailto, return it as-is.
  386. if (strpos($url, '://') !== FALSE || preg_match('!(mailto|callto|tel)\:!', $url)) {
  387. $url = str_replace(' ', '%20', $url);
  388. return $url;
  389. }
  390. // If the image embedding is disabled, return the absolute URL for the image.
  391. elseif (variable_get('mimemail_linkonly', 0) && preg_match('!\.(png|gif|jpg|jpeg)$!i', $url)) {
  392. $url = $base_url . $url;
  393. $url = str_replace(' ', '%20', $url);
  394. return $url;
  395. }
  396. $url = preg_replace('!^' . base_path() . '!', '', $url, 1);
  397. // If we're processing to embed the file, we're done here so return.
  398. if ($embed_file) {
  399. return $url;
  400. }
  401. if (!preg_match('!^\?q=*!', $url)) {
  402. $strip_clean = TRUE;
  403. }
  404. $url = str_replace('?q=', '', $url);
  405. @list($url, $fragment) = explode('#', $url, 2);
  406. @list($path, $query) = explode('?', $url, 2);
  407. // If we're dealing with an intra-document reference, return it.
  408. if (empty($path)) {
  409. return '#' . $fragment;
  410. }
  411. // Get a list of enabled languages.
  412. $languages = language_list('enabled');
  413. $languages = $languages[1];
  414. // Default language settings.
  415. $prefix = '';
  416. $language = language_default();
  417. // Check for language prefix.
  418. $args = explode('/', $path);
  419. foreach ($languages as $lang) {
  420. if ($args[0] == $lang->prefix) {
  421. $prefix = array_shift($args);
  422. $language = $lang;
  423. $path = implode('/', $args);
  424. break;
  425. }
  426. }
  427. $options = array(
  428. 'query' => ($query) ? drupal_get_query_array($query) : array(),
  429. 'fragment' => $fragment,
  430. 'absolute' => TRUE,
  431. 'language' => $language,
  432. 'prefix' => $prefix,
  433. );
  434. $url = url($path, $options);
  435. // If url() added a ?q= where there should not be one, remove it.
  436. if (isset($strip_clean) && $strip_clean) {
  437. $url = preg_replace('!\?q=!', '', $url);
  438. }
  439. $url = str_replace('+', '%2B', $url);
  440. return $url;
  441. }
  442. /**
  443. * Formats an address string.
  444. *
  445. * @todo Could use some enhancement and stress testing.
  446. *
  447. * @param mixed $address
  448. * A user object, a text email address or an array containing name, mail.
  449. * @param boolean $simplify
  450. * Determines if the address needs to be simplified. Defaults to FALSE.
  451. *
  452. * @return string
  453. * A formatted address string or FALSE.
  454. */
  455. function mimemail_address($address, $simplify = FALSE) {
  456. if (is_array($address)) {
  457. // It's an array containing 'mail' and/or 'name'.
  458. if (isset($address['mail'])) {
  459. $output = '';
  460. if (empty($address['name']) || $simplify) {
  461. return $address['mail'];
  462. }
  463. else {
  464. return '"' . addslashes(mime_header_encode($address['name'])) . '" <' . $address['mail'] . '>';
  465. }
  466. }
  467. // It's an array of address items.
  468. $addresses = array();
  469. foreach ($address as $a) {
  470. $addresses[] = mimemail_address($a);
  471. }
  472. return $addresses;
  473. }
  474. // It's a user object.
  475. if (is_object($address) && isset($address->mail)) {
  476. if (empty($address->name) || $simplify) {
  477. return $address->mail;
  478. }
  479. else {
  480. return '"' . addslashes(mime_header_encode($address->name)) . '" <' . $address->mail . '>';
  481. }
  482. }
  483. // It's formatted or unformatted string.
  484. // @todo: shouldn't assume it's valid - should try to re-parse
  485. if (is_string($address)) {
  486. return $address;
  487. }
  488. return FALSE;
  489. }