Rewrite foldLine(). The old version *might* have been fixable, but it contained...
[squirrelmail.git] / class / deliver / Deliver.class.php
CommitLineData
4960ec8e 1<?php
4b4abf93 2
69298c28 3/**
4 * Deliver.class.php
5 *
69298c28 6 * This contains all the functions needed to send messages through
7 * a delivery backend.
8 *
4b4abf93 9 * @author Marc Groot Koerkamp
4b5049de 10 * @copyright &copy; 1999-2007 The SquirrelMail Project Team
4b4abf93 11 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
883d9cd3 12 * @version $Id$
72b004e6 13 * @package squirrelmail
69298c28 14 */
15
72b004e6 16/**
17 * Deliver Class - called to actually deliver the message
18 *
19 * This class is called by compose.php and other code that needs
20 * to send messages. All delivery functionality should be centralized
21 * in this class.
22 *
23 * Do not place UI code in this class, as UI code should be placed in templates
24 * going forward.
25 *
26 * @author Marc Groot Koerkamp
27 * @package squirrelmail
28 */
4960ec8e 29class Deliver {
30
72b004e6 31 /**
32 * function mail - send the message parts to the SMTP stream
33 *
0fdb0aa1 34 * @param Message $message Message object to send
10adeb76 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
e2ccf284 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)
0fdb0aa1 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)
69bb88aa 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
65 * to change.
33f0da43 66 * @param mixed $extra Any implementation-specific variables
67 * can be passed in here and used in
68 * an overloaded version of this method
69 * if needed.
72b004e6 70 *
10adeb76 71 * @return integer The number of bytes written (or that would have been
72 * written) to the output stream.
b67d61ee 73 *
72b004e6 74 */
10adeb76 75 function mail(&$message, $stream=false, $reply_id=0, $reply_ent_id=0,
33f0da43 76 $imap_stream=NULL, $extra=NULL) {
e2ccf284 77
10adeb76 78 $rfc822_header = &$message->rfc822_header;
79
78f39e78 80 if (count($message->entities)) {
81 $boundary = $this->mimeBoundary();
82 $rfc822_header->content_type->properties['boundary']='"'.$boundary.'"';
83 } else {
84 $boundary='';
85 }
86 $raw_length = 0;
0fdb0aa1 87
88
89 // calculate reply header if needed
90 //
91 if ($reply_id) {
92 global $imapConnection, $username, $imapServerAddress,
93 $imapPort, $mailbox;
e2ccf284 94
69bb88aa 95 // try our best to use an existing IMAP handle
96 //
97 $close_imap_stream = FALSE;
98 if (is_resource($imap_stream)) {
99 $my_imap_stream = $imap_stream;
100
101 } else if (is_resource($imapConnection)) {
102 $my_imap_stream = $imapConnection;
103
104 } else {
105 $close_imap_stream = TRUE;
106 $my_imap_stream = sqimap_login($username, FALSE,
0fdb0aa1 107 $imapServerAddress, $imapPort, 0);
69bb88aa 108 }
109
110 sqimap_mailbox_select($my_imap_stream, $mailbox);
111 $reply_message = sqimap_get_message($my_imap_stream, $reply_id, $mailbox);
0fdb0aa1 112
69bb88aa 113 if ($close_imap_stream) {
114 sqimap_logout($my_imap_stream);
115 }
0fdb0aa1 116
117 if ($reply_ent_id) {
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
123 */
124
125 $orig_header = $reply_message->rfc822_header; /* here is the envelope located */
126
127 } else {
128 $orig_header = $reply_message->rfc822_header;
129 }
08e33475 130 $message->reply_rfc822_header = $orig_header;
0fdb0aa1 131 }
0fdb0aa1 132
133
72b004e6 134 $reply_rfc822_header = (isset($message->reply_rfc822_header)
78f39e78 135 ? $message->reply_rfc822_header : '');
136 $header = $this->prepareRFC822_Header($rfc822_header, $reply_rfc822_header, $raw_length);
75b14a70 137
e2ccf284 138 $this->send_mail($message, $header, $boundary, $stream, $raw_length, $extra);
139
10adeb76 140 return $raw_length;
e2ccf284 141 }
142
143 /**
144 * function send_mail - send the message parts to the IMAP stream
145 *
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
162 * if needed.
163 *
164 * @return void
165 *
166 */
167 function send_mail($message, $header, $boundary, $stream=false,
168 &$raw_length, $extra=NULL) {
169
170
78f39e78 171 if ($stream) {
59387d1c 172 $this->preWriteToStream($header);
173 $this->writeToStream($stream, $header);
78f39e78 174 }
175 $this->writeBody($message, $stream, $raw_length, $boundary);
3b53d87f 176 }
78f39e78 177
72b004e6 178 /**
179 * function writeBody - generate and write the mime boundaries around each part to the stream
180 *
181 * Recursively formats and writes the MIME boundaries of the $message
182 * to the output stream.
183 *
184 * @param Message $message Message object to transform
185 * @param resource $stream SMTP output stream
e2ccf284 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)
72b004e6 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
194 *
3a70ee56 195 * @return void
72b004e6 196 */
3b53d87f 197 function writeBody($message, $stream, &$length_raw, $boundary='') {
caf6a9a4 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_'));
ba86230e 202
593d083d 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
ba86230e 208 //
209 } else if (strpos($boundary,'_trap_')) {
210 $boundary = substr(strrev($boundary),0,strpos(strrev($boundary),'_part_'));
caf6a9a4 211 }
ba86230e 212 $boundary_new = strrev($boundary . '_part_'.$message->entity_id);
caf6a9a4 213 } else {
214 $boundary_new = $boundary;
215 }
1274f430 216 if ($boundary && !$message->rfc822_header) {
78f39e78 217 $s = '--'.$boundary."\r\n";
caf6a9a4 218 $s .= $this->prepareMIME_Header($message, $boundary_new);
78f39e78 219 $length_raw += strlen($s);
220 if ($stream) {
3b53d87f 221 $this->preWriteToStream($s);
78f39e78 222 $this->writeToStream($stream, $s);
223 }
224 }
225 $this->writeBodyPart($message, $stream, $length_raw);
72b004e6 226
78f39e78 227 $last = false;
228 for ($i=0, $entCount=count($message->entities);$i<$entCount;$i++) {
caf6a9a4 229 $msg = $this->writeBody($message->entities[$i], $stream, $length_raw, $boundary_new);
78f39e78 230 if ($i == $entCount-1) $last = true;
231 }
1274f430 232 if ($boundary && $last) {
dcc5c913 233 $s = "--".$boundary_new."--\r\n\r\n";
78f39e78 234 $length_raw += strlen($s);
235 if ($stream) {
3b53d87f 236 $this->preWriteToStream($s);
78f39e78 237 $this->writeToStream($stream, $s);
238 }
239 }
3b53d87f 240 }
241
72b004e6 242 /**
243 * function writeBodyPart - write each individual mimepart
244 *
245 * Recursively called by WriteBody to write each mime part to the SMTP stream
246 *
247 * @param Message $message Message object to transform
248 * @param resource $stream SMTP output stream
e2ccf284 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)
72b004e6 254 * @param integer &$length length of the message part
255 * as returned by mail fn
256 *
3a70ee56 257 * @return void
72b004e6 258 */
3b53d87f 259 function writeBodyPart($message, $stream, &$length) {
1274f430 260 if ($message->mime_header) {
78f39e78 261 $type0 = $message->mime_header->type0;
262 } else {
263 $type0 = $message->rfc822_header->content_type->type0;
264 }
265
266 $body_part_trailing = $last = '';
267 switch ($type0)
268 {
269 case 'text':
270 case 'message':
271 if ($message->body_part) {
272 $body_part = $message->body_part;
4cf85ddd 273 // remove NUL characters
274 $body_part = str_replace("\0",'',$body_part);
78f39e78 275 $length += $this->clean_crlf($body_part);
276 if ($stream) {
72b004e6 277 $this->preWriteToStream($body_part);
78f39e78 278 $this->writeToStream($stream, $body_part);
279 }
280 $last = $body_part;
281 } elseif ($message->att_local_name) {
1f270d3c 282 global $username, $attachment_dir;
283 $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
78f39e78 284 $filename = $message->att_local_name;
1f270d3c 285 $file = fopen ($hashed_attachment_dir . '/' . $filename, 'rb');
78f39e78 286 while ($body_part = fgets($file, 4096)) {
257dea3f 287 // remove NUL characters
288 $body_part = str_replace("\0",'',$body_part);
78f39e78 289 $length += $this->clean_crlf($body_part);
290 if ($stream) {
291 $this->preWriteToStream($body_part);
292 $this->writeToStream($stream, $body_part);
293 }
294 $last = $body_part;
295 }
296 fclose($file);
297 }
298 break;
299 default:
300 if ($message->body_part) {
301 $body_part = $message->body_part;
257dea3f 302 // remove NUL characters
303 $body_part = str_replace("\0",'',$body_part);
78f39e78 304 $length += $this->clean_crlf($body_part);
305 if ($stream) {
306 $this->writeToStream($stream, $body_part);
307 }
308 } elseif ($message->att_local_name) {
1f270d3c 309 global $username, $attachment_dir;
310 $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
78f39e78 311 $filename = $message->att_local_name;
1f270d3c 312 $file = fopen ($hashed_attachment_dir . '/' . $filename, 'rb');
78f39e78 313 while ($tmp = fread($file, 570)) {
27ff4efb 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" )
319 $body_part .= "\n";
78f39e78 320 $length += $this->clean_crlf($body_part);
321 if ($stream) {
322 $this->writeToStream($stream, $body_part);
323 }
324 }
325 fclose($file);
3b53d87f 326 }
78f39e78 327 break;
328 }
329 $body_part_trailing = '';
330 if ($last && substr($last,-1) != "\n") {
331 $body_part_trailing = "\r\n";
332 }
333 if ($body_part_trailing) {
334 $length += strlen($body_part_trailing);
335 if ($stream) {
336 $this->preWriteToStream($body_part_trailing);
337 $this->writeToStream($stream, $body_part_trailing);
338 }
339 }
3b53d87f 340 }
78f39e78 341
72b004e6 342 /**
343 * function clean_crlf - change linefeeds and newlines to legal characters
344 *
345 * The SMTP format only allows CRLF as line terminators.
346 * This function replaces illegal teminators with the correct terminator.
347 *
348 * @param string &$s string to clean linefeeds on
349 *
3a70ee56 350 * @return void
72b004e6 351 */
69298c28 352 function clean_crlf(&$s) {
3b53d87f 353 $s = str_replace("\r\n", "\n", $s);
354 $s = str_replace("\r", "\n", $s);
355 $s = str_replace("\n", "\r\n", $s);
78f39e78 356 return strlen($s);
3b53d87f 357 }
78f39e78 358
72b004e6 359 /**
360 * function strip_crlf - strip linefeeds and newlines from a string
361 *
362 * The SMTP format only allows CRLF as line terminators.
363 * This function strips all line terminators from the string.
364 *
365 * @param string &$s string to clean linefeeds on
366 *
3a70ee56 367 * @return void
72b004e6 368 */
e4f9307a 369 function strip_crlf(&$s) {
370 $s = str_replace("\r\n ", '', $s);
78f39e78 371 $s = str_replace("\r", '', $s);
372 $s = str_replace("\n", '', $s);
e4f9307a 373 }
3b53d87f 374
72b004e6 375 /**
376 * function preWriteToStream - reserved for extended functionality
377 *
378 * This function is not yet implemented.
379 * Reserved for extended functionality.
380 *
381 * @param string &$s string to operate on
382 *
3a70ee56 383 * @return void
72b004e6 384 */
69298c28 385 function preWriteToStream(&$s) {
3b53d87f 386 }
78f39e78 387
72b004e6 388 /**
389 * function writeToStream - write data to the SMTP stream
390 *
391 * @param resource $stream SMTP output stream
392 * @param string $data string with data to send to the SMTP stream
393 *
3a70ee56 394 * @return void
72b004e6 395 */
3b53d87f 396 function writeToStream($stream, $data) {
78f39e78 397 fputs($stream, $data);
3b53d87f 398 }
78f39e78 399
72b004e6 400 /**
401 * function initStream - reserved for extended functionality
402 *
403 * This function is not yet implemented.
404 * Reserved for extended functionality.
405 *
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
411 *
3a70ee56 412 * @return handle $stream file handle resource to SMTP stream
72b004e6 413 */
3b53d87f 414 function initStream($message, $length=0, $host='', $port='', $user='', $pass='') {
78f39e78 415 return $stream;
3b53d87f 416 }
72b004e6 417
418 /**
419 * function getBCC - reserved for extended functionality
420 *
421 * This function is not yet implemented.
422 * Reserved for extended functionality.
423 *
424 */
425 function getBCC() {
78f39e78 426 return false;
59387d1c 427 }
3b53d87f 428
72b004e6 429 /**
430 * function prepareMIME_Header - creates the mime header
431 *
432 * @param Message $message Message object to act on
433 * @param string $boundary mime boundary from fn MimeBoundary
434 *
3a70ee56 435 * @return string $header properly formatted mime header
72b004e6 436 */
3b53d87f 437 function prepareMIME_Header($message, $boundary) {
78f39e78 438 $mime_header = $message->mime_header;
439 $rn="\r\n";
440 $header = array();
1274f430 441
78f39e78 442 $contenttype = 'Content-Type: '. $mime_header->type0 .'/'.
443 $mime_header->type1;
444 if (count($message->entities)) {
fcce1b3c 445 $contenttype .= ';' . 'boundary="'.$boundary.'"';
78f39e78 446 }
447 if (isset($mime_header->parameters['name'])) {
448 $contenttype .= '; name="'.
449 encodeHeader($mime_header->parameters['name']). '"';
450 }
451 if (isset($mime_header->parameters['charset'])) {
452 $charset = $mime_header->parameters['charset'];
453 $contenttype .= '; charset="'.
454 encodeHeader($charset). '"';
455 }
1274f430 456
78f39e78 457 $header[] = $contenttype . $rn;
458 if ($mime_header->description) {
eb72e151 459 $header[] = 'Content-Description: ' . $mime_header->description . $rn;
78f39e78 460 }
461 if ($mime_header->encoding) {
eb72e151 462 $header[] = 'Content-Transfer-Encoding: ' . $mime_header->encoding . $rn;
78f39e78 463 } else {
464 if ($mime_header->type0 == 'text' || $mime_header->type0 == 'message') {
eb72e151 465 $header[] = 'Content-Transfer-Encoding: 8bit' . $rn;
c6dd29ba 466 } else if ($mime_header->type0 == 'multipart' || $mime_header->type0 == 'alternative') {
467 /* no-op; no encoding needed */
78f39e78 468 } else {
eb72e151 469 $header[] = 'Content-Transfer-Encoding: base64' . $rn;
78f39e78 470 }
471 }
472 if ($mime_header->id) {
eb72e151 473 $header[] = 'Content-ID: ' . $mime_header->id . $rn;
78f39e78 474 }
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')). '"';
481 }
72b004e6 482 $header[] = $contentdisp . $rn;
78f39e78 483 }
484 if ($mime_header->md5) {
eb72e151 485 $header[] = 'Content-MD5: ' . $mime_header->md5 . $rn;
78f39e78 486 }
487 if ($mime_header->language) {
eb72e151 488 $header[] = 'Content-Language: ' . $mime_header->language . $rn;
78f39e78 489 }
4960ec8e 490
78f39e78 491 $cnt = count($header);
492 $hdr_s = '';
493 for ($i = 0 ; $i < $cnt ; $i++) {
9bcf508d 494 $hdr_s .= $this->foldLine($header[$i], 78);
78f39e78 495 }
496 $header = $hdr_s;
497 $header .= $rn; /* One blank line to separate mimeheader and body-entity */
498 return $header;
499 }
4960ec8e 500
72b004e6 501 /**
502 * function prepareRFC822_Header - prepares the RFC822 header string from Rfc822Header object(s)
503 *
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.
507 *
508 * @param Rfc822Header $rfc822_header
509 * @param Rfc822Header $reply_rfc822_header
510 * @param integer &$raw_length length of the message
511 *
3a70ee56 512 * @return string $header
72b004e6 513 */
10adeb76 514 function prepareRFC822_Header(&$rfc822_header, $reply_rfc822_header, &$raw_length) {
b37e457f 515 global $domain, $username, $encode_header_key,
e930c9fe 516 $edit_identity, $hide_auth_header;
60a46e65 517
49c7f411 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) {
60a46e65 522 $SERVER_NAME = $domain;
78f39e78 523 }
60a46e65 524
78f39e78 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);
60a46e65 530
78f39e78 531 $rn = "\r\n";
223872cb 532
78f39e78 533 /* This creates an RFC 822 date */
867fed37 534 $date = date('D, j M Y H:i:s ', time()) . $this->timezone();
49c7f411 535
78f39e78 536 /* Create a message-id */
10adeb76 537 $message_id = 'MESSAGE ID GENERATION ERROR! PLEASE CONTACT SQUIRRELMAIL DEVELOPERS';
538 if (empty($rfc822_header->message_id)) {
539 $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 .'>';
548 }
b67d61ee 549
78f39e78 550 /* Make an RFC822 Received: line */
551 if (isset($REMOTE_HOST)) {
552 $received_from = "$REMOTE_HOST ([$REMOTE_ADDR])";
553 } else {
554 $received_from = $REMOTE_ADDR;
555 }
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';
559 }
560 $received_from .= " (proxying for $HTTP_X_FORWARDED_FOR)";
561 }
562 $header = array();
432db2fc 563
564 /**
565 * SquirrelMail header
566 *
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.
bb08da84 572 *
904849cc 573 * Add hide_squirrelmail_header as a candidate for config_local.php
574 * (must be defined as a constant: define('hide_squirrelmail_header', 1);
bb08da84 575 * to allow completely hiding SquirrelMail participation in message
539a323f 576 * processing; This is dangerous, especially if users can modify their
e930c9fe 577 * account information, as it makes mapping a sent message back to the
578 * original sender almost impossible.
432db2fc 579 */
e930c9fe 580 $show_sm_header = ( defined('hide_squirrelmail_header') ? ! hide_squirrelmail_header : 1 );
bb08da84 581
10adeb76 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
bb08da84 583 if ( $show_sm_header ) {
584 if (isset($encode_header_key) &&
432db2fc 585 trim($encode_header_key)!='') {
586 // use encoded headers, if encryption key is set and not empty
b81efcb4 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;
432db2fc 589 if (isset($HTTP_X_FORWARDED_FOR))
b81efcb4 590 $header[] = 'X-Squirrel-ProxyHash:'.OneTimePadEncrypt($this->ip2hex($HTTP_X_FORWARDED_FOR),base64_encode($encode_header_key)).$rn;
bb08da84 591 } else {
432db2fc 592 // use default received headers
593 $header[] = "Received: from $received_from" . $rn;
2405c716 594 if (!isset($hide_auth_header) || !$hide_auth_header)
432db2fc 595 $header[] = " (SquirrelMail authenticated user $username)" . $rn;
596 $header[] = " by $SERVER_NAME with HTTP;" . $rn;
597 $header[] = " $date" . $rn;
bb08da84 598 }
35aaf666 599 }
432db2fc 600
59387d1c 601 /* Insert the rest of the header fields */
10adeb76 602
603 if (!empty($rfc822_header->message_id)) {
604 $header[] = 'Message-ID: '. $rfc822_header->message_id . $rn;
605 } else {
606 $header[] = 'Message-ID: '. $message_id . $rn;
607 $rfc822_header->message_id = $message_id;
608 }
609
ea87de81 610 if (is_object($reply_rfc822_header) &&
b4316b34 611 isset($reply_rfc822_header->message_id) &&
612 $reply_rfc822_header->message_id) {
78f39e78 613 $rep_message_id = $reply_rfc822_header->message_id;
78f39e78 614 $header[] = 'In-Reply-To: '.$rep_message_id . $rn;
10adeb76 615 $rfc822_header->in_reply_to = $rep_message_id;
78f39e78 616 $references = $this->calculate_references($reply_rfc822_header);
617 $header[] = 'References: '.$references . $rn;
10adeb76 618 $rfc822_header->references = $references;
78f39e78 619 }
10adeb76 620
621 if (!empty($rfc822_header->date) && $rfc822_header->date != -1) {
622 $header[] = 'Date: '. $rfc822_header->date . $rn;
623 } else {
624 $header[] = "Date: $date" . $rn;
625 $rfc822_header->date = $date;
626 }
627
59387d1c 628 $header[] = 'Subject: '.encodeHeader($rfc822_header->subject) . $rn;
ac47827c 629 $header[] = 'From: '. $rfc822_header->getAddr_s('from',",$rn ",true) . $rn;
630
ea87de81 631 // folding address list [From|To|Cc|Bcc] happens by using ",$rn<space>"
632 // as delimiter
ac47827c 633 // Do not use foldLine for that.
634
635 // RFC2822 if from contains more then 1 address
59387d1c 636 if (count($rfc822_header->from) > 1) {
215de78c 637 $header[] = 'Sender: '. $rfc822_header->getAddr_s('sender',',',true) . $rn;
78f39e78 638 }
639 if (count($rfc822_header->to)) {
ac47827c 640 $header[] = 'To: '. $rfc822_header->getAddr_s('to',",$rn ",true) . $rn;
78f39e78 641 }
642 if (count($rfc822_header->cc)) {
ac47827c 643 $header[] = 'Cc: '. $rfc822_header->getAddr_s('cc',",$rn ",true) . $rn;
78f39e78 644 }
645 if (count($rfc822_header->reply_to)) {
215de78c 646 $header[] = 'Reply-To: '. $rfc822_header->getAddr_s('reply_to',',',true) . $rn;
78f39e78 647 }
648 /* Sendmail should return true. Default = false */
649 $bcc = $this->getBcc();
650 if (count($rfc822_header->bcc)) {
ac47827c 651 $s = 'Bcc: '. $rfc822_header->getAddr_s('bcc',",$rn ",true) . $rn;
78f39e78 652 if (!$bcc) {
78f39e78 653 $raw_length += strlen($s);
654 } else {
655 $header[] = $s;
656 }
657 }
72b004e6 658 /* Identify SquirrelMail */
b37e457f 659 $header[] = 'User-Agent: SquirrelMail/' . SM_VERSION . $rn;
78f39e78 660 /* Do the MIME-stuff */
661 $header[] = 'MIME-Version: 1.0' . $rn;
662 $contenttype = 'Content-Type: '. $rfc822_header->content_type->type0 .'/'.
59387d1c 663 $rfc822_header->content_type->type1;
78f39e78 664 if (count($rfc822_header->content_type->properties)) {
665 foreach ($rfc822_header->content_type->properties as $k => $v) {
666 if ($k && $v) {
72b004e6 667 $contenttype .= ';' .$k.'='.$v;
78f39e78 668 }
669 }
670 }
59387d1c 671 $header[] = $contenttype . $rn;
78f39e78 672 if ($encoding = $rfc822_header->encoding) {
23301b33 673 $header[] = 'Content-Transfer-Encoding: ' . $encoding . $rn;
72b004e6 674 }
539a323f 675 if (isset($rfc822_header->dnt) && $rfc822_header->dnt) {
72b004e6 676 $dnt = $rfc822_header->getAddr_s('dnt');
78f39e78 677 /* Pegasus Mail */
678 $header[] = 'X-Confirm-Reading-To: '.$dnt. $rn;
679 /* RFC 2298 */
680 $header[] = 'Disposition-Notification-To: '.$dnt. $rn;
681 }
682 if ($rfc822_header->priority) {
d1db3699 683 switch($rfc822_header->priority)
78f39e78 684 {
d1db3699 685 case 1:
91e0dccc 686 $header[] = 'X-Priority: 1 (Highest)'.$rn;
687 $header[] = 'Importance: High'. $rn; break;
d1db3699 688 case 5:
91e0dccc 689 $header[] = 'X-Priority: 5 (Lowest)'.$rn;
690 $header[] = 'Importance: Low'. $rn; break;
78f39e78 691 default: break;
692 }
693 }
72b004e6 694 /* Insert headers from the $more_headers array */
78f39e78 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;
699 }
700 }
701 $cnt = count($header);
702 $hdr_s = '';
5d2a7b15 703
78f39e78 704 for ($i = 0 ; $i < $cnt ; $i++) {
5d2a7b15 705 $sKey = substr($header[$i],0,strpos($header[$i],':'));
706 switch ($sKey)
707 {
708 case 'Message-ID':
709 case 'In-Reply_To':
710 $hdr_s .= $header[$i];
711 break;
712 case 'References':
713 $sRefs = substr($header[$i],12);
714 $aRefs = explode(' ',$sRefs);
715 $sLine = 'References:';
716 foreach ($aRefs as $sReference) {
cc201b46 717 if ( trim($sReference) == '' ) {
718 /* Don't add spaces. */
719 } elseif (strlen($sLine)+strlen($sReference) >76) {
5d2a7b15 720 $hdr_s .= $sLine;
721 $sLine = $rn . ' ' . $sReference;
722 } else {
723 $sLine .= ' '. $sReference;
724 }
725 }
726 $hdr_s .= $sLine;
727 break;
ac47827c 728 case 'To':
729 case 'Cc':
730 case 'Bcc':
731 case 'From':
732 $hdr_s .= $header[$i];
733 break;
9bcf508d 734 default: $hdr_s .= $this->foldLine($header[$i], 78); break;
5d2a7b15 735 }
78f39e78 736 }
78f39e78 737 $header = $hdr_s;
738 $header .= $rn; /* One blank line to separate header and body */
739 $raw_length += strlen($header);
740 return $header;
4960ec8e 741 }
742
72b004e6 743 /**
9bcf508d 744 * Fold header lines per RFC 2822/2.2.3 and RFC 822/3.1.1
745 *
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
752 * spaces.
753 *
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.
758 *
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)
773 *
774 * @return string The folded header content, with a trailing CRLF.
775 *
776 */
777 function foldLine($header, $soft_wrap=78, $indent='', $hard_wrap=998) {
778
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)
784 //
785 // the order of these is significant - preferred
786 // fold points should be listed first
787 //
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
794 //
795 $hard_break_tokens = array(
796 '=', // includes encoded word detection
797 ',',
798 ';',
799 );
800
801 // the order of these is significant too
802 //
803 $whitespace = array(
804 ' ',
805 "\t",
806 );
807
808 $CRLF = "\r\n";
809
810 $folded_header = '';
811
812 // if using an indent string, reduce wrap limits by its size
813 //
814 if (!empty($indent)) {
815 $soft_wrap -= strlen($indent);
816 $hard_wrap -= strlen($indent);
817 }
818
819 while (strlen($header) > $soft_wrap) {
820
821 $soft_wrapped_line = substr($header, 0, $soft_wrap);
822
823 // look for a token as close to the end of the soft wrap limit as possible
824 //
825 foreach ($whitespace as $token) {
826
827 // note that this if statement also fails when $pos === 0,
828 // which is intended, since blank lines are not allowed
829 //
830 if ($pos = strrpos($soft_wrapped_line, $token))
831 {
832 $new_fold = substr($header, 0, $pos);
833
834 // make sure proposed fold doesn't create a blank line
835 //
836 if (!trim($new_fold)) continue;
837
838 // with whitespace breaks, we fold BEFORE the token
839 //
840 $folded_header .= $new_fold . $CRLF . $indent;
841 $header = substr($header, $pos);
842
843 // ready for next while() iteration
844 //
845 continue 2;
846
847 }
848
849 }
850
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
854 //
855 $hard_wrapped_line = substr($header, 0, $hard_wrap);
856
857 // look for a *SOFT* token as close to the
858 // beginning of the hard wrap limit as possible
859 //
860 foreach ($whitespace as $token) {
861
862 // use while loop instead of if block because it
863 // is possible we don't want the first one we find
864 //
865 $pos = $soft_wrap - 1; // -1 is corrected by +1 on next line
866 while ($pos = strpos($hard_wrapped_line, $token, $pos + 1))
867 {
868
869 $new_fold = substr($header, 0, $pos);
870
871 // make sure proposed fold doesn't create a blank line
872 //
873 if (!trim($new_fold)) continue;
874
875 // with whitespace breaks, we fold BEFORE the token
876 //
877 $folded_header .= $new_fold . $CRLF . $indent;
878 $header = substr($header, $pos);
879
880 // ready for next outter while() iteration
881 //
882 continue 3;
883
884 }
885
886 }
887
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
892 //
893 if (strlen($header) <= strlen($hard_wrapped_line))
894 break;
895
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
898 //
899 foreach ($hard_break_tokens as $token) {
900
901 // note that this if statement also fails when $pos === 0,
902 // which is intended, since blank lines are not allowed
903 //
904 if ($pos = strrpos($hard_wrapped_line, $token))
905 {
906
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
910 //
911 if ($token == '=') {
912
913 // if we found the beginning of an encoded word,
914 // we want to break BEFORE the token
915 //
916 if (preg_match('/^(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)/Ui',
917 substr($header, $pos))) {
918 $pos--;
72b004e6 919 }
9bcf508d 920
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
925 //
926 // of course, this is only possible if there is
927 // more content after the next hard wrap
928 //
929 // then look for the end of an encoded word in
930 // the next part (past the next hard wrap)
931 //
932 // then see if it is in fact part of a legitimate
933 // encoded word
934 //
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),
939 $matches)) {
940
941 $pos = $hard_wrap + $end_pos + 2 - strlen($matches[1]) - 1;
942
72b004e6 943 }
72b004e6 944
fb1d6cb6 945 }
9bcf508d 946
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
950 //
951 if ($pos === 0) continue;
952
953 // we are dealing with a simple token break...
954 //
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
958 //
959 $folded_header .= substr($header, 0, $pos + 1) . $CRLF;
960
961 // don't go beyond end of $header, though
962 //
963 if (strlen($header) > $pos + 1) {
964 $header = substr($header, $pos + 1);
965 if (!in_array($header{0}, $whitespace))
966 $header = ' ' . $indent . $header;
967 } else {
968 $header = '';
fb1d6cb6 969 }
9bcf508d 970
971 // ready for next while() iteration
972 //
973 continue 2;
974
fb1d6cb6 975 }
9bcf508d 976
fb1d6cb6 977 }
9bcf508d 978
979 // finally, we just couldn't find anything to fold on, so we
980 // have to just cut it off at the hard limit
981 //
982 $folded_header .= $hard_wrapped_line . $CRLF;
983
984 // is there more?
985 //
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;
990 } else {
991 $header = '';
fb1d6cb6 992 }
9bcf508d 993
fb1d6cb6 994 }
9bcf508d 995
996
997 // add any left-overs
998 //
999 $folded_header .= $header;
1000
1001
1002 // make sure it ends with a CRLF
1003 //
1004 if (substr($folded_header, -2) != $CRLF) $folded_header .= $CRLF;
1005
1006
1007 return $folded_header;
fb1d6cb6 1008 }
3b53d87f 1009
72b004e6 1010 /**
1011 * function mimeBoundary - calculates the mime boundary to use
1012 *
1013 * This function will generate a random mime boundary base part
1014 * for the message if the boundary has not already been set.
1015 *
3a70ee56 1016 * @return string $mimeBoundaryString random mime boundary string
72b004e6 1017 */
59387d1c 1018 function mimeBoundary () {
78f39e78 1019 static $mimeBoundaryString;
3b53d87f 1020
78f39e78 1021 if ( !isset( $mimeBoundaryString ) ||
1022 $mimeBoundaryString == '') {
1023 $mimeBoundaryString = '----=_' . date( 'YmdHis' ) . '_' .
1024 mt_rand( 10000, 99999 );
1025 }
1026 return $mimeBoundaryString;
3b53d87f 1027 }
1028
72b004e6 1029 /**
1030 * function timezone - Time offset for correct timezone
1031 *
3a70ee56 1032 * @return string $result with timezone and offset
72b004e6 1033 */
59387d1c 1034 function timezone () {
0bbf8622 1035 global $invert_time, $show_timezone_name;
78f39e78 1036
1037 $diff_second = date('Z');
1038 if ($invert_time) {
1039 $diff_second = - $diff_second;
1040 }
1041 if ($diff_second > 0) {
1042 $sign = '+';
1043 } else {
1044 $sign = '-';
1045 }
1046 $diff_second = abs($diff_second);
1047 $diff_hour = floor ($diff_second / 3600);
1048 $diff_minute = floor (($diff_second-3600*$diff_hour) / 60);
0bbf8622 1049
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)"
1060 //
1061 if ($show_timezone_name) {
1062 $zonename = '('.strftime('%Z').')';
1063 $result = sprintf ("%s%02d%02d %s", $sign, $diff_hour, $diff_minute, $zonename);
1064 } else {
1065 $result = sprintf ("%s%02d%02d", $sign, $diff_hour, $diff_minute);
1066 }
78f39e78 1067 return ($result);
59387d1c 1068 }
3b53d87f 1069
72b004e6 1070 /**
f2ac3325 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.
72b004e6 1076 *
1077 * @param Rfc822Header $hdr message header to calculate from
1078 *
f2ac3325 1079 * @return string $refer concatenated and trimmed References string
72b004e6 1080 */
1274f430 1081 function calculate_references($hdr) {
f2ac3325 1082 $aReferences = preg_split('/\s+/', $hdr->references);
78f39e78 1083 $message_id = $hdr->message_id;
1084 $in_reply_to = $hdr->in_reply_to;
539a323f 1085
f2ac3325 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;
1090 }
1091 $aReferences[] = $message_id;
1092
1093 // sanitize the array: trim whitespace, remove dupes
86e6a9eb 1094 array_walk($aReferences, 'sq_trim_value');
f2ac3325 1095 $aReferences = array_unique($aReferences);
1096
1097 while ( count($aReferences) > 4 && strlen(implode(' ', $aReferences)) >= 986 ) {
1098 $aReferences = array_merge(array_slice($aReferences,0,1),array_slice($aReferences,2));
78f39e78 1099 }
f2ac3325 1100 return implode(' ', $aReferences);
1101 }
1102
1103 /**
432db2fc 1104 * Converts ip address to hexadecimal string
1105 *
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
ea87de81 1109 * address. ipv4 addresses are represented as 8 byte strings, ipv6 addresses
432db2fc 1110 * are represented as 32 byte string.
1111 *
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
94511d23 1115 * @since 1.5.1 and 1.4.5
432db2fc 1116 */
1117 function ip2hex($string) {
1118 if (preg_match("/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/",$string,$match)) {
1119 // ipv4 address
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]);
1137 $ret='';
1138 foreach ($aAddr as $addr) {
1139 $ret.=str_pad($addr,4,'0',STR_PAD_LEFT);
1140 }
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]);
1145 $sStart='';
1146 foreach($aStart as $addr) {
1147 $sStart.=str_pad($addr,4,'0',STR_PAD_LEFT);
1148 }
1149 $aEnd = explode(':',$match[2]);
1150 $sEnd='';
1151 foreach($aEnd as $addr) {
1152 $sEnd.=str_pad($addr,4,'0',STR_PAD_LEFT);
1153 }
1154 $ret = $sStart
1155 . str_pad('',(32 - strlen($sStart . $sEnd)),'0',STR_PAD_LEFT)
1156 . $sEnd;
1157 } else {
1158 // unknown addressing
1159 $ret = $string;
1160 }
1161 return $ret;
1162 }
59387d1c 1163}