X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=class%2Fdeliver%2FDeliver.class.php;h=1cb3826d4f667ff902377b67a9f99d421461a9b5;hb=caf0ab1de11a5cafc878e424ef0d55dbc0350dd1;hp=504b61f9cc0c185572964f5d775f9a39dd1f5fe3;hpb=69bb88aa7887eb7259926da61d80b123db2a0524;p=squirrelmail.git diff --git a/class/deliver/Deliver.class.php b/class/deliver/Deliver.class.php index 504b61f9..1cb3826d 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-2019 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; @@ -90,7 +90,7 @@ 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 // @@ -103,8 +103,8 @@ class Deliver { } else { $close_imap_stream = TRUE; - $my_imap_stream = sqimap_login($username, FALSE, - $imapServerAddress, $imapPort, 0); + $my_imap_stream = sqimap_login($username, FALSE, $imapServerAddress, + $imapPort, 0, $imap_stream_options); } sqimap_mailbox_select($my_imap_stream, $mailbox); @@ -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; @@ -402,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; } @@ -461,10 +507,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 +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 */ @@ -536,15 +601,9 @@ class Deliver { /* Create a message-id */ $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 = '<' + . md5(GenerateRandomString(16, '', 7) . uniqid(mt_rand(),true)) + . '.squirrel@' . $SERVER_NAME .'>'; } /* Make an RFC822 Received: line */ @@ -570,7 +629,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 +650,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 +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; @@ -740,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; } /** @@ -858,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) { @@ -872,9 +1105,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); }