6 * This contains all the functions needed to send messages through
9 * @author Marc Groot Koerkamp
10 * @copyright © 1999-2007 The SquirrelMail Project Team
11 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
13 * @package squirrelmail
17 * Deliver Class - called to actually deliver the message
19 * This class is called by compose.php and other code that needs
20 * to send messages. All delivery functionality should be centralized
23 * Do not place UI code in this class, as UI code should be placed in templates
26 * @author Marc Groot Koerkamp
27 * @package squirrelmail
32 * function mail - send the message parts to the SMTP stream
34 * @param Message $message Message object to send
35 * NOTE that this is passed by
36 * reference and will be modified
37 * upon return with updated
38 * fields such as Message ID, References,
39 * In-Reply-To and Date headers.
40 * @param resource $stream Handle to the outgoing stream
41 * (when FALSE, nothing will be
42 * written to the stream; this can
43 * be used to determine the actual
44 * number of bytes that will be
45 * written to the stream)
46 * @param string $reply_id Identifies message being replied to
47 * (OPTIONAL; caller should ONLY specify
48 * a value for this when the message
49 * being sent is a reply)
50 * @param string $reply_ent_id Identifies message being replied to
51 * in the case it was an embedded/attached
52 * message inside another (OPTIONAL; caller
53 * should ONLY specify a value for this
54 * when the message being sent is a reply)
55 * @param resource $imap_stream If there is an open IMAP stream in
56 * the caller's context, it should be
57 * passed in here. This is OPTIONAL,
58 * as one will be created if not given,
59 * but as some IMAP servers may baulk
60 * at opening more than one connection
61 * at a time, the caller should always
62 * abide if possible. Currently, this
63 * stream is only used when $reply_id
64 * is also non-zero, but that is subject
66 * @param mixed $extra Any implementation-specific variables
67 * can be passed in here and used in
68 * an overloaded version of this method
71 * @return integer The number of bytes written (or that would have been
72 * written) to the output stream.
75 function mail(&$message, $stream=false, $reply_id=0, $reply_ent_id=0,
76 $imap_stream=NULL, $extra=NULL) {
78 $rfc822_header = &$message->rfc822_header
;
80 if (count($message->entities
)) {
81 $boundary = $this->mimeBoundary();
82 $rfc822_header->content_type
->properties
['boundary']='"'.$boundary.'"';
89 // calculate reply header if needed
92 global $imapConnection, $username, $imapServerAddress,
95 // try our best to use an existing IMAP handle
97 $close_imap_stream = FALSE;
98 if (is_resource($imap_stream)) {
99 $my_imap_stream = $imap_stream;
101 } else if (is_resource($imapConnection)) {
102 $my_imap_stream = $imapConnection;
105 $close_imap_stream = TRUE;
106 $my_imap_stream = sqimap_login($username, FALSE,
107 $imapServerAddress, $imapPort, 0);
110 sqimap_mailbox_select($my_imap_stream, $mailbox);
111 $reply_message = sqimap_get_message($my_imap_stream, $reply_id, $mailbox);
113 if ($close_imap_stream) {
114 sqimap_logout($my_imap_stream);
118 /* redefine the messsage in case of message/rfc822 */
119 $reply_message = $message->getEntity($reply_ent_id);
120 /* message is an entity which contains the envelope and type0=message
121 * and type1=rfc822. The actual entities are childs from
122 * $reply_message->entities[0]. That's where the encoding and is located
125 $orig_header = $reply_message->rfc822_header
; /* here is the envelope located */
128 $orig_header = $reply_message->rfc822_header
;
130 $message->reply_rfc822_header
= $orig_header;
134 $reply_rfc822_header = (isset($message->reply_rfc822_header
)
135 ?
$message->reply_rfc822_header
: '');
136 $header = $this->prepareRFC822_Header($rfc822_header, $reply_rfc822_header, $raw_length);
138 $this->send_mail($message, $header, $boundary, $stream, $raw_length, $extra);
144 * function send_mail - send the message parts to the IMAP stream
146 * @param Message $message Message object to send
147 * @param string $header Headers ready to send
148 * @param string $boundary Message parts boundary
149 * @param resource $stream Handle to the SMTP stream
150 * (when FALSE, nothing will be
151 * written to the stream; this can
152 * be used to determine the actual
153 * number of bytes that will be
154 * written to the stream)
155 * @param int &$raw_length The number of bytes written (or that
156 * would have been written) to the
157 * output stream - NOTE that this is
158 * passed by reference
159 * @param mixed $extra Any implementation-specific variables
160 * can be passed in here and used in
161 * an overloaded version of this method
167 function send_mail($message, $header, $boundary, $stream=false,
168 &$raw_length, $extra=NULL) {
172 $this->preWriteToStream($header);
173 $this->writeToStream($stream, $header);
175 $this->writeBody($message, $stream, $raw_length, $boundary);
179 * function writeBody - generate and write the mime boundaries around each part to the stream
181 * Recursively formats and writes the MIME boundaries of the $message
182 * to the output stream.
184 * @param Message $message Message object to transform
185 * @param resource $stream SMTP output stream
186 * (when FALSE, nothing will be
187 * written to the stream; this can
188 * be used to determine the actual
189 * number of bytes that will be
190 * written to the stream)
191 * @param integer &$length_raw raw length of the message (part)
192 * as returned by mail fn
193 * @param string $boundary custom boundary to call, usually for subparts
197 function writeBody($message, $stream, &$length_raw, $boundary='') {
198 // calculate boundary in case of multidimensional mime structures
199 if ($boundary && $message->entity_id
&& count($message->entities
)) {
200 if (strpos($boundary,'_part_')) {
201 $boundary = substr($boundary,0,strpos($boundary,'_part_'));
203 // the next four lines use strrev to reverse any nested boundaries
204 // because RFC 2046 (5.1.1) says that if a line starts with the outer
205 // boundary string (doesn't matter what the line ends with), that
206 // can be considered a match for the outer boundary; thus the nested
207 // boundary needs to be unique from the outer one
209 } else if (strpos($boundary,'_trap_')) {
210 $boundary = substr(strrev($boundary),0,strpos(strrev($boundary),'_part_'));
212 $boundary_new = strrev($boundary . '_part_'.$message->entity_id
);
214 $boundary_new = $boundary;
216 if ($boundary && !$message->rfc822_header
) {
217 $s = '--'.$boundary."\r\n";
218 $s .= $this->prepareMIME_Header($message, $boundary_new);
219 $length_raw +
= strlen($s);
221 $this->preWriteToStream($s);
222 $this->writeToStream($stream, $s);
225 $this->writeBodyPart($message, $stream, $length_raw);
228 for ($i=0, $entCount=count($message->entities
);$i<$entCount;$i++
) {
229 $msg = $this->writeBody($message->entities
[$i], $stream, $length_raw, $boundary_new);
230 if ($i == $entCount-1) $last = true;
232 if ($boundary && $last) {
233 $s = "--".$boundary_new."--\r\n\r\n";
234 $length_raw +
= strlen($s);
236 $this->preWriteToStream($s);
237 $this->writeToStream($stream, $s);
243 * function writeBodyPart - write each individual mimepart
245 * Recursively called by WriteBody to write each mime part to the SMTP stream
247 * @param Message $message Message object to transform
248 * @param resource $stream SMTP output stream
249 * (when FALSE, nothing will be
250 * written to the stream; this can
251 * be used to determine the actual
252 * number of bytes that will be
253 * written to the stream)
254 * @param integer &$length length of the message part
255 * as returned by mail fn
259 function writeBodyPart($message, $stream, &$length) {
260 if ($message->mime_header
) {
261 $type0 = $message->mime_header
->type0
;
263 $type0 = $message->rfc822_header
->content_type
->type0
;
266 $body_part_trailing = $last = '';
271 if ($message->body_part
) {
272 $body_part = $message->body_part
;
273 // remove NUL characters
274 $body_part = str_replace("\0",'',$body_part);
275 $length +
= $this->clean_crlf($body_part);
277 $this->preWriteToStream($body_part);
278 $this->writeToStream($stream, $body_part);
281 } elseif ($message->att_local_name
) {
282 global $username, $attachment_dir;
283 $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
284 $filename = $message->att_local_name
;
285 $file = fopen ($hashed_attachment_dir . '/' . $filename, 'rb');
286 while ($body_part = fgets($file, 4096)) {
287 // remove NUL characters
288 $body_part = str_replace("\0",'',$body_part);
289 $length +
= $this->clean_crlf($body_part);
291 $this->preWriteToStream($body_part);
292 $this->writeToStream($stream, $body_part);
300 if ($message->body_part
) {
301 $body_part = $message->body_part
;
302 // remove NUL characters
303 $body_part = str_replace("\0",'',$body_part);
304 $length +
= $this->clean_crlf($body_part);
306 $this->writeToStream($stream, $body_part);
308 } elseif ($message->att_local_name
) {
309 global $username, $attachment_dir;
310 $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
311 $filename = $message->att_local_name
;
312 $file = fopen ($hashed_attachment_dir . '/' . $filename, 'rb');
313 while ($tmp = fread($file, 570)) {
314 $body_part = chunk_split(base64_encode($tmp));
315 // Up to 4.3.10 chunk_split always appends a newline,
316 // while in 4.3.11 it doesn't if the string to split
317 // is shorter than the chunk length.
318 if( substr($body_part, -1 , 1 ) != "\n" )
320 $length +
= $this->clean_crlf($body_part);
322 $this->writeToStream($stream, $body_part);
329 $body_part_trailing = '';
330 if ($last && substr($last,-1) != "\n") {
331 $body_part_trailing = "\r\n";
333 if ($body_part_trailing) {
334 $length +
= strlen($body_part_trailing);
336 $this->preWriteToStream($body_part_trailing);
337 $this->writeToStream($stream, $body_part_trailing);
343 * function clean_crlf - change linefeeds and newlines to legal characters
345 * The SMTP format only allows CRLF as line terminators.
346 * This function replaces illegal teminators with the correct terminator.
348 * @param string &$s string to clean linefeeds on
352 function clean_crlf(&$s) {
353 $s = str_replace("\r\n", "\n", $s);
354 $s = str_replace("\r", "\n", $s);
355 $s = str_replace("\n", "\r\n", $s);
360 * function strip_crlf - strip linefeeds and newlines from a string
362 * The SMTP format only allows CRLF as line terminators.
363 * This function strips all line terminators from the string.
365 * @param string &$s string to clean linefeeds on
369 function strip_crlf(&$s) {
370 $s = str_replace("\r\n ", '', $s);
371 $s = str_replace("\r", '', $s);
372 $s = str_replace("\n", '', $s);
376 * function preWriteToStream - reserved for extended functionality
378 * This function is not yet implemented.
379 * Reserved for extended functionality.
381 * @param string &$s string to operate on
385 function preWriteToStream(&$s) {
389 * function writeToStream - write data to the SMTP stream
391 * @param resource $stream SMTP output stream
392 * @param string $data string with data to send to the SMTP stream
396 function writeToStream($stream, $data) {
397 fputs($stream, $data);
401 * function initStream - reserved for extended functionality
403 * This function is not yet implemented.
404 * Reserved for extended functionality.
406 * @param Message $message Message object
407 * @param string $host host name or IP to connect to
408 * @param string $user username to log into the SMTP server with
409 * @param string $pass password to log into the SMTP server with
410 * @param integer $length
412 * @return handle $stream file handle resource to SMTP stream
414 function initStream($message, $length=0, $host='', $port='', $user='', $pass='') {
419 * function getBCC - reserved for extended functionality
421 * This function is not yet implemented.
422 * Reserved for extended functionality.
430 * function prepareMIME_Header - creates the mime header
432 * @param Message $message Message object to act on
433 * @param string $boundary mime boundary from fn MimeBoundary
435 * @return string $header properly formatted mime header
437 function prepareMIME_Header($message, $boundary) {
438 $mime_header = $message->mime_header
;
442 $contenttype = 'Content-Type: '. $mime_header->type0
.'/'.
444 if (count($message->entities
)) {
445 $contenttype .= ';' . 'boundary="'.$boundary.'"';
447 if (isset($mime_header->parameters
['name'])) {
448 $contenttype .= '; name="'.
449 encodeHeader($mime_header->parameters
['name']). '"';
451 if (isset($mime_header->parameters
['charset'])) {
452 $charset = $mime_header->parameters
['charset'];
453 $contenttype .= '; charset="'.
454 encodeHeader($charset). '"';
457 $header[] = $contenttype . $rn;
458 if ($mime_header->description
) {
459 $header[] = 'Content-Description: ' . $mime_header->description
. $rn;
461 if ($mime_header->encoding
) {
462 $header[] = 'Content-Transfer-Encoding: ' . $mime_header->encoding
. $rn;
464 if ($mime_header->type0
== 'text' ||
$mime_header->type0
== 'message') {
465 $header[] = 'Content-Transfer-Encoding: 8bit' . $rn;
466 } else if ($mime_header->type0
== 'multipart' ||
$mime_header->type0
== 'alternative') {
467 /* no-op; no encoding needed */
469 $header[] = 'Content-Transfer-Encoding: base64' . $rn;
472 if ($mime_header->id
) {
473 $header[] = 'Content-ID: ' . $mime_header->id
. $rn;
475 if ($mime_header->disposition
) {
476 $disposition = $mime_header->disposition
;
477 $contentdisp = 'Content-Disposition: ' . $disposition->name
;
478 if ($disposition->getProperty('filename')) {
479 $contentdisp .= '; filename="'.
480 encodeHeader($disposition->getProperty('filename')). '"';
482 $header[] = $contentdisp . $rn;
484 if ($mime_header->md5
) {
485 $header[] = 'Content-MD5: ' . $mime_header->md5
. $rn;
487 if ($mime_header->language
) {
488 $header[] = 'Content-Language: ' . $mime_header->language
. $rn;
491 $cnt = count($header);
493 for ($i = 0 ; $i < $cnt ; $i++
) {
494 $hdr_s .= $this->foldLine($header[$i], 78);
497 $header .= $rn; /* One blank line to separate mimeheader and body-entity */
502 * function prepareRFC822_Header - prepares the RFC822 header string from Rfc822Header object(s)
504 * This function takes the Rfc822Header object(s) and formats them
505 * into the RFC822Header string to send to the SMTP server as part
506 * of the SMTP message.
508 * @param Rfc822Header $rfc822_header
509 * @param Rfc822Header $reply_rfc822_header
510 * @param integer &$raw_length length of the message
512 * @return string $header
514 function prepareRFC822_Header(&$rfc822_header, $reply_rfc822_header, &$raw_length) {
515 global $domain, $username, $encode_header_key,
516 $edit_identity, $hide_auth_header;
518 /* if server var SERVER_NAME not available, or contains
519 ":" (e.g. IPv6) which is illegal in a Message-ID, use $domain */
520 if(!sqGetGlobalVar('SERVER_NAME', $SERVER_NAME, SQ_SERVER
) ||
521 strpos($SERVER_NAME,':') !== FALSE) {
522 $SERVER_NAME = $domain;
525 sqGetGlobalVar('REMOTE_ADDR', $REMOTE_ADDR, SQ_SERVER
);
526 sqGetGlobalVar('REMOTE_PORT', $REMOTE_PORT, SQ_SERVER
);
527 sqGetGlobalVar('REMOTE_HOST', $REMOTE_HOST, SQ_SERVER
);
528 sqGetGlobalVar('HTTP_VIA', $HTTP_VIA, SQ_SERVER
);
529 sqGetGlobalVar('HTTP_X_FORWARDED_FOR', $HTTP_X_FORWARDED_FOR, SQ_SERVER
);
533 /* This creates an RFC 822 date */
534 $date = date('D, j M Y H:i:s ', time()) . $this->timezone();
536 /* Create a message-id */
537 $message_id = 'MESSAGE ID GENERATION ERROR! PLEASE CONTACT SQUIRRELMAIL DEVELOPERS';
538 if (empty($rfc822_header->message_id
)) {
540 /* user-specifc data to decrease collision chance */
541 $seed_data = $username . '.';
542 $seed_data .= (!empty($REMOTE_PORT) ?
$REMOTE_PORT . '.' : '');
543 $seed_data .= (!empty($REMOTE_ADDR) ?
$REMOTE_ADDR . '.' : '');
544 /* add the current time in milliseconds and randomness */
545 $seed_data .= uniqid(mt_rand(),true);
546 /* put it through one-way hash and add it to the ID */
547 $message_id .= md5($seed_data) . '.squirrel@' . $SERVER_NAME .'>';
550 /* Make an RFC822 Received: line */
551 if (isset($REMOTE_HOST)) {
552 $received_from = "$REMOTE_HOST ([$REMOTE_ADDR])";
554 $received_from = $REMOTE_ADDR;
556 if (isset($HTTP_VIA) ||
isset ($HTTP_X_FORWARDED_FOR)) {
557 if (!isset($HTTP_X_FORWARDED_FOR) ||
$HTTP_X_FORWARDED_FOR == '') {
558 $HTTP_X_FORWARDED_FOR = 'unknown';
560 $received_from .= " (proxying for $HTTP_X_FORWARDED_FOR)";
565 * SquirrelMail header
567 * This Received: header provides information that allows to track
568 * user and machine that was used to send email. Don't remove it
569 * unless you understand all possible forging issues or your
570 * webmail installation does not prevent changes in user's email address.
571 * See SquirrelMail bug tracker #847107 for more details about it.
573 * Add hide_squirrelmail_header as a candidate for config_local.php
574 * (must be defined as a constant: define('hide_squirrelmail_header', 1);
575 * to allow completely hiding SquirrelMail participation in message
576 * processing; This is dangerous, especially if users can modify their
577 * account information, as it makes mapping a sent message back to the
578 * original sender almost impossible.
580 $show_sm_header = ( defined('hide_squirrelmail_header') ?
! hide_squirrelmail_header
: 1 );
582 // 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
583 if ( $show_sm_header ) {
584 if (isset($encode_header_key) &&
585 trim($encode_header_key)!='') {
586 // use encoded headers, if encryption key is set and not empty
587 $header[] = 'X-Squirrel-UserHash: '.OneTimePadEncrypt($username,base64_encode($encode_header_key)).$rn;
588 $header[] = 'X-Squirrel-FromHash: '.OneTimePadEncrypt($this->ip2hex($REMOTE_ADDR),base64_encode($encode_header_key)).$rn;
589 if (isset($HTTP_X_FORWARDED_FOR))
590 $header[] = 'X-Squirrel-ProxyHash:'.OneTimePadEncrypt($this->ip2hex($HTTP_X_FORWARDED_FOR),base64_encode($encode_header_key)).$rn;
592 // use default received headers
593 $header[] = "Received: from $received_from" . $rn;
594 if (!isset($hide_auth_header) ||
!$hide_auth_header)
595 $header[] = " (SquirrelMail authenticated user $username)" . $rn;
596 $header[] = " by $SERVER_NAME with HTTP;" . $rn;
597 $header[] = " $date" . $rn;
601 /* Insert the rest of the header fields */
603 if (!empty($rfc822_header->message_id
)) {
604 $header[] = 'Message-ID: '. $rfc822_header->message_id
. $rn;
606 $header[] = 'Message-ID: '. $message_id . $rn;
607 $rfc822_header->message_id
= $message_id;
610 if (is_object($reply_rfc822_header) &&
611 isset($reply_rfc822_header->message_id
) &&
612 $reply_rfc822_header->message_id
) {
613 $rep_message_id = $reply_rfc822_header->message_id
;
614 $header[] = 'In-Reply-To: '.$rep_message_id . $rn;
615 $rfc822_header->in_reply_to
= $rep_message_id;
616 $references = $this->calculate_references($reply_rfc822_header);
617 $header[] = 'References: '.$references . $rn;
618 $rfc822_header->references
= $references;
621 if (!empty($rfc822_header->date
) && $rfc822_header->date
!= -1) {
622 $header[] = 'Date: '. $rfc822_header->date
. $rn;
624 $header[] = "Date: $date" . $rn;
625 $rfc822_header->date
= $date;
628 $header[] = 'Subject: '.encodeHeader($rfc822_header->subject
) . $rn;
629 $header[] = 'From: '. $rfc822_header->getAddr_s('from',",$rn ",true) . $rn;
631 // folding address list [From|To|Cc|Bcc] happens by using ",$rn<space>"
633 // Do not use foldLine for that.
635 // RFC2822 if from contains more then 1 address
636 if (count($rfc822_header->from
) > 1) {
637 $header[] = 'Sender: '. $rfc822_header->getAddr_s('sender',',',true) . $rn;
639 if (count($rfc822_header->to
)) {
640 $header[] = 'To: '. $rfc822_header->getAddr_s('to',",$rn ",true) . $rn;
642 if (count($rfc822_header->cc
)) {
643 $header[] = 'Cc: '. $rfc822_header->getAddr_s('cc',",$rn ",true) . $rn;
645 if (count($rfc822_header->reply_to
)) {
646 $header[] = 'Reply-To: '. $rfc822_header->getAddr_s('reply_to',',',true) . $rn;
648 /* Sendmail should return true. Default = false */
649 $bcc = $this->getBcc();
650 if (count($rfc822_header->bcc
)) {
651 $s = 'Bcc: '. $rfc822_header->getAddr_s('bcc',",$rn ",true) . $rn;
653 $raw_length +
= strlen($s);
658 /* Identify SquirrelMail */
659 $header[] = 'User-Agent: SquirrelMail/' . SM_VERSION
. $rn;
660 /* Do the MIME-stuff */
661 $header[] = 'MIME-Version: 1.0' . $rn;
662 $contenttype = 'Content-Type: '. $rfc822_header->content_type
->type0
.'/'.
663 $rfc822_header->content_type
->type1
;
664 if (count($rfc822_header->content_type
->properties
)) {
665 foreach ($rfc822_header->content_type
->properties
as $k => $v) {
667 $contenttype .= ';' .$k.'='.$v;
671 $header[] = $contenttype . $rn;
672 if ($encoding = $rfc822_header->encoding
) {
673 $header[] = 'Content-Transfer-Encoding: ' . $encoding . $rn;
675 if (isset($rfc822_header->dnt
) && $rfc822_header->dnt
) {
676 $dnt = $rfc822_header->getAddr_s('dnt');
678 $header[] = 'X-Confirm-Reading-To: '.$dnt. $rn;
680 $header[] = 'Disposition-Notification-To: '.$dnt. $rn;
682 if ($rfc822_header->priority
) {
683 switch($rfc822_header->priority
)
686 $header[] = 'X-Priority: 1 (Highest)'.$rn;
687 $header[] = 'Importance: High'. $rn; break;
689 $header[] = 'X-Priority: 5 (Lowest)'.$rn;
690 $header[] = 'Importance: Low'. $rn; break;
694 /* Insert headers from the $more_headers array */
695 if(count($rfc822_header->more_headers
)) {
696 reset($rfc822_header->more_headers
);
697 foreach ($rfc822_header->more_headers
as $k => $v) {
698 $header[] = $k.': '.$v .$rn;
701 $cnt = count($header);
704 for ($i = 0 ; $i < $cnt ; $i++
) {
705 $sKey = substr($header[$i],0,strpos($header[$i],':'));
710 $hdr_s .= $header[$i];
713 $sRefs = substr($header[$i],12);
714 $aRefs = explode(' ',$sRefs);
715 $sLine = 'References:';
716 foreach ($aRefs as $sReference) {
717 if ( trim($sReference) == '' ) {
718 /* Don't add spaces. */
719 } elseif (strlen($sLine)+
strlen($sReference) >76) {
721 $sLine = $rn . ' ' . $sReference;
723 $sLine .= ' '. $sReference;
732 $hdr_s .= $header[$i];
734 default: $hdr_s .= $this->foldLine($header[$i], 78); break;
738 $header .= $rn; /* One blank line to separate header and body */
739 $raw_length +
= strlen($header);
744 * Fold header lines per RFC 2822/2.2.3 and RFC 822/3.1.1
746 * Herein "soft" folding/wrapping (with whitespace tokens) is
747 * what we refer to as the preferred method of wrapping - that
748 * which we'd like to do within the $soft_wrap limit, but if
749 * not possible, we will try to do as soon as possible after
750 * $soft_wrap up to the $hard_wrap limit. Encoded words don't
751 * need to be detected in this phase, since they cannot contain
754 * "Hard" folding/wrapping (with "hard" tokens) is what we refer
755 * to as less ideal wrapping that will be done to keep within
756 * the $hard_wrap limit. This adds other syntactical breaking
757 * elements such as commas and encoded words.
759 * @param string $header The header content being folded
760 * @param integer $soft_wrap The desirable maximum line length
761 * (OPTIONAL; default is 78, per RFC)
762 * @param string $indent Wrapped lines will already have
763 * whitespace following the CRLF wrap,
764 * but you can add more indentation (or
765 * whatever) with this. The use of this
766 * parameter is DISCOURAGED, since it
767 * can corrupt the redisplay (unfolding)
768 * of headers whose content is space-
769 * sensitive, like subjects, etc.
770 * (OPTIONAL; default is an empty string)
771 * @param string $hard_wrap The absolute maximum line length
772 * (OPTIONAL; default is 998, per RFC)
774 * @return string The folded header content, with a trailing CRLF.
777 function foldLine($header, $soft_wrap=78, $indent='', $hard_wrap=998) {
779 // the "hard" token list can be altered if desired,
780 // for example, by adding ":"
781 // (in the future, we can take optional arguments
782 // for overriding or adding elements to the "hard"
783 // token list if we want to get fancy)
785 // the order of these is significant - preferred
786 // fold points should be listed first
788 // it is advised that the "=" always come first
789 // since it also finds encoded words, thus if it
790 // comes after some other token that happens to
791 // fall within the encoded word, the encoded word
792 // could be inadvertently broken in half, which
793 // is not allowable per RFC
795 $hard_break_tokens = array(
796 '=', // includes encoded word detection
801 // the order of these is significant too
812 // if using an indent string, reduce wrap limits by its size
814 if (!empty($indent)) {
815 $soft_wrap -= strlen($indent);
816 $hard_wrap -= strlen($indent);
819 while (strlen($header) > $soft_wrap) {
821 $soft_wrapped_line = substr($header, 0, $soft_wrap);
823 // look for a token as close to the end of the soft wrap limit as possible
825 foreach ($whitespace as $token) {
827 // note that this if statement also fails when $pos === 0,
828 // which is intended, since blank lines are not allowed
830 if ($pos = strrpos($soft_wrapped_line, $token))
832 $new_fold = substr($header, 0, $pos);
834 // make sure proposed fold doesn't create a blank line
836 if (!trim($new_fold)) continue;
838 // with whitespace breaks, we fold BEFORE the token
840 $folded_header .= $new_fold . $CRLF . $indent;
841 $header = substr($header, $pos);
843 // ready for next while() iteration
851 // we were unable to find a wrapping point within the soft
852 // wrap limit, so now we'll try to find the first possible
853 // soft wrap point within the hard wrap limit
855 $hard_wrapped_line = substr($header, 0, $hard_wrap);
857 // look for a *SOFT* token as close to the
858 // beginning of the hard wrap limit as possible
860 foreach ($whitespace as $token) {
862 // use while loop instead of if block because it
863 // is possible we don't want the first one we find
865 $pos = $soft_wrap - 1; // -1 is corrected by +1 on next line
866 while ($pos = strpos($hard_wrapped_line, $token, $pos +
1))
869 $new_fold = substr($header, 0, $pos);
871 // make sure proposed fold doesn't create a blank line
873 if (!trim($new_fold)) continue;
875 // with whitespace breaks, we fold BEFORE the token
877 $folded_header .= $new_fold . $CRLF . $indent;
878 $header = substr($header, $pos);
880 // ready for next outter while() iteration
888 // we were still unable to find a soft wrapping point within
889 // both the soft and hard wrap limits, so if the length of
890 // what is left is no more than the hard wrap limit, we'll
891 // simply take the whole thing
893 if (strlen($header) <= strlen($hard_wrapped_line))
896 // otherwise, we can't quit yet - look for a "hard" token
897 // as close to the end of the hard wrap limit as possible
899 foreach ($hard_break_tokens as $token) {
901 // note that this if statement also fails when $pos === 0,
902 // which is intended, since blank lines are not allowed
904 if ($pos = strrpos($hard_wrapped_line, $token))
907 // if we found a "=" token, we must determine whether,
908 // if it is part of an encoded word, it is the beginning
909 // or middle of one, where we need to readjust $pos a bit
913 // if we found the beginning of an encoded word,
914 // we want to break BEFORE the token
916 if (preg_match('/^(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)/Ui',
917 substr($header, $pos))) {
921 // check if we found this token in the *middle*
922 // of an encoded word, in which case we have to
923 // ignore it, pushing back to the token that
924 // starts the encoded word instead
926 // of course, this is only possible if there is
927 // more content after the next hard wrap
929 // then look for the end of an encoded word in
930 // the next part (past the next hard wrap)
932 // then see if it is in fact part of a legitimate
935 else if (strlen($header) > $hard_wrap
936 && ($end_pos = strpos(substr($header, $hard_wrap), '?=')) !== FALSE
937 && preg_match('/(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)$/Ui',
938 substr($header, 0, $hard_wrap +
$end_pos +
2),
941 $pos = $hard_wrap +
$end_pos +
2 - strlen($matches[1]) - 1;
947 // $pos could have been changed; make sure it's
948 // not at the beginning of the line, as blank
949 // lines are not allowed
951 if ($pos === 0) continue;
953 // we are dealing with a simple token break...
955 // for non-whitespace breaks, we fold AFTER the token
956 // and add a space after the fold if not immediately
957 // followed by a whitespace character in the next part
959 $folded_header .= substr($header, 0, $pos +
1) . $CRLF;
961 // don't go beyond end of $header, though
963 if (strlen($header) > $pos +
1) {
964 $header = substr($header, $pos +
1);
965 if (!in_array($header{0}, $whitespace))
966 $header = ' ' . $indent . $header;
971 // ready for next while() iteration
979 // finally, we just couldn't find anything to fold on, so we
980 // have to just cut it off at the hard limit
982 $folded_header .= $hard_wrapped_line . $CRLF;
986 if (strlen($header) > strlen($hard_wrapped_line)) {
987 $header = substr($header, strlen($hard_wrapped_line));
988 if (!in_array($header{0}, $whitespace))
989 $header = ' ' . $indent . $header;
997 // add any left-overs
999 $folded_header .= $header;
1002 // make sure it ends with a CRLF
1004 if (substr($folded_header, -2) != $CRLF) $folded_header .= $CRLF;
1007 return $folded_header;
1011 * function mimeBoundary - calculates the mime boundary to use
1013 * This function will generate a random mime boundary base part
1014 * for the message if the boundary has not already been set.
1016 * @return string $mimeBoundaryString random mime boundary string
1018 function mimeBoundary () {
1019 static $mimeBoundaryString;
1021 if ( !isset( $mimeBoundaryString ) ||
1022 $mimeBoundaryString == '') {
1023 $mimeBoundaryString = '----=_' . date( 'YmdHis' ) . '_' .
1024 mt_rand( 10000, 99999 );
1026 return $mimeBoundaryString;
1030 * function timezone - Time offset for correct timezone
1032 * @return string $result with timezone and offset
1034 function timezone () {
1035 global $invert_time, $show_timezone_name;
1037 $diff_second = date('Z');
1039 $diff_second = - $diff_second;
1041 if ($diff_second > 0) {
1046 $diff_second = abs($diff_second);
1047 $diff_hour = floor ($diff_second / 3600);
1048 $diff_minute = floor (($diff_second-3600*$diff_hour) / 60);
1050 // If an administrator wants to add the timezone name to the
1051 // end of the date header, they can set $show_timezone_name
1052 // to boolean TRUE in config/config_local.php, but that is
1053 // NOT RFC-822 compliant (see section 5.1). Moreover, some
1054 // Windows users reported that strftime('%Z') was returning
1055 // the full zone name (not the abbreviation) which in some
1056 // cases included 8-bit characters (not allowed as is in headers).
1057 // The PHP manual actually does NOT promise what %Z will return
1058 // for strftime!: "The time zone offset/abbreviation option NOT
1059 // given by %z (depends on operating system)"
1061 if ($show_timezone_name) {
1062 $zonename = '('.strftime('%Z').')';
1063 $result = sprintf ("%s%02d%02d %s", $sign, $diff_hour, $diff_minute, $zonename);
1065 $result = sprintf ("%s%02d%02d", $sign, $diff_hour, $diff_minute);
1071 * function calculate_references - calculate correct References string
1072 * Adds the current message ID, and makes sure it doesn't grow forever,
1073 * to that extent it drops message-ID's in a smart way until the string
1074 * length is under the recommended value of 1000 ("References: <986>\r\n").
1075 * It always keeps the first and the last three ID's.
1077 * @param Rfc822Header $hdr message header to calculate from
1079 * @return string $refer concatenated and trimmed References string
1081 function calculate_references($hdr) {
1082 $aReferences = preg_split('/\s+/', $hdr->references
);
1083 $message_id = $hdr->message_id
;
1084 $in_reply_to = $hdr->in_reply_to
;
1086 // if References already exists, add the current message ID at the end.
1087 // no References exists; if we know a IRT, add that aswell
1088 if (count($aReferences) == 0 && $in_reply_to) {
1089 $aReferences[] = $in_reply_to;
1091 $aReferences[] = $message_id;
1093 // sanitize the array: trim whitespace, remove dupes
1094 array_walk($aReferences, 'sq_trim_value');
1095 $aReferences = array_unique($aReferences);
1097 while ( count($aReferences) > 4 && strlen(implode(' ', $aReferences)) >= 986 ) {
1098 $aReferences = array_merge(array_slice($aReferences,0,1),array_slice($aReferences,2));
1100 return implode(' ', $aReferences);
1104 * Converts ip address to hexadecimal string
1106 * Function is used to convert ipv4 and ipv6 addresses to hex strings.
1107 * It removes all delimiter symbols from ip addresses, converts decimal
1108 * ipv4 numbers to hex and pads strings in order to present full length
1109 * address. ipv4 addresses are represented as 8 byte strings, ipv6 addresses
1110 * are represented as 32 byte string.
1112 * If function fails to detect address format, it returns unprocessed string.
1113 * @param string $string ip address string
1114 * @return string processed ip address string
1115 * @since 1.5.1 and 1.4.5
1117 function ip2hex($string) {
1118 if (preg_match("/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/",$string,$match)) {
1120 $ret = str_pad(dechex($match[1]),2,'0',STR_PAD_LEFT
)
1121 . str_pad(dechex($match[2]),2,'0',STR_PAD_LEFT
)
1122 . str_pad(dechex($match[3]),2,'0',STR_PAD_LEFT
)
1123 . str_pad(dechex($match[4]),2,'0',STR_PAD_LEFT
);
1124 } 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)) {
1125 // full ipv6 address
1126 $ret = str_pad($match[1],4,'0',STR_PAD_LEFT
)
1127 . str_pad($match[2],4,'0',STR_PAD_LEFT
)
1128 . str_pad($match[3],4,'0',STR_PAD_LEFT
)
1129 . str_pad($match[4],4,'0',STR_PAD_LEFT
)
1130 . str_pad($match[5],4,'0',STR_PAD_LEFT
)
1131 . str_pad($match[6],4,'0',STR_PAD_LEFT
)
1132 . str_pad($match[7],4,'0',STR_PAD_LEFT
)
1133 . str_pad($match[8],4,'0',STR_PAD_LEFT
);
1134 } elseif (preg_match("/^\:\:([0-9a-h\:]+)$/i",$string,$match)) {
1135 // short ipv6 with all starting symbols nulled
1136 $aAddr=explode(':',$match[1]);
1138 foreach ($aAddr as $addr) {
1139 $ret.=str_pad($addr,4,'0',STR_PAD_LEFT
);
1141 $ret=str_pad($ret,32,'0',STR_PAD_LEFT
);
1142 } elseif (preg_match("/^([0-9a-h\:]+)::([0-9a-h\:]+)$/i",$string,$match)) {
1143 // short ipv6 with middle part nulled
1144 $aStart=explode(':',$match[1]);
1146 foreach($aStart as $addr) {
1147 $sStart.=str_pad($addr,4,'0',STR_PAD_LEFT
);
1149 $aEnd = explode(':',$match[2]);
1151 foreach($aEnd as $addr) {
1152 $sEnd.=str_pad($addr,4,'0',STR_PAD_LEFT
);
1155 . str_pad('',(32 - strlen($sStart . $sEnd)),'0',STR_PAD_LEFT
)
1158 // unknown addressing