Encode outgoing attachments that have lines longer than allowed per RFC. Otherwise...
[squirrelmail.git] / class / deliver / Deliver.class.php
index d86a125bdc27a8a2c5fd5c220d9fb6d2c23e62b6..db9e35562409b14baee21d1d324f5ec24ee2ef36 100644 (file)
@@ -282,17 +282,52 @@ class Deliver {
                 global $username, $attachment_dir;
                 $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
                 $filename = $message->att_local_name;
+
+                // inspect attached file for lines longer than allowed by RFC,
+                // in which case we'll be using base64 encoding (so we can split
+                // the lines up without corrupting them) instead of 8bit unencoded...
+                // (see RFC 2822/2.1.1)
+                //
+                // using 990 because someone somewhere is folding lines at
+                // 990 instead of 998 and I'm too lazy to find who it is
+                //
+                $file_has_long_lines = file_has_long_lines($hashed_attachment_dir
+                                                           . '/' . $filename, 990);
+
                 $file = fopen ($hashed_attachment_dir . '/' . $filename, 'rb');
-                while ($body_part = fgets($file, 4096)) {
-                    // remove NUL characters
-                    $body_part = str_replace("\0",'',$body_part);
-                    $length += $this->clean_crlf($body_part);
-                    if ($stream) {
-                        $this->preWriteToStream($body_part);
-                        $this->writeToStream($stream, $body_part);
+
+                // long lines were found, need to use base64 encoding
+                //
+                if ($file_has_long_lines) {
+                    while ($tmp = fread($file, 570)) {
+                        $body_part = chunk_split(base64_encode($tmp));
+                        // Up to 4.3.10 chunk_split always appends a newline,
+                        // while in 4.3.11 it doesn't if the string to split
+                        // is shorter than the chunk length.
+                        if( substr($body_part, -1 , 1 ) != "\n" )
+                            $body_part .= "\n";
+                        $length += $this->clean_crlf($body_part);
+                        if ($stream) {
+                            $this->writeToStream($stream, $body_part);
+                        }
+                    }
+                }
+
+                // no excessively long lines - normal 8bit
+                //
+                else {
+                    while ($body_part = fgets($file, 4096)) {
+                        // remove NUL characters
+                        $body_part = str_replace("\0",'',$body_part);
+                        $length += $this->clean_crlf($body_part);
+                        if ($stream) {
+                            $this->preWriteToStream($body_part);
+                            $this->writeToStream($stream, $body_part);
+                        }
+                        $last = $body_part;
                     }
-                    $last = $body_part;
                 }
+
                 fclose($file);
             }
             break;
@@ -461,10 +496,29 @@ class Deliver {
         if ($mime_header->encoding) {
             $header[] = 'Content-Transfer-Encoding: ' . $mime_header->encoding . $rn;
         } else {
-            if ($mime_header->type0 == 'text' || $mime_header->type0 == 'message') {
-                $header[] = 'Content-Transfer-Encoding: 8bit' .  $rn;
-            } else if ($mime_header->type0 == 'multipart' || $mime_header->type0 == 'alternative') {
+
+            // inspect attached file for lines longer than allowed by RFC,
+            // in which case we'll be using base64 encoding (so we can split
+            // the lines up without corrupting them) instead of 8bit unencoded...
+            // (see RFC 2822/2.1.1)
+            //
+            if (!empty($message->att_local_name)) { // is this redundant? I have no idea
+                global $username, $attachment_dir;
+                $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
+                $filename = $hashed_attachment_dir . '/' . $message->att_local_name;
+
+                // using 990 because someone somewhere is folding lines at
+                // 990 instead of 998 and I'm too lazy to find who it is
+                //
+                $file_has_long_lines = file_has_long_lines($filename, 990);
+            } else
+                $file_has_long_lines = FALSE;
+
+            if ($mime_header->type0 == 'multipart' || $mime_header->type0 == 'alternative') {
                 /* no-op; no encoding needed */
+            } else if (($mime_header->type0 == 'text' || $mime_header->type0 == 'message')
+                    && !$file_has_long_lines) {
+                $header[] = 'Content-Transfer-Encoding: 8bit' .  $rn;
             } else {
                 $header[] = 'Content-Transfer-Encoding: base64' .  $rn;
             }
@@ -491,7 +545,7 @@ class Deliver {
         $cnt = count($header);
         $hdr_s = '';
         for ($i = 0 ; $i < $cnt ; $i++)    {
-            $hdr_s .= $this->foldLine($header[$i], 78,str_pad('',4));
+            $hdr_s .= $this->foldLine($header[$i], 78);
         }
         $header = $hdr_s;
         $header .= $rn; /* One blank line to separate mimeheader and body-entity */
@@ -570,7 +624,8 @@ class Deliver {
          * webmail installation does not prevent changes in user's email address.
          * See SquirrelMail bug tracker #847107 for more details about it.
          *
-         * Add $hide_squirrelmail_header as a candidate for config_local.php
+         * Add hide_squirrelmail_header as a candidate for config_local.php
+         * (must be defined as a constant:  define('hide_squirrelmail_header', 1);
          * to allow completely hiding SquirrelMail participation in message
          * processing; This is dangerous, especially if users can modify their
          * account information, as it makes mapping a sent message back to the
@@ -730,7 +785,7 @@ class Deliver {
             case 'From':
                 $hdr_s .= $header[$i];
                 break;
-            default: $hdr_s .= $this->foldLine($header[$i], 78, str_pad('',4)); break;
+            default: $hdr_s .= $this->foldLine($header[$i], 78); break;
             }
         }
         $header = $hdr_s;
@@ -740,97 +795,270 @@ class Deliver {
     }
 
     /**
-     * function foldLine - for cleanly folding of headerlines
-     *
-     * @param   string  $line
-     * @param   integer $length length to fold the line at
-     * @param   string  $pre    prefix the line with...
-     *
-     * @return string   $line folded line with trailing CRLF
-     */
-    function foldLine($line, $length, $pre='') {
-        $line = substr($line,0, -2);
-        $length -= 2; /* do not fold between \r and \n */
-        $cnt = strlen($line);
-        if ($cnt > $length) { /* try folding */
-            $fold_string = "\r\n " . $pre;
-            $bFirstFold = false;
-            $aFoldLine = array();
-            while (strlen($line) > $length) {
-                $fold = false;
-                /* handle encoded parts */
-                if (preg_match('/(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)(\s+|.*)/Ui',$line,$regs)) {
-                    $fold_tmp = $regs[1];
-                    if (!trim($regs[5])) {
-                        $fold_tmp .= $regs[5];
-                    }
-                    $iPosEnc = strpos($line,$fold_tmp);
-                    $iLengthEnc = strlen($fold_tmp);
-                    $iPosEncEnd = $iPosEnc+$iLengthEnc;
-                    if ($iPosEnc < $length && (($iPosEncEnd) > $length)) {
-                        $fold = true;
-                        /* fold just before the start of the encoded string */
-                        if ($iPosEnc) {
-                            $aFoldLine[] = substr($line,0,$iPosEnc);
-                        }
-                        $line = substr($line,$iPosEnc);
-                        if (!$bFirstFold) {
-                            $bFirstFold = true;
-                            $length -= strlen($fold_string);
-                        }
-                        if ($iLengthEnc > $length) { /* place the encoded
-                            string on a separate line and do not fold inside it*/
-                            /* minimize foldstring */
-                            $fold_string = "\r\n ";
-                            $aFoldLine[] = substr($line,0,$iLengthEnc);
-                            $line = substr($line,$iLengthEnc);
+      * Fold header lines per RFC 2822/2.2.3 and RFC 822/3.1.1
+      *
+      * Herein "soft" folding/wrapping (with whitespace tokens) is
+      * what we refer to as the preferred method of wrapping - that
+      * which we'd like to do within the $soft_wrap limit, but if
+      * not possible, we will try to do as soon as possible after
+      * $soft_wrap up to the $hard_wrap limit.  Encoded words don't
+      * need to be detected in this phase, since they cannot contain
+      * spaces.
+      *
+      * "Hard" folding/wrapping (with "hard" tokens) is what we refer
+      * to as less ideal wrapping that will be done to keep within
+      * the $hard_wrap limit.  This adds other syntactical breaking
+      * elements such as commas and encoded words.
+      *
+      * @param string  $header    The header content being folded
+      * @param integer $soft_wrap The desirable maximum line length
+      *                           (OPTIONAL; default is 78, per RFC)
+      * @param string  $indent    Wrapped lines will already have
+      *                           whitespace following the CRLF wrap,
+      *                           but you can add more indentation (or
+      *                           whatever) with this.  The use of this
+      *                           parameter is DISCOURAGED, since it
+      *                           can corrupt the redisplay (unfolding)
+      *                           of headers whose content is space-
+      *                           sensitive, like subjects, etc.
+      *                           (OPTIONAL; default is an empty string)
+      * @param string  $hard_wrap The absolute maximum line length
+      *                           (OPTIONAL; default is 998, per RFC)
+      *
+      * @return string The folded header content, with a trailing CRLF.
+      *
+      */
+    function foldLine($header, $soft_wrap=78, $indent='', $hard_wrap=998) {
+
+        // the "hard" token list can be altered if desired,
+        // for example, by adding ":"
+        // (in the future, we can take optional arguments
+        // for overriding or adding elements to the "hard"
+        // token list if we want to get fancy)
+        //
+        // the order of these is significant - preferred
+        // fold points should be listed first
+        //
+        // it is advised that the "=" always come first
+        // since it also finds encoded words, thus if it
+        // comes after some other token that happens to
+        // fall within the encoded word, the encoded word
+        // could be inadvertently broken in half, which
+        // is not allowable per RFC
+        //
+        $hard_break_tokens = array(
+            '=',  // includes encoded word detection
+            ',',
+            ';',
+        );
+
+        // the order of these is significant too
+        //
+        $whitespace = array(
+            ' ',
+            "\t",
+        );
+
+        $CRLF = "\r\n";
+
+        $folded_header = '';
+
+        // if using an indent string, reduce wrap limits by its size
+        //
+        if (!empty($indent)) {
+            $soft_wrap -= strlen($indent);
+            $hard_wrap -= strlen($indent);
+        }
+
+        while (strlen($header) > $soft_wrap) {
+
+            $soft_wrapped_line = substr($header, 0, $soft_wrap);
+
+            // look for a token as close to the end of the soft wrap limit as possible
+            //
+            foreach ($whitespace as $token) {
+
+                // note that this if statement also fails when $pos === 0,
+                // which is intended, since blank lines are not allowed
+                //
+                if ($pos = strrpos($soft_wrapped_line, $token))
+                {
+                    $new_fold = substr($header, 0, $pos);
+
+                    // make sure proposed fold doesn't create a blank line
+                    //
+                    if (!trim($new_fold)) continue;
+
+                    // with whitespace breaks, we fold BEFORE the token
+                    //
+                    $folded_header .= $new_fold . $CRLF . $indent;
+                    $header = substr($header, $pos);
+
+                    // ready for next while() iteration
+                    //
+                    continue 2;
+
+                }
+
+            }
+
+            // we were unable to find a wrapping point within the soft
+            // wrap limit, so now we'll try to find the first possible
+            // soft wrap point within the hard wrap limit
+            //
+            $hard_wrapped_line = substr($header, 0, $hard_wrap);
+
+            // look for a *SOFT* token as close to the
+            // beginning of the hard wrap limit as possible
+            //
+            foreach ($whitespace as $token) {
+
+                // use while loop instead of if block because it
+                // is possible we don't want the first one we find
+                //
+                $pos = $soft_wrap - 1; // -1 is corrected by +1 on next line
+                while ($pos = strpos($hard_wrapped_line, $token, $pos + 1))
+                {
+
+                    $new_fold = substr($header, 0, $pos);
+
+                    // make sure proposed fold doesn't create a blank line
+                    //
+                    if (!trim($new_fold)) continue;
+
+                    // with whitespace breaks, we fold BEFORE the token
+                    //
+                    $folded_header .= $new_fold . $CRLF . $indent;
+                    $header = substr($header, $pos);
+
+                    // ready for next outter while() iteration
+                    //
+                    continue 3;
+
+                }
+
+            }
+
+            // we were still unable to find a soft wrapping point within
+            // both the soft and hard wrap limits, so if the length of
+            // what is left is no more than the hard wrap limit, we'll
+            // simply take the whole thing
+            //
+            if (strlen($header) <= strlen($hard_wrapped_line))
+                break;
+
+            // otherwise, we can't quit yet - look for a "hard" token
+            // as close to the end of the hard wrap limit as possible
+            //
+            foreach ($hard_break_tokens as $token) {
+
+                // note that this if statement also fails when $pos === 0,
+                // which is intended, since blank lines are not allowed
+                //
+                if ($pos = strrpos($hard_wrapped_line, $token))
+                {
+
+                    // if we found a "=" token, we must determine whether,
+                    // if it is part of an encoded word, it is the beginning
+                    // or middle of one, where we need to readjust $pos a bit
+                    //
+                    if ($token == '=') {
+
+                        // if we found the beginning of an encoded word,
+                        // we want to break BEFORE the token
+                        //
+                        if (preg_match('/^(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)/Ui',
+                                       substr($header, $pos))) {
+                            $pos--;
                         }
-                    } else if ($iPosEnc < $length) { /* the encoded string fits into the foldlength */
-                        /*remainder */
-                        $sLineRem = substr($line,$iPosEncEnd,$length - $iPosEncEnd);
-                        if (preg_match('/^(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)(.*)/Ui',$sLineRem) || !preg_match('/[=,;\s]/',$sLineRem)) {
-                            /*impossible to fold clean in the next part -> fold after the enc string */
-                            $aFoldLine[] = substr($line,0,$iPosEncEnd);
-                            $line = substr($line,$iPosEncEnd);
-                            $fold = true;
-                            if (!$bFirstFold) {
-                                $bFirstFold = true;
-                                $length -= strlen($fold_string);
-                            }
+
+                        // check if we found this token in the *middle*
+                        // of an encoded word, in which case we have to
+                        // ignore it, pushing back to the token that
+                        // starts the encoded word instead
+                        //
+                        // of course, this is only possible if there is
+                        // more content after the next hard wrap
+                        //
+                        // then look for the end of an encoded word in
+                        // the next part (past the next hard wrap)
+                        //
+                        // then see if it is in fact part of a legitimate
+                        // encoded word
+                        //
+                        else if (strlen($header) > $hard_wrap
+                         && ($end_pos = strpos(substr($header, $hard_wrap), '?=')) !== FALSE
+                         && preg_match('/(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)$/Ui',
+                                       substr($header, 0, $hard_wrap + $end_pos + 2),
+                                       $matches)) {
+
+                            $pos = $hard_wrap + $end_pos + 2 - strlen($matches[1]) - 1;
+
                         }
-                    }
-                }
-                if (!$fold) {
-                    $line_tmp = substr($line,0,$length);
-                    $iFoldPos = false;
-                    /* try to fold at logical places */
-                    switch (true)
-                    {
-                    case ($iFoldPos = strrpos($line_tmp,',')): break;
-                    case ($iFoldPos = strrpos($line_tmp,';')): break;
-                    case ($iFoldPos = strrpos($line_tmp,' ')): break;
-                    case ($iFoldPos = strrpos($line_tmp,'=')): break;
-                    default: break;
-                    }
 
-                    if (!$iFoldPos) { /* clean folding didn't work */
-                        $iFoldPos = $length;
                     }
-                    $aFoldLine[] = substr($line,0,$iFoldPos+1);
-                    $line = substr($line,$iFoldPos+1);
-                    if (!$bFirstFold) {
-                        $bFirstFold = true;
-                        $length -= strlen($fold_string);
+
+                    // $pos could have been changed; make sure it's
+                    // not at the beginning of the line, as blank
+                    // lines are not allowed
+                    //
+                    if ($pos === 0) continue;
+
+                    // we are dealing with a simple token break...
+                    //
+                    // for non-whitespace breaks, we fold AFTER the token
+                    // and add a space after the fold if not immediately
+                    // followed by a whitespace character in the next part
+                    //
+                    $folded_header .= substr($header, 0, $pos + 1) . $CRLF;
+
+                    // don't go beyond end of $header, though
+                    //
+                    if (strlen($header) > $pos + 1) {
+                        $header = substr($header, $pos + 1);
+                        if (!in_array($header{0}, $whitespace))
+                            $header = ' ' . $indent . $header;
+                    } else {
+                        $header = '';
                     }
+
+                    // ready for next while() iteration
+                    //
+                    continue 2;
+
                 }
+
             }
-            /*$reconstruct the line */
-            if ($line) {
-                $aFoldLine[] = $line;
+
+            // finally, we just couldn't find anything to fold on, so we
+            // have to just cut it off at the hard limit
+            //
+            $folded_header .= $hard_wrapped_line . $CRLF;
+
+            // is there more?
+            //
+            if (strlen($header) > strlen($hard_wrapped_line)) {
+                $header = substr($header, strlen($hard_wrapped_line));
+                if (!in_array($header{0}, $whitespace))
+                    $header = ' ' . $indent . $header;
+            } else {
+                $header = '';
             }
-            $line = implode($fold_string,$aFoldLine);
+
         }
-        return $line."\r\n";
+
+
+        // add any left-overs
+        //
+        $folded_header .= $header;
+
+
+        // make sure it ends with a CRLF
+        //
+        if (substr($folded_header, -2) != $CRLF) $folded_header .= $CRLF;
+
+
+        return $folded_header;
     }
 
     /**