Encode outgoing attachments that have lines longer than allowed per RFC. Otherwise...
[squirrelmail.git] / class / deliver / Deliver.class.php
1 <?php
2
3 /**
4 * Deliver.class.php
5 *
6 * This contains all the functions needed to send messages through
7 * a delivery backend.
8 *
9 * @author Marc Groot Koerkamp
10 * @copyright &copy; 1999-2007 The SquirrelMail Project Team
11 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
12 * @version $Id$
13 * @package squirrelmail
14 */
15
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 */
29 class Deliver {
30
31 /**
32 * function mail - send the message parts to the SMTP stream
33 *
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
65 * to change.
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.
70 *
71 * @return integer The number of bytes written (or that would have been
72 * written) to the output stream.
73 *
74 */
75 function mail(&$message, $stream=false, $reply_id=0, $reply_ent_id=0,
76 $imap_stream=NULL, $extra=NULL) {
77
78 $rfc822_header = &$message->rfc822_header;
79
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;
87
88
89 // calculate reply header if needed
90 //
91 if ($reply_id) {
92 global $imapConnection, $username, $imapServerAddress,
93 $imapPort, $mailbox;
94
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,
107 $imapServerAddress, $imapPort, 0);
108 }
109
110 sqimap_mailbox_select($my_imap_stream, $mailbox);
111 $reply_message = sqimap_get_message($my_imap_stream, $reply_id, $mailbox);
112
113 if ($close_imap_stream) {
114 sqimap_logout($my_imap_stream);
115 }
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 }
130 $message->reply_rfc822_header = $orig_header;
131 }
132
133
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);
137
138 $this->send_mail($message, $header, $boundary, $stream, $raw_length, $extra);
139
140 return $raw_length;
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
171 if ($stream) {
172 $this->preWriteToStream($header);
173 $this->writeToStream($stream, $header);
174 }
175 $this->writeBody($message, $stream, $raw_length, $boundary);
176 }
177
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
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
194 *
195 * @return void
196 */
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_'));
202
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
208 //
209 } else if (strpos($boundary,'_trap_')) {
210 $boundary = substr(strrev($boundary),0,strpos(strrev($boundary),'_part_'));
211 }
212 $boundary_new = strrev($boundary . '_part_'.$message->entity_id);
213 } else {
214 $boundary_new = $boundary;
215 }
216 if ($boundary && !$message->rfc822_header) {
217 $s = '--'.$boundary."\r\n";
218 $s .= $this->prepareMIME_Header($message, $boundary_new);
219 $length_raw += strlen($s);
220 if ($stream) {
221 $this->preWriteToStream($s);
222 $this->writeToStream($stream, $s);
223 }
224 }
225 $this->writeBodyPart($message, $stream, $length_raw);
226
227 $last = false;
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;
231 }
232 if ($boundary && $last) {
233 $s = "--".$boundary_new."--\r\n\r\n";
234 $length_raw += strlen($s);
235 if ($stream) {
236 $this->preWriteToStream($s);
237 $this->writeToStream($stream, $s);
238 }
239 }
240 }
241
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
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
256 *
257 * @return void
258 */
259 function writeBodyPart($message, $stream, &$length) {
260 if ($message->mime_header) {
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;
273 // remove NUL characters
274 $body_part = str_replace("\0",'',$body_part);
275 $length += $this->clean_crlf($body_part);
276 if ($stream) {
277 $this->preWriteToStream($body_part);
278 $this->writeToStream($stream, $body_part);
279 }
280 $last = $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
286 // inspect attached file for lines longer than allowed by RFC,
287 // in which case we'll be using base64 encoding (so we can split
288 // the lines up without corrupting them) instead of 8bit unencoded...
289 // (see RFC 2822/2.1.1)
290 //
291 // using 990 because someone somewhere is folding lines at
292 // 990 instead of 998 and I'm too lazy to find who it is
293 //
294 $file_has_long_lines = file_has_long_lines($hashed_attachment_dir
295 . '/' . $filename, 990);
296
297 $file = fopen ($hashed_attachment_dir . '/' . $filename, 'rb');
298
299 // long lines were found, need to use base64 encoding
300 //
301 if ($file_has_long_lines) {
302 while ($tmp = fread($file, 570)) {
303 $body_part = chunk_split(base64_encode($tmp));
304 // Up to 4.3.10 chunk_split always appends a newline,
305 // while in 4.3.11 it doesn't if the string to split
306 // is shorter than the chunk length.
307 if( substr($body_part, -1 , 1 ) != "\n" )
308 $body_part .= "\n";
309 $length += $this->clean_crlf($body_part);
310 if ($stream) {
311 $this->writeToStream($stream, $body_part);
312 }
313 }
314 }
315
316 // no excessively long lines - normal 8bit
317 //
318 else {
319 while ($body_part = fgets($file, 4096)) {
320 // remove NUL characters
321 $body_part = str_replace("\0",'',$body_part);
322 $length += $this->clean_crlf($body_part);
323 if ($stream) {
324 $this->preWriteToStream($body_part);
325 $this->writeToStream($stream, $body_part);
326 }
327 $last = $body_part;
328 }
329 }
330
331 fclose($file);
332 }
333 break;
334 default:
335 if ($message->body_part) {
336 $body_part = $message->body_part;
337 // remove NUL characters
338 $body_part = str_replace("\0",'',$body_part);
339 $length += $this->clean_crlf($body_part);
340 if ($stream) {
341 $this->writeToStream($stream, $body_part);
342 }
343 } elseif ($message->att_local_name) {
344 global $username, $attachment_dir;
345 $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
346 $filename = $message->att_local_name;
347 $file = fopen ($hashed_attachment_dir . '/' . $filename, 'rb');
348 while ($tmp = fread($file, 570)) {
349 $body_part = chunk_split(base64_encode($tmp));
350 // Up to 4.3.10 chunk_split always appends a newline,
351 // while in 4.3.11 it doesn't if the string to split
352 // is shorter than the chunk length.
353 if( substr($body_part, -1 , 1 ) != "\n" )
354 $body_part .= "\n";
355 $length += $this->clean_crlf($body_part);
356 if ($stream) {
357 $this->writeToStream($stream, $body_part);
358 }
359 }
360 fclose($file);
361 }
362 break;
363 }
364 $body_part_trailing = '';
365 if ($last && substr($last,-1) != "\n") {
366 $body_part_trailing = "\r\n";
367 }
368 if ($body_part_trailing) {
369 $length += strlen($body_part_trailing);
370 if ($stream) {
371 $this->preWriteToStream($body_part_trailing);
372 $this->writeToStream($stream, $body_part_trailing);
373 }
374 }
375 }
376
377 /**
378 * function clean_crlf - change linefeeds and newlines to legal characters
379 *
380 * The SMTP format only allows CRLF as line terminators.
381 * This function replaces illegal teminators with the correct terminator.
382 *
383 * @param string &$s string to clean linefeeds on
384 *
385 * @return void
386 */
387 function clean_crlf(&$s) {
388 $s = str_replace("\r\n", "\n", $s);
389 $s = str_replace("\r", "\n", $s);
390 $s = str_replace("\n", "\r\n", $s);
391 return strlen($s);
392 }
393
394 /**
395 * function strip_crlf - strip linefeeds and newlines from a string
396 *
397 * The SMTP format only allows CRLF as line terminators.
398 * This function strips all line terminators from the string.
399 *
400 * @param string &$s string to clean linefeeds on
401 *
402 * @return void
403 */
404 function strip_crlf(&$s) {
405 $s = str_replace("\r\n ", '', $s);
406 $s = str_replace("\r", '', $s);
407 $s = str_replace("\n", '', $s);
408 }
409
410 /**
411 * function preWriteToStream - reserved for extended functionality
412 *
413 * This function is not yet implemented.
414 * Reserved for extended functionality.
415 *
416 * @param string &$s string to operate on
417 *
418 * @return void
419 */
420 function preWriteToStream(&$s) {
421 }
422
423 /**
424 * function writeToStream - write data to the SMTP stream
425 *
426 * @param resource $stream SMTP output stream
427 * @param string $data string with data to send to the SMTP stream
428 *
429 * @return void
430 */
431 function writeToStream($stream, $data) {
432 fputs($stream, $data);
433 }
434
435 /**
436 * function initStream - reserved for extended functionality
437 *
438 * This function is not yet implemented.
439 * Reserved for extended functionality.
440 *
441 * @param Message $message Message object
442 * @param string $host host name or IP to connect to
443 * @param string $user username to log into the SMTP server with
444 * @param string $pass password to log into the SMTP server with
445 * @param integer $length
446 *
447 * @return handle $stream file handle resource to SMTP stream
448 */
449 function initStream($message, $length=0, $host='', $port='', $user='', $pass='') {
450 return $stream;
451 }
452
453 /**
454 * function getBCC - reserved for extended functionality
455 *
456 * This function is not yet implemented.
457 * Reserved for extended functionality.
458 *
459 */
460 function getBCC() {
461 return false;
462 }
463
464 /**
465 * function prepareMIME_Header - creates the mime header
466 *
467 * @param Message $message Message object to act on
468 * @param string $boundary mime boundary from fn MimeBoundary
469 *
470 * @return string $header properly formatted mime header
471 */
472 function prepareMIME_Header($message, $boundary) {
473 $mime_header = $message->mime_header;
474 $rn="\r\n";
475 $header = array();
476
477 $contenttype = 'Content-Type: '. $mime_header->type0 .'/'.
478 $mime_header->type1;
479 if (count($message->entities)) {
480 $contenttype .= ';' . 'boundary="'.$boundary.'"';
481 }
482 if (isset($mime_header->parameters['name'])) {
483 $contenttype .= '; name="'.
484 encodeHeader($mime_header->parameters['name']). '"';
485 }
486 if (isset($mime_header->parameters['charset'])) {
487 $charset = $mime_header->parameters['charset'];
488 $contenttype .= '; charset="'.
489 encodeHeader($charset). '"';
490 }
491
492 $header[] = $contenttype . $rn;
493 if ($mime_header->description) {
494 $header[] = 'Content-Description: ' . $mime_header->description . $rn;
495 }
496 if ($mime_header->encoding) {
497 $header[] = 'Content-Transfer-Encoding: ' . $mime_header->encoding . $rn;
498 } else {
499
500 // inspect attached file for lines longer than allowed by RFC,
501 // in which case we'll be using base64 encoding (so we can split
502 // the lines up without corrupting them) instead of 8bit unencoded...
503 // (see RFC 2822/2.1.1)
504 //
505 if (!empty($message->att_local_name)) { // is this redundant? I have no idea
506 global $username, $attachment_dir;
507 $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
508 $filename = $hashed_attachment_dir . '/' . $message->att_local_name;
509
510 // using 990 because someone somewhere is folding lines at
511 // 990 instead of 998 and I'm too lazy to find who it is
512 //
513 $file_has_long_lines = file_has_long_lines($filename, 990);
514 } else
515 $file_has_long_lines = FALSE;
516
517 if ($mime_header->type0 == 'multipart' || $mime_header->type0 == 'alternative') {
518 /* no-op; no encoding needed */
519 } else if (($mime_header->type0 == 'text' || $mime_header->type0 == 'message')
520 && !$file_has_long_lines) {
521 $header[] = 'Content-Transfer-Encoding: 8bit' . $rn;
522 } else {
523 $header[] = 'Content-Transfer-Encoding: base64' . $rn;
524 }
525 }
526 if ($mime_header->id) {
527 $header[] = 'Content-ID: ' . $mime_header->id . $rn;
528 }
529 if ($mime_header->disposition) {
530 $disposition = $mime_header->disposition;
531 $contentdisp = 'Content-Disposition: ' . $disposition->name;
532 if ($disposition->getProperty('filename')) {
533 $contentdisp .= '; filename="'.
534 encodeHeader($disposition->getProperty('filename')). '"';
535 }
536 $header[] = $contentdisp . $rn;
537 }
538 if ($mime_header->md5) {
539 $header[] = 'Content-MD5: ' . $mime_header->md5 . $rn;
540 }
541 if ($mime_header->language) {
542 $header[] = 'Content-Language: ' . $mime_header->language . $rn;
543 }
544
545 $cnt = count($header);
546 $hdr_s = '';
547 for ($i = 0 ; $i < $cnt ; $i++) {
548 $hdr_s .= $this->foldLine($header[$i], 78);
549 }
550 $header = $hdr_s;
551 $header .= $rn; /* One blank line to separate mimeheader and body-entity */
552 return $header;
553 }
554
555 /**
556 * function prepareRFC822_Header - prepares the RFC822 header string from Rfc822Header object(s)
557 *
558 * This function takes the Rfc822Header object(s) and formats them
559 * into the RFC822Header string to send to the SMTP server as part
560 * of the SMTP message.
561 *
562 * @param Rfc822Header $rfc822_header
563 * @param Rfc822Header $reply_rfc822_header
564 * @param integer &$raw_length length of the message
565 *
566 * @return string $header
567 */
568 function prepareRFC822_Header(&$rfc822_header, $reply_rfc822_header, &$raw_length) {
569 global $domain, $username, $encode_header_key,
570 $edit_identity, $hide_auth_header;
571
572 /* if server var SERVER_NAME not available, or contains
573 ":" (e.g. IPv6) which is illegal in a Message-ID, use $domain */
574 if(!sqGetGlobalVar('SERVER_NAME', $SERVER_NAME, SQ_SERVER) ||
575 strpos($SERVER_NAME,':') !== FALSE) {
576 $SERVER_NAME = $domain;
577 }
578
579 sqGetGlobalVar('REMOTE_ADDR', $REMOTE_ADDR, SQ_SERVER);
580 sqGetGlobalVar('REMOTE_PORT', $REMOTE_PORT, SQ_SERVER);
581 sqGetGlobalVar('REMOTE_HOST', $REMOTE_HOST, SQ_SERVER);
582 sqGetGlobalVar('HTTP_VIA', $HTTP_VIA, SQ_SERVER);
583 sqGetGlobalVar('HTTP_X_FORWARDED_FOR', $HTTP_X_FORWARDED_FOR, SQ_SERVER);
584
585 $rn = "\r\n";
586
587 /* This creates an RFC 822 date */
588 $date = date('D, j M Y H:i:s ', time()) . $this->timezone();
589
590 /* Create a message-id */
591 $message_id = 'MESSAGE ID GENERATION ERROR! PLEASE CONTACT SQUIRRELMAIL DEVELOPERS';
592 if (empty($rfc822_header->message_id)) {
593 $message_id = '<';
594 /* user-specifc data to decrease collision chance */
595 $seed_data = $username . '.';
596 $seed_data .= (!empty($REMOTE_PORT) ? $REMOTE_PORT . '.' : '');
597 $seed_data .= (!empty($REMOTE_ADDR) ? $REMOTE_ADDR . '.' : '');
598 /* add the current time in milliseconds and randomness */
599 $seed_data .= uniqid(mt_rand(),true);
600 /* put it through one-way hash and add it to the ID */
601 $message_id .= md5($seed_data) . '.squirrel@' . $SERVER_NAME .'>';
602 }
603
604 /* Make an RFC822 Received: line */
605 if (isset($REMOTE_HOST)) {
606 $received_from = "$REMOTE_HOST ([$REMOTE_ADDR])";
607 } else {
608 $received_from = $REMOTE_ADDR;
609 }
610 if (isset($HTTP_VIA) || isset ($HTTP_X_FORWARDED_FOR)) {
611 if (!isset($HTTP_X_FORWARDED_FOR) || $HTTP_X_FORWARDED_FOR == '') {
612 $HTTP_X_FORWARDED_FOR = 'unknown';
613 }
614 $received_from .= " (proxying for $HTTP_X_FORWARDED_FOR)";
615 }
616 $header = array();
617
618 /**
619 * SquirrelMail header
620 *
621 * This Received: header provides information that allows to track
622 * user and machine that was used to send email. Don't remove it
623 * unless you understand all possible forging issues or your
624 * webmail installation does not prevent changes in user's email address.
625 * See SquirrelMail bug tracker #847107 for more details about it.
626 *
627 * Add hide_squirrelmail_header as a candidate for config_local.php
628 * (must be defined as a constant: define('hide_squirrelmail_header', 1);
629 * to allow completely hiding SquirrelMail participation in message
630 * processing; This is dangerous, especially if users can modify their
631 * account information, as it makes mapping a sent message back to the
632 * original sender almost impossible.
633 */
634 $show_sm_header = ( defined('hide_squirrelmail_header') ? ! hide_squirrelmail_header : 1 );
635
636 // 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
637 if ( $show_sm_header ) {
638 if (isset($encode_header_key) &&
639 trim($encode_header_key)!='') {
640 // use encoded headers, if encryption key is set and not empty
641 $header[] = 'X-Squirrel-UserHash: '.OneTimePadEncrypt($username,base64_encode($encode_header_key)).$rn;
642 $header[] = 'X-Squirrel-FromHash: '.OneTimePadEncrypt($this->ip2hex($REMOTE_ADDR),base64_encode($encode_header_key)).$rn;
643 if (isset($HTTP_X_FORWARDED_FOR))
644 $header[] = 'X-Squirrel-ProxyHash:'.OneTimePadEncrypt($this->ip2hex($HTTP_X_FORWARDED_FOR),base64_encode($encode_header_key)).$rn;
645 } else {
646 // use default received headers
647 $header[] = "Received: from $received_from" . $rn;
648 if (!isset($hide_auth_header) || !$hide_auth_header)
649 $header[] = " (SquirrelMail authenticated user $username)" . $rn;
650 $header[] = " by $SERVER_NAME with HTTP;" . $rn;
651 $header[] = " $date" . $rn;
652 }
653 }
654
655 /* Insert the rest of the header fields */
656
657 if (!empty($rfc822_header->message_id)) {
658 $header[] = 'Message-ID: '. $rfc822_header->message_id . $rn;
659 } else {
660 $header[] = 'Message-ID: '. $message_id . $rn;
661 $rfc822_header->message_id = $message_id;
662 }
663
664 if (is_object($reply_rfc822_header) &&
665 isset($reply_rfc822_header->message_id) &&
666 $reply_rfc822_header->message_id) {
667 $rep_message_id = $reply_rfc822_header->message_id;
668 $header[] = 'In-Reply-To: '.$rep_message_id . $rn;
669 $rfc822_header->in_reply_to = $rep_message_id;
670 $references = $this->calculate_references($reply_rfc822_header);
671 $header[] = 'References: '.$references . $rn;
672 $rfc822_header->references = $references;
673 }
674
675 if (!empty($rfc822_header->date) && $rfc822_header->date != -1) {
676 $header[] = 'Date: '. $rfc822_header->date . $rn;
677 } else {
678 $header[] = "Date: $date" . $rn;
679 $rfc822_header->date = $date;
680 }
681
682 $header[] = 'Subject: '.encodeHeader($rfc822_header->subject) . $rn;
683 $header[] = 'From: '. $rfc822_header->getAddr_s('from',",$rn ",true) . $rn;
684
685 // folding address list [From|To|Cc|Bcc] happens by using ",$rn<space>"
686 // as delimiter
687 // Do not use foldLine for that.
688
689 // RFC2822 if from contains more then 1 address
690 if (count($rfc822_header->from) > 1) {
691 $header[] = 'Sender: '. $rfc822_header->getAddr_s('sender',',',true) . $rn;
692 }
693 if (count($rfc822_header->to)) {
694 $header[] = 'To: '. $rfc822_header->getAddr_s('to',",$rn ",true) . $rn;
695 }
696 if (count($rfc822_header->cc)) {
697 $header[] = 'Cc: '. $rfc822_header->getAddr_s('cc',",$rn ",true) . $rn;
698 }
699 if (count($rfc822_header->reply_to)) {
700 $header[] = 'Reply-To: '. $rfc822_header->getAddr_s('reply_to',',',true) . $rn;
701 }
702 /* Sendmail should return true. Default = false */
703 $bcc = $this->getBcc();
704 if (count($rfc822_header->bcc)) {
705 $s = 'Bcc: '. $rfc822_header->getAddr_s('bcc',",$rn ",true) . $rn;
706 if (!$bcc) {
707 $raw_length += strlen($s);
708 } else {
709 $header[] = $s;
710 }
711 }
712 /* Identify SquirrelMail */
713 $header[] = 'User-Agent: SquirrelMail/' . SM_VERSION . $rn;
714 /* Do the MIME-stuff */
715 $header[] = 'MIME-Version: 1.0' . $rn;
716 $contenttype = 'Content-Type: '. $rfc822_header->content_type->type0 .'/'.
717 $rfc822_header->content_type->type1;
718 if (count($rfc822_header->content_type->properties)) {
719 foreach ($rfc822_header->content_type->properties as $k => $v) {
720 if ($k && $v) {
721 $contenttype .= ';' .$k.'='.$v;
722 }
723 }
724 }
725 $header[] = $contenttype . $rn;
726 if ($encoding = $rfc822_header->encoding) {
727 $header[] = 'Content-Transfer-Encoding: ' . $encoding . $rn;
728 }
729 if (isset($rfc822_header->dnt) && $rfc822_header->dnt) {
730 $dnt = $rfc822_header->getAddr_s('dnt');
731 /* Pegasus Mail */
732 $header[] = 'X-Confirm-Reading-To: '.$dnt. $rn;
733 /* RFC 2298 */
734 $header[] = 'Disposition-Notification-To: '.$dnt. $rn;
735 }
736 if ($rfc822_header->priority) {
737 switch($rfc822_header->priority)
738 {
739 case 1:
740 $header[] = 'X-Priority: 1 (Highest)'.$rn;
741 $header[] = 'Importance: High'. $rn; break;
742 case 5:
743 $header[] = 'X-Priority: 5 (Lowest)'.$rn;
744 $header[] = 'Importance: Low'. $rn; break;
745 default: break;
746 }
747 }
748 /* Insert headers from the $more_headers array */
749 if(count($rfc822_header->more_headers)) {
750 reset($rfc822_header->more_headers);
751 foreach ($rfc822_header->more_headers as $k => $v) {
752 $header[] = $k.': '.$v .$rn;
753 }
754 }
755 $cnt = count($header);
756 $hdr_s = '';
757
758 for ($i = 0 ; $i < $cnt ; $i++) {
759 $sKey = substr($header[$i],0,strpos($header[$i],':'));
760 switch ($sKey)
761 {
762 case 'Message-ID':
763 case 'In-Reply_To':
764 $hdr_s .= $header[$i];
765 break;
766 case 'References':
767 $sRefs = substr($header[$i],12);
768 $aRefs = explode(' ',$sRefs);
769 $sLine = 'References:';
770 foreach ($aRefs as $sReference) {
771 if ( trim($sReference) == '' ) {
772 /* Don't add spaces. */
773 } elseif (strlen($sLine)+strlen($sReference) >76) {
774 $hdr_s .= $sLine;
775 $sLine = $rn . ' ' . $sReference;
776 } else {
777 $sLine .= ' '. $sReference;
778 }
779 }
780 $hdr_s .= $sLine;
781 break;
782 case 'To':
783 case 'Cc':
784 case 'Bcc':
785 case 'From':
786 $hdr_s .= $header[$i];
787 break;
788 default: $hdr_s .= $this->foldLine($header[$i], 78); break;
789 }
790 }
791 $header = $hdr_s;
792 $header .= $rn; /* One blank line to separate header and body */
793 $raw_length += strlen($header);
794 return $header;
795 }
796
797 /**
798 * Fold header lines per RFC 2822/2.2.3 and RFC 822/3.1.1
799 *
800 * Herein "soft" folding/wrapping (with whitespace tokens) is
801 * what we refer to as the preferred method of wrapping - that
802 * which we'd like to do within the $soft_wrap limit, but if
803 * not possible, we will try to do as soon as possible after
804 * $soft_wrap up to the $hard_wrap limit. Encoded words don't
805 * need to be detected in this phase, since they cannot contain
806 * spaces.
807 *
808 * "Hard" folding/wrapping (with "hard" tokens) is what we refer
809 * to as less ideal wrapping that will be done to keep within
810 * the $hard_wrap limit. This adds other syntactical breaking
811 * elements such as commas and encoded words.
812 *
813 * @param string $header The header content being folded
814 * @param integer $soft_wrap The desirable maximum line length
815 * (OPTIONAL; default is 78, per RFC)
816 * @param string $indent Wrapped lines will already have
817 * whitespace following the CRLF wrap,
818 * but you can add more indentation (or
819 * whatever) with this. The use of this
820 * parameter is DISCOURAGED, since it
821 * can corrupt the redisplay (unfolding)
822 * of headers whose content is space-
823 * sensitive, like subjects, etc.
824 * (OPTIONAL; default is an empty string)
825 * @param string $hard_wrap The absolute maximum line length
826 * (OPTIONAL; default is 998, per RFC)
827 *
828 * @return string The folded header content, with a trailing CRLF.
829 *
830 */
831 function foldLine($header, $soft_wrap=78, $indent='', $hard_wrap=998) {
832
833 // the "hard" token list can be altered if desired,
834 // for example, by adding ":"
835 // (in the future, we can take optional arguments
836 // for overriding or adding elements to the "hard"
837 // token list if we want to get fancy)
838 //
839 // the order of these is significant - preferred
840 // fold points should be listed first
841 //
842 // it is advised that the "=" always come first
843 // since it also finds encoded words, thus if it
844 // comes after some other token that happens to
845 // fall within the encoded word, the encoded word
846 // could be inadvertently broken in half, which
847 // is not allowable per RFC
848 //
849 $hard_break_tokens = array(
850 '=', // includes encoded word detection
851 ',',
852 ';',
853 );
854
855 // the order of these is significant too
856 //
857 $whitespace = array(
858 ' ',
859 "\t",
860 );
861
862 $CRLF = "\r\n";
863
864 $folded_header = '';
865
866 // if using an indent string, reduce wrap limits by its size
867 //
868 if (!empty($indent)) {
869 $soft_wrap -= strlen($indent);
870 $hard_wrap -= strlen($indent);
871 }
872
873 while (strlen($header) > $soft_wrap) {
874
875 $soft_wrapped_line = substr($header, 0, $soft_wrap);
876
877 // look for a token as close to the end of the soft wrap limit as possible
878 //
879 foreach ($whitespace as $token) {
880
881 // note that this if statement also fails when $pos === 0,
882 // which is intended, since blank lines are not allowed
883 //
884 if ($pos = strrpos($soft_wrapped_line, $token))
885 {
886 $new_fold = substr($header, 0, $pos);
887
888 // make sure proposed fold doesn't create a blank line
889 //
890 if (!trim($new_fold)) continue;
891
892 // with whitespace breaks, we fold BEFORE the token
893 //
894 $folded_header .= $new_fold . $CRLF . $indent;
895 $header = substr($header, $pos);
896
897 // ready for next while() iteration
898 //
899 continue 2;
900
901 }
902
903 }
904
905 // we were unable to find a wrapping point within the soft
906 // wrap limit, so now we'll try to find the first possible
907 // soft wrap point within the hard wrap limit
908 //
909 $hard_wrapped_line = substr($header, 0, $hard_wrap);
910
911 // look for a *SOFT* token as close to the
912 // beginning of the hard wrap limit as possible
913 //
914 foreach ($whitespace as $token) {
915
916 // use while loop instead of if block because it
917 // is possible we don't want the first one we find
918 //
919 $pos = $soft_wrap - 1; // -1 is corrected by +1 on next line
920 while ($pos = strpos($hard_wrapped_line, $token, $pos + 1))
921 {
922
923 $new_fold = substr($header, 0, $pos);
924
925 // make sure proposed fold doesn't create a blank line
926 //
927 if (!trim($new_fold)) continue;
928
929 // with whitespace breaks, we fold BEFORE the token
930 //
931 $folded_header .= $new_fold . $CRLF . $indent;
932 $header = substr($header, $pos);
933
934 // ready for next outter while() iteration
935 //
936 continue 3;
937
938 }
939
940 }
941
942 // we were still unable to find a soft wrapping point within
943 // both the soft and hard wrap limits, so if the length of
944 // what is left is no more than the hard wrap limit, we'll
945 // simply take the whole thing
946 //
947 if (strlen($header) <= strlen($hard_wrapped_line))
948 break;
949
950 // otherwise, we can't quit yet - look for a "hard" token
951 // as close to the end of the hard wrap limit as possible
952 //
953 foreach ($hard_break_tokens as $token) {
954
955 // note that this if statement also fails when $pos === 0,
956 // which is intended, since blank lines are not allowed
957 //
958 if ($pos = strrpos($hard_wrapped_line, $token))
959 {
960
961 // if we found a "=" token, we must determine whether,
962 // if it is part of an encoded word, it is the beginning
963 // or middle of one, where we need to readjust $pos a bit
964 //
965 if ($token == '=') {
966
967 // if we found the beginning of an encoded word,
968 // we want to break BEFORE the token
969 //
970 if (preg_match('/^(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)/Ui',
971 substr($header, $pos))) {
972 $pos--;
973 }
974
975 // check if we found this token in the *middle*
976 // of an encoded word, in which case we have to
977 // ignore it, pushing back to the token that
978 // starts the encoded word instead
979 //
980 // of course, this is only possible if there is
981 // more content after the next hard wrap
982 //
983 // then look for the end of an encoded word in
984 // the next part (past the next hard wrap)
985 //
986 // then see if it is in fact part of a legitimate
987 // encoded word
988 //
989 else if (strlen($header) > $hard_wrap
990 && ($end_pos = strpos(substr($header, $hard_wrap), '?=')) !== FALSE
991 && preg_match('/(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)$/Ui',
992 substr($header, 0, $hard_wrap + $end_pos + 2),
993 $matches)) {
994
995 $pos = $hard_wrap + $end_pos + 2 - strlen($matches[1]) - 1;
996
997 }
998
999 }
1000
1001 // $pos could have been changed; make sure it's
1002 // not at the beginning of the line, as blank
1003 // lines are not allowed
1004 //
1005 if ($pos === 0) continue;
1006
1007 // we are dealing with a simple token break...
1008 //
1009 // for non-whitespace breaks, we fold AFTER the token
1010 // and add a space after the fold if not immediately
1011 // followed by a whitespace character in the next part
1012 //
1013 $folded_header .= substr($header, 0, $pos + 1) . $CRLF;
1014
1015 // don't go beyond end of $header, though
1016 //
1017 if (strlen($header) > $pos + 1) {
1018 $header = substr($header, $pos + 1);
1019 if (!in_array($header{0}, $whitespace))
1020 $header = ' ' . $indent . $header;
1021 } else {
1022 $header = '';
1023 }
1024
1025 // ready for next while() iteration
1026 //
1027 continue 2;
1028
1029 }
1030
1031 }
1032
1033 // finally, we just couldn't find anything to fold on, so we
1034 // have to just cut it off at the hard limit
1035 //
1036 $folded_header .= $hard_wrapped_line . $CRLF;
1037
1038 // is there more?
1039 //
1040 if (strlen($header) > strlen($hard_wrapped_line)) {
1041 $header = substr($header, strlen($hard_wrapped_line));
1042 if (!in_array($header{0}, $whitespace))
1043 $header = ' ' . $indent . $header;
1044 } else {
1045 $header = '';
1046 }
1047
1048 }
1049
1050
1051 // add any left-overs
1052 //
1053 $folded_header .= $header;
1054
1055
1056 // make sure it ends with a CRLF
1057 //
1058 if (substr($folded_header, -2) != $CRLF) $folded_header .= $CRLF;
1059
1060
1061 return $folded_header;
1062 }
1063
1064 /**
1065 * function mimeBoundary - calculates the mime boundary to use
1066 *
1067 * This function will generate a random mime boundary base part
1068 * for the message if the boundary has not already been set.
1069 *
1070 * @return string $mimeBoundaryString random mime boundary string
1071 */
1072 function mimeBoundary () {
1073 static $mimeBoundaryString;
1074
1075 if ( !isset( $mimeBoundaryString ) ||
1076 $mimeBoundaryString == '') {
1077 $mimeBoundaryString = '----=_' . date( 'YmdHis' ) . '_' .
1078 mt_rand( 10000, 99999 );
1079 }
1080 return $mimeBoundaryString;
1081 }
1082
1083 /**
1084 * function timezone - Time offset for correct timezone
1085 *
1086 * @return string $result with timezone and offset
1087 */
1088 function timezone () {
1089 global $invert_time, $show_timezone_name;
1090
1091 $diff_second = date('Z');
1092 if ($invert_time) {
1093 $diff_second = - $diff_second;
1094 }
1095 if ($diff_second > 0) {
1096 $sign = '+';
1097 } else {
1098 $sign = '-';
1099 }
1100 $diff_second = abs($diff_second);
1101 $diff_hour = floor ($diff_second / 3600);
1102 $diff_minute = floor (($diff_second-3600*$diff_hour) / 60);
1103
1104 // If an administrator wants to add the timezone name to the
1105 // end of the date header, they can set $show_timezone_name
1106 // to boolean TRUE in config/config_local.php, but that is
1107 // NOT RFC-822 compliant (see section 5.1). Moreover, some
1108 // Windows users reported that strftime('%Z') was returning
1109 // the full zone name (not the abbreviation) which in some
1110 // cases included 8-bit characters (not allowed as is in headers).
1111 // The PHP manual actually does NOT promise what %Z will return
1112 // for strftime!: "The time zone offset/abbreviation option NOT
1113 // given by %z (depends on operating system)"
1114 //
1115 if ($show_timezone_name) {
1116 $zonename = '('.strftime('%Z').')';
1117 $result = sprintf ("%s%02d%02d %s", $sign, $diff_hour, $diff_minute, $zonename);
1118 } else {
1119 $result = sprintf ("%s%02d%02d", $sign, $diff_hour, $diff_minute);
1120 }
1121 return ($result);
1122 }
1123
1124 /**
1125 * function calculate_references - calculate correct References string
1126 * Adds the current message ID, and makes sure it doesn't grow forever,
1127 * to that extent it drops message-ID's in a smart way until the string
1128 * length is under the recommended value of 1000 ("References: <986>\r\n").
1129 * It always keeps the first and the last three ID's.
1130 *
1131 * @param Rfc822Header $hdr message header to calculate from
1132 *
1133 * @return string $refer concatenated and trimmed References string
1134 */
1135 function calculate_references($hdr) {
1136 $aReferences = preg_split('/\s+/', $hdr->references);
1137 $message_id = $hdr->message_id;
1138 $in_reply_to = $hdr->in_reply_to;
1139
1140 // if References already exists, add the current message ID at the end.
1141 // no References exists; if we know a IRT, add that aswell
1142 if (count($aReferences) == 0 && $in_reply_to) {
1143 $aReferences[] = $in_reply_to;
1144 }
1145 $aReferences[] = $message_id;
1146
1147 // sanitize the array: trim whitespace, remove dupes
1148 array_walk($aReferences, 'sq_trim_value');
1149 $aReferences = array_unique($aReferences);
1150
1151 while ( count($aReferences) > 4 && strlen(implode(' ', $aReferences)) >= 986 ) {
1152 $aReferences = array_merge(array_slice($aReferences,0,1),array_slice($aReferences,2));
1153 }
1154 return implode(' ', $aReferences);
1155 }
1156
1157 /**
1158 * Converts ip address to hexadecimal string
1159 *
1160 * Function is used to convert ipv4 and ipv6 addresses to hex strings.
1161 * It removes all delimiter symbols from ip addresses, converts decimal
1162 * ipv4 numbers to hex and pads strings in order to present full length
1163 * address. ipv4 addresses are represented as 8 byte strings, ipv6 addresses
1164 * are represented as 32 byte string.
1165 *
1166 * If function fails to detect address format, it returns unprocessed string.
1167 * @param string $string ip address string
1168 * @return string processed ip address string
1169 * @since 1.5.1 and 1.4.5
1170 */
1171 function ip2hex($string) {
1172 if (preg_match("/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/",$string,$match)) {
1173 // ipv4 address
1174 $ret = str_pad(dechex($match[1]),2,'0',STR_PAD_LEFT)
1175 . str_pad(dechex($match[2]),2,'0',STR_PAD_LEFT)
1176 . str_pad(dechex($match[3]),2,'0',STR_PAD_LEFT)
1177 . str_pad(dechex($match[4]),2,'0',STR_PAD_LEFT);
1178 } 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)) {
1179 // full ipv6 address
1180 $ret = str_pad($match[1],4,'0',STR_PAD_LEFT)
1181 . str_pad($match[2],4,'0',STR_PAD_LEFT)
1182 . str_pad($match[3],4,'0',STR_PAD_LEFT)
1183 . str_pad($match[4],4,'0',STR_PAD_LEFT)
1184 . str_pad($match[5],4,'0',STR_PAD_LEFT)
1185 . str_pad($match[6],4,'0',STR_PAD_LEFT)
1186 . str_pad($match[7],4,'0',STR_PAD_LEFT)
1187 . str_pad($match[8],4,'0',STR_PAD_LEFT);
1188 } elseif (preg_match("/^\:\:([0-9a-h\:]+)$/i",$string,$match)) {
1189 // short ipv6 with all starting symbols nulled
1190 $aAddr=explode(':',$match[1]);
1191 $ret='';
1192 foreach ($aAddr as $addr) {
1193 $ret.=str_pad($addr,4,'0',STR_PAD_LEFT);
1194 }
1195 $ret=str_pad($ret,32,'0',STR_PAD_LEFT);
1196 } elseif (preg_match("/^([0-9a-h\:]+)::([0-9a-h\:]+)$/i",$string,$match)) {
1197 // short ipv6 with middle part nulled
1198 $aStart=explode(':',$match[1]);
1199 $sStart='';
1200 foreach($aStart as $addr) {
1201 $sStart.=str_pad($addr,4,'0',STR_PAD_LEFT);
1202 }
1203 $aEnd = explode(':',$match[2]);
1204 $sEnd='';
1205 foreach($aEnd as $addr) {
1206 $sEnd.=str_pad($addr,4,'0',STR_PAD_LEFT);
1207 }
1208 $ret = $sStart
1209 . str_pad('',(32 - strlen($sStart . $sEnd)),'0',STR_PAD_LEFT)
1210 . $sEnd;
1211 } else {
1212 // unknown addressing
1213 $ret = $string;
1214 }
1215 return $ret;
1216 }
1217 }