PageRenderTime 58ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/trunk/squirrelmail/functions/mime.php

#
PHP | 1657 lines | 919 code | 129 blank | 609 comment | 230 complexity | f90c323199f7df1969b61a921877d5c5 MD5 | raw file
Possible License(s): AGPL-1.0, GPL-2.0

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

  1. <?php
  2. /**
  3. * mime.php
  4. *
  5. * This contains the functions necessary to detect and decode MIME
  6. * messages.
  7. *
  8. * @copyright 1999-2012 The SquirrelMail Project Team
  9. * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  10. * @version $Id: mime.php 14249 2012-01-02 02:09:17Z pdontthink $
  11. * @package squirrelmail
  12. */
  13. /**
  14. * dependency information
  15. functions dependency
  16. mime_structure
  17. class/mime/Message.class.php
  18. Message::parseStructure
  19. functions/page_header.php
  20. displayPageHeader
  21. functions/display_messages.php
  22. plain_error_message
  23. mime_fetch_body
  24. functions/imap_general.php
  25. sqimap_run_command
  26. mime_print_body_lines
  27. functions/imap.php
  28. functions/attachment_common.php
  29. functions/display_messages.php
  30. magicHtml => url_parser
  31. translateText => url_parser
  32. */
  33. /* -------------------------------------------------------------------------- */
  34. /* MIME DECODING */
  35. /* -------------------------------------------------------------------------- */
  36. /**
  37. * Get the MIME structure
  38. *
  39. * This function gets the structure of a message and stores it in the "message" class.
  40. * It will return this object for use with all relevant header information and
  41. * fully parsed into the standard "message" object format.
  42. */
  43. function mime_structure ($bodystructure, $flags=array()) {
  44. /* Isolate the body structure and remove beginning and end parenthesis. */
  45. $read = trim(substr ($bodystructure, strpos(strtolower($bodystructure), 'bodystructure') + 13));
  46. $read = trim(substr ($read, 0, -1));
  47. $i = 0;
  48. $msg = Message::parseStructure($read,$i);
  49. if (!is_object($msg)) {
  50. global $color, $mailbox;
  51. displayPageHeader( $color, $mailbox );
  52. $errormessage = _("SquirrelMail could not decode the bodystructure of the message");
  53. $errormessage .= '<br />'._("The bodystructure provided by your IMAP server:").'<br /><br />';
  54. $errormessage .= '<pre>' . htmlspecialchars($read) . '</pre>';
  55. plain_error_message( $errormessage );
  56. echo '</body></html>';
  57. exit;
  58. }
  59. if (count($flags)) {
  60. foreach ($flags as $flag) {
  61. //FIXME: please document why it is we have to check the first char of the flag but we then go ahead and do a full string comparison anyway. Is this a speed enhancement? If not, let's keep it simple and just compare the full string and forget the switch block.
  62. $char = strtoupper($flag{1});
  63. switch ($char) {
  64. case 'S':
  65. if (strtolower($flag) == '\\seen') {
  66. $msg->is_seen = true;
  67. }
  68. break;
  69. case 'A':
  70. if (strtolower($flag) == '\\answered') {
  71. $msg->is_answered = true;
  72. }
  73. break;
  74. case 'D':
  75. if (strtolower($flag) == '\\deleted') {
  76. $msg->is_deleted = true;
  77. }
  78. break;
  79. case 'F':
  80. if (strtolower($flag) == '\\flagged') {
  81. $msg->is_flagged = true;
  82. }
  83. else if (strtolower($flag) == '$forwarded') {
  84. $msg->is_forwarded = true;
  85. }
  86. break;
  87. case 'M':
  88. if (strtolower($flag) == '$mdnsent') {
  89. $msg->is_mdnsent = true;
  90. }
  91. break;
  92. default:
  93. break;
  94. }
  95. }
  96. }
  97. // listEntities($msg);
  98. return $msg;
  99. }
  100. /* This starts the parsing of a particular structure. It is called recursively,
  101. * so it can be passed different structures. It returns an object of type
  102. * $message.
  103. * First, it checks to see if it is a multipart message. If it is, then it
  104. * handles that as it sees is necessary. If it is just a regular entity,
  105. * then it parses it and adds the necessary header information (by calling out
  106. * to mime_get_elements()
  107. */
  108. function mime_fetch_body($imap_stream, $id, $ent_id=1, $fetch_size=0) {
  109. /* Do a bit of error correction. If we couldn't find the entity id, just guess
  110. * that it is the first one. That is usually the case anyway.
  111. */
  112. if (!$ent_id) {
  113. $cmd = "FETCH $id BODY[]";
  114. } else {
  115. $cmd = "FETCH $id BODY[$ent_id]";
  116. }
  117. if ($fetch_size!=0) $cmd .= "<0.$fetch_size>";
  118. $data = sqimap_run_command ($imap_stream, $cmd, true, $response, $message, TRUE);
  119. do {
  120. $topline = trim(array_shift($data));
  121. } while($topline && ($topline[0] == '*') && !preg_match('/\* [0-9]+ FETCH.*/i', $topline)) ;
  122. $wholemessage = implode('', $data);
  123. if (preg_match('/\{([^\}]*)\}/', $topline, $regs)) {
  124. $ret = substr($wholemessage, 0, $regs[1]);
  125. /* There is some information in the content info header that could be important
  126. * in order to parse html messages. Let's get them here.
  127. */
  128. // if ($ret{0} == '<') {
  129. // $data = sqimap_run_command ($imap_stream, "FETCH $id BODY[$ent_id.MIME]", true, $response, $message, TRUE);
  130. // }
  131. } else if (preg_match('/"([^"]*)"/', $topline, $regs)) {
  132. $ret = $regs[1];
  133. } else if ((stristr($topline, 'nil') !== false) && (empty($wholemessage))) {
  134. $ret = $wholemessage;
  135. } else {
  136. global $where, $what, $mailbox, $passed_id, $startMessage;
  137. $par = 'mailbox=' . urlencode($mailbox) . '&amp;passed_id=' . $passed_id;
  138. if (isset($where) && isset($what)) {
  139. $par .= '&amp;where=' . urlencode($where) . '&amp;what=' . urlencode($what);
  140. } else {
  141. $par .= '&amp;startMessage=' . $startMessage . '&amp;show_more=0';
  142. }
  143. $par .= '&amp;response=' . urlencode($response) .
  144. '&amp;message=' . urlencode($message) .
  145. '&amp;topline=' . urlencode($topline);
  146. echo '<tt><br />' .
  147. '<table width="80%"><tr>' .
  148. '<tr><td colspan="2">' .
  149. _("Body retrieval error. The reason for this is most probably that the message is malformed.") .
  150. '</td></tr>' .
  151. '<tr><td><b>' . _("Command:") . "</td><td>$cmd</td></tr>" .
  152. '<tr><td><b>' . _("Response:") . "</td><td>$response</td></tr>" .
  153. '<tr><td><b>' . _("Message:") . "</td><td>$message</td></tr>" .
  154. '<tr><td><b>' . _("FETCH line:") . "</td><td>$topline</td></tr>" .
  155. "</table><br /></tt></font><hr />";
  156. $data = sqimap_run_command ($imap_stream, "FETCH $passed_id BODY[]", true, $response, $message, TRUE);
  157. array_shift($data);
  158. $wholemessage = implode('', $data);
  159. $ret = $wholemessage;
  160. }
  161. return $ret;
  162. }
  163. function mime_print_body_lines ($imap_stream, $id, $ent_id=1, $encoding, $rStream='php://stdout', $force_crlf='') {
  164. /* Don't kill the connection if the browser is over a dialup
  165. * and it would take over 30 seconds to download it.
  166. * Don't call set_time_limit in safe mode.
  167. */
  168. if (!ini_get('safe_mode')) {
  169. set_time_limit(0);
  170. }
  171. /* in case of base64 encoded attachments, do not buffer them.
  172. Instead, echo the decoded attachment directly to screen */
  173. if (strtolower($encoding) == 'base64') {
  174. if (!$ent_id) {
  175. $query = "FETCH $id BODY[]";
  176. } else {
  177. $query = "FETCH $id BODY[$ent_id]";
  178. }
  179. sqimap_run_command($imap_stream,$query,true,$response,$message,TRUE,'sqimap_base64_decode',$rStream,true);
  180. } else {
  181. $body = mime_fetch_body ($imap_stream, $id, $ent_id);
  182. if (is_resource($rStream)) {
  183. fputs($rStream,decodeBody($body, $encoding, $force_crlf));
  184. } else {
  185. echo decodeBody($body, $encoding, $force_crlf);
  186. }
  187. }
  188. /*
  189. TODO, use the same method for quoted printable.
  190. However, I assume that quoted printable attachments aren't that large
  191. so the performancegain / memory usage drop will be minimal.
  192. If we decide to add that then we need to adapt sqimap_fread because
  193. we need to split te result on \n and fread doesn't stop at \n. That
  194. means we also should provide $results from sqimap_fread (by ref) to
  195. te function and set $no_return to false. The $filter function for
  196. quoted printable should handle unsetting of $results.
  197. */
  198. /*
  199. TODO 2: find out how we write to the output stream php://stdout. fwrite
  200. doesn't work because 'php://stdout isn't a stream.
  201. */
  202. return;
  203. }
  204. /* -[ END MIME DECODING ]----------------------------------------------------------- */
  205. /* This is here for debugging purposes. It will print out a list
  206. * of all the entity IDs that are in the $message object.
  207. */
  208. function listEntities ($message) {
  209. if ($message) {
  210. echo "<tt>" . $message->entity_id . ' : ' . $message->type0 . '/' . $message->type1 . ' parent = '. $message->parent->entity_id. '<br />';
  211. for ($i = 0; isset($message->entities[$i]); $i++) {
  212. echo "$i : ";
  213. $msg = listEntities($message->entities[$i]);
  214. if ($msg) {
  215. echo "return: ";
  216. return $msg;
  217. }
  218. }
  219. }
  220. }
  221. function getPriorityStr($priority) {
  222. $priority_level = substr($priority,0,1);
  223. switch($priority_level) {
  224. /* Check for a higher then normal priority. */
  225. case '1':
  226. case '2':
  227. $priority_string = _("High");
  228. break;
  229. /* Check for a lower then normal priority. */
  230. case '4':
  231. case '5':
  232. $priority_string = _("Low");
  233. break;
  234. /* Check for a normal priority. */
  235. case '3':
  236. default:
  237. $priority_level = '3';
  238. $priority_string = _("Normal");
  239. break;
  240. }
  241. return $priority_string;
  242. }
  243. /* returns a $message object for a particular entity id */
  244. function getEntity ($message, $ent_id) {
  245. return $message->getEntity($ent_id);
  246. }
  247. /* translateText
  248. * Extracted from strings.php 23/03/2002
  249. */
  250. function translateText(&$body, $wrap_at, $charset) {
  251. global $where, $what; /* from searching */
  252. global $color; /* color theme */
  253. // require_once(SM_PATH . 'functions/url_parser.php');
  254. $body_ary = explode("\n", $body);
  255. for ($i=0; $i < count($body_ary); $i++) {
  256. $line = rtrim($body_ary[$i],"\r");
  257. if (strlen($line) - 2 >= $wrap_at) {
  258. sqWordWrap($line, $wrap_at, $charset);
  259. }
  260. $line = charset_decode($charset, $line);
  261. $line = str_replace("\t", ' ', $line);
  262. parseUrl ($line);
  263. $quotes = 0;
  264. $pos = 0;
  265. $j = strlen($line);
  266. while ($pos < $j) {
  267. if ($line[$pos] == ' ') {
  268. $pos++;
  269. } else if (strpos($line, '&gt;', $pos) === $pos) {
  270. $pos += 4;
  271. $quotes++;
  272. } else {
  273. break;
  274. }
  275. }
  276. if ($quotes % 2) {
  277. $line = '<span class="quote1">' . $line . '</span>';
  278. } elseif ($quotes) {
  279. $line = '<span class="quote2">' . $line . '</span>';
  280. }
  281. $body_ary[$i] = $line;
  282. }
  283. $body = '<pre>' . implode("\n", $body_ary) . '</pre>';
  284. }
  285. /**
  286. * This returns a parsed string called $body. That string can then
  287. * be displayed as the actual message in the HTML. It contains
  288. * everything needed, including HTML Tags, Attachments at the
  289. * bottom, etc.
  290. *
  291. * Since 1.2.0 function uses message_body hook.
  292. * Till 1.3.0 function included output of formatAttachments().
  293. *
  294. * @param resource $imap_stream imap connection resource
  295. * @param object $message squirrelmail message object
  296. * @param array $color squirrelmail color theme array
  297. * @param integer $wrap_at number of characters per line
  298. * @param string $ent_num (since 1.3.0) message part id
  299. * @param integer $id (since 1.3.0) message id
  300. * @param string $mailbox (since 1.3.0) imap folder name
  301. * @return string html formated message text
  302. */
  303. function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $mailbox='INBOX') {
  304. /* This if statement checks for the entity to show as the
  305. * primary message. To add more of them, just put them in the
  306. * order that is their priority.
  307. */
  308. global $startMessage, $languages, $squirrelmail_language,
  309. $show_html_default, $sort, $has_unsafe_images, $passed_ent_id,
  310. $use_iframe, $iframe_height, $download_and_unsafe_link,
  311. $download_href, $unsafe_image_toggle_href, $unsafe_image_toggle_text,
  312. $oTemplate, $nbsp;
  313. // workaround for not updated config.php
  314. if (! isset($use_iframe)) $use_iframe = false;
  315. // If there's no "view_unsafe_images" variable in the URL, turn unsafe
  316. // images off by default.
  317. sqgetGlobalVar('view_unsafe_images', $view_unsafe_images, SQ_GET, FALSE);
  318. $body = '';
  319. $urlmailbox = urlencode($mailbox);
  320. $body_message = getEntity($message, $ent_num);
  321. if (($body_message->header->type0 == 'text') ||
  322. ($body_message->header->type0 == 'rfc822')) {
  323. $body = mime_fetch_body ($imap_stream, $id, $ent_num);
  324. $body = decodeBody($body, $body_message->header->encoding);
  325. if (isset($languages[$squirrelmail_language]['XTRA_CODE']) &&
  326. function_exists($languages[$squirrelmail_language]['XTRA_CODE'] . '_decode')) {
  327. if (mb_detect_encoding($body) != 'ASCII') {
  328. $body = call_user_func($languages[$squirrelmail_language]['XTRA_CODE'] . '_decode',$body);
  329. }
  330. }
  331. /* As of 1.5.2, $body is passed (and modified) by reference */
  332. do_hook('message_body', $body);
  333. /* If there are other types that shouldn't be formatted, add
  334. * them here.
  335. */
  336. if ($body_message->header->type1 == 'html') {
  337. if ($show_html_default <> 1) {
  338. $entity_conv = array('&nbsp;' => ' ',
  339. '<p>' => "\n",
  340. '<P>' => "\n",
  341. '<br>' => "\n",
  342. '<BR>' => "\n",
  343. '<br />' => "\n",
  344. '<BR />' => "\n",
  345. '&gt;' => '>',
  346. '&lt;' => '<');
  347. $body = strtr($body, $entity_conv);
  348. $body = strip_tags($body);
  349. $body = trim($body);
  350. translateText($body, $wrap_at,
  351. $body_message->header->getParameter('charset'));
  352. } elseif ($use_iframe) {
  353. /**
  354. * If we don't add html message between iframe tags,
  355. * we must detect unsafe images and modify $has_unsafe_images.
  356. */
  357. $html_body = magicHTML($body, $id, $message, $mailbox);
  358. // Convert character set in order to display html mails in different character set
  359. $html_body = charset_decode($body_message->header->getParameter('charset'),$html_body,false,true);
  360. // creating iframe url
  361. $iframeurl=sqm_baseuri().'src/view_html.php?'
  362. . 'mailbox=' . $urlmailbox
  363. . '&amp;passed_id=' . $id
  364. . '&amp;ent_id=' . $ent_num
  365. . '&amp;view_unsafe_images=' . (int) $view_unsafe_images;
  366. global $oTemplate;
  367. $oTemplate->assign('iframe_url', $iframeurl);
  368. $oTemplate->assign('iframe_height', $iframe_height);
  369. $oTemplate->assign('html_body', $html_body);
  370. $body = $oTemplate->fetch('read_html_iframe.tpl');
  371. } else {
  372. // old way of html rendering
  373. /**
  374. * convert character set. charset_decode does not remove html special chars
  375. * applied by magicHTML functions and does not sanitize them second time if
  376. * fourth argument is true.
  377. */
  378. $charset = $body_message->header->getParameter('charset');
  379. if (!empty($charset)) {
  380. $body = charset_decode($charset,$body,false,true);
  381. }
  382. $body = magicHTML($body, $id, $message, $mailbox);
  383. }
  384. } else {
  385. translateText($body, $wrap_at,
  386. $body_message->header->getParameter('charset'));
  387. }
  388. /*
  389. * Previously the links for downloading and unsafe images were printed
  390. * under the mail. By putting the links in a global variable we can
  391. * print it in the toolbar where it belongs. Since the original code was
  392. * in this place it's left here. It might be possible to move it to some
  393. * other place if that makes sense. The possibility to do so has not
  394. * been evaluated yet.
  395. */
  396. // Initialize the global variable to an empty string.
  397. // FIXME: To have $download_and_unsafe_link as a global variable might not be needed since the use of separate variables ($download_href, $unsafe_image_toggle_href, and $unsafe_image_toggle_text) for the templates was introduced.
  398. $download_and_unsafe_link = '';
  399. // Prepare and build a link for downloading the mail.
  400. $link = 'passed_id=' . $id . '&amp;ent_id='.$ent_num.
  401. '&amp;mailbox=' . $urlmailbox .'&amp;sort=' . $sort .
  402. '&amp;startMessage=' . $startMessage . '&amp;show_more=0';
  403. if (isset($passed_ent_id)) {
  404. $link .= '&amp;passed_ent_id='.$passed_ent_id;
  405. }
  406. $download_href = SM_PATH . 'src/download.php?absolute_dl=true&amp;' . $link;
  407. // Always add the link for downloading the mail as a file to the global
  408. // variable.
  409. $download_and_unsafe_link .= "$nbsp|$nbsp"
  410. . create_hyperlink($download_href, _("Download this as a file"));
  411. // Find out the right text to use in the link depending on the
  412. // circumstances. If the unsafe images are displayed the link should
  413. // hide them, if they aren't displayed the link should only appear if
  414. // the mail really contains unsafe images.
  415. if ($view_unsafe_images) {
  416. $text = _("Hide Unsafe Images");
  417. } else {
  418. if (isset($has_unsafe_images) && $has_unsafe_images) {
  419. $link .= '&amp;view_unsafe_images=1';
  420. $text = _("View Unsafe Images");
  421. } else {
  422. $text = '';
  423. }
  424. }
  425. // Only create a link for unsafe images if there's need for one. If so:
  426. // add it to the global variable.
  427. if($text != '') {
  428. $unsafe_image_toggle_href = SM_PATH . 'src/read_body.php?'.$link;
  429. $unsafe_image_toggle_text = $text;
  430. $download_and_unsafe_link .= "$nbsp|$nbsp"
  431. . create_hyperlink($unsafe_image_toggle_href, $text);
  432. }
  433. }
  434. return $body;
  435. }
  436. /**
  437. * Generate attachments array for passing to templates.
  438. *
  439. * @since 1.5.2
  440. * @param object $message SquirrelMail message object
  441. * @param array $exclude_id message parts that are not attachments.
  442. * @param string $mailbox mailbox name
  443. * @param integer $id message id
  444. */
  445. function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
  446. global $where, $what, $startMessage, $color, $passed_ent_id, $base_uri;
  447. $att_ar = $message->getAttachments($exclude_id);
  448. $urlMailbox = urlencode($mailbox);
  449. $attachments = array();
  450. foreach ($att_ar as $att) {
  451. $ent = $att->entity_id;
  452. $header = $att->header;
  453. $type0 = strtolower($header->type0);
  454. $type1 = strtolower($header->type1);
  455. $name = '';
  456. $links = array();
  457. $links['download link']['text'] = _("Download");
  458. $links['download link']['href'] = $base_uri .
  459. "src/download.php?absolute_dl=true&amp;passed_id=$id&amp;mailbox=$urlMailbox&amp;ent_id=$ent";
  460. if ($type0 =='message' && $type1 == 'rfc822') {
  461. $default_page = $base_uri . 'src/read_body.php';
  462. $rfc822_header = $att->rfc822_header;
  463. $filename = $rfc822_header->subject;
  464. if (trim( $filename ) == '') {
  465. $filename = 'untitled-[' . $ent . ']' ;
  466. }
  467. $from_o = $rfc822_header->from;
  468. if (is_object($from_o)) {
  469. $from_name = decodeHeader($from_o->getAddress(false));
  470. } elseif (is_array($from_o) && count($from_o) && is_object($from_o[0])) {
  471. // something weird happens when a digest message is opened and you return to the digest
  472. // now the from object is part of an array. Probably the parseHeader call overwrites the info
  473. // retrieved from the bodystructure in a different way. We need to fix this later.
  474. // possible starting point, do not fetch header we already have and inspect how
  475. // the rfc822_header object behaves.
  476. $from_name = decodeHeader($from_o[0]->getAddress(false));
  477. } else {
  478. $from_name = _("Unknown sender");
  479. }
  480. $description = _("From").': '.$from_name;
  481. } else {
  482. $default_page = $base_uri . 'src/download.php';
  483. $filename = $att->getFilename();
  484. if ($header->description) {
  485. $description = decodeHeader($header->description);
  486. } else {
  487. $description = '';
  488. }
  489. }
  490. $display_filename = $filename;
  491. if (isset($passed_ent_id)) {
  492. $passed_ent_id_link = '&amp;passed_ent_id='.$passed_ent_id;
  493. } else {
  494. $passed_ent_id_link = '';
  495. }
  496. $defaultlink = $default_page . "?startMessage=$startMessage"
  497. . "&amp;passed_id=$id&amp;mailbox=$urlMailbox"
  498. . '&amp;ent_id='.$ent.$passed_ent_id_link;
  499. if ($where && $what) {
  500. $defaultlink .= '&amp;where='. urlencode($where).'&amp;what='.urlencode($what);
  501. }
  502. // IE does make use of mime content sniffing. Forcing a download
  503. // prohibit execution of XSS inside an application/octet-stream attachment
  504. if ($type0 == 'application' && $type1 == 'octet-stream') {
  505. $defaultlink .= '&amp;absolute_dl=true';
  506. }
  507. /* This executes the attachment hook with a specific MIME-type.
  508. * It also allows plugins to run if there's a rule for a more
  509. * generic type. Finally, a hook for ALL attachment types is
  510. * run as well.
  511. */
  512. // First remember the default link.
  513. $defaultlink_orig = $defaultlink;
  514. /* The API for this hook has changed as of 1.5.2 so that all plugin
  515. arguments are passed in an array instead of each their own plugin
  516. argument, and arguments are passed by reference, so instead of
  517. returning any changes, changes should simply be made to the original
  518. arguments themselves. */
  519. $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent,
  520. &$defaultlink, &$display_filename, &$where, &$what);
  521. do_hook("attachment $type0/$type1", $temp);
  522. /* The API for this hook has changed as of 1.5.2 so that all plugin
  523. arguments are passed in an array instead of each their own plugin
  524. argument, and arguments are passed by reference, so instead of
  525. returning any changes, changes should simply be made to the original
  526. arguments themselves. */
  527. $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent,
  528. &$defaultlink, &$display_filename, &$where, &$what);
  529. // Do not let a generic plugin change the default link if a more
  530. // specialized one already did it...
  531. if ($defaultlink != $defaultlink_orig) {
  532. $dummy = '';
  533. $temp[5] = &$dummy;
  534. }
  535. do_hook("attachment $type0/*", $temp);
  536. /* The API for this hook has changed as of 1.5.2 so that all plugin
  537. arguments are passed in an array instead of each their own plugin
  538. argument, and arguments are passed by reference, so instead of
  539. returning any changes, changes should simply be made to the original
  540. arguments themselves. */
  541. $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent,
  542. &$defaultlink, &$display_filename, &$where, &$what);
  543. // Do not let a generic plugin change the default link if a more
  544. // specialized one already did it...
  545. if ($defaultlink != $defaultlink_orig) {
  546. $dummy = '';
  547. $temp[5] = &$dummy;
  548. }
  549. do_hook("attachment */*", $temp);
  550. $this_attachment = array();
  551. $this_attachment['Name'] = decodeHeader($display_filename);
  552. $this_attachment['Description'] = $description;
  553. $this_attachment['DefaultHREF'] = $defaultlink;
  554. $this_attachment['DownloadHREF'] = $links['download link']['href'];
  555. $this_attachment['ViewHREF'] = isset($links['attachment_common']) ? $links['attachment_common']['href'] : '';
  556. $this_attachment['Size'] = $header->size;
  557. $this_attachment['ContentType'] = htmlspecialchars($type0 .'/'. $type1);
  558. $this_attachment['OtherLinks'] = array();
  559. foreach ($links as $val) {
  560. if ($val['text']==_("Download") || $val['text'] == _("View"))
  561. continue;
  562. if (empty($val['text']) && empty($val['extra']))
  563. continue;
  564. $temp = array();
  565. $temp['HREF'] = $val['href'];
  566. $temp['Text'] = (empty($val['text']) ? '' : $val['text']) . (empty($val['extra']) ? '' : $val['extra']);
  567. $this_attachment['OtherLinks'][] = $temp;
  568. }
  569. $attachments[] = $this_attachment;
  570. unset($links);
  571. }
  572. return $attachments;
  573. }
  574. /**
  575. * Displays attachment links and information
  576. *
  577. * Since 1.3.0 function is not included in formatBody() call.
  578. *
  579. * Since 1.0.2 uses attachment $type0/$type1 hook.
  580. * Since 1.2.5 uses attachment $type0/* hook.
  581. * Since 1.5.0 uses attachments_bottom hook.
  582. * Since 1.5.2 uses templates and does *not* return a value.
  583. *
  584. * @param object $message SquirrelMail message object
  585. * @param array $exclude_id message parts that are not attachments.
  586. * @param string $mailbox mailbox name
  587. * @param integer $id message id
  588. */
  589. function formatAttachments($message, $exclude_id, $mailbox, $id) {
  590. global $oTemplate;
  591. $attach = buildAttachmentArray($message, $exclude_id, $mailbox, $id);
  592. $oTemplate->assign('attachments', $attach);
  593. $oTemplate->display('read_attachments.tpl');
  594. }
  595. function sqimap_base64_decode(&$string) {
  596. // Base64 encoded data goes in pairs of 4 bytes. To achieve on the
  597. // fly decoding (to reduce memory usage) you have to check if the
  598. // data has incomplete pairs
  599. // Remove the noise in order to check if the 4 bytes pairs are complete
  600. $string = str_replace(array("\r\n","\n", "\r", " "),array('','','',''),$string);
  601. $sStringRem = '';
  602. $iMod = strlen($string) % 4;
  603. if ($iMod) {
  604. $sStringRem = substr($string,-$iMod);
  605. // Check if $sStringRem contains padding characters
  606. if (substr($sStringRem,-1) != '=') {
  607. $string = substr($string,0,-$iMod);
  608. } else {
  609. $sStringRem = '';
  610. }
  611. }
  612. $string = base64_decode($string);
  613. return $sStringRem;
  614. }
  615. /**
  616. * Decodes encoded string (usually message body)
  617. *
  618. * This function decodes a string (usually the message body)
  619. * depending on the encoding type. Currently quoted-printable
  620. * and base64 encodings are supported.
  621. *
  622. * The decode_body hook was added to this function in 1.4.2/1.5.0.
  623. * The $force_crlf parameter was added in 1.5.2.
  624. *
  625. * @param string $string The encoded string
  626. * @param string $encoding used encoding
  627. * @param string $force_crlf Whether or not to force CRLF or LF
  628. * line endings (or to leave as is).
  629. * If given as "LF", line endings will
  630. * all be converted to LF; if "CRLF",
  631. * line endings will all be converted
  632. * to CRLF. If given as an empty value,
  633. * the global $force_crlf_default will
  634. * be consulted (it can be specified in
  635. * config/config_local.php). Otherwise,
  636. * any other value will cause the string
  637. * to be left alone. Note that this will
  638. * be overridden to "LF" if not using at
  639. * least PHP version 4.3.0. (OPTIONAL;
  640. * default is empty - consult global
  641. * default value)
  642. *
  643. * @return string The decoded string
  644. *
  645. * @since 1.0
  646. *
  647. */
  648. function decodeBody($string, $encoding, $force_crlf='') {
  649. global $force_crlf_default;
  650. if (empty($force_crlf)) $force_crlf = $force_crlf_default;
  651. $force_crlf = strtoupper($force_crlf);
  652. // must force line endings to LF due to broken
  653. // quoted_printable_decode() in PHP versions
  654. // before 4.3.0 (see below)
  655. //
  656. if (!check_php_version(4, 3, 0) || $force_crlf == 'LF')
  657. $string = str_replace("\r\n", "\n", $string);
  658. else if ($force_crlf == 'CRLF')
  659. $string = str_replace("\n", "\r\n", $string);
  660. $encoding = strtolower($encoding);
  661. $encoding_handler = do_hook('decode_body', $encoding);
  662. // plugins get first shot at decoding the string
  663. //
  664. if (!empty($encoding_handler) && function_exists($encoding_handler)) {
  665. $string = $encoding_handler('decode', $string);
  666. } elseif ($encoding == 'quoted-printable' ||
  667. $encoding == 'quoted_printable') {
  668. // quoted_printable_decode() function is broken in older
  669. // php versions. Text with \r\n decoding was fixed only
  670. // in php 4.3.0. Minimal code requirement is PHP 4.0.4+
  671. // and the above call to: str_replace("\r\n", "\n", $string);
  672. //
  673. $string = quoted_printable_decode($string);
  674. } elseif ($encoding == 'base64') {
  675. $string = base64_decode($string);
  676. }
  677. // All other encodings are returned raw.
  678. return $string;
  679. }
  680. /**
  681. * Decodes headers
  682. *
  683. * This function decodes strings that are encoded according to
  684. * RFC1522 (MIME Part Two: Message Header Extensions for Non-ASCII Text).
  685. * Patched by Christian Schmidt <christian@ostenfeld.dk> 23/03/2002
  686. *
  687. * @param string $string header string that has to be made readable
  688. * @param boolean $utfencode change message in order to be readable on user's charset. defaults to true
  689. * @param boolean $htmlsafe preserve spaces and sanitize html special characters. defaults to true
  690. * @param boolean $decide decide if string can be utfencoded. defaults to false
  691. * @return string decoded header string
  692. */
  693. function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
  694. global $languages, $squirrelmail_language,$default_charset;
  695. if (is_array($string)) {
  696. $string = implode("\n", $string);
  697. }
  698. if (isset($languages[$squirrelmail_language]['XTRA_CODE']) &&
  699. function_exists($languages[$squirrelmail_language]['XTRA_CODE'] . '_decodeheader')) {
  700. $string = call_user_func($languages[$squirrelmail_language]['XTRA_CODE'] . '_decodeheader', $string);
  701. // Do we need to return at this point?
  702. // return $string;
  703. }
  704. $i = 0;
  705. $iLastMatch = -2;
  706. $encoded = true;
  707. $aString = explode(' ',$string);
  708. $ret = '';
  709. foreach ($aString as $chunk) {
  710. if ($encoded && $chunk === '') {
  711. continue;
  712. } elseif ($chunk === '') {
  713. $ret .= ' ';
  714. continue;
  715. }
  716. $encoded = false;
  717. /* if encoded words are not separated by a linear-space-white we still catch them */
  718. $j = $i-1;
  719. while ($match = preg_match('/^(.*)=\?([^?]*)\?(Q|B)\?([^?]*)\?=(.*)$/Ui',$chunk,$res)) {
  720. /* if the last chunk isn't an encoded string then put back the space, otherwise don't */
  721. if ($iLastMatch !== $j) {
  722. if ($htmlsafe) {
  723. $ret .= '&#32;';
  724. } else {
  725. $ret .= ' ';
  726. }
  727. }
  728. $iLastMatch = $i;
  729. $j = $i;
  730. if ($htmlsafe) {
  731. $ret .= htmlspecialchars($res[1]);
  732. } else {
  733. $ret .= $res[1];
  734. }
  735. $encoding = ucfirst($res[3]);
  736. /* decide about valid decoding */
  737. if ($decide && is_conversion_safe($res[2])) {
  738. $utfencode=true;
  739. $can_be_encoded=true;
  740. } else {
  741. $can_be_encoded=false;
  742. }
  743. switch ($encoding)
  744. {
  745. case 'B':
  746. $replace = base64_decode($res[4]);
  747. if ($utfencode) {
  748. if ($can_be_encoded) {
  749. /* convert string to different charset,
  750. * if functions asks for it (usually in compose)
  751. */
  752. $ret .= charset_convert($res[2],$replace,$default_charset,$htmlsafe);
  753. } else {
  754. // convert string to html codes in order to display it
  755. $ret .= charset_decode($res[2],$replace);
  756. }
  757. } else {
  758. if ($htmlsafe) {
  759. $replace = htmlspecialchars($replace);
  760. }
  761. $ret.= $replace;
  762. }
  763. break;
  764. case 'Q':
  765. $replace = str_replace('_', ' ', $res[4]);
  766. $replace = preg_replace('/=([0-9a-f]{2})/ie', 'chr(hexdec("\1"))',
  767. $replace);
  768. if ($utfencode) {
  769. if ($can_be_encoded) {
  770. /* convert string to different charset,
  771. * if functions asks for it (usually in compose)
  772. */
  773. $replace = charset_convert($res[2], $replace,$default_charset,$htmlsafe);
  774. } else {
  775. // convert string to html codes in order to display it
  776. $replace = charset_decode($res[2], $replace);
  777. }
  778. } else {
  779. if ($htmlsafe) {
  780. $replace = htmlspecialchars($replace);
  781. }
  782. }
  783. $ret .= $replace;
  784. break;
  785. default:
  786. break;
  787. }
  788. $chunk = $res[5];
  789. $encoded = true;
  790. }
  791. if (!$encoded) {
  792. if ($htmlsafe) {
  793. $ret .= '&#32;';
  794. } else {
  795. $ret .= ' ';
  796. }
  797. }
  798. if (!$encoded && $htmlsafe) {
  799. $ret .= htmlspecialchars($chunk);
  800. } else {
  801. $ret .= $chunk;
  802. }
  803. ++$i;
  804. }
  805. /* remove the first added space */
  806. if ($ret) {
  807. if ($htmlsafe) {
  808. $ret = substr($ret,5);
  809. } else {
  810. $ret = substr($ret,1);
  811. }
  812. }
  813. return $ret;
  814. }
  815. /**
  816. * Encodes header
  817. *
  818. * Function uses XTRA_CODE _encodeheader function, if such function exists.
  819. *
  820. * Function uses Q encoding by default and encodes a string according to RFC
  821. * 1522 for use in headers if it contains 8-bit characters or anything that
  822. * looks like it should be encoded.
  823. *
  824. * Function switches to B encoding and encodeHeaderBase64() function, if
  825. * string is 8bit and multibyte character set supported by mbstring extension
  826. * is used. It can cause E_USER_NOTICE errors, if interface is used with
  827. * multibyte character set unsupported by mbstring extension.
  828. *
  829. * @param string $string header string, that has to be encoded
  830. * @return string quoted-printable encoded string
  831. * @todo make $mb_charsets system wide constant
  832. */
  833. function encodeHeader ($string) {
  834. global $default_charset, $languages, $squirrelmail_language;
  835. if (isset($languages[$squirrelmail_language]['XTRA_CODE']) &&
  836. function_exists($languages[$squirrelmail_language]['XTRA_CODE'] . '_encodeheader')) {
  837. return call_user_func($languages[$squirrelmail_language]['XTRA_CODE'] . '_encodeheader', $string);
  838. }
  839. // Use B encoding for multibyte charsets
  840. $mb_charsets = array('utf-8','big5','gb2313','euc-kr');
  841. if (in_array($default_charset,$mb_charsets) &&
  842. in_array($default_charset,sq_mb_list_encodings()) &&
  843. sq_is8bit($string)) {
  844. return encodeHeaderBase64($string,$default_charset);
  845. } elseif (in_array($default_charset,$mb_charsets) &&
  846. sq_is8bit($string) &&
  847. ! in_array($default_charset,sq_mb_list_encodings())) {
  848. // Add E_USER_NOTICE error here (can cause 'Cannot add header information' warning in compose.php)
  849. // trigger_error('encodeHeader: Multibyte character set unsupported by mbstring extension.',E_USER_NOTICE);
  850. }
  851. // Encode only if the string contains 8-bit characters or =?
  852. $j = strlen($string);
  853. $max_l = 75 - strlen($default_charset) - 7;
  854. $aRet = array();
  855. $ret = '';
  856. $iEncStart = $enc_init = false;
  857. $cur_l = $iOffset = 0;
  858. for($i = 0; $i < $j; ++$i) {
  859. switch($string{$i})
  860. {
  861. case '"':
  862. case '=':
  863. case '<':
  864. case '>':
  865. case ',':
  866. case '?':
  867. case '_':
  868. if ($iEncStart === false) {
  869. $iEncStart = $i;
  870. }
  871. $cur_l+=3;
  872. if ($cur_l > ($max_l-2)) {
  873. /* if there is an stringpart that doesn't need encoding, add it */
  874. $aRet[] = substr($string,$iOffset,$iEncStart-$iOffset);
  875. $aRet[] = "=?$default_charset?Q?$ret?=";
  876. $iOffset = $i;
  877. $cur_l = 0;
  878. $ret = '';
  879. $iEncStart = false;
  880. } else {
  881. $ret .= sprintf("=%02X",ord($string{$i}));
  882. }
  883. break;
  884. case '(':
  885. case ')':
  886. if ($iEncStart !== false) {
  887. $aRet[] = substr($string,$iOffset,$iEncStart-$iOffset);
  888. $aRet[] = "=?$default_charset?Q?$ret?=";
  889. $iOffset = $i;
  890. $cur_l = 0;
  891. $ret = '';
  892. $iEncStart = false;
  893. }
  894. break;
  895. case ' ':
  896. if ($iEncStart !== false) {
  897. $cur_l++;
  898. if ($cur_l > $max_l) {
  899. $aRet[] = substr($string,$iOffset,$iEncStart-$iOffset);
  900. $aRet[] = "=?$default_charset?Q?$ret?=";
  901. $iOffset = $i;
  902. $cur_l = 0;
  903. $ret = '';
  904. $iEncStart = false;
  905. } else {
  906. $ret .= '_';
  907. }
  908. }
  909. break;
  910. default:
  911. $k = ord($string{$i});
  912. if ($k > 126) {
  913. if ($iEncStart === false) {
  914. // do not start encoding in the middle of a string, also take the rest of the word.
  915. $sLeadString = substr($string,0,$i);
  916. $aLeadString = explode(' ',$sLeadString);
  917. $sToBeEncoded = array_pop($aLeadString);
  918. $iEncStart = $i - strlen($sToBeEncoded);
  919. $ret .= $sToBeEncoded;
  920. $cur_l += strlen($sToBeEncoded);
  921. }
  922. $cur_l += 3;
  923. /* first we add the encoded string that reached it's max size */
  924. if ($cur_l > ($max_l-2)) {
  925. $aRet[] = substr($string,$iOffset,$iEncStart-$iOffset);
  926. $aRet[] = "=?$default_charset?Q?$ret?= "; /* the next part is also encoded => separate by space */
  927. $cur_l = 3;
  928. $ret = '';
  929. $iOffset = $i;
  930. $iEncStart = $i;
  931. }
  932. $enc_init = true;
  933. $ret .= sprintf("=%02X", $k);
  934. } else {
  935. if ($iEncStart !== false) {
  936. $cur_l++;
  937. if ($cur_l > $max_l) {
  938. $aRet[] = substr($string,$iOffset,$iEncStart-$iOffset);
  939. $aRet[] = "=?$default_charset?Q?$ret?=";
  940. $iEncStart = false;
  941. $iOffset = $i;
  942. $cur_l = 0;
  943. $ret = '';
  944. } else {
  945. $ret .= $string{$i};
  946. }
  947. }
  948. }
  949. break;
  950. }
  951. }
  952. if ($enc_init) {
  953. if ($iEncStart !== false) {
  954. $aRet[] = substr($string,$iOffset,$iEncStart-$iOffset);
  955. $aRet[] = "=?$default_charset?Q?$ret?=";
  956. } else {
  957. $aRet[] = substr($string,$iOffset);
  958. }
  959. $string = implode('',$aRet);
  960. }
  961. return $string;
  962. }
  963. /**
  964. * Encodes string according to rfc2047 B encoding header formating rules
  965. *
  966. * It is recommended way to encode headers with character sets that store
  967. * symbols in more than one byte.
  968. *
  969. * Function requires mbstring support. If required mbstring functions are missing,
  970. * function returns false and sets E_USER_WARNING level error message.
  971. *
  972. * Minimal requirements - php 4.0.6 with mbstring extension. Please note,
  973. * that mbstring functions will generate E_WARNING errors, if unsupported
  974. * character set is used. mb_encode_mimeheader function provided by php
  975. * mbstring extension is not used in order to get better control of header
  976. * encoding.
  977. *
  978. * Used php code functions - function_exists(), trigger_error(), strlen()
  979. * (is used with charset names and base64 strings). Used php mbstring
  980. * functions - mb_strlen and mb_substr.
  981. *
  982. * Related documents: rfc 2045 (BASE64 encoding), rfc 2047 (mime header
  983. * encoding), rfc 2822 (header folding)
  984. *
  985. * @param string $string header string that must be encoded
  986. * @param string $charset character set. Must be supported by mbstring extension.
  987. * Use sq_mb_list_encodings() to detect supported charsets.
  988. * @return string string encoded according to rfc2047 B encoding formating rules
  989. * @since 1.5.1
  990. * @todo First header line can be wrapped to $iMaxLength - $HeaderFieldLength - 1
  991. * @todo Do we want to control max length of header?
  992. * @todo Do we want to control EOL (end-of-line) marker?
  993. * @todo Do we want to translate error message?
  994. */
  995. function encodeHeaderBase64($string,$charset) {
  996. /**
  997. * Check mbstring function requirements.
  998. */
  999. if (! function_exists('mb_strlen') ||
  1000. ! function_exists('mb_substr')) {
  1001. // set E_USER_WARNING
  1002. trigger_error('encodeHeaderBase64: Required mbstring functions are missing.',E_USER_WARNING);
  1003. // return false
  1004. return false;
  1005. }
  1006. // initial return array
  1007. $aRet = array();
  1008. /**
  1009. * header length = 75 symbols max (same as in encodeHeader)
  1010. * remove $charset length
  1011. * remove =? ? ?= (5 chars)
  1012. * remove 2 more chars (\r\n ?)
  1013. */
  1014. $iMaxLength = 75 - strlen($charset) - 7;
  1015. // set first character position
  1016. $iStartCharNum = 0;
  1017. // loop through all characters. count characters and not bytes.
  1018. for ($iCharNum=1; $iCharNum<=mb_strlen($string,$charset); $iCharNum++) {
  1019. // encode string from starting character to current character.
  1020. $encoded_string = base64_encode(mb_substr($string,$iStartCharNum,$iCharNum-$iStartCharNum,$charset));
  1021. // Check encoded string length
  1022. if(strlen($encoded_string)>$iMaxLength) {
  1023. // if string exceeds max length, reduce number of encoded characters and add encoded string part to array
  1024. $aRet[] = base64_encode(mb_substr($string,$iStartCharNum,$iCharNum-$iStartCharNum-1,$charset));
  1025. // set new starting character
  1026. $iStartCharNum = $iCharNum-1;
  1027. // encode last char (in case it is last character in string)
  1028. $encoded_string = base64_encode(mb_substr($string,$iStartCharNum,$iCharNum-$iStartCharNum,$charset));
  1029. } // if string is shorter than max length - add next character
  1030. }
  1031. // add last encoded string to array
  1032. $aRet[] = $encoded_string;
  1033. // set initial return string
  1034. $sRet = '';
  1035. // loop through encoded strings
  1036. foreach($aRet as $string) {
  1037. // TODO: Do we want to control EOL (end-of-line) marker
  1038. if ($sRet!='') $sRet.= " ";
  1039. // add header tags and encoded string to return string
  1040. $sRet.= '=?'.$charset.'?B?'.$string.'?=';
  1041. }
  1042. return $sRet;
  1043. }
  1044. /* This function trys to locate the entity_id of a specific mime element */
  1045. function find_ent_id($id, $message) {
  1046. for ($i = 0, $ret = ''; $ret == '' && $i < count($message->entities); $i++) {
  1047. if ($message->entities[$i]->header->type0 == 'multipart') {
  1048. $ret = find_ent_id($id, $message->entities[$i]);
  1049. } else {
  1050. if (strcasecmp($message->entities[$i]->header->id, $id) == 0) {
  1051. // if (sq_check_save_extension($message->entities[$i])) {
  1052. return $message->entities[$i]->entity_id;
  1053. // }
  1054. } elseif (!empty($message->entities[$i]->header->parameters['name'])) {
  1055. /**
  1056. * This is part of a fix for Outlook Express 6.x generating
  1057. * cid URLs without creating content-id headers
  1058. * @@JA - 20050207
  1059. */
  1060. if (strcasecmp($message->entities[$i]->header->parameters['name'], $id) == 0) {
  1061. return $message->entities[$i]->entity_id;
  1062. }
  1063. }
  1064. }
  1065. }
  1066. return $ret;
  1067. }
  1068. function sq_check_save_extension($message) {
  1069. $filename = $message->getFilename();
  1070. $ext = substr($filename, strrpos($filename,'.')+1);
  1071. $save_extensions = array('jpg','jpeg','gif','png','bmp');
  1072. return in_array($ext, $save_extensions);
  1073. }
  1074. /**
  1075. ** HTMLFILTER ROUTINES
  1076. */
  1077. /**
  1078. * This function checks attribute values for entity-encoded values
  1079. * and returns them translated into 8-bit strings so we can run
  1080. * checks on them.
  1081. *
  1082. * @param $attvalue A string to run entity check against.
  1083. * @return Nothing, modifies a reference value.
  1084. */
  1085. function sq_defang(&$attvalue){
  1086. $me = 'sq_defang';
  1087. /**
  1088. * Skip this if there aren't ampersands or backslashes.
  1089. */
  1090. if (strpos($attvalue, '&') === false
  1091. && strpos($attvalue, '\\') === false){
  1092. return;
  1093. }
  1094. $m = false;
  1095. // before deent, translate the dangerous unicode characters and ... to safe values
  1096. // otherwise the regular expressions do not match.
  1097. do {
  1098. $m = false;
  1099. $m = $m || sq_deent($attvalue, '/\&#0*(\d+);*/s');
  1100. $m = $m || sq_deent($attvalue, '/\&#x0*((\d|[a-f])+);*/si', true);
  1101. $m = $m || sq_deent($attvalue, '/\\\\(\d+)/s', true);
  1102. } while ($m == true);
  1103. $attvalue = stripslashes($attvalue);
  1104. }
  1105. /**
  1106. * Kill any tabs, newlines, or carriage returns. Our friends the
  1107. * makers of the browser with 95% market value decided that it'd
  1108. * be funny to make "java[tab]script" be just as good as "javascript".
  1109. *
  1110. * @param attvalue The attribute value before extraneous spaces removed.
  1111. * @return attvalue Nothing, modifies a reference value.
  1112. */
  1113. function sq_unspace(&$attvalue){
  1114. $me = 'sq_unspace';
  1115. if (strcspn($attvalue, "\t\r\n\0 ") != strlen($attvalue)){
  1116. $attvalue = str_replace(Array("\t", "\r", "\n", "\0", " "),
  1117. Array('', '', '', '', ''), $attvalue);
  1118. }
  1119. }
  1120. /**
  1121. * Translate all dangerous Unicode or Shift_JIS characters which are accepted by
  1122. * IE as regular characters.
  1123. *
  1124. * @param attvalue The attribute value before dangerous characters are translated.
  1125. * @return attvalue Nothing, modifies a reference value.
  1126. * @author Marc Groot Koerkamp.
  1127. */
  1128. function sq_fixIE_idiocy(&$attvalue) {
  1129. // remove NUL
  1130. $attvalue = str_replace("\0", "", $attvalue);
  1131. // remove comments
  1132. $attvalue = preg_replace("/(\/\*.*?\*\/)/","",$attvalue);
  1133. // IE has the evil habit of accepting every possible value for the attribute expression.
  1134. // The table below contains characters which are parsed by IE if they are used in the "expression"
  1135. // attribute value.
  1136. $aDangerousCharsReplacementTable = array(
  1137. array('&#x029F;', '&#0671;' ,/* L UNICODE IPA Extension */
  1138. '&#x0280;', '&#0640;' ,/* R UNICODE IPA Extension */
  1139. '&#x0274;', '&#0628;' ,/* N UNICODE IPA Extension */
  1140. '&#xFF25;', '&#65317;' ,/* Unicode FULLWIDTH LATIN CAPITAL LETTER E */
  1141. '&#xFF45;', '&#65349;' ,/* Unicode FULLWIDTH LATIN SMALL LETTER E */
  1142. '&#xFF38;', '&#65336;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER X */
  1143. '&#xFF58;', '&#65368;',/* Unic…

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