Make our Date header RFC-compliant. Redundant timezone info does not comply with...
[squirrelmail.git] / class / deliver / Deliver.class.php
index 59312ebf5802fd1e99ae9b3f34cda8095d8900fc..d86a125bdc27a8a2c5fd5c220d9fb6d2c23e62b6 100644 (file)
@@ -32,7 +32,12 @@ class Deliver {
      * function mail - send the message parts to the SMTP stream
      *
      * @param Message  $message      Message object to send
      * 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
      *                               (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)
      *                               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.
      *
      * @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.'"';
         if (count($message->entities)) {
             $boundary = $this->mimeBoundary();
             $rfc822_header->content_type->properties['boundary']='"'.$boundary.'"';
@@ -74,12 +92,27 @@ class Deliver {
             global $imapConnection, $username, $imapServerAddress, 
                    $imapPort, $mailbox;
 
             global $imapConnection, $username, $imapServerAddress, 
                    $imapPort, $mailbox;
 
-            if (!is_resource($imapConnection))
-                $imapConnection = sqimap_login($username, FALSE,
+            // 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);
                                                $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 */
 
             if ($reply_ent_id) {
                 /* redefine the messsage in case of message/rfc822 */
@@ -94,8 +127,8 @@ class Deliver {
             } else {
                 $orig_header = $reply_message->rfc822_header;
             }
             } else {
                 $orig_header = $reply_message->rfc822_header;
             }
+            $message->reply_rfc822_header = $orig_header;
         }
         }
-        $message->reply_rfc822_header = $orig_header;
 
 
         $reply_rfc822_header = (isset($message->reply_rfc822_header)
 
 
         $reply_rfc822_header = (isset($message->reply_rfc822_header)
@@ -430,6 +463,8 @@ class Deliver {
         } else {
             if ($mime_header->type0 == 'text' || $mime_header->type0 == 'message') {
                 $header[] = 'Content-Transfer-Encoding: 8bit' .  $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') {
+                /* no-op; no encoding needed */
             } else {
                 $header[] = 'Content-Transfer-Encoding: base64' .  $rn;
             }
             } else {
                 $header[] = 'Content-Transfer-Encoding: base64' .  $rn;
             }
@@ -476,12 +511,14 @@ class Deliver {
      *
      * @return string $header
      */
      *
      * @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;
 
         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;
         }
 
             $SERVER_NAME = $domain;
         }
 
@@ -495,15 +532,21 @@ class Deliver {
 
         /* This creates an RFC 822 date */
         $date = date('D, j M Y H:i:s ', time()) . $this->timezone();
 
         /* This creates an RFC 822 date */
         $date = date('D, j M Y H:i:s ', time()) . $this->timezone();
+
         /* Create a message-id */
         /* Create a message-id */
-        $message_id = '<' . $REMOTE_PORT . '.';
-        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 = '<';
+            /* user-specifc data to decrease collision chance */
+            $seed_data = $username . '.';
+            $seed_data .= (!empty($REMOTE_PORT) ? $REMOTE_PORT . '.' : '');
+            $seed_data .= (!empty($REMOTE_ADDR) ? $REMOTE_ADDR . '.' : '');
+            /* add the current time in milliseconds and randomness */
+            $seed_data .= uniqid(mt_rand(),true);
+            /* put it through one-way hash and add it to the ID */
+            $message_id .= md5($seed_data) . '.squirrel@' . $SERVER_NAME .'>';
         }
         }
-        $message_id .= '.' . time() . '.squirrel@' . $SERVER_NAME .'>';
+
         /* Make an RFC822 Received: line */
         if (isset($REMOTE_HOST)) {
             $received_from = "$REMOTE_HOST ([$REMOTE_ADDR])";
         /* Make an RFC822 Received: line */
         if (isset($REMOTE_HOST)) {
             $received_from = "$REMOTE_HOST ([$REMOTE_ADDR])";
@@ -535,6 +578,7 @@ class Deliver {
          */
         $show_sm_header = ( defined('hide_squirrelmail_header') ? ! hide_squirrelmail_header : 1 );
 
          */
         $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)!='') {
         if ( $show_sm_header ) {
           if (isset($encode_header_key) &&
             trim($encode_header_key)!='') {
@@ -546,7 +590,7 @@ class Deliver {
           } else {
             // use default received headers
             $header[] = "Received: from $received_from" . $rn;
           } 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[] = "        (SquirrelMail authenticated user $username)" . $rn;
             $header[] = "        by $SERVER_NAME with HTTP;" . $rn;
             $header[] = "        $date" . $rn;
@@ -554,17 +598,32 @@ class Deliver {
         }
 
         /* Insert the rest of the header fields */
         }
 
         /* 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;
         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;
             $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;
             $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;
 
         $header[] = 'Subject: '.encodeHeader($rfc822_header->subject) . $rn;
         $header[] = 'From: '. $rfc822_header->getAddr_s('from',",$rn ",true) . $rn;
 
@@ -799,7 +858,7 @@ class Deliver {
      * @return string $result with timezone and offset
      */
     function timezone () {
      * @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) {
 
         $diff_second = date('Z');
         if ($invert_time) {
@@ -813,9 +872,24 @@ class Deliver {
         $diff_second = abs($diff_second);
         $diff_hour = floor ($diff_second / 3600);
         $diff_minute = floor (($diff_second-3600*$diff_hour) / 60);
         $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);
     }
 
         return ($result);
     }