Documentation fix
[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 $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);
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;
302 // remove NUL characters
303 $body_part = str_replace("\0",'',$body_part);
304 $length += $this->clean_crlf($body_part);
305 if ($stream) {
306 $this->writeToStream($stream, $body_part);
307 }
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" )
319 $body_part .= "\n";
320 $length += $this->clean_crlf($body_part);
321 if ($stream) {
322 $this->writeToStream($stream, $body_part);
323 }
324 }
325 fclose($file);
326 }
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 }
340 }
341
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 *
350 * @return void
351 */
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);
356 return strlen($s);
357 }
358
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 *
367 * @return void
368 */
369 function strip_crlf(&$s) {
370 $s = str_replace("\r\n ", '', $s);
371 $s = str_replace("\r", '', $s);
372 $s = str_replace("\n", '', $s);
373 }
374
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 *
383 * @return void
384 */
385 function preWriteToStream(&$s) {
386 }
387
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 *
394 * @return void
395 */
396 function writeToStream($stream, $data) {
397 fputs($stream, $data);
398 }
399
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 *
412 * @return handle $stream file handle resource to SMTP stream
413 */
414 function initStream($message, $length=0, $host='', $port='', $user='', $pass='') {
415 return $stream;
416 }
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() {
426 return false;
427 }
428
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 *
435 * @return string $header properly formatted mime header
436 */
437 function prepareMIME_Header($message, $boundary) {
438 $mime_header = $message->mime_header;
439 $rn="\r\n";
440 $header = array();
441
442 $contenttype = 'Content-Type: '. $mime_header->type0 .'/'.
443 $mime_header->type1;
444 if (count($message->entities)) {
445 $contenttype .= ';' . 'boundary="'.$boundary.'"';
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 }
456
457 $header[] = $contenttype . $rn;
458 if ($mime_header->description) {
459 $header[] = 'Content-Description: ' . $mime_header->description . $rn;
460 }
461 if ($mime_header->encoding) {
462 $header[] = 'Content-Transfer-Encoding: ' . $mime_header->encoding . $rn;
463 } else {
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 */
468 } else {
469 $header[] = 'Content-Transfer-Encoding: base64' . $rn;
470 }
471 }
472 if ($mime_header->id) {
473 $header[] = 'Content-ID: ' . $mime_header->id . $rn;
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 }
482 $header[] = $contentdisp . $rn;
483 }
484 if ($mime_header->md5) {
485 $header[] = 'Content-MD5: ' . $mime_header->md5 . $rn;
486 }
487 if ($mime_header->language) {
488 $header[] = 'Content-Language: ' . $mime_header->language . $rn;
489 }
490
491 $cnt = count($header);
492 $hdr_s = '';
493 for ($i = 0 ; $i < $cnt ; $i++) {
494 $hdr_s .= $this->foldLine($header[$i], 78,str_pad('',4));
495 }
496 $header = $hdr_s;
497 $header .= $rn; /* One blank line to separate mimeheader and body-entity */
498 return $header;
499 }
500
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 *
512 * @return string $header
513 */
514 function prepareRFC822_Header(&$rfc822_header, $reply_rfc822_header, &$raw_length) {
515 global $domain, $username, $encode_header_key,
516 $edit_identity, $hide_auth_header;
517
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;
523 }
524
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);
530
531 $rn = "\r\n";
532
533 /* This creates an RFC 822 date */
534 $date = date('D, j M Y H:i:s ', time()) . $this->timezone();
535
536 /* Create a message-id */
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 }
549
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();
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.
572 *
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.
579 */
580 $show_sm_header = ( defined('hide_squirrelmail_header') ? ! hide_squirrelmail_header : 1 );
581
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;
591 } else {
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;
598 }
599 }
600
601 /* Insert the rest of the header fields */
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
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;
619 }
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
628 $header[] = 'Subject: '.encodeHeader($rfc822_header->subject) . $rn;
629 $header[] = 'From: '. $rfc822_header->getAddr_s('from',",$rn ",true) . $rn;
630
631 // folding address list [From|To|Cc|Bcc] happens by using ",$rn<space>"
632 // as delimiter
633 // Do not use foldLine for that.
634
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;
638 }
639 if (count($rfc822_header->to)) {
640 $header[] = 'To: '. $rfc822_header->getAddr_s('to',",$rn ",true) . $rn;
641 }
642 if (count($rfc822_header->cc)) {
643 $header[] = 'Cc: '. $rfc822_header->getAddr_s('cc',",$rn ",true) . $rn;
644 }
645 if (count($rfc822_header->reply_to)) {
646 $header[] = 'Reply-To: '. $rfc822_header->getAddr_s('reply_to',',',true) . $rn;
647 }
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;
652 if (!$bcc) {
653 $raw_length += strlen($s);
654 } else {
655 $header[] = $s;
656 }
657 }
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) {
666 if ($k && $v) {
667 $contenttype .= ';' .$k.'='.$v;
668 }
669 }
670 }
671 $header[] = $contenttype . $rn;
672 if ($encoding = $rfc822_header->encoding) {
673 $header[] = 'Content-Transfer-Encoding: ' . $encoding . $rn;
674 }
675 if (isset($rfc822_header->dnt) && $rfc822_header->dnt) {
676 $dnt = $rfc822_header->getAddr_s('dnt');
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) {
683 switch($rfc822_header->priority)
684 {
685 case 1:
686 $header[] = 'X-Priority: 1 (Highest)'.$rn;
687 $header[] = 'Importance: High'. $rn; break;
688 case 5:
689 $header[] = 'X-Priority: 5 (Lowest)'.$rn;
690 $header[] = 'Importance: Low'. $rn; break;
691 default: break;
692 }
693 }
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;
699 }
700 }
701 $cnt = count($header);
702 $hdr_s = '';
703
704 for ($i = 0 ; $i < $cnt ; $i++) {
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) {
717 if ( trim($sReference) == '' ) {
718 /* Don't add spaces. */
719 } elseif (strlen($sLine)+strlen($sReference) >76) {
720 $hdr_s .= $sLine;
721 $sLine = $rn . ' ' . $sReference;
722 } else {
723 $sLine .= ' '. $sReference;
724 }
725 }
726 $hdr_s .= $sLine;
727 break;
728 case 'To':
729 case 'Cc':
730 case 'Bcc':
731 case 'From':
732 $hdr_s .= $header[$i];
733 break;
734 default: $hdr_s .= $this->foldLine($header[$i], 78, str_pad('',4)); break;
735 }
736 }
737 $header = $hdr_s;
738 $header .= $rn; /* One blank line to separate header and body */
739 $raw_length += strlen($header);
740 return $header;
741 }
742
743 /**
744 * function foldLine - for cleanly folding of headerlines
745 *
746 * @param string $line
747 * @param integer $length length to fold the line at
748 * @param string $pre prefix the line with...
749 *
750 * @return string $line folded line with trailing CRLF
751 */
752 function foldLine($line, $length, $pre='') {
753 $line = substr($line,0, -2);
754 $length -= 2; /* do not fold between \r and \n */
755 $cnt = strlen($line);
756 if ($cnt > $length) { /* try folding */
757 $fold_string = "\r\n " . $pre;
758 $bFirstFold = false;
759 $aFoldLine = array();
760 while (strlen($line) > $length) {
761 $fold = false;
762 /* handle encoded parts */
763 if (preg_match('/(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)(\s+|.*)/Ui',$line,$regs)) {
764 $fold_tmp = $regs[1];
765 if (!trim($regs[5])) {
766 $fold_tmp .= $regs[5];
767 }
768 $iPosEnc = strpos($line,$fold_tmp);
769 $iLengthEnc = strlen($fold_tmp);
770 $iPosEncEnd = $iPosEnc+$iLengthEnc;
771 if ($iPosEnc < $length && (($iPosEncEnd) > $length)) {
772 $fold = true;
773 /* fold just before the start of the encoded string */
774 if ($iPosEnc) {
775 $aFoldLine[] = substr($line,0,$iPosEnc);
776 }
777 $line = substr($line,$iPosEnc);
778 if (!$bFirstFold) {
779 $bFirstFold = true;
780 $length -= strlen($fold_string);
781 }
782 if ($iLengthEnc > $length) { /* place the encoded
783 string on a separate line and do not fold inside it*/
784 /* minimize foldstring */
785 $fold_string = "\r\n ";
786 $aFoldLine[] = substr($line,0,$iLengthEnc);
787 $line = substr($line,$iLengthEnc);
788 }
789 } else if ($iPosEnc < $length) { /* the encoded string fits into the foldlength */
790 /*remainder */
791 $sLineRem = substr($line,$iPosEncEnd,$length - $iPosEncEnd);
792 if (preg_match('/^(=\?([^?]*)\?(Q|B)\?([^?]*)\?=)(.*)/Ui',$sLineRem) || !preg_match('/[=,;\s]/',$sLineRem)) {
793 /*impossible to fold clean in the next part -> fold after the enc string */
794 $aFoldLine[] = substr($line,0,$iPosEncEnd);
795 $line = substr($line,$iPosEncEnd);
796 $fold = true;
797 if (!$bFirstFold) {
798 $bFirstFold = true;
799 $length -= strlen($fold_string);
800 }
801 }
802 }
803 }
804 if (!$fold) {
805 $line_tmp = substr($line,0,$length);
806 $iFoldPos = false;
807 /* try to fold at logical places */
808 switch (true)
809 {
810 case ($iFoldPos = strrpos($line_tmp,',')): break;
811 case ($iFoldPos = strrpos($line_tmp,';')): break;
812 case ($iFoldPos = strrpos($line_tmp,' ')): break;
813 case ($iFoldPos = strrpos($line_tmp,'=')): break;
814 default: break;
815 }
816
817 if (!$iFoldPos) { /* clean folding didn't work */
818 $iFoldPos = $length;
819 }
820 $aFoldLine[] = substr($line,0,$iFoldPos+1);
821 $line = substr($line,$iFoldPos+1);
822 if (!$bFirstFold) {
823 $bFirstFold = true;
824 $length -= strlen($fold_string);
825 }
826 }
827 }
828 /*$reconstruct the line */
829 if ($line) {
830 $aFoldLine[] = $line;
831 }
832 $line = implode($fold_string,$aFoldLine);
833 }
834 return $line."\r\n";
835 }
836
837 /**
838 * function mimeBoundary - calculates the mime boundary to use
839 *
840 * This function will generate a random mime boundary base part
841 * for the message if the boundary has not already been set.
842 *
843 * @return string $mimeBoundaryString random mime boundary string
844 */
845 function mimeBoundary () {
846 static $mimeBoundaryString;
847
848 if ( !isset( $mimeBoundaryString ) ||
849 $mimeBoundaryString == '') {
850 $mimeBoundaryString = '----=_' . date( 'YmdHis' ) . '_' .
851 mt_rand( 10000, 99999 );
852 }
853 return $mimeBoundaryString;
854 }
855
856 /**
857 * function timezone - Time offset for correct timezone
858 *
859 * @return string $result with timezone and offset
860 */
861 function timezone () {
862 global $invert_time, $show_timezone_name;
863
864 $diff_second = date('Z');
865 if ($invert_time) {
866 $diff_second = - $diff_second;
867 }
868 if ($diff_second > 0) {
869 $sign = '+';
870 } else {
871 $sign = '-';
872 }
873 $diff_second = abs($diff_second);
874 $diff_hour = floor ($diff_second / 3600);
875 $diff_minute = floor (($diff_second-3600*$diff_hour) / 60);
876
877 // If an administrator wants to add the timezone name to the
878 // end of the date header, they can set $show_timezone_name
879 // to boolean TRUE in config/config_local.php, but that is
880 // NOT RFC-822 compliant (see section 5.1). Moreover, some
881 // Windows users reported that strftime('%Z') was returning
882 // the full zone name (not the abbreviation) which in some
883 // cases included 8-bit characters (not allowed as is in headers).
884 // The PHP manual actually does NOT promise what %Z will return
885 // for strftime!: "The time zone offset/abbreviation option NOT
886 // given by %z (depends on operating system)"
887 //
888 if ($show_timezone_name) {
889 $zonename = '('.strftime('%Z').')';
890 $result = sprintf ("%s%02d%02d %s", $sign, $diff_hour, $diff_minute, $zonename);
891 } else {
892 $result = sprintf ("%s%02d%02d", $sign, $diff_hour, $diff_minute);
893 }
894 return ($result);
895 }
896
897 /**
898 * function calculate_references - calculate correct References string
899 * Adds the current message ID, and makes sure it doesn't grow forever,
900 * to that extent it drops message-ID's in a smart way until the string
901 * length is under the recommended value of 1000 ("References: <986>\r\n").
902 * It always keeps the first and the last three ID's.
903 *
904 * @param Rfc822Header $hdr message header to calculate from
905 *
906 * @return string $refer concatenated and trimmed References string
907 */
908 function calculate_references($hdr) {
909 $aReferences = preg_split('/\s+/', $hdr->references);
910 $message_id = $hdr->message_id;
911 $in_reply_to = $hdr->in_reply_to;
912
913 // if References already exists, add the current message ID at the end.
914 // no References exists; if we know a IRT, add that aswell
915 if (count($aReferences) == 0 && $in_reply_to) {
916 $aReferences[] = $in_reply_to;
917 }
918 $aReferences[] = $message_id;
919
920 // sanitize the array: trim whitespace, remove dupes
921 array_walk($aReferences, 'sq_trim_value');
922 $aReferences = array_unique($aReferences);
923
924 while ( count($aReferences) > 4 && strlen(implode(' ', $aReferences)) >= 986 ) {
925 $aReferences = array_merge(array_slice($aReferences,0,1),array_slice($aReferences,2));
926 }
927 return implode(' ', $aReferences);
928 }
929
930 /**
931 * Converts ip address to hexadecimal string
932 *
933 * Function is used to convert ipv4 and ipv6 addresses to hex strings.
934 * It removes all delimiter symbols from ip addresses, converts decimal
935 * ipv4 numbers to hex and pads strings in order to present full length
936 * address. ipv4 addresses are represented as 8 byte strings, ipv6 addresses
937 * are represented as 32 byte string.
938 *
939 * If function fails to detect address format, it returns unprocessed string.
940 * @param string $string ip address string
941 * @return string processed ip address string
942 * @since 1.5.1 and 1.4.5
943 */
944 function ip2hex($string) {
945 if (preg_match("/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/",$string,$match)) {
946 // ipv4 address
947 $ret = str_pad(dechex($match[1]),2,'0',STR_PAD_LEFT)
948 . str_pad(dechex($match[2]),2,'0',STR_PAD_LEFT)
949 . str_pad(dechex($match[3]),2,'0',STR_PAD_LEFT)
950 . str_pad(dechex($match[4]),2,'0',STR_PAD_LEFT);
951 } 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)) {
952 // full ipv6 address
953 $ret = str_pad($match[1],4,'0',STR_PAD_LEFT)
954 . str_pad($match[2],4,'0',STR_PAD_LEFT)
955 . str_pad($match[3],4,'0',STR_PAD_LEFT)
956 . str_pad($match[4],4,'0',STR_PAD_LEFT)
957 . str_pad($match[5],4,'0',STR_PAD_LEFT)
958 . str_pad($match[6],4,'0',STR_PAD_LEFT)
959 . str_pad($match[7],4,'0',STR_PAD_LEFT)
960 . str_pad($match[8],4,'0',STR_PAD_LEFT);
961 } elseif (preg_match("/^\:\:([0-9a-h\:]+)$/i",$string,$match)) {
962 // short ipv6 with all starting symbols nulled
963 $aAddr=explode(':',$match[1]);
964 $ret='';
965 foreach ($aAddr as $addr) {
966 $ret.=str_pad($addr,4,'0',STR_PAD_LEFT);
967 }
968 $ret=str_pad($ret,32,'0',STR_PAD_LEFT);
969 } elseif (preg_match("/^([0-9a-h\:]+)::([0-9a-h\:]+)$/i",$string,$match)) {
970 // short ipv6 with middle part nulled
971 $aStart=explode(':',$match[1]);
972 $sStart='';
973 foreach($aStart as $addr) {
974 $sStart.=str_pad($addr,4,'0',STR_PAD_LEFT);
975 }
976 $aEnd = explode(':',$match[2]);
977 $sEnd='';
978 foreach($aEnd as $addr) {
979 $sEnd.=str_pad($addr,4,'0',STR_PAD_LEFT);
980 }
981 $ret = $sStart
982 . str_pad('',(32 - strlen($sStart . $sEnd)),'0',STR_PAD_LEFT)
983 . $sEnd;
984 } else {
985 // unknown addressing
986 $ret = $string;
987 }
988 return $ret;
989 }
990 }