* 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
* 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
*
*/
function mail(&$message, $stream=false, $reply_id=0, $reply_ent_id=0,
- $extra=NULL) {
+ $imap_stream=NULL, $extra=NULL) {
$rfc822_header = &$message->rfc822_header;
//
if ($reply_id) {
global $imapConnection, $username, $imapServerAddress,
- $imapPort, $mailbox;
+ $imapPort, $imap_stream_options, $mailbox;
- if (!is_resource($imapConnection))
- $imapConnection = sqimap_login($username, FALSE,
- $imapServerAddress, $imapPort, 0);
+ // 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;
- sqimap_mailbox_select($imapConnection, $mailbox);
- $reply_message = sqimap_get_message($imapConnection, $reply_id, $mailbox);
+ } 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 */
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;
*
* 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;
}
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;
$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 */
/* 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 */
* 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
} 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;
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;
}
/**
- * 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;
}
/**
* @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 = 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);
}