Add option to allow returning to the message one had been replying to after sending
[squirrelmail.git] / functions / mime.php
index ae3c7ac42e372c3e423455903633fef54c6ac8fa..3227aebb3caa292df10f7945ae285109b9e72a33 100644 (file)
@@ -6,7 +6,7 @@
  * This contains the functions necessary to detect and decode MIME
  * messages.
  *
- * @copyright © 1999-2007 The SquirrelMail Project Team
+ * @copyright 1999-2017 The SquirrelMail Project Team
  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
  * @package squirrelmail
@@ -63,7 +63,7 @@ function mime_structure ($bodystructure, $flags=array()) {
         displayPageHeader( $color, $mailbox );
         $errormessage  = _("SquirrelMail could not decode the bodystructure of the message");
         $errormessage .= '<br />'._("The bodystructure provided by your IMAP server:").'<br /><br />';
-        $errormessage .= '<pre>' . htmlspecialchars($read) . '</pre>';
+        $errormessage .= '<pre>' . sm_encode_html_special_chars($read) . '</pre>';
         plain_error_message( $errormessage );
         echo '</body></html>';
         exit;
@@ -92,6 +92,9 @@ function mime_structure ($bodystructure, $flags=array()) {
                     if (strtolower($flag) == '\\flagged') {
                         $msg->is_flagged = true;
                     }
+                    else if (strtolower($flag) == '$forwarded') {
+                        $msg->is_forwarded = true;
+                    }
                     break;
                 case 'M':
                     if (strtolower($flag) == '$mdnsent') {
@@ -134,10 +137,16 @@ function mime_fetch_body($imap_stream, $id, $ent_id=1, $fetch_size=0) {
     $data = sqimap_run_command ($imap_stream, $cmd, true, $response, $message, TRUE);
     do {
         $topline = trim(array_shift($data));
-    } while($topline && ($topline[0] == '*') && !preg_match('/\* [0-9]+ FETCH.*/i', $topline)) ;
+    } while($topline && ($topline[0] == '*') && !preg_match('/\* [0-9]+ FETCH .*BODY.*/i', $topline)) ;
+    // Matching with "BODY" above is difficult: in most cases "FETCH \(BODY" would work
+    // but some servers may put other things in the same result, perhaps something such
+    // as "* 23 FETCH (FLAGS (\Seen) BODY[1] {174}".  There is some small chance that
+    // if the character sequence "BODY" appears in a response where it isn't actually
+    // a FETCH response data item name, the current regex will break things.  The better
+    // way to do this would be to parse the response correctly and not use a regex.
 
     $wholemessage = implode('', $data);
-    if (ereg('\\{([^\\}]*)\\}', $topline, $regs)) {
+    if (preg_match('/\{([^\}]*)\}/', $topline, $regs)) {
         $ret = substr($wholemessage, 0, $regs[1]);
         /* There is some information in the content info header that could be important
          * in order to parse html messages. Let's get them here.
@@ -145,7 +154,7 @@ function mime_fetch_body($imap_stream, $id, $ent_id=1, $fetch_size=0) {
 //        if ($ret{0} == '<') {
 //            $data = sqimap_run_command ($imap_stream, "FETCH $id BODY[$ent_id.MIME]", true, $response, $message, TRUE);
 //        }
-    } else if (ereg('"([^"]*)"', $topline, $regs)) {
+    } else if (preg_match('/"([^"]*)"/', $topline, $regs)) {
         $ret = $regs[1];
     } else if ((stristr($topline, 'nil') !== false) && (empty($wholemessage))) {
         $ret = $wholemessage;
@@ -181,7 +190,7 @@ function mime_fetch_body($imap_stream, $id, $ent_id=1, $fetch_size=0) {
     return $ret;
 }
 
-function mime_print_body_lines ($imap_stream, $id, $ent_id=1, $encoding, $rStream='php://stdout') {
+function mime_print_body_lines ($imap_stream, $id, $ent_id=1, $encoding, $rStream='php://stdout', $force_crlf='') {
 
     /* Don't kill the connection if the browser is over a dialup
      * and it would take over 30 seconds to download it.
@@ -203,9 +212,9 @@ function mime_print_body_lines ($imap_stream, $id, $ent_id=1, $encoding, $rStrea
     } else {
         $body = mime_fetch_body ($imap_stream, $id, $ent_id);
         if (is_resource($rStream)) {
-            fputs($rStream,decodeBody($body,$encoding));
+            fputs($rStream,decodeBody($body, $encoding, $force_crlf));
         } else {
-            echo decodeBody($body, $encoding);
+            echo decodeBody($body, $encoding, $force_crlf);
         }
     }
 
@@ -291,7 +300,8 @@ function translateText(&$body, $wrap_at, $charset) {
 
     $body_ary = explode("\n", $body);
     for ($i=0; $i < count($body_ary); $i++) {
-        $line = $body_ary[$i];
+        $line = rtrim($body_ary[$i],"\r");
+
         if (strlen($line) - 2 >= $wrap_at) {
             sqWordWrap($line, $wrap_at, $charset);
         }
@@ -571,9 +581,9 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
         }
 
         /* This executes the attachment hook with a specific MIME-type.
-         * If that doesn't have results, it tries if there's a rule
-         * for a more generic type. Finally, a hook for ALL attachment
-         * types is run as well.
+         * It also allows plugins to run if there's a rule for a more
+         * generic type. Finally, a hook for ALL attachment types is
+         * run as well.
          */
         // First remember the default link.
         $defaultlink_orig = $defaultlink;
@@ -583,19 +593,23 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
            argument, and arguments are passed by reference, so instead of
            returning any changes, changes should simply be made to the original
            arguments themselves. */
-        $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent, 
+        $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent,
                     &$defaultlink, &$display_filename, &$where, &$what);
         do_hook("attachment $type0/$type1", $temp);
-        if(count($links) <= 1 && $defaultlink == $defaultlink_orig) {
-            /* The API for this hook has changed as of 1.5.2 so that all plugin
-               arguments are passed in an array instead of each their own plugin
-               argument, and arguments are passed by reference, so instead of
-               returning any changes, changes should simply be made to the original
-               arguments themselves. */
-            $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent, 
-                          &$defaultlink, &$display_filename, &$where, &$what);
-            do_hook("attachment $type0/*", $temp);
+        /* The API for this hook has changed as of 1.5.2 so that all plugin
+           arguments are passed in an array instead of each their own plugin
+           argument, and arguments are passed by reference, so instead of
+           returning any changes, changes should simply be made to the original
+           arguments themselves. */
+        $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent,
+                      &$defaultlink, &$display_filename, &$where, &$what);
+        // Do not let a generic plugin change the default link if a more
+        // specialized one already did it...
+        if ($defaultlink != $defaultlink_orig) {
+            $dummy = '';
+            $temp[5] = &$dummy;
         }
+        do_hook("attachment $type0/*", $temp);
         /* The API for this hook has changed as of 1.5.2 so that all plugin
            arguments are passed in an array instead of each their own plugin
            argument, and arguments are passed by reference, so instead of
@@ -618,7 +632,7 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
         $this_attachment['DownloadHREF'] = $links['download link']['href'];
         $this_attachment['ViewHREF'] = isset($links['attachment_common']) ? $links['attachment_common']['href'] : '';
         $this_attachment['Size'] = $header->size;
-        $this_attachment['ContentType'] = htmlspecialchars($type0 .'/'. $type1);
+        $this_attachment['ContentType'] = sm_encode_html_special_chars($type0 .'/'. $type1);
         $this_attachment['OtherLinks'] = array();
         foreach ($links as $val) {
             if ($val['text']==_("Download") || $val['text'] == _("View"))
@@ -688,44 +702,79 @@ function sqimap_base64_decode(&$string) {
 }
 
 /**
- * Decodes encoded message body
+ * Decodes encoded string (usually message body)
+ *
+ * This function decodes a string (usually the message body)
+ * depending on the encoding type.  Currently quoted-printable
+ * and base64 encodings are supported.
+ *
+ * The decode_body hook was added to this function in 1.4.2/1.5.0.
+ * The $force_crlf parameter was added in 1.5.2.
+ *
+ * @param string $string     The encoded string
+ * @param string $encoding   used encoding
+ * @param string $force_crlf Whether or not to force CRLF or LF
+ *                           line endings (or to leave as is).
+ *                           If given as "LF", line endings will
+ *                           all be converted to LF; if "CRLF",
+ *                           line endings will all be converted
+ *                           to CRLF.  If given as an empty value,
+ *                           the global $force_crlf_default will
+ *                           be consulted (it can be specified in
+ *                           config/config_local.php).  Otherwise,
+ *                           any other value will cause the string
+ *                           to be left alone.  Note that this will
+ *                           be overridden to "LF" if not using at
+ *                           least PHP version 4.3.0. (OPTIONAL;
+ *                           default is empty - consult global
+ *                           default value)
+ *
+ * @return string The decoded string
  *
- * This function decodes the body depending on the encoding type.
- * Currently quoted-printable and base64 encodings are supported.
- * decode_body hook was added to this function in 1.4.2/1.5.0
- * @param string $body encoded message body
- * @param string $encoding used encoding
- * @return string decoded string
  * @since 1.0
+ *
  */
-function decodeBody($body, $encoding) {
+function decodeBody($string, $encoding, $force_crlf='') {
+
+    global $force_crlf_default;
+    if (empty($force_crlf)) $force_crlf = $force_crlf_default;
+    $force_crlf = strtoupper($force_crlf);
+
+    // must force line endings to LF due to broken
+    // quoted_printable_decode() in PHP versions
+    // before 4.3.0 (see below)
+    //
+    if (!check_php_version(4, 3, 0) || $force_crlf == 'LF')
+        $string = str_replace("\r\n", "\n", $string);
+    else if ($force_crlf == 'CRLF')
+        $string = str_replace("\n", "\r\n", $string);
 
-    $body = str_replace("\r\n", "\n", $body);
     $encoding = strtolower($encoding);
 
     $encoding_handler = do_hook('decode_body', $encoding);
 
 
-    // plugins get first shot at decoding the body
+    // plugins get first shot at decoding the string
     //
     if (!empty($encoding_handler) && function_exists($encoding_handler)) {
-        $body = $encoding_handler('decode', $body);
+        $string = $encoding_handler('decode', $string);
 
     } elseif ($encoding == 'quoted-printable' ||
             $encoding == 'quoted_printable') {
-        /**
-         * quoted_printable_decode() function is broken in older
-         * php versions. Text with \r\n decoding was fixed only
-         * in php 4.3.0. Minimal code requirement 4.0.4 +
-         * str_replace("\r\n", "\n", $body); call.
-         */
-        $body = quoted_printable_decode($body);
+
+        // quoted_printable_decode() function is broken in older
+        // php versions.  Text with \r\n decoding was fixed only
+        // in php 4.3.0.  Minimal code requirement is PHP 4.0.4+
+        // and the above call to:  str_replace("\r\n", "\n", $string);
+        //
+        $string = quoted_printable_decode($string);
+
     } elseif ($encoding == 'base64') {
-        $body = base64_decode($body);
+        $string = base64_decode($string);
     }
 
     // All other encodings are returned raw.
-    return $body;
+    return $string;
 }
 
 /**
@@ -757,6 +806,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
     $iLastMatch = -2;
     $encoded = true;
 
+// FIXME: spaces are allowed inside quoted-printable encoding, but the following line will bust up any such encoded strings
     $aString = explode(' ',$string);
     $ret = '';
     foreach ($aString as $chunk) {
@@ -782,7 +832,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
             $iLastMatch = $i;
             $j = $i;
             if ($htmlsafe) {
-                $ret .= htmlspecialchars($res[1]);
+                $ret .= sm_encode_html_special_chars($res[1]);
             } else {
                 $ret .= $res[1];
             }
@@ -811,14 +861,15 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
                         }
                     } else {
                         if ($htmlsafe) {
-                            $replace = htmlspecialchars($replace);
+                            $replace = sm_encode_html_special_chars($replace);
                         }
                         $ret.= $replace;
                     }
                     break;
                 case 'Q':
                     $replace = str_replace('_', ' ', $res[4]);
-                    $replace = preg_replace('/=([0-9a-f]{2})/ie', 'chr(hexdec("\1"))',
+                    $replace = preg_replace_callback('/=([0-9a-f]{2})/i',
+                            create_function ('$matches', 'return chr(hexdec($matches[1]));'),
                             $replace);
                     if ($utfencode) {
                         if ($can_be_encoded) {
@@ -832,7 +883,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
                         }
                     } else {
                         if ($htmlsafe) {
-                            $replace = htmlspecialchars($replace);
+                            $replace = sm_encode_html_special_chars($replace);
                         }
                     }
                     $ret .= $replace;
@@ -852,7 +903,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
         }
 
         if (!$encoded && $htmlsafe) {
-            $ret .= htmlspecialchars($chunk);
+            $ret .= sm_encode_html_special_chars($chunk);
         } else {
             $ret .= $chunk;
         }
@@ -919,6 +970,7 @@ function encodeHeader ($string) {
     for($i = 0; $i < $j; ++$i) {
         switch($string{$i})
         {
+            case '"':
             case '=':
             case '<':
             case '>':
@@ -1846,7 +1898,9 @@ function sq_fix_url($attname, &$attvalue, $message, $id, $mailbox,$sQuote = '"')
     // images off by default.
     sqgetGlobalVar('view_unsafe_images', $view_unsafe_images, SQ_GET, FALSE);
 
-    $secremoveimg = '../images/' . _("sec_remove_eng.png");
+    global $use_transparent_security_image;
+    if ($use_transparent_security_image) $secremoveimg = '../images/spacer.png';
+    else $secremoveimg = '../images/' . _("sec_remove_eng.png");
 
     /**
      * Replace empty src tags with the blank image.  src is only used
@@ -1944,6 +1998,8 @@ function sq_fix_url($attname, &$attvalue, $message, $id, $mailbox,$sQuote = '"')
                                     $attvalue = $sQuote . SM_PATH . 'images/blank.png'. $sQuote;
                                 }
                             }
+                        } else {
+                            $attvalue = $sQuote . $attvalue . $sQuote;
                         }
                         break;
                     case 'outbind':
@@ -1952,20 +2008,20 @@ function sq_fix_url($attname, &$attvalue, $message, $id, $mailbox,$sQuote = '"')
                          * One day MS might actually make it match something useful, for now, falling
                          * back to using cid2http, so we can grab the blank.png.
                          */
-                        $attvalue = sq_cid2http($message, $id, $attvalue, $mailbox);
+                        $attvalue = $sQuote . sq_cid2http($message, $id, $attvalue, $mailbox) . $sQuote;
                         break;
                     case 'cid':
                         /**
                             * Turn cid: urls into http-friendly ones.
                             */
-                        $attvalue = sq_cid2http($message, $id, $attvalue, $mailbox);
+                        $attvalue = $sQuote . sq_cid2http($message, $id, $attvalue, $mailbox) . $sQuote;
                         break;
                     default:
                         $attvalue = $sQuote . SM_PATH . 'images/blank.png' . $sQuote;
                         break;
                 }
             } else {
-                if (!(isset($aUrl['path']) && $aUrl['path'] == $secremoveimg)) {
+                if (!isset($aUrl['path']) || $aUrl['path'] != $secremoveimg) {
                     // parse_url did not lead to satisfying result
                     $attvalue = $sQuote . SM_PATH . 'images/blank.png' . $sQuote;
                 }
@@ -2057,7 +2113,11 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
      * and change it to .bodyclass so we can just assign it to a <div>
      */
     $content = preg_replace("|body(\s*\{.*?\})|si", ".bodyclass\\1", $content);
-    $secremoveimg = '../images/' . _("sec_remove_eng.png");
+
+    global $use_transparent_security_image;
+    if ($use_transparent_security_image) $secremoveimg = '../images/spacer.png';
+    else $secremoveimg = '../images/' . _("sec_remove_eng.png");
+
     /**
     * Fix url('blah') declarations.
     */
@@ -2102,6 +2162,12 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
     /**
      * Fix stupid css declarations which lead to vulnerabilities
      * in IE.
+     *
+     * Also remove "position" attribute, as it can easily be set
+     * to "fixed" or "absolute" with "left" and "top" attributes
+     * of zero, taking over the whole content frame.  It can also
+     * be set to relative and move itself anywhere it wants to,
+     * displaying content in areas it shouldn't be allowed to touch.
      */
     $match   = Array('/\/\*.*\*\//',
                     '/expression/i',
@@ -2109,8 +2175,9 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
                     '/binding/i',
                     '/include-source/i',
                     '/javascript/i',
-                    '/script/i');
-    $replace = Array('','idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy');
+                    '/script/i',
+                    '/position/i');
+    $replace = Array('','idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy', '');
     $contentNew = preg_replace($match, $replace, $contentTemp);
     if ($contentNew !== $contentTemp) {
         // insecure css declarations are used. From now on we don't care
@@ -2171,7 +2238,7 @@ function sq_cid2http($message, $id, $cidurl, $mailbox){
     }
 
     if (!empty($linkurl)) {
-        $httpurl = $quotchar . SM_PATH . 'src/download.php?absolute_dl=true&amp;' .
+        $httpurl = $quotchar . sqm_baseuri() . 'src/download.php?absolute_dl=true&amp;' .
             "passed_id=$id&amp;mailbox=" . urlencode($mailbox) .
             '&amp;ent_id=' . $linkurl . $quotchar;
     } else {
@@ -2297,6 +2364,17 @@ function sq_sanitize($body,
             list($free_content, $curpos) =
                 sq_fixstyle($body, $gt+1, $message, $id, $mailbox);
             if ($free_content != FALSE){
+                if ( !empty($attary) ) {
+                    $attary = sq_fixatts($tagname,
+                                         $attary,
+                                         $rm_attnames,
+                                         $bad_attvals,
+                                         $add_attr_to_tag,
+                                         $message,
+                                         $id,
+                                         $mailbox
+                                         );
+                }
                 $trusted .= sq_tagprint($tagname, $attary, $tagtype);
                 $trusted .= $free_content;
                 $trusted .= sq_tagprint($tagname, false, 2);
@@ -2477,7 +2555,10 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
                 )
             );
 
-    $secremoveimg = "../images/" . _("sec_remove_eng.png");
+    global $use_transparent_security_image;
+    if ($use_transparent_security_image) $secremoveimg = '../images/spacer.png';
+    else $secremoveimg = '../images/' . _("sec_remove_eng.png");
+
     $bad_attvals = Array(
             "/.*/" =>
             Array(
@@ -2515,12 +2596,28 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
                     "/binding/i",
                     "/behaviou*r/i",
                     "/include-source/i",
-                    "/position\s*:\s*absolute/i",
+
+                    // position:relative can also be exploited
+                    // to put content outside of email body area
+                    // and position:fixed is similarly exploitable
+                    // as position:absolute, so we'll remove it
+                    // altogether....
+                    //
+                    // Does this screw up legitimate HTML messages?
+                    // If so, the only fix I see is to allow position
+                    // attributes (any values?  I think we still have
+                    // to block static and fixed) only if $use_iframe
+                    // is enabled (1.5.0+)
+                    //
+                    // was:   "/position\s*:\s*absolute/i",
+                    //
+                    "/position\s*:/i",
+
                     "/(\\\\)?u(\\\\)?r(\\\\)?l(\\\\)?/i",
                     "/url\s*\(\s*([\'\"])\s*\S+script\s*:.*([\'\"])\s*\)/si",
                     "/url\s*\(\s*([\'\"])\s*mocha\s*:.*([\'\"])\s*\)/si",
                     "/url\s*\(\s*([\'\"])\s*about\s*:.*([\'\"])\s*\)/si",
-                    "/(.*)\s*:\s*url\s*\(\s*([\'\"]*)\s*\S+script\s*:.*([\'\"]*)\s*\)/si"
+                    "/(.*)\s*:\s*url\s*\(\s*([\'\"]*)\s*\S+script\s*:.*([\'\"]*)\s*\)/si",
                     ),
                 Array(
                     "",
@@ -2670,7 +2767,7 @@ function SendDownloadHeaders($type0, $type1, $filename, $force, $filesize=0) {
         $filename =
             call_user_func($languages[$squirrelmail_language]['XTRA_CODE'] . '_downloadfilename', $filename, $HTTP_USER_AGENT);
     } else {
-        $filename = ereg_replace('[\\/:\*\?"<>\|;]', '_', str_replace('&nbsp;', ' ', $filename));
+        $filename = preg_replace('/[\\\\\/:*?"<>|;]/', '_', str_replace('&nbsp;', ' ', $filename));
     }
 
     // A Pox on Microsoft and it's Internet Explorer!
@@ -2689,7 +2786,8 @@ function SendDownloadHeaders($type0, $type1, $filename, $force, $filesize=0) {
         $filename=rawurlencode($filename);
         header ("Pragma: public");
         header ("Cache-Control: no-store, max-age=0, no-cache, must-revalidate"); // HTTP/1.1
-        header ("Cache-Control: post-check=0, pre-check=0", false);
+        // does nothing - see: https://blogs.msdn.microsoft.com/ieinternals/2009/07/20/internet-explorers-cache-control-extensions/
+        // header ("Cache-Control: post-check=0, pre-check=0", false);
         header ("Cache-Control: private");
 
         //set the inline header for IE, we'll add the attachment header later if we need it