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