Happy New Year
[squirrelmail.git] / class / deliver / Deliver.class.php
index bb02bdde09bdb5599eae75564a3e5af896499f78..d145cefeb0e0beb01998fc625a9fc8c4b911737c 100644 (file)
@@ -7,7 +7,7 @@
  * a delivery backend.
  *
  * @author Marc Groot Koerkamp
- * @copyright © 1999-2007 The SquirrelMail Project Team
+ * @copyright 1999-2020 The SquirrelMail Project Team
  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
  * @package squirrelmail
@@ -32,7 +32,12 @@ class Deliver {
      * function mail - send the message parts to the SMTP stream
      *
      * @param Message  $message      Message object to send
-     * @param resource $stream       Handle to the SMTP stream
+     *                               NOTE that this is passed by
+     *                               reference and will be modified
+     *                               upon return with updated
+     *                               fields such as Message ID, References,
+     *                               In-Reply-To and Date headers.
+     * @param resource $stream       Handle to the outgoing stream
      *                               (when FALSE, nothing will be
      *                               written to the stream; this can
      *                               be used to determine the actual
@@ -47,18 +52,31 @@ class Deliver {
      *                               message inside another (OPTIONAL; caller
      *                               should ONLY specify a value for this 
      *                               when the message being sent is a reply)
+     * @param resource $imap_stream  If there is an open IMAP stream in
+     *                               the caller's context, it should be
+     *                               passed in here.  This is OPTIONAL,
+     *                               as one will be created if not given,
+     *                               but as some IMAP servers may baulk
+     *                               at opening more than one connection
+     *                               at a time, the caller should always
+     *                               abide if possible.  Currently, this
+     *                               stream is only used when $reply_id
+     *                               is also non-zero, but that is subject
+     *                               to change.
      * @param mixed    $extra        Any implementation-specific variables
      *                               can be passed in here and used in
      *                               an overloaded version of this method
      *                               if needed.
      *
-     * @return integer $raw_length The number of bytes written (or that would 
-     *                             have been written) to the output stream
+     * @return integer The number of bytes written (or that would have been
+     *                 written) to the output stream.
+     *
      */
-    function mail($message, $stream=false, $reply_id=0, $reply_ent_id=0,
-                  $extra=NULL) {
+    function mail(&$message, $stream=false, $reply_id=0, $reply_ent_id=0,
+                  $imap_stream=NULL, $extra=NULL) {
+
+        $rfc822_header = &$message->rfc822_header;
 
-        $rfc822_header = $message->rfc822_header;
         if (count($message->entities)) {
             $boundary = $this->mimeBoundary();
             $rfc822_header->content_type->properties['boundary']='"'.$boundary.'"';
@@ -72,14 +90,29 @@ class Deliver {
         //
         if ($reply_id) {
             global $imapConnection, $username, $imapServerAddress, 
-                   $imapPort, $mailbox;
+                   $imapPort, $imap_stream_options, $mailbox;
+
+            // try our best to use an existing IMAP handle
+            //
+            $close_imap_stream = FALSE;
+            if (is_resource($imap_stream)) {
+                $my_imap_stream = $imap_stream;
+
+            } else if (is_resource($imapConnection)) {
+                $my_imap_stream = $imapConnection;
+
+            } else {
+                $close_imap_stream = TRUE;
+                $my_imap_stream = sqimap_login($username, FALSE, $imapServerAddress,
+                                               $imapPort, 0, $imap_stream_options);
+            }
 
-            if (!is_resource($imapConnection))
-                $imapConnection = sqimap_login($username, FALSE,
-                                               $imapServerAddress, $imapPort, 0);
+            sqimap_mailbox_select($my_imap_stream, $mailbox);
+            $reply_message = sqimap_get_message($my_imap_stream, $reply_id, $mailbox);
 
-            sqimap_mailbox_select($imapConnection, $mailbox);
-            $reply_message = sqimap_get_message($imapConnection, $reply_id, $mailbox);
+            if ($close_imap_stream) {
+                sqimap_logout($my_imap_stream);
+            }
 
             if ($reply_ent_id) {
                 /* redefine the messsage in case of message/rfc822 */
@@ -249,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);
+                        }
                     }
-                    $last = $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;
+                    }
+                }
+
                 fclose($file);
             }
             break;
@@ -369,16 +437,27 @@ class Deliver {
      *
      * This function is not yet implemented.
      * Reserved for extended functionality.
+     * UPDATE: It is implemented in Deliver_SMTP and Deliver_SendMail classes,
+     *         but it remains unimplemented in this base class (and thus not
+     *         in Deliver_IMAP or other child classes that don't define it)
+     *
+     * NOTE: some parameters are specific to the child class
+     *       that is implementing this method
      *
      * @param Message $message  Message object
+     * @param string  $domain
+     * @param integer $length
      * @param string  $host     host name or IP to connect to
+     * @param integer $port
      * @param string  $user     username to log into the SMTP server with
      * @param string  $pass     password to log into the SMTP server with
-     * @param integer $length
+     * @param boolean $authpop  whether or not to use POP-before-SMTP authorization
+     * @param string  $pop_host host name or IP to connect to for POP-before-SMTP authorization
+     * @param array   $stream_options Stream context options, see config_local.example.php for more details (OPTIONAL)
      *
      * @return handle $stream file handle resource to SMTP stream
      */
-    function initStream($message, $length=0, $host='', $port='', $user='', $pass='') {
+    function initStream($message, $domain, $length=0, $host='', $port='', $user='', $pass='', $authpop=false, $pop_host='', $stream_options=array()) {
         return $stream;
     }
 
@@ -428,7 +507,28 @@ class Deliver {
         if ($mime_header->encoding) {
             $header[] = 'Content-Transfer-Encoding: ' . $mime_header->encoding . $rn;
         } else {
-            if ($mime_header->type0 == 'text' || $mime_header->type0 == 'message') {
+
+            // 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;
@@ -456,7 +556,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 */
@@ -476,12 +576,14 @@ class Deliver {
      *
      * @return string $header
      */
-    function prepareRFC822_Header($rfc822_header, $reply_rfc822_header, &$raw_length) {
+    function prepareRFC822_Header(&$rfc822_header, $reply_rfc822_header, &$raw_length) {
         global $domain, $username, $encode_header_key,
                $edit_identity, $hide_auth_header;
 
-        /* if server var SERVER_NAME not available, use $domain */
-        if(!sqGetGlobalVar('SERVER_NAME', $SERVER_NAME, SQ_SERVER)) {
+        /* if server var SERVER_NAME not available, or contains
+           ":" (e.g. IPv6) which is illegal in a Message-ID, use $domain */
+        if(!sqGetGlobalVar('SERVER_NAME', $SERVER_NAME, SQ_SERVER) ||
+            strpos($SERVER_NAME,':') !== FALSE) {
             $SERVER_NAME = $domain;
         }
 
@@ -494,17 +596,24 @@ class Deliver {
         $rn = "\r\n";
 
         /* This creates an RFC 822 date */
-        $date = date('D, j M Y H:i:s ', time()) . $this->timezone();
+        $now = time();
+        $now_date = date('D, j M Y H:i:s ', $now) . $this->timezone();
+        // TODO: Do we really want to preserve possibly old date?  Date header should always have "now"... but here is not where this decision should be made -- the caller really should blank out $rfc822_header->date even for drafts being re-edited or sent
+        if (!empty($rfc822_header->date) && $rfc822_header->date != -1)
+            $message_date = date('D, j M Y H:i:s ', $rfc822_header->date) . $this->timezone();
+        else {
+            $message_date = $now_date;
+            $rfc822_header->date = $now;
+        }
+
         /* Create a message-id */
-        $message_id = '<' . (!empty($REMOTE_PORT) ? $REMOTE_PORT . '.' : '');
-//FIXME: if $REMOTE_ADDR is missing, should we skip this if/else block?  or perhaps try to generate it with some different kind of info?
-        if (isset($encode_header_key) && trim($encode_header_key)!='') {
-            // use encrypted form of remote address
-            $message_id.= OneTimePadEncrypt($this->ip2hex($REMOTE_ADDR),base64_encode($encode_header_key));
-        } else {
-            $message_id.= $REMOTE_ADDR;
+        $message_id = 'MESSAGE ID GENERATION ERROR! PLEASE CONTACT SQUIRRELMAIL DEVELOPERS';
+        if (empty($rfc822_header->message_id)) {
+            $message_id = '<'
+                        . md5(GenerateRandomString(16, '', 7) . uniqid(mt_rand(),true))
+                        . '.squirrel@' . $SERVER_NAME .'>';
         }
-        $message_id .= '.' . time() . '.squirrel@' . $SERVER_NAME .'>';
+
         /* Make an RFC822 Received: line */
         if (isset($REMOTE_HOST)) {
             $received_from = "$REMOTE_HOST ([$REMOTE_ADDR])";
@@ -528,7 +637,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
@@ -536,6 +646,7 @@ class Deliver {
          */
         $show_sm_header = ( defined('hide_squirrelmail_header') ? ! hide_squirrelmail_header : 1 );
 
+        // FIXME: The following headers may generate slightly differently between the message sent to the destination and that stored in the Sent folder because this code will be called before both actions.  This is not necessarily a big problem, but other headers such as Message-ID and Date are preserved between both actions 
         if ( $show_sm_header ) {
           if (isset($encode_header_key) &&
             trim($encode_header_key)!='') {
@@ -547,25 +658,35 @@ class Deliver {
           } else {
             // use default received headers
             $header[] = "Received: from $received_from" . $rn;
-            if ($edit_identity || ! isset($hide_auth_header) || ! $hide_auth_header)
+            if (!isset($hide_auth_header) || !$hide_auth_header)
                 $header[] = "        (SquirrelMail authenticated user $username)" . $rn;
             $header[] = "        by $SERVER_NAME with HTTP;" . $rn;
-            $header[] = "        $date" . $rn;
+            $header[] = "        $now_date" . $rn;
           }
         }
 
         /* Insert the rest of the header fields */
-        $header[] = 'Message-ID: '. $message_id . $rn;
+
+        if (!empty($rfc822_header->message_id)) {
+            $header[] = 'Message-ID: '. $rfc822_header->message_id . $rn;
+        } else {
+            $header[] = 'Message-ID: '. $message_id . $rn;
+            $rfc822_header->message_id = $message_id;
+        }
+
         if (is_object($reply_rfc822_header) &&
             isset($reply_rfc822_header->message_id) &&
             $reply_rfc822_header->message_id) {
             $rep_message_id = $reply_rfc822_header->message_id;
-        //        $this->strip_crlf($message_id);
             $header[] = 'In-Reply-To: '.$rep_message_id . $rn;
+            $rfc822_header->in_reply_to = $rep_message_id;
             $references = $this->calculate_references($reply_rfc822_header);
             $header[] = 'References: '.$references . $rn;
+            $rfc822_header->references = $references;
         }
-        $header[] = "Date: $date" . $rn;
+
+        $header[] = "Date: $message_date" . $rn;
+
         $header[] = 'Subject: '.encodeHeader($rfc822_header->subject) . $rn;
         $header[] = 'From: '. $rfc822_header->getAddr_s('from',",$rn ",true) . $rn;
 
@@ -672,7 +793,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;
@@ -682,97 +803,321 @@ 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) {
+
+        // allow folding after the initial colon and space?
+        // (only supported if the header name is within the $soft_wrap limit)
+        //
+        $allow_fold_after_header_name = FALSE;
+
+        // 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";
+
+        // switch that helps compact the last line, pasting it at the
+        // end of the one before if the one before is already over the
+        // soft limit and it wouldn't go over the hard limit
+        //
+        $pull_last_line_up_if_second_to_last_is_already_over_soft_limit = FALSE;
+
+
+        // ----- end configurable behaviors -----
+
+
+        $folded_header = '';
+
+        // if we want to prevent a wrap right after the
+        // header name, make note of the position here
+        //
+        if (!$allow_fold_after_header_name
+         && ($header_name_end_pos = strpos($header, ':'))
+         && strlen($header) > $header_name_end_pos + 1
+         && in_array($header{$header_name_end_pos + 1}, $whitespace))
+            $header_name_end_pos++;
+
+        // 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))
+                {
+
+                    // make sure proposed fold isn't forbidden
+                    //
+                    if (!$allow_fold_after_header_name
+                     && $pos === $header_name_end_pos)
+                        continue;
+
+                    $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) <= $hard_wrap) {
+
+                // if the header has been folded at least once before now,
+                // let's see if we can add the remaining chunk to the last
+                // fold (this is mainly just aesthetic)
+                //
+                if ($pull_last_line_up_if_second_to_last_is_already_over_soft_limit
+                 && strlen($folded_header)
+                 // last fold is conveniently in $new_fold
+                 && strlen($new_fold) + strlen($header) <= $hard_wrap) {
+                    // $last_fold = substr(substr($folded_header, 0, -(strlen($CRLF) + strlen($indent))), 
+                    // remove CRLF and indentation and paste the rest of the header on
+                    $folded_header = substr($folded_header, 0, -(strlen($CRLF) + strlen($indent))) . $header;
+                    $header = '';
+                }
+
+                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
+                    //
+                    // $new_fold is used above, it's assumed we update it upon every fold action
+                    $new_fold = substr($header, 0, $pos + 1);
+                    $folded_header .= $new_fold . $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
+            //
+            // $new_fold is used above, it's assumed we update it upon every fold action
+            $new_fold = $hard_wrapped_line;
+            $folded_header .= $new_fold . $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;
     }
 
     /**
@@ -800,7 +1145,7 @@ class Deliver {
      * @return string $result with timezone and offset
      */
     function timezone () {
-        global $invert_time;
+        global $invert_time, $show_timezone_name;
 
         $diff_second = date('Z');
         if ($invert_time) {
@@ -814,9 +1159,24 @@ class Deliver {
         $diff_second = abs($diff_second);
         $diff_hour = floor ($diff_second / 3600);
         $diff_minute = floor (($diff_second-3600*$diff_hour) / 60);
-        $zonename = '('.strftime('%Z').')';
-        $result = sprintf ("%s%02d%02d %s", $sign, $diff_hour, $diff_minute,
-                       $zonename);
+
+        // If an administrator wants to add the timezone name to the
+        // end of the date header, they can set $show_timezone_name
+        // to boolean TRUE in config/config_local.php, but that is
+        // NOT RFC-822 compliant (see section 5.1).  Moreover, some
+        // Windows users reported that strftime('%Z') was returning
+        // the full zone name (not the abbreviation) which in some
+        // cases included 8-bit characters (not allowed as is in headers).
+        // The PHP manual actually does NOT promise what %Z will return
+        // for strftime!:  "The time zone offset/abbreviation option NOT
+        // given by %z (depends on operating system)"
+        //
+        if ($show_timezone_name) {
+            $zonename = '('.strftime('%Z').')';
+            $result = sprintf ("%s%02d%02d %s", $sign, $diff_hour, $diff_minute, $zonename);
+        } else {
+            $result = sprintf ("%s%02d%02d", $sign, $diff_hour, $diff_minute);
+        }
         return ($result);
     }