fsf changes, meant to be rebased on upstream
[squirrelmail.git] / class / mime / Message.class.php
... / ...
CommitLineData
1<?php
2
3/**
4 * Message.class.php
5 *
6 * This file contains functions needed to handle mime messages.
7 *
8 * @copyright 2003-2024 The SquirrelMail Project Team
9 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
10 * @version $Id$
11 * @package squirrelmail
12 * @subpackage mime
13 * @since 1.3.2
14 */
15
16/**
17 * The object that contains a message.
18 *
19 * message is the object that contains messages. It is a recursive object in
20 * that through the $entities variable, it can contain more objects of type
21 * message. See documentation in mime.txt for a better description of how this
22 * works.
23 * @package squirrelmail
24 * @subpackage mime
25 * @since 1.3.0
26 */
27class Message {
28 var $header;
29 /**
30 * rfc822header object
31 * @var object
32 */
33 var $rfc822_header = '';
34 /**
35 * MessageHeader object
36 * @var object
37 */
38 var $mime_header = '';
39 /**
40 * @var mixed
41 */
42 var $flags = '';
43 /**
44 * Media type
45 * @var string
46 */
47 var $type0='';
48 /**
49 * Media subtype
50 * @var string
51 */
52 var $type1='';
53 /**
54 * Nested mime parts
55 * @var array
56 */
57 var $entities = array();
58 /**
59 * Message part id
60 * @var string
61 */
62 var $entity_id = '';
63 /**
64 * Parent message part id
65 * @var string
66 */
67 var $parent_ent;
68 /**
69 * @var mixed
70 */
71 var $entity;
72 /**
73 * @var mixed
74 */
75 var $parent = '';
76 /**
77 * @var string
78 */
79 var $decoded_body='';
80 /**
81 * Message \seen status
82 * @var boolean
83 */
84 var $is_seen = 0;
85 /**
86 * Message \answered status
87 * @var boolean
88 */
89 var $is_answered = 0;
90 /**
91 * Message forward status
92 * @var boolean
93 */
94 var $is_forwarded = 0;
95 /**
96 * Message \deleted status
97 * @var boolean
98 */
99 var $is_deleted = 0;
100 /**
101 * Message \flagged status
102 * @var boolean
103 */
104 var $is_flagged = 0;
105 /**
106 * Message mdn status
107 * @var boolean
108 */
109 var $is_mdnsent = 0;
110 /**
111 * Message text body
112 * @var string
113 */
114 var $body_part = '';
115 /**
116 * Message part offset
117 * for fetching body parts out of raw messages
118 * @var integer
119 */
120 var $offset = 0;
121 /**
122 * Message part length
123 * for fetching body parts out of raw messages
124 * @var integer
125 */
126 var $length = 0;
127 /**
128 * Local attachment filename location where the tempory attachment is
129 * stored. For use in delivery class.
130 * @var string
131 */
132 var $att_local_name = '';
133
134 /**
135 * @param string $ent entity id
136 */
137 function setEnt($ent) {
138 $this->entity_id= $ent;
139 }
140
141 /**
142 * Add nested message part
143 * @param object $msg
144 */
145 function addEntity ($msg) {
146 $this->entities[] = $msg;
147 }
148
149 /**
150 * Get file name used for mime part
151 * @return string file name
152 * @since 1.3.2
153 */
154 function getFilename() {
155 $filename = '';
156 $header = $this->header;
157 if (is_object($header->disposition)) {
158 $filename = $header->disposition->getProperty('filename');
159 if (trim($filename) == '') {
160 $name = decodeHeader($header->disposition->getProperty('name'));
161 if (!trim($name)) {
162 $name = $header->getParameter('name');
163 if(!trim($name)) {
164 if (!trim( $header->id )) {
165 $filename = 'untitled-[' . $this->entity_id . ']' . '.' . strtolower($header->type1);
166 } else {
167 $filename = 'cid: ' . $header->id . '.' . strtolower($header->type1);
168 }
169 } else {
170 $filename = $name;
171 }
172 } else {
173 $filename = $name;
174 }
175 }
176 } else {
177 $filename = $header->getParameter('filename');
178 if (!trim($filename)) {
179 $filename = $header->getParameter('name');
180 if (!trim($filename)) {
181 if (!trim( $header->id )) {
182 $filename = 'untitled-[' . $this->entity_id . ']' . '.' . strtolower($header->type1);
183 } else {
184 $filename = 'cid: ' . $header->id . '.' . strtolower($header->type1);
185 }
186 }
187 }
188 }
189 return $filename;
190 }
191
192 /**
193 * Add header object to message object.
194 * WARNING: Unfinished code. Don't expect it to work in older sm versions.
195 * @param mixed $read array or string with message headers
196 * @todo FIXME: rfc822header->parseHeader() does not return rfc822header object
197 */
198 function addRFC822Header($read) {
199 $header = new Rfc822Header();
200 $this->rfc822_header = $header->parseHeader($read);
201 }
202
203 /**
204 * @param string $ent
205 * @return mixed (object or string?)
206 */
207 function getEntity($ent) {
208 $cur_ent = $this->entity_id;
209 $msg = $this;
210 if (($cur_ent == '') || ($cur_ent == '0')) {
211 $cur_ent_a = array();
212 } else {
213 $cur_ent_a = explode('.', $this->entity_id);
214 }
215 $ent_a = explode('.', $ent);
216
217 for ($i = 0,$entCount = count($ent_a) - 1; $i < $entCount; ++$i) {
218 if (isset($cur_ent_a[$i]) && ($cur_ent_a[$i] != $ent_a[$i])) {
219 $msg = $msg->parent;
220 $cur_ent_a = explode('.', $msg->entity_id);
221 --$i;
222 } else if (!isset($cur_ent_a[$i])) {
223 if (isset($msg->entities[($ent_a[$i]-1)])) {
224 $msg = $msg->entities[($ent_a[$i]-1)];
225 } else {
226 $msg = $msg->entities[0];
227 }
228 }
229 if (($msg->type0 == 'message') && ($msg->type1 == 'rfc822')) {
230 /*this is a header for a message/rfc822 entity */
231 $msg = $msg->entities[0];
232 }
233 }
234
235 if (($msg->type0 == 'message') && ($msg->type1 == 'rfc822')) {
236 /*this is a header for a message/rfc822 entity */
237 $msg = $msg->entities[0];
238 }
239
240 if (isset($msg->entities[($ent_a[$entCount])-1])) {
241 if (is_object($msg->entities[($ent_a[$entCount])-1])) {
242 $msg = $msg->entities[($ent_a[$entCount]-1)];
243 }
244 }
245
246 return $msg;
247 }
248
249 /**
250 * Set message body
251 * @param string $s message body
252 */
253 function setBody($s) {
254 $this->body_part = $s;
255 }
256
257 /**
258 * Clean message object
259 */
260 function clean_up() {
261 $msg = $this;
262 $msg->body_part = '';
263
264 foreach ($msg->entities as $m) {
265 $m->clean_up();
266 }
267 }
268
269 /**
270 * @return string
271 */
272 function getMailbox() {
273 $msg = $this;
274 while (is_object($msg->parent)) {
275 $msg = $msg->parent;
276 }
277 return $msg->mailbox;
278 }
279
280 /*
281 * Bodystructure parser, a recursive function for generating the
282 * entity-tree with all the mime-parts.
283 *
284 * It follows RFC2060 and stores all the described fields in the
285 * message object.
286 *
287 * Question/Bugs:
288 *
289 * Ask for me (Marc Groot Koerkamp, stekkel@users.sourceforge.net)
290 * @param string $read
291 * @param integer $i
292 * @param mixed $sub_msg
293 * @return object Message object
294 * @todo define argument and return types
295 */
296 static function parseStructure($read, &$i, $sub_msg = '') {
297 $msg = Message::parseBodyStructure($read, $i, $sub_msg);
298 if($msg) $msg->setEntIds($msg,false,0);
299 return $msg;
300 }
301
302 /**
303 * @param object $msg
304 * @param mixed $init
305 * @param integer $i
306 * @todo document me
307 * @since 1.4.0
308 */
309 function setEntIds(&$msg,$init=false,$i=0) {
310 $iCnt = count($msg->entities);
311 if ($init !==false) {
312 $iEntSub = $i+1;
313 if ($msg->parent->type0 == 'message' &&
314 $msg->parent->type1 == 'rfc822' &&
315 $msg->type0 == 'multipart') {
316 $iEntSub = '0';
317 }
318 if ($init) {
319 $msg->entity_id = "$init.$iEntSub";
320 } else {
321 $msg->entity_id = $iEntSub;
322 }
323 } else if ($iCnt) {
324 $msg->entity_id='0';
325 } else {
326 $msg->entity_id='1';
327 }
328 for ($i=0;$i<$iCnt;++$i) {
329 $msg->entities[$i]->parent =& $msg;
330 if (strrchr($msg->entity_id, '.') != '.0') {
331 $msg->entities[$i]->setEntIds($msg->entities[$i],$msg->entity_id,$i);
332 } else {
333 $msg->entities[$i]->setEntIds($msg->entities[$i],$msg->parent->entity_id,$i);
334 }
335 }
336 }
337
338 /**
339 * @param string $read
340 * @param integer $i
341 * @param mixed $sub_msg
342 * @return object Message object
343 * @todo document me
344 * @since 1.4.0 (code was part of parseStructure() in 1.3.x)
345 */
346 static function parseBodyStructure($read, &$i, $sub_msg = '') {
347 $arg_no = 0;
348 $arg_a = array();
349 if ($sub_msg) {
350 $message = $sub_msg;
351 } else {
352 $message = new Message();
353 }
354
355 for ($cnt = strlen($read); $i < $cnt; ++$i) {
356 $char = strtoupper($read[$i]);
357 switch ($char) {
358 case '(':
359 switch($arg_no) {
360 case 0:
361 if (!isset($msg)) {
362 $msg = new Message();
363 $hdr = new MessageHeader();
364 $hdr->type0 = 'text';
365 $hdr->type1 = 'plain';
366 $hdr->encoding = '7bit';
367 $msg->header = $hdr;
368 } else {
369 $msg->header->type0 = 'multipart';
370 $msg->type0 = 'multipart';
371 while ($read[$i] == '(') {
372 $msg->addEntity($msg->parseBodyStructure($read, $i, $msg));
373 }
374 }
375 break;
376 case 1:
377 /* multipart properties */
378 ++$i;
379 $arg_a[] = $msg->parseProperties($read, $i);
380 ++$arg_no;
381 break;
382 case 2:
383 if (isset($msg->type0) && ($msg->type0 == 'multipart')) {
384 ++$i;
385 $arg_a[] = $msg->parseDisposition($read, $i);
386 } else { /* properties */
387 $arg_a[] = $msg->parseProperties($read, $i);
388 }
389 ++$arg_no;
390 break;
391 case 3:
392 if (isset($msg->type0) && ($msg->type0 == 'multipart')) {
393 ++$i;
394 $arg_a[]= $msg->parseLanguage($read, $i);
395 }
396 case 7:
397 if (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822')) {
398 $msg->header->type0 = $arg_a[0];
399 $msg->header->type1 = $arg_a[1];
400 $msg->type0 = $arg_a[0];
401 $msg->type1 = $arg_a[1];
402 $rfc822_hdr = new Rfc822Header();
403 $msg->rfc822_header = $msg->parseEnvelope($read, $i, $rfc822_hdr);
404 while (($i < $cnt) && ($read[$i] != '(')) {
405 ++$i;
406 }
407 $msg->addEntity($msg->parseBodyStructure($read, $i,$msg));
408 }
409 break;
410 case 8:
411 ++$i;
412 $arg_a[] = $msg->parseDisposition($read, $i);
413 ++$arg_no;
414 break;
415 case 9:
416 ++$i;
417 if (($arg_a[0] == 'text') || (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822'))) {
418 $arg_a[] = $msg->parseDisposition($read, $i);
419 } else {
420 $arg_a[] = $msg->parseLanguage($read, $i);
421 }
422 ++$arg_no;
423 break;
424 case 10:
425 if (($arg_a[0] == 'text') || (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822'))) {
426 ++$i;
427 $arg_a[] = $msg->parseLanguage($read, $i);
428 } else {
429 $i = $msg->parseParenthesis($read, $i);
430 $arg_a[] = ''; /* not yet described in rfc2060 */
431 }
432 ++$arg_no;
433 break;
434 default:
435 /* unknown argument, skip this part */
436 $i = $msg->parseParenthesis($read, $i);
437 $arg_a[] = '';
438 ++$arg_no;
439 break;
440 } /* switch */
441 break;
442 case '"':
443 /* inside an entity -> start processing */
444 $arg_s = $msg->parseQuote($read, $i);
445 ++$arg_no;
446 if ($arg_no < 3) {
447 $arg_s = strtolower($arg_s); /* type0 and type1 */
448 }
449 $arg_a[] = $arg_s;
450 break;
451 case 'n':
452 case 'N':
453 /* probably NIL argument */
454 $tmpnil = strtoupper(substr($read, $i, 4));
455 if ($tmpnil == 'NIL ' || $tmpnil == 'NIL)') {
456 $arg_a[] = '';
457 ++$arg_no;
458 $i += 2;
459 }
460 break;
461 case '{':
462 /* process the literal value */
463 $arg_a[] = $msg->parseLiteral($read, $i);
464 ++$arg_no;
465 break;
466 case '0':
467 case is_numeric($read[$i]):
468 /* process integers */
469 if ($read[$i] == ' ') { break; }
470 ++$arg_no;
471 if (preg_match('/^([0-9]+).*/',substr($read,$i), $regs)) {
472 $i += strlen($regs[1])-1;
473 $arg_a[] = $regs[1];
474 } else {
475 $arg_a[] = 0;
476 }
477 break;
478 case ')':
479 $multipart = (isset($msg->type0) && ($msg->type0 == 'multipart'));
480 if (!$multipart) {
481 $shifted_args = (($arg_a[0] == 'text') || (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822')));
482 $hdr->type0 = $arg_a[0];
483 $hdr->type1 = $arg_a[1];
484
485 $msg->type0 = $arg_a[0];
486 $msg->type1 = $arg_a[1];
487 $arr = $arg_a[2];
488 if (is_array($arr)) {
489 $hdr->parameters = $arg_a[2];
490 }
491 $hdr->id = str_replace('<', '', str_replace('>', '', $arg_a[3]));
492 $hdr->description = $arg_a[4];
493 $hdr->encoding = strtolower($arg_a[5]);
494 $hdr->entity_id = $msg->entity_id;
495 $hdr->size = $arg_a[6];
496 if ($shifted_args) {
497 $hdr->lines = $arg_a[7];
498 $s = 1;
499 } else {
500 $s = 0;
501 }
502 $hdr->md5 = (isset($arg_a[7+$s]) ? $arg_a[7+$s] : $hdr->md5);
503 $hdr->disposition = (isset($arg_a[8+$s]) ? $arg_a[8+$s] : $hdr->disposition);
504 $hdr->language = (isset($arg_a[9+$s]) ? $arg_a[9+$s] : $hdr->language);
505 $msg->header = $hdr;
506 } else {
507 $hdr->type0 = 'multipart';
508 $hdr->type1 = $arg_a[0];
509 $msg->type0 = 'multipart';
510 $msg->type1 = $arg_a[0];
511 $hdr->parameters = (isset($arg_a[1]) ? $arg_a[1] : $hdr->parameters);
512 $hdr->disposition = (isset($arg_a[2]) ? $arg_a[2] : $hdr->disposition);
513 $hdr->language = (isset($arg_a[3]) ? $arg_a[3] : $hdr->language);
514 $msg->header = $hdr;
515 }
516 return $msg;
517 default: break;
518 } /* switch */
519 } /* for */
520 } /* parsestructure */
521
522 /**
523 * @param string $read
524 * @param integer $i
525 * @return array
526 */
527 function parseProperties($read, &$i) {
528 $properties = array();
529 $prop_name = '';
530
531 for (; $read[$i] != ')'; ++$i) {
532 $arg_s = '';
533 if ($read[$i] == '"') {
534 $arg_s = $this->parseQuote($read, $i);
535 } else if ($read[$i] == '{') {
536 $arg_s = $this->parseLiteral($read, $i);
537 }
538
539 if ($arg_s != '') {
540 if ($prop_name == '') {
541 $prop_name = strtolower($arg_s);
542 $properties[$prop_name] = '';
543 } else if ($prop_name != '') {
544 $properties[$prop_name] = $arg_s;
545 $prop_name = '';
546 }
547 }
548 }
549 return $this->handleRfc2231($properties);
550 }
551
552 /**
553 * Joins RFC-2231 continuations, converts encoding to RFC-2047 style
554 * @param array $properties
555 * @return array
556 */
557 function handleRfc2231($properties) {
558
559 /* STAGE 1: look for multi-line parameters, convert to the single line
560 form, and normalize values */
561
562 $cont = array();
563 foreach($properties as $key=>$value) {
564 /* Look for parameters followed by "*", a number, and an optional "*"
565 at the end. */
566 if (preg_match('/^(.*\*)(\d+)(|\*)$/', $key, $matches)) {
567 unset($properties[$key]);
568 $prop_name = $matches[1];
569 if (!isset($cont[$prop_name])) $cont[$prop_name] = array();
570
571 /* An asterisk at the end of parameter name indicates that there
572 may be an encoding information present, and the parameter
573 value is percent-hex encoded. If parameter is not encoded, we
574 encode it to simplify further processing.
575 */
576 if ($matches[3] == '') $value = rawurlencode($value);
577 /* Use the number from parameter name as segment index */
578 $cont[$prop_name][$matches[2]] = $value;
579 }
580 }
581 foreach($cont as $key=>$values) {
582 /* Sort segments of multi-line parameters by index number. */
583 ksort($values);
584 /* Join segments. We can do it safely, because:
585 - All segments are encoded.
586 - Per RFC-2231, chapter 4.1 notes.
587 */
588 $value = implode('', $values);
589 /* Add language and character set field delimiters if not present,
590 as required per RFC-2231, chapter 4.1, note #5. */
591 if (strpos($value, "'") === false) $value = "''".$value;
592 $properties[$key] = $value;
593 }
594
595 /* STAGE 2: Convert single line RFC-2231 encoded parameters, and
596 previously converted multi-line parameters, to RFC-2047 encoding */
597
598 foreach($properties as $key=>$value) {
599 if ($idx = strpos($key, '*')) {
600 unset($properties[$key]);
601 /* Extract the charset & language. */
602 $charset = substr($value,0,strpos($value,"'"));
603 $value = substr($value,strlen($charset)+1);
604 $language = substr($value,0,strpos($value,"'"));
605 $value = substr($value,strlen($language)+1);
606 /* No character set defaults to US-ASCII */
607 if (!$charset) $charset = 'US-ASCII';
608 if ($language) $language = '*'.$language;
609 /* Convert to RFC-2047 base64 encoded string. */
610 $properties[substr($key, 0, $idx)] = '=?'.$charset.$language.'?B?'.base64_encode(rawurldecode($value)).'?=';
611 }
612 }
613 return $properties;
614 }
615
616 /**
617 * @param string $read
618 * @param integer $i
619 * @param object $hdr MessageHeader object
620 * @return object MessageHeader object
621 */
622 function parseEnvelope($read, &$i, $hdr) {
623 $arg_no = 0;
624 $arg_a = array();
625 ++$i;
626 for ($cnt = strlen($read); ($i < $cnt) && ($read[$i] != ')'); ++$i) {
627 $char = strtoupper($read[$i]);
628 switch ($char) {
629 case '"':
630 $arg_a[] = $this->parseQuote($read, $i);
631 ++$arg_no;
632 break;
633 case '{':
634 $arg_a[] = $this->parseLiteral($read, $i);
635 /* temp bugfix (SM 1.5 will have a working clean version)
636 too much work to implement that version right now */
637// --$i;
638 ++$arg_no;
639 break;
640 case 'N':
641 /* probably NIL argument */
642 if (strtoupper(substr($read, $i, 3)) == 'NIL') {
643 $arg_a[] = '';
644 ++$arg_no;
645 $i += 2;
646 }
647 break;
648 case '(':
649 /* Address structure (with group support)
650 * Note: Group support is useless on SMTP connections
651 * because the protocol doesn't support it
652 */
653 $addr_a = array();
654 $group = '';
655 $a=0;
656 for (; $i < $cnt && $read[$i] != ')'; ++$i) {
657 if ($read[$i] == '(') {
658 $addr = $this->parseAddress($read, $i);
659 if (($addr->host == '') && ($addr->mailbox != '')) {
660 /* start of group */
661 $group = $addr->mailbox;
662 $group_addr = $addr;
663 $j = $a;
664 } else if ($group && ($addr->host == '') && ($addr->mailbox == '')) {
665 /* end group */
666 if ($a == ($j+1)) { /* no group members */
667 $group_addr->group = $group;
668 $group_addr->mailbox = '';
669 $group_addr->personal = "$group: Undisclosed recipients;";
670 $addr_a[] = $group_addr;
671 $group ='';
672 }
673 } else {
674 $addr->group = $group;
675 $addr_a[] = $addr;
676 }
677 ++$a;
678 }
679 }
680 $arg_a[] = $addr_a;
681 break;
682 default: break;
683 }
684 }
685
686 if (count($arg_a) > 9) {
687 $d = strtr($arg_a[0], array(' ' => ' '));
688 $d_parts = explode(' ', $d);
689 if (!$arg_a[1]) $arg_a[1] = _("(no subject)");
690
691 $hdr->date = getTimeStamp($d_parts); /* argument 1: date */
692 $hdr->date_unparsed = strtr($d,'<>',' '); /* original date */
693 $hdr->subject = $arg_a[1]; /* argument 2: subject */
694 $hdr->from = is_array($arg_a[2]) ? $arg_a[2][0] : ''; /* argument 3: from */
695 $hdr->sender = is_array($arg_a[3]) ? $arg_a[3][0] : ''; /* argument 4: sender */
696 $hdr->reply_to = is_array($arg_a[4]) ? $arg_a[4][0] : ''; /* argument 5: reply-to */
697 $hdr->to = $arg_a[5]; /* argument 6: to */
698 $hdr->cc = $arg_a[6]; /* argument 7: cc */
699 $hdr->bcc = $arg_a[7]; /* argument 8: bcc */
700 $hdr->in_reply_to = $arg_a[8]; /* argument 9: in-reply-to */
701 $hdr->message_id = $arg_a[9]; /* argument 10: message-id */
702 }
703 return $hdr;
704 }
705
706 /**
707 * @param string $read
708 * @param integer $i
709 * @return string
710 * @todo document me
711 */
712 function parseLiteral($read, &$i) {
713 $lit_cnt = '';
714 ++$i;
715 $iPos = strpos($read,'}',$i);
716 if ($iPos) {
717 $lit_cnt = substr($read, $i, $iPos - $i);
718 $i += strlen($lit_cnt) + 3; /* skip } + \r + \n */
719 /* Now read the literal */
720 $s = ($lit_cnt ? substr($read,$i,$lit_cnt): '');
721 $i += $lit_cnt;
722 /* temp bugfix (SM 1.5 will have a working clean version)
723 too much work to implement that version right now */
724 --$i;
725 } else { /* should never happen */
726 $i += 3; /* } + \r + \n */
727 $s = '';
728 }
729 return $s;
730 }
731
732 /**
733 * function parseQuote
734 *
735 * This extract the string value from a quoted string. After the end-quote
736 * character is found it returns the string. The offset $i when calling
737 * this function points to the first double quote. At the end it points to
738 * The ending quote. This function takes care of escaped double quotes.
739 * "some \"string\""
740 * ^ ^
741 * initial $i end position $i
742 *
743 * @param string $read
744 * @param integer $i offset in $read
745 * @return string string inbetween the double quotes
746 * @author Marc Groot Koerkamp
747 */
748 function parseQuote($read, &$i) {
749 $s = '';
750 $iPos = ++$i;
751 $iPosStart = $iPos;
752 while (true) {
753 $iPos = strpos($read,'"',$iPos);
754 if (!$iPos) break;
755 if ($iPos && $read[$iPos -1] != '\\') {
756 $s = substr($read,$i,($iPos-$i));
757 $i = $iPos;
758 break;
759 } else if ($iPos > 1 && $read[$iPos -1] == '\\' && $read[$iPos-2] == '\\') {
760 // This is an unique situation where the fast detection of the string
761 // fails. If the quote string ends with \\ then we need to iterate
762 // through the entire string to make sure we detect the unexcaped
763 // double quotes correctly.
764 $s = '';
765 $bEscaped = false;
766 $k = 0;
767 for ($j=$iPosStart,$iCnt=strlen($read);$j<$iCnt;++$j) {
768 $cChar = $read[$j];
769 switch ($cChar) {
770 case '\\':
771 $bEscaped = !$bEscaped;
772 $s .= $cChar;
773 break;
774 case '"':
775 if ($bEscaped) {
776 $s .= $cChar;
777 $bEscaped = false;
778 } else {
779 $i = $j;
780 break 3;
781 }
782 break;
783 default:
784 if ($bEscaped) {
785 $bEscaped = false;
786 }
787 $s .= $cChar;
788 break;
789 }
790 }
791 }
792 ++$iPos;
793 if ($iPos > strlen($read)) {
794 break;
795 }
796 }
797 return $s;
798 }
799
800 /**
801 * @param string $read
802 * @param integer $i
803 * @return object AddressStructure object
804 */
805 function parseAddress($read, &$i) {
806 $arg_a = array();
807 for (; $read[$i] != ')'; ++$i) {
808 $char = strtoupper($read[$i]);
809 switch ($char) {
810 case '"': $arg_a[] = $this->parseQuote($read, $i); break;
811 case '{': $arg_a[] = $this->parseLiteral($read, $i); break;
812 case 'n':
813 case 'N':
814 if (strtoupper(substr($read, $i, 3)) == 'NIL') {
815 $arg_a[] = '';
816 $i += 2;
817 }
818 break;
819 default: break;
820 }
821 }
822
823 if (count($arg_a) == 4) {
824 $adr = new AddressStructure();
825 $adr->personal = $arg_a[0];
826 $adr->adl = $arg_a[1];
827 $adr->mailbox = $arg_a[2];
828 $adr->host = $arg_a[3];
829 } else {
830 $adr = '';
831 }
832 return $adr;
833 }
834
835 /**
836 * @param string $read
837 * @param integer $i
838 * @param object Disposition object or empty string
839 */
840 function parseDisposition($read, &$i) {
841 $arg_a = array();
842 for (; $read[$i] != ')'; ++$i) {
843 switch ($read[$i]) {
844 case '"': $arg_a[] = $this->parseQuote($read, $i); break;
845 case '{': $arg_a[] = $this->parseLiteral($read, $i); break;
846 case '(': $arg_a[] = $this->parseProperties($read, $i); break;
847 default: break;
848 }
849 }
850
851 if (isset($arg_a[0])) {
852 $disp = new Disposition($arg_a[0]);
853 if (isset($arg_a[1])) {
854 $disp->properties = $arg_a[1];
855 }
856 }
857 return (is_object($disp) ? $disp : '');
858 }
859
860 /**
861 * @param string $read
862 * @param integer $i
863 * @return object Language object or empty string
864 */
865 function parseLanguage($read, &$i) {
866 /* no idea how to process this one without examples */
867 $arg_a = array();
868
869 for (; $read[$i] != ')'; ++$i) {
870 switch ($read[$i]) {
871 case '"': $arg_a[] = $this->parseQuote($read, $i); break;
872 case '{': $arg_a[] = $this->parseLiteral($read, $i); break;
873 case '(': $arg_a[] = $this->parseProperties($read, $i); break;
874 default: break;
875 }
876 }
877
878 if (isset($arg_a[0])) {
879 $lang = new Language($arg_a[0]);
880 if (isset($arg_a[1])) {
881 $lang->properties = $arg_a[1];
882 }
883 }
884 return (is_object($lang) ? $lang : '');
885 }
886
887 /**
888 * Parse message text enclosed in parenthesis
889 * @param string $read
890 * @param integer $i
891 * @return integer
892 */
893 function parseParenthesis($read, $i) {
894 for ($i++; $read[$i] != ')'; ++$i) {
895 switch ($read[$i]) {
896 case '"': $this->parseQuote($read, $i); break;
897 case '{': $this->parseLiteral($read, $i); break;
898 case '(': $this->parseProperties($read, $i); break;
899 default: break;
900 }
901 }
902 return $i;
903 }
904
905 /**
906 * Function to fill the message structure in case the
907 * bodystructure is not available
908 * NOT FINISHED YET
909 * @param string $read
910 * @param string $type0 message part type
911 * @param string $type1 message part subtype
912 * @return string (only when type0 is not message or multipart)
913 */
914 function parseMessage($read, $type0, $type1) {
915 switch ($type0) {
916 case 'message':
917 $rfc822_header = true;
918 $mime_header = false;
919 break;
920 case 'multipart':
921 $rfc822_header = false;
922 $mime_header = true;
923 break;
924 default: return $read;
925 }
926
927 for ($i = 1; $i < $count; ++$i) {
928 $line = trim($body[$i]);
929 if (($mime_header || $rfc822_header) &&
930 (preg_match("/^.*boundary=\"?(.+(?=\")|.+).*/i", $line, $reg))) {
931 $bnd = $reg[1];
932 $bndreg = $bnd;
933 $bndreg = str_replace("\\", "\\\\", $bndreg);
934 $bndreg = str_replace("?", "\\?", $bndreg);
935 $bndreg = str_replace("+", "\\+", $bndreg);
936 $bndreg = str_replace(".", "\\.", $bndreg);
937 $bndreg = str_replace("/", "\\/", $bndreg);
938 $bndreg = str_replace("-", "\\-", $bndreg);
939 $bndreg = str_replace("(", "\\(", $bndreg);
940 $bndreg = str_replace(")", "\\)", $bndreg);
941 } else if ($rfc822_header && $line == '') {
942 $rfc822_header = false;
943 if ($msg->type0 == 'multipart') {
944 $mime_header = true;
945 }
946 }
947
948 if ((($line[0] == '-') || $rfc822_header) && isset($boundaries[0])) {
949 $cnt = count($boundaries)-1;
950 $bnd = $boundaries[$cnt]['bnd'];
951 $bndreg = $boundaries[$cnt]['bndreg'];
952
953 $regstr = '/^--'."($bndreg)".".*".'/';
954 if (preg_match($regstr, $line, $reg)) {
955 $bndlen = strlen($reg[1]);
956 $bndend = false;
957 if (strlen($line) > ($bndlen + 3)) {
958 if (($line[$bndlen+2] == '-') && ($line[$bndlen+3] == '-')) {
959 $bndend = true;
960 }
961 }
962 if ($bndend) {
963 /* calc offset and return $msg */
964 //$entStr = CalcEntity("$entStr", -1);
965 array_pop($boundaries);
966 $mime_header = true;
967 $bnd_end = true;
968 } else {
969 $mime_header = true;
970 $bnd_end = false;
971 //$entStr = CalcEntity("$entStr", 0);
972 ++$content_indx;
973 }
974 } else {
975 if ($header) { }
976 }
977 }
978 }
979 }
980
981 /**
982 * @param array $entity
983 * @param array $alt_order
984 * @param boolean $strict
985 * @return array
986 */
987 function findDisplayEntity($entity = array(), $alt_order = array('text/plain', 'text/html'), $strict=false) {
988 $found = false;
989 if ($this->type0 == 'multipart') {
990 if($this->type1 == 'alternative') {
991 $msg = $this->findAlternativeEntity($alt_order);
992 if ( ! is_null($msg) ) {
993 if (count($msg->entities) == 0) {
994 $entity[] = $msg->entity_id;
995 } else {
996 $entity = $msg->findDisplayEntity($entity, $alt_order, $strict);
997 }
998 $found = true;
999 }
1000 } else if ($this->type1 == 'related') { /* RFC 2387 */
1001 $msgs = $this->findRelatedEntity();
1002 foreach ($msgs as $msg) {
1003 if (count($msg->entities) == 0) {
1004 $entity[] = $msg->entity_id;
1005 } else {
1006 $entity = $msg->findDisplayEntity($entity, $alt_order, $strict);
1007 }
1008 }
1009 if (count($msgs) > 0) {
1010 $found = true;
1011 }
1012 } else { /* Treat as multipart/mixed */
1013 foreach ($this->entities as $ent) {
1014 if(!(is_object($ent->header->disposition) && strtolower($ent->header->disposition->name) == 'attachment') &&
1015 (!isset($ent->header->parameters['filename'])) &&
1016 (!isset($ent->header->parameters['name'])) &&
1017 (($ent->type0 != 'message') && ($ent->type1 != 'rfc822'))) {
1018 $entity = $ent->findDisplayEntity($entity, $alt_order, $strict);
1019 $found = true;
1020 }
1021 }
1022 }
1023 } else { /* If not multipart, then just compare with each entry from $alt_order */
1024 $type = $this->type0.'/'.$this->type1;
1025// $alt_order[] = "message/rfc822";
1026 foreach ($alt_order as $alt) {
1027 if( ($alt == $type) && isset($this->entity_id) ) {
1028 if ((count($this->entities) == 0) &&
1029 (!isset($this->header->parameters['filename'])) &&
1030 (!isset($this->header->parameters['name'])) &&
1031 isset($this->header->disposition) && is_object($this->header->disposition) &&
1032 !(is_object($this->header->disposition) && strtolower($this->header->disposition->name) == 'attachment')) {
1033 $entity[] = $this->entity_id;
1034 $found = true;
1035 }
1036 }
1037 }
1038 }
1039 if(!$found) {
1040 foreach ($this->entities as $ent) {
1041 if(!(is_object($ent->header->disposition) && strtolower($ent->header->disposition->name) == 'attachment') &&
1042 (($ent->type0 != 'message') && ($ent->type1 != 'rfc822'))) {
1043 $entity = $ent->findDisplayEntity($entity, $alt_order, $strict);
1044 $found = true;
1045 }
1046 }
1047 }
1048 if(!$strict && !$found) {
1049 if (($this->type0 == 'text') &&
1050 in_array($this->type1, array('plain', 'html', 'message')) &&
1051 isset($this->entity_id)) {
1052 if (count($this->entities) == 0) {
1053 if (!is_object($this->header->disposition) || strtolower($this->header->disposition->name) != 'attachment') {
1054 $entity[] = $this->entity_id;
1055 }
1056 }
1057 }
1058 }
1059 return $entity;
1060 }
1061
1062 /**
1063 * @param array $alt_order
1064 * @return entity
1065 */
1066 function findAlternativeEntity($alt_order) {
1067 /* If we are dealing with alternative parts then we */
1068 /* choose the best viewable message supported by SM. */
1069 $best_view = 0;
1070 $entity = null;
1071 foreach($this->entities as $ent) {
1072 $type = $ent->header->type0 . '/' . $ent->header->type1;
1073 if ($type == 'multipart/related') {
1074 $type = $ent->header->getParameter('type');
1075 // Mozilla bug. Mozilla does not provide the parameter type.
1076 if (!$type) $type = 'text/html';
1077 }
1078 $altCount = count($alt_order);
1079 for ($j = $best_view; $j < $altCount; ++$j) {
1080 if (($alt_order[$j] == $type) && ($j >= $best_view)) {
1081 $best_view = $j;
1082 $entity = $ent;
1083 }
1084 }
1085 }
1086 return $entity;
1087 }
1088
1089 /**
1090 * @return array
1091 */
1092 function findRelatedEntity() {
1093 $msgs = array();
1094 $related_type = $this->header->getParameter('type');
1095 // Mozilla bug. Mozilla does not provide the parameter type.
1096 if (!$related_type) $related_type = 'text/html';
1097 $entCount = count($this->entities);
1098 for ($i = 0; $i < $entCount; ++$i) {
1099 $type = $this->entities[$i]->header->type0.'/'.$this->entities[$i]->header->type1;
1100 if ($related_type == $type) {
1101 $msgs[] = $this->entities[$i];
1102 }
1103 }
1104 return $msgs;
1105 }
1106
1107 /**
1108 * @param array $exclude_id
1109 * @param array $result
1110 * @return array
1111 */
1112 function getAttachments($exclude_id=array(), $result = array()) {
1113/*
1114 if (($this->type0 == 'message') &&
1115 ($this->type1 == 'rfc822') &&
1116 ($this->entity_id) ) {
1117 $this = $this->entities[0];
1118 }
1119*/
1120 if (count($this->entities)) {
1121 foreach ($this->entities as $entity) {
1122 $exclude = false;
1123 foreach ($exclude_id as $excl) {
1124 if ($entity->entity_id === $excl) {
1125 $exclude = true;
1126 }
1127 }
1128
1129 if (!$exclude) {
1130 if ($entity->type0 == 'multipart') {
1131 $result = $entity->getAttachments($exclude_id, $result);
1132 } else if ($entity->type0 != 'multipart') {
1133 $result[] = $entity;
1134 }
1135 }
1136 }
1137 } else {
1138 $exclude = false;
1139 foreach ($exclude_id as $excl) {
1140 $exclude = $exclude || ($this->entity_id == $excl);
1141 }
1142
1143 if (!$exclude) {
1144 $result[] = $this;
1145 }
1146 }
1147 return $result;
1148 }
1149
1150 /**
1151 * Add attachment to message object
1152 * @param string $type attachment type
1153 * @param string $name attachment name
1154 * @param string $location path to attachment
1155 */
1156 function initAttachment($type, $name, $location) {
1157 $attachment = new Message();
1158 $mime_header = new MessageHeader();
1159 $mime_header->setParameter('name', $name);
1160 // FIXME: duplicate code. see ContentType class
1161 $pos = strpos($type, '/');
1162 if ($pos > 0) {
1163 $mime_header->type0 = substr($type, 0, $pos);
1164 $mime_header->type1 = substr($type, $pos+1);
1165 } else {
1166 $mime_header->type0 = $type;
1167 }
1168 $attachment->att_local_name = $location;
1169 $disposition = new Disposition('attachment');
1170 $disposition->properties['filename'] = $name;
1171 $mime_header->disposition = $disposition;
1172 $attachment->mime_header = $mime_header;
1173 $this->entities[]=$attachment;
1174 }
1175
1176 /**
1177 * Delete all attachments from this object from disk.
1178 * @since 1.5.1
1179 */
1180 function purgeAttachments() {
1181 if ($this->att_local_name) {
1182 global $username, $attachment_dir;
1183 $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
1184 if ( file_exists($hashed_attachment_dir . '/' . $this->att_local_name) ) {
1185 unlink($hashed_attachment_dir . '/' . $this->att_local_name);
1186 }
1187 }
1188 // recursively delete attachments from entities contained in this object
1189 for ($i=0, $entCount=count($this->entities);$i< $entCount; ++$i) {
1190 $this->entities[$i]->purgeAttachments();
1191 }
1192 }
1193}