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