Increment year in copyright notice.
[squirrelmail.git] / class / mime / Rfc822Header.class.php
1 <?php
2
3 /**
4 * Rfc822Header.class.php
5 *
6 * Copyright (c) 2003-2005 The SquirrelMail Project Team
7 * Licensed under the GNU GPL. For full terms see the file COPYING.
8 *
9 * This contains functions needed to handle mime messages.
10 *
11 * @version $Id$
12 * @package squirrelmail
13 */
14
15 /**
16 * input: header_string or array
17 * @package squirrelmail
18 */
19 class Rfc822Header {
20 var $date = -1,
21 $subject = '',
22 $from = array(),
23 $sender = '',
24 $reply_to = array(),
25 $mail_followup_to = array(),
26 $to = array(),
27 $cc = array(),
28 $bcc = array(),
29 $in_reply_to = '',
30 $message_id = '',
31 $references = '',
32 $mime = false,
33 $content_type = '',
34 $disposition = '',
35 $xmailer = '',
36 $priority = 3,
37 $dnt = '',
38 $encoding = '',
39 $content_id = '',
40 $content_desc = '',
41 $mlist = array(),
42 $more_headers = array(); /* only needed for constructing headers
43 in smtp.php */
44 function parseHeader($hdr) {
45 if (is_array($hdr)) {
46 $hdr = implode('', $hdr);
47 }
48 /* First we replace \r\n by \n and unfold the header */
49 $hdr = trim(str_replace(array("\r\n", "\n\t", "\n "),array("\n", ' ', ' '), $hdr));
50
51 /* Now we can make a new header array with */
52 /* each element representing a headerline */
53 $hdr = explode("\n" , $hdr);
54 foreach ($hdr as $line) {
55 $pos = strpos($line, ':');
56 if ($pos > 0) {
57 $field = substr($line, 0, $pos);
58 if (!strstr($field,' ')) { /* valid field */
59 $value = trim(substr($line, $pos+1));
60 $this->parseField($field, $value);
61 }
62 }
63 }
64 if ($this->content_type == '') {
65 $this->parseContentType('text/plain; charset=us-ascii');
66 }
67 }
68
69 function stripComments($value) {
70 $result = '';
71 $cnt = strlen($value);
72 for ($i = 0; $i < $cnt; ++$i) {
73 switch ($value{$i}) {
74 case '"':
75 $result .= '"';
76 while ((++$i < $cnt) && ($value{$i} != '"')) {
77 if ($value{$i} == '\\') {
78 $result .= '\\';
79 ++$i;
80 }
81 $result .= $value{$i};
82 }
83 $result .= $value{$i};
84 break;
85 case '(':
86 $depth = 1;
87 while (($depth > 0) && (++$i < $cnt)) {
88 switch($value{$i}) {
89 case '\\':
90 ++$i;
91 break;
92 case '(':
93 ++$depth;
94 break;
95 case ')':
96 --$depth;
97 break;
98 default:
99 break;
100 }
101 }
102 break;
103 default:
104 $result .= $value{$i};
105 break;
106 }
107 }
108 return $result;
109 }
110
111 function parseField($field, $value) {
112 $field = strtolower($field);
113 switch($field) {
114 case 'date':
115 $value = $this->stripComments($value);
116 $d = strtr($value, array(' ' => ' '));
117 $d = explode(' ', $d);
118 $this->date = getTimeStamp($d);
119 break;
120 case 'subject':
121 $this->subject = $value;
122 break;
123 case 'from':
124 $this->from = $this->parseAddress($value,true);
125 break;
126 case 'sender':
127 $this->sender = $this->parseAddress($value);
128 break;
129 case 'reply-to':
130 $this->reply_to = $this->parseAddress($value, true);
131 break;
132 case 'mail-followup-to':
133 $this->mail_followup_to = $this->parseAddress($value, true);
134 break;
135 case 'to':
136 $this->to = $this->parseAddress($value, true);
137 break;
138 case 'cc':
139 $this->cc = $this->parseAddress($value, true);
140 break;
141 case 'bcc':
142 $this->bcc = $this->parseAddress($value, true);
143 break;
144 case 'in-reply-to':
145 $this->in_reply_to = $value;
146 break;
147 case 'message-id':
148 $value = $this->stripComments($value);
149 $this->message_id = $value;
150 break;
151 case 'references':
152 $value = $this->stripComments($value);
153 $this->references = $value;
154 break;
155 case 'x-confirm-reading-to':
156 case 'return-receipt-to':
157 case 'disposition-notification-to':
158 $value = $this->stripComments($value);
159 $this->dnt = $this->parseAddress($value);
160 break;
161 case 'mime-version':
162 $value = $this->stripComments($value);
163 $value = str_replace(' ', '', $value);
164 $this->mime = ($value == '1.0' ? true : $this->mime);
165 break;
166 case 'content-type':
167 $value = $this->stripComments($value);
168 $this->parseContentType($value);
169 break;
170 case 'content-disposition':
171 $value = $this->stripComments($value);
172 $this->parseDisposition($value);
173 break;
174 case 'content-transfer-encoding':
175 $this->encoding = $value;
176 break;
177 case 'content-description':
178 $this->content_desc = $value;
179 break;
180 case 'content-id':
181 $value = $this->stripComments($value);
182 $this->content_id = $value;
183 break;
184 case 'user-agent':
185 case 'x-mailer':
186 $this->xmailer = $value;
187 break;
188 case 'x-priority':
189 case 'importance':
190 case 'priority':
191 $this->priority = $this->parsePriority($value);
192 break;
193 case 'list-post':
194 $value = $this->stripComments($value);
195 $this->mlist('post', $value);
196 break;
197 case 'list-reply':
198 $value = $this->stripComments($value);
199 $this->mlist('reply', $value);
200 break;
201 case 'list-subscribe':
202 $value = $this->stripComments($value);
203 $this->mlist('subscribe', $value);
204 break;
205 case 'list-unsubscribe':
206 $value = $this->stripComments($value);
207 $this->mlist('unsubscribe', $value);
208 break;
209 case 'list-archive':
210 $value = $this->stripComments($value);
211 $this->mlist('archive', $value);
212 break;
213 case 'list-owner':
214 $value = $this->stripComments($value);
215 $this->mlist('owner', $value);
216 break;
217 case 'list-help':
218 $value = $this->stripComments($value);
219 $this->mlist('help', $value);
220 break;
221 case 'list-id':
222 $value = $this->stripComments($value);
223 $this->mlist('id', $value);
224 break;
225 default:
226 break;
227 }
228 }
229
230 function getAddressTokens($address) {
231 $aTokens = array();
232 $aSpecials = array('(' ,'<' ,',' ,';' ,':');
233 $aReplace = array(' (',' <',' ,',' ;',' :');
234 $address = str_replace($aSpecials,$aReplace,$address);
235 $iCnt = strlen($address);
236 $i = 0;
237 while ($i < $iCnt) {
238 $cChar = $address{$i};
239 switch($cChar)
240 {
241 case '<':
242 $iEnd = strpos($address,'>',$i+1);
243 if (!$iEnd) {
244 $sToken = substr($address,$i);
245 $i = $iCnt;
246 } else {
247 $sToken = substr($address,$i,$iEnd - $i +1);
248 $i = $iEnd;
249 }
250 $sToken = str_replace($aReplace, $aSpecials,$sToken);
251 if ($sToken) $aTokens[] = $sToken;
252 break;
253 case '"':
254 $iEnd = strpos($address,$cChar,$i+1);
255 if ($iEnd) {
256 // skip escaped quotes
257 $prev_char = $address{$iEnd-1};
258 while ($prev_char === '\\' && substr($address,$iEnd-2,2) !== '\\\\') {
259 $iEnd = strpos($address,$cChar,$iEnd+1);
260 if ($iEnd) {
261 $prev_char = $address{$iEnd-1};
262 } else {
263 $prev_char = false;
264 }
265 }
266 }
267 if (!$iEnd) {
268 $sToken = substr($address,$i);
269 $i = $iCnt;
270 } else {
271 // also remove the surrounding quotes
272 $sToken = substr($address,$i+1,$iEnd - $i -1);
273 $i = $iEnd;
274 }
275 $sToken = str_replace($aReplace, $aSpecials,$sToken);
276 if ($sToken) $aTokens[] = $sToken;
277 break;
278 case '(':
279 array_pop($aTokens); //remove inserted space
280 $iEnd = strpos($address,')',$i);
281 if (!$iEnd) {
282 $sToken = substr($address,$i);
283 $i = $iCnt;
284 } else {
285 $iDepth = 1;
286 $iComment = $i;
287 while (($iDepth > 0) && (++$iComment < $iCnt)) {
288 $cCharComment = $address{$iComment};
289 switch($cCharComment) {
290 case '\\':
291 ++$iComment;
292 break;
293 case '(':
294 ++$iDepth;
295 break;
296 case ')':
297 --$iDepth;
298 break;
299 default:
300 break;
301 }
302 }
303 if ($iDepth == 0) {
304 $sToken = substr($address,$i,$iComment - $i +1);
305 $i = $iComment;
306 } else {
307 $sToken = substr($address,$i,$iEnd - $i + 1);
308 $i = $iEnd;
309 }
310 }
311 // check the next token in case comments appear in the middle of email addresses
312 $prevToken = end($aTokens);
313 if (!in_array($prevToken,$aSpecials,true)) {
314 if ($i+1<strlen($address) && !in_array($address{$i+1},$aSpecials,true)) {
315 $iEnd = strpos($address,' ',$i+1);
316 if ($iEnd) {
317 $sNextToken = trim(substr($address,$i+1,$iEnd - $i -1));
318 $i = $iEnd-1;
319 } else {
320 $sNextToken = trim(substr($address,$i+1));
321 $i = $iCnt;
322 }
323 // remove the token
324 array_pop($aTokens);
325 // create token and add it again
326 $sNewToken = $prevToken . $sNextToken;
327 if($sNewToken) $aTokens[] = $sNewToken;
328 }
329 }
330 $sToken = str_replace($aReplace, $aSpecials,$sToken);
331 if ($sToken) $aTokens[] = $sToken;
332 break;
333 case ',':
334 case ':':
335 case ';':
336 case ' ':
337 $aTokens[] = $cChar;
338 break;
339 default:
340 $iEnd = strpos($address,' ',$i+1);
341 if ($iEnd) {
342 $sToken = trim(substr($address,$i,$iEnd - $i));
343 $i = $iEnd-1;
344 } else {
345 $sToken = trim(substr($address,$i));
346 $i = $iCnt;
347 }
348 if ($sToken) $aTokens[] = $sToken;
349 }
350 ++$i;
351 }
352 return $aTokens;
353 }
354 function createAddressObject(&$aStack,&$aComment,&$sEmail,$sGroup='') {
355 //$aStack=explode(' ',implode('',$aStack));
356 if (!$sEmail) {
357 while (count($aStack) && !$sEmail) {
358 $sEmail = trim(array_pop($aStack));
359 }
360 }
361 if (count($aStack)) {
362 $sPersonal = trim(implode('',$aStack));
363 } else {
364 $sPersonal = '';
365 }
366 if (!$sPersonal && count($aComment)) {
367 $sComment = trim(implode(' ',$aComment));
368 $sPersonal .= $sComment;
369 }
370 $oAddr =& new AddressStructure();
371 if ($sPersonal && substr($sPersonal,0,2) == '=?') {
372 $oAddr->personal = encodeHeader($sPersonal);
373 } else {
374 $oAddr->personal = $sPersonal;
375 }
376 // $oAddr->group = $sGroup;
377 $iPosAt = strpos($sEmail,'@');
378 if ($iPosAt) {
379 $oAddr->mailbox = substr($sEmail, 0, $iPosAt);
380 $oAddr->host = substr($sEmail, $iPosAt+1);
381 } else {
382 $oAddr->mailbox = $sEmail;
383 $oAddr->host = false;
384 }
385 $sEmail = '';
386 $aStack = $aComment = array();
387 return $oAddr;
388 }
389
390 /*
391 * parseAddress: recursive function for parsing address strings and store
392 * them in an address stucture object.
393 * input: $address = string
394 * $ar = boolean (return array instead of only the
395 * first element)
396 * $addr_ar = array with parsed addresses // obsolete
397 * $group = string // obsolete
398 * $host = string (default domainname in case of
399 * addresses without a domainname)
400 * $lookup = callback function (for lookup address
401 * strings which are probably nicks
402 * (without @ ) )
403 * output: array with addressstructure objects or only one
404 * address_structure object.
405 * personal name: encoded: =?charset?Q|B?string?=
406 * quoted: "string"
407 * normal: string
408 * email : <mailbox@host>
409 * : mailbox@host
410 * This function is also used for validating addresses returned from compose
411 * That's also the reason that the function became a little bit huge
412 */
413
414 function parseAddress($address,$ar=false,$aAddress=array(),$sGroup='',$sHost='',$lookup=false) {
415 $aTokens = $this->getAddressTokens($address);
416 $sPersonal = $sEmail = $sGroup = '';
417 $aStack = $aComment = array();
418 foreach ($aTokens as $sToken) {
419 $cChar = $sToken{0};
420 switch ($cChar)
421 {
422 case '=':
423 case '"':
424 case ' ':
425 $aStack[] = $sToken;
426 break;
427 case '(':
428 $aComment[] = substr($sToken,1,-1);
429 break;
430 case ';':
431 if ($sGroup) {
432 $aAddress[] = $this->createAddressObject($aStack,$aComment,$sEmail,$sGroup);
433 $oAddr = end($aAddress);
434 if(!$oAddr || ((isset($oAddr)) && !$oAddr->mailbox && !$oAddr->personal)) {
435 $sEmail = $sGroup . ':;';
436 }
437 $aAddress[] = $this->createAddressObject($aStack,$aComment,$sEmail,$sGroup);
438 $sGroup = '';
439 $aStack = $aComment = array();
440 break;
441 }
442 case ',':
443 $aAddress[] = $this->createAddressObject($aStack,$aComment,$sEmail,$sGroup);
444 break;
445 case ':':
446 $sGroup = trim(implode(' ',$aStack));
447 $sGroup = preg_replace('/\s+/',' ',$sGroup);
448 $aStack = array();
449 break;
450 case '<':
451 $sEmail = trim(substr($sToken,1,-1));
452 break;
453 case '>':
454 /* skip */
455 break;
456 default: $aStack[] = $sToken; break;
457 }
458 }
459 /* now do the action again for the last address */
460 $aAddress[] = $this->createAddressObject($aStack,$aComment,$sEmail);
461 /* try to lookup the addresses in case of invalid email addresses */
462 $aProcessedAddress = array();
463 foreach ($aAddress as $oAddr) {
464 $aAddrBookAddress = array();
465 if (!$oAddr->host) {
466 $grouplookup = false;
467 if ($lookup) {
468 $aAddr = call_user_func_array($lookup,array($oAddr->mailbox));
469 if (isset($aAddr['email'])) {
470 if (strpos($aAddr['email'],',')) {
471 $grouplookup = true;
472 $aAddrBookAddress = $this->parseAddress($aAddr['email'],true);
473 } else {
474 $iPosAt = strpos($aAddr['email'], '@');
475 $oAddr->mailbox = substr($aAddr['email'], 0, $iPosAt);
476 $oAddr->host = substr($aAddr['email'], $iPosAt+1);
477 if (isset($aAddr['name'])) {
478 $oAddr->personal = $aAddr['name'];
479 } else {
480 $oAddr->personal = encodeHeader($sPersonal);
481 }
482 }
483 }
484 }
485 if (!$grouplookup && !$oAddr->mailbox) {
486 $oAddr->mailbox = trim($sEmail);
487 if ($sHost && $oAddr->mailbox) {
488 $oAddr->host = $sHost;
489 }
490 } else if (!$grouplookup && !$oAddr->host) {
491 if ($sHost && $oAddr->mailbox) {
492 $oAddr->host = $sHost;
493 }
494 }
495 }
496 if (!$aAddrBookAddress && $oAddr->mailbox) {
497 $aProcessedAddress[] = $oAddr;
498 } else {
499 $aProcessedAddress = array_merge($aProcessedAddress,$aAddrBookAddress);
500 }
501 }
502 if ($ar) {
503 return $aProcessedAddress;
504 } else {
505 return $aProcessedAddress[0];
506 }
507 }
508
509 /**
510 * Normalise the different Priority headers into a uniform value,
511 * namely that of the X-Priority header (1, 3, 5). Supports:
512 * Prioirty, X-Priority, Importance.
513 * X-MS-Mail-Priority is not parsed because it always coincides
514 * with one of the other headers.
515 *
516 * NOTE: this is actually a duplicate from the function in
517 * functions/imap_messages. I'm not sure if it's ok here to call
518 * that function?
519 */
520 function parsePriority($value) {
521 $value = strtolower(array_shift(split('/\w/',trim($value))));
522 if ( is_numeric($value) ) {
523 return $value;
524 }
525 if ( $value == 'urgent' || $value == 'high' ) {
526 return 1;
527 } elseif ( $value == 'non-urgent' || $value == 'low' ) {
528 return 5;
529 }
530 // default is normal priority
531 return 3;
532 }
533
534 function parseContentType($value) {
535 $pos = strpos($value, ';');
536 $props = '';
537 if ($pos > 0) {
538 $type = trim(substr($value, 0, $pos));
539 $props = trim(substr($value, $pos+1));
540 } else {
541 $type = $value;
542 }
543 $content_type = new ContentType($type);
544 if ($props) {
545 $properties = $this->parseProperties($props);
546 if (!isset($properties['charset'])) {
547 $properties['charset'] = 'us-ascii';
548 }
549 $content_type->properties = $this->parseProperties($props);
550 }
551 $this->content_type = $content_type;
552 }
553
554 /* RFC2184 */
555 function processParameters($aParameters) {
556 $aResults = array();
557 $aCharset = array();
558 // handle multiline parameters
559 foreach($aParameters as $key => $value) {
560 if ($iPos = strpos($key,'*')) {
561 $sKey = substr($key,0,$iPos);
562 if (!isset($aResults[$sKey])) {
563 $aResults[$sKey] = $value;
564 if (substr($key,-1) == '*') { // parameter contains language/charset info
565 $aCharset[] = $sKey;
566 }
567 } else {
568 $aResults[$sKey] .= $value;
569 }
570 } else {
571 $aResults[$key] = $value;
572 }
573 }
574 foreach ($aCharset as $key) {
575 $value = $aResults[$key];
576 // extract the charset & language
577 $charset = substr($value,0,strpos($value,"'"));
578 $value = substr($value,strlen($charset)+1);
579 $language = substr($value,0,strpos($value,"'"));
580 $value = substr($value,strlen($charset)+1);
581 // FIX ME What's the status of charset decode with language information ????
582 $value = charset_decode($charset,$value);
583 $aResults[$key] = $value;
584 }
585 return $aResults;
586 }
587
588 function parseProperties($value) {
589 $propArray = explode(';', $value);
590 $propResultArray = array();
591 foreach ($propArray as $prop) {
592 $prop = trim($prop);
593 $pos = strpos($prop, '=');
594 if ($pos > 0) {
595 $key = trim(substr($prop, 0, $pos));
596 $val = trim(substr($prop, $pos+1));
597 if (strlen($val) > 0 && $val{0} == '"') {
598 $val = substr($val, 1, -1);
599 }
600 $propResultArray[$key] = $val;
601 }
602 }
603 return $this->processParameters($propResultArray);
604 }
605
606 function parseDisposition($value) {
607 $pos = strpos($value, ';');
608 $props = '';
609 if ($pos > 0) {
610 $name = trim(substr($value, 0, $pos));
611 $props = trim(substr($value, $pos+1));
612 } else {
613 $name = $value;
614 }
615 $props_a = $this->parseProperties($props);
616 $disp = new Disposition($name);
617 $disp->properties = $props_a;
618 $this->disposition = $disp;
619 }
620
621 function mlist($field, $value) {
622 $res_a = array();
623 $value_a = explode(',', $value);
624 foreach ($value_a as $val) {
625 $val = trim($val);
626 if ($val{0} == '<') {
627 $val = substr($val, 1, -1);
628 }
629 if (substr($val, 0, 7) == 'mailto:') {
630 $res_a['mailto'] = substr($val, 7);
631 } else {
632 $res_a['href'] = $val;
633 }
634 }
635 $this->mlist[$field] = $res_a;
636 }
637
638 /*
639 * function to get the addres strings out of the header.
640 * Arguments: string or array of strings !
641 * example1: header->getAddr_s('to').
642 * example2: header->getAddr_s(array('to', 'cc', 'bcc'))
643 */
644 function getAddr_s($arr, $separator = ',',$encoded=false) {
645 $s = '';
646
647 if (is_array($arr)) {
648 foreach($arr as $arg) {
649 if ($this->getAddr_s($arg, $separator, $encoded)) {
650 $s .= $separator;
651 }
652 }
653 $s = ($s ? substr($s, 2) : $s);
654 } else {
655 $addr = $this->{$arr};
656 if (is_array($addr)) {
657 foreach ($addr as $addr_o) {
658 if (is_object($addr_o)) {
659 if ($encoded) {
660 $s .= $addr_o->getEncodedAddress() . $separator;
661 } else {
662 $s .= $addr_o->getAddress() . $separator;
663 }
664 }
665 }
666 $s = substr($s, 0, -strlen($separator));
667 } else {
668 if (is_object($addr)) {
669 if ($encoded) {
670 $s .= $addr->getEncodedAddress();
671 } else {
672 $s .= $addr->getAddress();
673 }
674 }
675 }
676 }
677 return $s;
678 }
679
680 function getAddr_a($arg, $excl_arr = array(), $arr = array()) {
681 if (is_array($arg)) {
682 foreach($arg as $argument) {
683 $arr = $this->getAddr_a($argument, $excl_arr, $arr);
684 }
685 } else {
686 $addr = $this->{$arg};
687 if (is_array($addr)) {
688 foreach ($addr as $next_addr) {
689 if (is_object($next_addr)) {
690 if (isset($next_addr->host) && ($next_addr->host != '')) {
691 $email = $next_addr->mailbox . '@' . $next_addr->host;
692 } else {
693 $email = $next_addr->mailbox;
694 }
695 $email = strtolower($email);
696 if ($email && !isset($arr[$email]) && !isset($excl_arr[$email])) {
697 $arr[$email] = $next_addr->personal;
698 }
699 }
700 }
701 } else {
702 if (is_object($addr)) {
703 $email = $addr->mailbox;
704 $email .= (isset($addr->host) ? '@' . $addr->host : '');
705 $email = strtolower($email);
706 if ($email && !isset($arr[$email]) && !isset($excl_arr[$email])) {
707 $arr[$email] = $addr->personal;
708 }
709 }
710 }
711 }
712 return $arr;
713 }
714
715 function findAddress($address, $recurs = false) {
716 $result = false;
717 if (is_array($address)) {
718 $i=0;
719 foreach($address as $argument) {
720 $match = $this->findAddress($argument, true);
721 if ($match[1]) {
722 return $i;
723 } else {
724 if (count($match[0]) && !$result) {
725 $result = $i;
726 }
727 }
728 ++$i;
729 }
730 } else {
731 if (!is_array($this->cc)) $this->cc = array();
732 $srch_addr = $this->parseAddress($address);
733 $results = array();
734 foreach ($this->to as $to) {
735 if ($to->host == $srch_addr->host) {
736 if ($to->mailbox == $srch_addr->mailbox) {
737 $results[] = $srch_addr;
738 if ($to->personal == $srch_addr->personal) {
739 if ($recurs) {
740 return array($results, true);
741 } else {
742 return true;
743 }
744 }
745 }
746 }
747 }
748 foreach ($this->cc as $cc) {
749 if ($cc->host == $srch_addr->host) {
750 if ($cc->mailbox == $srch_addr->mailbox) {
751 $results[] = $srch_addr;
752 if ($cc->personal == $srch_addr->personal) {
753 if ($recurs) {
754 return array($results, true);
755 } else {
756 return true;
757 }
758 }
759 }
760 }
761 }
762 if ($recurs) {
763 return array($results, false);
764 } elseif (count($result)) {
765 return true;
766 } else {
767 return false;
768 }
769 }
770 //exit;
771 return $result;
772 }
773
774 function getContentType($type0, $type1) {
775 $type0 = $this->content_type->type0;
776 $type1 = $this->content_type->type1;
777 return $this->content_type->properties;
778 }
779 }
780
781 ?>