Legacy code was probably wrong
[squirrelmail.git] / class / deliver / Deliver.class.php
index c732bea5669204fe60c18b76c2eae50ffcb099fb..aa88eee68aae77f41412c98f672955bbc53b110c 100644 (file)
@@ -1,15 +1,15 @@
 <?php
+
 /**
  * Deliver.class.php
  *
- * Copyright (c) 1999-2005 The SquirrelMail Project Team
- * Licensed under the GNU GPL. For full terms see the file COPYING.
- *
  * This contains all the functions needed to send messages through
  * a delivery backend.
  *
+ * @author Marc Groot Koerkamp
+ * @copyright 1999-2017 The SquirrelMail Project Team
+ * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
- * @author  Marc Groot Koerkamp
  * @package squirrelmail
  */
 
@@ -31,13 +31,52 @@ class Deliver {
     /**
      * function mail - send the message parts to the SMTP stream
      *
-     * @param Message  $message  Message class to send
-     * @param resource $stream   file handle to the SMTP stream
+     * @param Message  $message      Message object to send
+     *                               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
+     *                               number of bytes that will be
+     *                               written to the stream)
+     * @param string   $reply_id     Identifies message being replied to
+     *                               (OPTIONAL; caller should ONLY specify
+     *                               a value for this when the message
+     *                               being sent is a reply)
+     * @param string   $reply_ent_id Identifies message being replied to
+     *                               in the case it was an embedded/attached
+     *                               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 The number of bytes written (or that would have been
+     *                 written) to the output stream.
      *
-     * @return integer $raw_length
      */
-    function mail($message, $stream=false) {
-        $rfc822_header = $message->rfc822_header;
+    function mail(&$message, $stream=false, $reply_id=0, $reply_ent_id=0,
+                  $imap_stream=NULL, $extra=NULL) {
+
+        $rfc822_header = &$message->rfc822_header;
+
         if (count($message->entities)) {
             $boundary = $this->mimeBoundary();
             $rfc822_header->content_type->properties['boundary']='"'.$boundary.'"';
@@ -45,16 +84,95 @@ class Deliver {
             $boundary='';
         }
         $raw_length = 0;
+
+
+        // calculate reply header if needed
+        //
+        if ($reply_id) {
+            global $imapConnection, $username, $imapServerAddress, 
+                   $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);
+            }
+
+            sqimap_mailbox_select($my_imap_stream, $mailbox);
+            $reply_message = sqimap_get_message($my_imap_stream, $reply_id, $mailbox);
+
+            if ($close_imap_stream) {
+                sqimap_logout($my_imap_stream);
+            }
+
+            if ($reply_ent_id) {
+                /* redefine the messsage in case of message/rfc822 */
+                $reply_message = $message->getEntity($reply_ent_id);
+                /* message is an entity which contains the envelope and type0=message
+                 * and type1=rfc822. The actual entities are childs from
+                 * $reply_message->entities[0]. That's where the encoding and is located
+                 */
+
+                $orig_header = $reply_message->rfc822_header; /* here is the envelope located */
+
+            } else {
+                $orig_header = $reply_message->rfc822_header;
+            }
+            $message->reply_rfc822_header = $orig_header;
+        }
+
+
         $reply_rfc822_header = (isset($message->reply_rfc822_header)
                              ? $message->reply_rfc822_header : '');
         $header = $this->prepareRFC822_Header($rfc822_header, $reply_rfc822_header, $raw_length);
 
+        $this->send_mail($message, $header, $boundary, $stream, $raw_length, $extra);
+
+        return $raw_length;
+    }
+
+    /**
+     * function send_mail - send the message parts to the IMAP stream
+     *
+     * @param Message  $message      Message object to send
+     * @param string   $header       Headers ready to send
+     * @param string   $boundary     Message parts boundary
+     * @param resource $stream       Handle to the SMTP stream
+     *                               (when FALSE, nothing will be
+     *                               written to the stream; this can
+     *                               be used to determine the actual
+     *                               number of bytes that will be
+     *                               written to the stream)
+     * @param int     &$raw_length   The number of bytes written (or that
+     *                               would have been written) to the 
+     *                               output stream - NOTE that this is
+     *                               passed by reference
+     * @param mixed    $extra        Any implementation-specific variables
+     *                               can be passed in here and used in
+     *                               an overloaded version of this method
+     *                               if needed.
+     *
+     * @return void
+     *
+     */
+    function send_mail($message, $header, $boundary, $stream=false, 
+                       &$raw_length, $extra=NULL) {
+
+
         if ($stream) {
             $this->preWriteToStream($header);
             $this->writeToStream($stream, $header);
         }
         $this->writeBody($message, $stream, $raw_length, $boundary);
-        return $raw_length;
     }
 
     /**
@@ -65,6 +183,11 @@ class Deliver {
      *
      * @param Message   $message      Message object to transform
      * @param resource  $stream       SMTP output stream
+     *                                (when FALSE, nothing will be
+     *                                written to the stream; this can
+     *                                be used to determine the actual
+     *                                number of bytes that will be
+     *                                written to the stream)
      * @param integer  &$length_raw   raw length of the message (part)
      *                                as returned by mail fn
      * @param string    $boundary     custom boundary to call, usually for subparts
@@ -123,6 +246,11 @@ class Deliver {
      *
      * @param Message   $message      Message object to transform
      * @param resource  $stream       SMTP output stream
+     *                                (when FALSE, nothing will be
+     *                                written to the stream; this can
+     *                                be used to determine the actual
+     *                                number of bytes that will be
+     *                                written to the stream)
      * @param integer  &$length       length of the message part
      *                                as returned by mail fn
      *
@@ -151,18 +279,55 @@ class Deliver {
                 }
                 $last = $body_part;
             } elseif ($message->att_local_name) {
+                global $username, $attachment_dir;
+                $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
                 $filename = $message->att_local_name;
-                $file = fopen ($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);
+
+                // 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');
+
+                // 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;
@@ -176,10 +341,17 @@ class Deliver {
                     $this->writeToStream($stream, $body_part);
                 }
             } elseif ($message->att_local_name) {
+                global $username, $attachment_dir;
+                $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
                 $filename = $message->att_local_name;
-                $file = fopen ($filename, 'rb');
+                $file = fopen ($hashed_attachment_dir . '/' . $filename, 'rb');
                 while ($tmp = fread($file, 570)) {
-                   $body_part = chunk_split(base64_encode($tmp));
+                    $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);
@@ -265,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;
     }
 
@@ -324,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;
@@ -352,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 */
@@ -372,11 +576,14 @@ class Deliver {
      *
      * @return string $header
      */
-    function prepareRFC822_Header($rfc822_header, $reply_rfc822_header, &$raw_length) {
-        global $domain, $version, $username, $skip_SM_header;
+    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;
         }
 
@@ -389,10 +596,16 @@ class Deliver {
         $rn = "\r\n";
 
         /* This creates an RFC 822 date */
-        $date = date('D, j M Y H:i:s ', mktime()) . $this->timezone();
+        $date = date('D, j M Y H:i:s ', time()) . $this->timezone();
+
         /* Create a message-id */
-        $message_id = '<' . $REMOTE_PORT . '.' . $REMOTE_ADDR . '.';
-        $message_id .= time() . '.squirrel@' . $SERVER_NAME .'>';
+        $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 .'>';
+        }
+
         /* Make an RFC822 Received: line */
         if (isset($REMOTE_HOST)) {
             $received_from = "$REMOTE_HOST ([$REMOTE_ADDR])";
@@ -406,27 +619,76 @@ class Deliver {
             $received_from .= " (proxying for $HTTP_X_FORWARDED_FOR)";
         }
         $header = array();
-        if ( !isset($skip_SM_header) || !$skip_SM_header )
-        {
-          $header[] = "Received: from $received_from" . $rn;
-          $header[] = "        (SquirrelMail authenticated user $username)" . $rn;
-          $header[] = "        by $SERVER_NAME with HTTP;" . $rn;
-          $header[] = "        $date" . $rn;
+
+        /**
+         * SquirrelMail header
+         *
+         * This Received: header provides information that allows to track
+         * user and machine that was used to send email. Don't remove it
+         * unless you understand all possible forging issues or your
+         * 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
+         * (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
+         * original sender almost impossible.
+         */
+        $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)!='') {
+            // use encoded headers, if encryption key is set and not empty
+            $header[] = 'X-Squirrel-UserHash: '.OneTimePadEncrypt($username,base64_encode($encode_header_key)).$rn;
+            $header[] = 'X-Squirrel-FromHash: '.OneTimePadEncrypt($this->ip2hex($REMOTE_ADDR),base64_encode($encode_header_key)).$rn;
+            if (isset($HTTP_X_FORWARDED_FOR))
+                $header[] = 'X-Squirrel-ProxyHash:'.OneTimePadEncrypt($this->ip2hex($HTTP_X_FORWARDED_FOR),base64_encode($encode_header_key)).$rn;
+          } else {
+            // use default received headers
+            $header[] = "Received: from $received_from" . $rn;
+            if (!isset($hide_auth_header) || !$hide_auth_header)
+                $header[] = "        (SquirrelMail authenticated user $username)" . $rn;
+            $header[] = "        by $SERVER_NAME with HTTP;" . $rn;
+            $header[] = "        $date" . $rn;
+          }
         }
+
         /* Insert the rest of the header fields */
-        $header[] = 'Message-ID: '. $message_id . $rn;
-        if ($reply_rfc822_header->message_id) {
+
+        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;
+        }
+
+        if (!empty($rfc822_header->date) && $rfc822_header->date != -1) {
+            $header[] = 'Date: '. $rfc822_header->date . $rn;
+        } else {
+            $header[] = "Date: $date" . $rn;
+            $rfc822_header->date = $date;
         }
-        $header[] = "Date: $date" . $rn;
+
         $header[] = 'Subject: '.encodeHeader($rfc822_header->subject) . $rn;
         $header[] = 'From: '. $rfc822_header->getAddr_s('from',",$rn ",true) . $rn;
 
-        // folding address list [From|To|Cc|Bcc] happens by using ",$rn<space>" as delimiter
+        // folding address list [From|To|Cc|Bcc] happens by using ",$rn<space>"
+        // as delimiter
         // Do not use foldLine for that.
 
         // RFC2822 if from contains more then 1 address
@@ -453,7 +715,7 @@ class Deliver {
             }
         }
         /* Identify SquirrelMail */
-        $header[] = 'User-Agent: SquirrelMail/' . $version . $rn;
+        $header[] = 'User-Agent: SquirrelMail/' . SM_VERSION . $rn;
         /* Do the MIME-stuff */
         $header[] = 'MIME-Version: 1.0' . $rn;
         $contenttype = 'Content-Type: '. $rfc822_header->content_type->type0 .'/'.
@@ -469,7 +731,7 @@ class Deliver {
         if ($encoding = $rfc822_header->encoding) {
             $header[] = 'Content-Transfer-Encoding: ' . $encoding .  $rn;
         }
-        if ($rfc822_header->dnt) {
+        if (isset($rfc822_header->dnt) && $rfc822_header->dnt) {
             $dnt = $rfc822_header->getAddr_s('dnt');
             /* Pegasus Mail */
             $header[] = 'X-Confirm-Reading-To: '.$dnt. $rn;
@@ -511,7 +773,9 @@ class Deliver {
                 $aRefs = explode(' ',$sRefs);
                 $sLine = 'References:';
                 foreach ($aRefs as $sReference) {
-                    if (strlen($sLine)+strlen($sReference) >76) {
+                    if ( trim($sReference) == '' ) {
+                         /* Don't add spaces. */
+                    } elseif (strlen($sLine)+strlen($sReference) >76) {
                         $hdr_s .= $sLine;
                         $sLine = $rn . '    ' . $sReference;
                     } else {
@@ -526,7 +790,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;
@@ -536,97 +800,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;
     }
 
     /**
@@ -654,7 +1091,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) {
@@ -668,34 +1105,118 @@ 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);
     }
 
     /**
-     * function calculate_references - calculate correct Referer string
+     * function calculate_references - calculate correct References string
+     * Adds the current message ID, and makes sure it doesn't grow forever,
+     * to that extent it drops message-ID's in a smart way until the string
+     * length is under the recommended value of 1000 ("References: <986>\r\n").
+     * It always keeps the first and the last three ID's.
      *
      * @param   Rfc822Header $hdr    message header to calculate from
      *
-     * @return  string       $refer  concatenated and trimmed Referer string
+     * @return  string       $refer  concatenated and trimmed References string
      */
     function calculate_references($hdr) {
-        $refer = $hdr->references;
+        $aReferences = preg_split('/\s+/', $hdr->references);
         $message_id = $hdr->message_id;
         $in_reply_to = $hdr->in_reply_to;
-        if (strlen($refer) > 2) {
-            $refer .= ' ' . $message_id;
-        } else {
-            if ($in_reply_to) {
-                $refer .= $in_reply_to . ' ' . $message_id;
-            } else {
-                $refer .= $message_id;
+
+        // if References already exists, add the current message ID at the end.
+        // no References exists; if we know a IRT, add that aswell
+        if (count($aReferences) == 0 && $in_reply_to) {
+            $aReferences[] = $in_reply_to;
+        }
+        $aReferences[] = $message_id;
+
+        // sanitize the array: trim whitespace, remove dupes
+        array_walk($aReferences, 'sq_trim_value');
+        $aReferences = array_unique($aReferences);
+
+        while ( count($aReferences) > 4 && strlen(implode(' ', $aReferences)) >= 986 ) {
+            $aReferences = array_merge(array_slice($aReferences,0,1),array_slice($aReferences,2));
+        }
+        return implode(' ', $aReferences);
+    }
+
+    /**
+     * Converts ip address to hexadecimal string
+     *
+     * Function is used to convert ipv4 and ipv6 addresses to hex strings.
+     * It removes all delimiter symbols from ip addresses, converts decimal
+     * ipv4 numbers to hex and pads strings in order to present full length
+     * address. ipv4 addresses are represented as 8 byte strings, ipv6 addresses
+     * are represented as 32 byte string.
+     *
+     * If function fails to detect address format, it returns unprocessed string.
+     * @param string $string ip address string
+     * @return string processed ip address string
+     * @since 1.5.1 and 1.4.5
+     */
+    function ip2hex($string) {
+        if (preg_match("/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/",$string,$match)) {
+            // ipv4 address
+            $ret = str_pad(dechex($match[1]),2,'0',STR_PAD_LEFT)
+                . str_pad(dechex($match[2]),2,'0',STR_PAD_LEFT)
+                . str_pad(dechex($match[3]),2,'0',STR_PAD_LEFT)
+                . str_pad(dechex($match[4]),2,'0',STR_PAD_LEFT);
+        } elseif (preg_match("/^([0-9a-h]+)\:([0-9a-h]+)\:([0-9a-h]+)\:([0-9a-h]+)\:([0-9a-h]+)\:([0-9a-h]+)\:([0-9a-h]+)\:([0-9a-h]+)$/i",$string,$match)) {
+            // full ipv6 address
+            $ret = str_pad($match[1],4,'0',STR_PAD_LEFT)
+                . str_pad($match[2],4,'0',STR_PAD_LEFT)
+                . str_pad($match[3],4,'0',STR_PAD_LEFT)
+                . str_pad($match[4],4,'0',STR_PAD_LEFT)
+                . str_pad($match[5],4,'0',STR_PAD_LEFT)
+                . str_pad($match[6],4,'0',STR_PAD_LEFT)
+                . str_pad($match[7],4,'0',STR_PAD_LEFT)
+                . str_pad($match[8],4,'0',STR_PAD_LEFT);
+        } elseif (preg_match("/^\:\:([0-9a-h\:]+)$/i",$string,$match)) {
+            // short ipv6 with all starting symbols nulled
+            $aAddr=explode(':',$match[1]);
+            $ret='';
+            foreach ($aAddr as $addr) {
+                $ret.=str_pad($addr,4,'0',STR_PAD_LEFT);
+            }
+            $ret=str_pad($ret,32,'0',STR_PAD_LEFT);
+        } elseif (preg_match("/^([0-9a-h\:]+)::([0-9a-h\:]+)$/i",$string,$match)) {
+            // short ipv6 with middle part nulled
+            $aStart=explode(':',$match[1]);
+            $sStart='';
+            foreach($aStart as $addr) {
+                $sStart.=str_pad($addr,4,'0',STR_PAD_LEFT);
             }
+            $aEnd = explode(':',$match[2]);
+            $sEnd='';
+            foreach($aEnd as $addr) {
+                $sEnd.=str_pad($addr,4,'0',STR_PAD_LEFT);
+            }
+            $ret = $sStart
+                . str_pad('',(32 - strlen($sStart . $sEnd)),'0',STR_PAD_LEFT)
+                . $sEnd;
+        } else {
+            // unknown addressing
+            $ret = $string;
         }
-        trim($refer);
-        return $refer;
+        return $ret;
     }
 }
-?>
\ No newline at end of file