Fix two time zone calculation bugs, thanks to david white for the
[squirrelmail.git] / class / mime / Rfc822Header.class.php
... / ...
CommitLineData
1<?php
2
3/**
4 * Rfc822Header.class.php
5 *
6 * Copyright (c) 2003-2004 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 */
19class 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 $this->priority = $value;
190 break;
191 case 'list-post':
192 $value = $this->stripComments($value);
193 $this->mlist('post', $value);
194 break;
195 case 'list-reply':
196 $value = $this->stripComments($value);
197 $this->mlist('reply', $value);
198 break;
199 case 'list-subscribe':
200 $value = $this->stripComments($value);
201 $this->mlist('subscribe', $value);
202 break;
203 case 'list-unsubscribe':
204 $value = $this->stripComments($value);
205 $this->mlist('unsubscribe', $value);
206 break;
207 case 'list-archive':
208 $value = $this->stripComments($value);
209 $this->mlist('archive', $value);
210 break;
211 case 'list-owner':
212 $value = $this->stripComments($value);
213 $this->mlist('owner', $value);
214 break;
215 case 'list-help':
216 $value = $this->stripComments($value);
217 $this->mlist('help', $value);
218 break;
219 case 'list-id':
220 $value = $this->stripComments($value);
221 $this->mlist('id', $value);
222 break;
223 default:
224 break;
225 }
226 }
227
228 function getAddressTokens($address) {
229 $aTokens = array();
230 $aSpecials = array('(' ,'<' ,',' ,';' ,':');
231 $aReplace = array(' (',' <',' ,',' ;',' :');
232 $address = str_replace($aSpecials,$aReplace,$address);
233 $iCnt = strlen($address);
234 $i = 0;
235 while ($i < $iCnt) {
236 $cChar = $address{$i};
237 switch($cChar)
238 {
239 case '<':
240 $iEnd = strpos($address,'>',$i+1);
241 if (!$iEnd) {
242 $sToken = substr($address,$i);
243 $i = $iCnt;
244 } else {
245 $sToken = substr($address,$i,$iEnd - $i +1);
246 $i = $iEnd;
247 }
248 $sToken = str_replace($aReplace, $aSpecials,$sToken);
249 if ($sToken) $aTokens[] = $sToken;
250 break;
251 case '"':
252 $iEnd = strpos($address,$cChar,$i+1);
253 if ($iEnd) {
254 // skip escaped quotes
255 $prev_char = $address{$iEnd-1};
256 while ($prev_char === '\\' && substr($address,$iEnd-2,2) !== '\\\\') {
257 $iEnd = strpos($address,$cChar,$iEnd+1);
258 if ($iEnd) {
259 $prev_char = $address{$iEnd-1};
260 } else {
261 $prev_char = false;
262 }
263 }
264 }
265 if (!$iEnd) {
266 $sToken = substr($address,$i);
267 $i = $iCnt;
268 } else {
269 // also remove the surrounding quotes
270 $sToken = substr($address,$i+1,$iEnd - $i -1);
271 $i = $iEnd;
272 }
273 $sToken = str_replace($aReplace, $aSpecials,$sToken);
274 if ($sToken) $aTokens[] = $sToken;
275 break;
276 case '(':
277 array_pop($aTokens); //remove inserted space
278 $iEnd = strpos($address,')',$i);
279 if (!$iEnd) {
280 $sToken = substr($address,$i);
281 $i = $iCnt;
282 } else {
283 $iDepth = 1;
284 $iComment = $i;
285 while (($iDepth > 0) && (++$iComment < $iCnt)) {
286 $cCharComment = $address{$iComment};
287 switch($cCharComment) {
288 case '\\':
289 ++$iComment;
290 break;
291 case '(':
292 ++$iDepth;
293 break;
294 case ')':
295 --$iDepth;
296 break;
297 default:
298 break;
299 }
300 }
301 if ($iDepth == 0) {
302 $sToken = substr($address,$i,$iComment - $i +1);
303 $i = $iComment;
304 } else {
305 $sToken = substr($address,$i,$iEnd - $i + 1);
306 $i = $iEnd;
307 }
308 }
309 // check the next token in case comments appear in the middle of email addresses
310 $prevToken = end($aTokens);
311 if (!in_array($prevToken,$aSpecials,true)) {
312 if ($i+1<strlen($address) && !in_array($address{$i+1},$aSpecials,true)) {
313 $iEnd = strpos($address,' ',$i+1);
314 if ($iEnd) {
315 $sNextToken = trim(substr($address,$i+1,$iEnd - $i -1));
316 $i = $iEnd-1;
317 } else {
318 $sNextToken = trim(substr($address,$i+1));
319 $i = $iCnt;
320 }
321 // remove the token
322 array_pop($aTokens);
323 // create token and add it again
324 $sNewToken = $prevToken . $sNextToken;
325 if($sNewToken) $aTokens[] = $sNewToken;
326 }
327 }
328 $sToken = str_replace($aReplace, $aSpecials,$sToken);
329 if ($sToken) $aTokens[] = $sToken;
330 break;
331 case ',':
332 case ':':
333 case ';':
334 case ' ':
335 $aTokens[] = $cChar;
336 break;
337 default:
338 $iEnd = strpos($address,' ',$i+1);
339 if ($iEnd) {
340 $sToken = trim(substr($address,$i,$iEnd - $i));
341 $i = $iEnd-1;
342 } else {
343 $sToken = trim(substr($address,$i));
344 $i = $iCnt;
345 }
346 if ($sToken) $aTokens[] = $sToken;
347 }
348 ++$i;
349 }
350 return $aTokens;
351 }
352 function createAddressObject(&$aStack,&$aComment,&$sEmail,$sGroup='') {
353 //$aStack=explode(' ',implode('',$aStack));
354 if (!$sEmail) {
355 while (count($aStack) && !$sEmail) {
356 $sEmail = trim(array_pop($aStack));
357 }
358 }
359 if (count($aStack)) {
360 $sPersonal = trim(implode('',$aStack));
361 } else {
362 $sPersonal = '';
363 }
364 if (!$sPersonal && count($aComment)) {
365 $sComment = trim(implode(' ',$aComment));
366 $sPersonal .= $sComment;
367 }
368 $oAddr =& new AddressStructure();
369 if ($sPersonal && substr($sPersonal,0,2) == '=?') {
370 $oAddr->personal = encodeHeader($sPersonal);
371 } else {
372 $oAddr->personal = $sPersonal;
373 }
374 // $oAddr->group = $sGroup;
375 $iPosAt = strpos($sEmail,'@');
376 if ($iPosAt) {
377 $oAddr->mailbox = substr($sEmail, 0, $iPosAt);
378 $oAddr->host = substr($sEmail, $iPosAt+1);
379 } else {
380 $oAddr->mailbox = $sEmail;
381 $oAddr->host = false;
382 }
383 $sEmail = '';
384 $aStack = $aComment = array();
385 return $oAddr;
386 }
387
388 /*
389 * parseAddress: recursive function for parsing address strings and store
390 * them in an address stucture object.
391 * input: $address = string
392 * $ar = boolean (return array instead of only the
393 * first element)
394 * $addr_ar = array with parsed addresses // obsolete
395 * $group = string // obsolete
396 * $host = string (default domainname in case of
397 * addresses without a domainname)
398 * $lookup = callback function (for lookup address
399 * strings which are probably nicks
400 * (without @ ) )
401 * output: array with addressstructure objects or only one
402 * address_structure object.
403 * personal name: encoded: =?charset?Q|B?string?=
404 * quoted: "string"
405 * normal: string
406 * email : <mailbox@host>
407 * : mailbox@host
408 * This function is also used for validating addresses returned from compose
409 * That's also the reason that the function became a little bit huge
410 */
411
412 function parseAddress($address,$ar=false,$aAddress=array(),$sGroup='',$sHost='',$lookup=false) {
413 $aTokens = $this->getAddressTokens($address);
414 $sPersonal = $sEmail = $sGroup = '';
415 $aStack = $aComment = array();
416 foreach ($aTokens as $sToken) {
417 $cChar = $sToken{0};
418 switch ($cChar)
419 {
420 case '=':
421 case '"':
422 case ' ':
423 $aStack[] = $sToken;
424 break;
425 case '(':
426 $aComment[] = substr($sToken,1,-1);
427 break;
428 case ';':
429 if ($sGroup) {
430 $aAddress[] = $this->createAddressObject($aStack,$aComment,$sEmail,$sGroup);
431 $oAddr = end($aAddress);
432 if(!$oAddr || ((isset($oAddr)) && !$oAddr->mailbox && !$oAddr->personal)) {
433 $sEmail = $sGroup . ':;';
434 }
435 $aAddress[] = $this->createAddressObject($aStack,$aComment,$sEmail,$sGroup);
436 $sGroup = '';
437 $aStack = $aComment = array();
438 break;
439 }
440 case ',':
441 $aAddress[] = $this->createAddressObject($aStack,$aComment,$sEmail,$sGroup);
442 break;
443 case ':':
444 $sGroup = trim(implode(' ',$aStack));
445 $sGroup = preg_replace('/\s+/',' ',$sGroup);
446 $aStack = array();
447 break;
448 case '<':
449 $sEmail = trim(substr($sToken,1,-1));
450 break;
451 case '>':
452 /* skip */
453 break;
454 default: $aStack[] = $sToken; break;
455 }
456 }
457 /* now do the action again for the last address */
458 $aAddress[] = $this->createAddressObject($aStack,$aComment,$sEmail);
459 /* try to lookup the addresses in case of invalid email addresses */
460 $aProcessedAddress = array();
461 foreach ($aAddress as $oAddr) {
462 $aAddrBookAddress = array();
463 if (!$oAddr->host) {
464 $grouplookup = false;
465 if ($lookup) {
466 $aAddr = call_user_func_array($lookup,array($oAddr->mailbox));
467 if (isset($aAddr['email'])) {
468 if (strpos($aAddr['email'],',')) {
469 $grouplookup = true;
470 $aAddrBookAddress = $this->parseAddress($aAddr['email'],true);
471 } else {
472 $iPosAt = strpos($aAddr['email'], '@');
473 $oAddr->mailbox = substr($aAddr['email'], 0, $iPosAt);
474 $oAddr->host = substr($aAddr['email'], $iPosAt+1);
475 if (isset($aAddr['name'])) {
476 $oAddr->personal = $aAddr['name'];
477 } else {
478 $oAddr->personal = encodeHeader($sPersonal);
479 }
480 }
481 }
482 }
483 if (!$grouplookup && !$oAddr->mailbox) {
484 $oAddr->mailbox = trim($sEmail);
485 if ($sHost && $oAddr->mailbox) {
486 $oAddr->host = $sHost;
487 }
488 } else if (!$grouplookup && !$oAddr->host) {
489 if ($sHost && $oAddr->mailbox) {
490 $oAddr->host = $sHost;
491 }
492 }
493 }
494 if (!$aAddrBookAddress && $oAddr->mailbox) {
495 $aProcessedAddress[] = $oAddr;
496 } else {
497 $aProcessedAddress = array_merge($aProcessedAddress,$aAddrBookAddress);
498 }
499 }
500 if ($ar) {
501 return $aProcessedAddress;
502 } else {
503 return $aProcessedAddress[0];
504 }
505 }
506
507 function parseContentType($value) {
508 $pos = strpos($value, ';');
509 $props = '';
510 if ($pos > 0) {
511 $type = trim(substr($value, 0, $pos));
512 $props = trim(substr($value, $pos+1));
513 } else {
514 $type = $value;
515 }
516 $content_type = new ContentType($type);
517 if ($props) {
518 $properties = $this->parseProperties($props);
519 if (!isset($properties['charset'])) {
520 $properties['charset'] = 'us-ascii';
521 }
522 $content_type->properties = $this->parseProperties($props);
523 }
524 $this->content_type = $content_type;
525 }
526
527 /* RFC2184 */
528 function processParameters($aParameters) {
529 $aResults = array();
530 $aCharset = array();
531 // handle multiline parameters
532 foreach($aParameters as $key => $value) {
533 if ($iPos = strpos($key,'*')) {
534 $sKey = substr($key,0,$iPos);
535 if (!isset($aResults[$sKey])) {
536 $aResults[$sKey] = $value;
537 if (substr($key,-1) == '*') { // parameter contains language/charset info
538 $aCharset[] = $sKey;
539 }
540 } else {
541 $aResults[$sKey] .= $value;
542 }
543 } else {
544 $aResults[$key] = $value;
545 }
546 }
547 foreach ($aCharset as $key) {
548 $value = $aResults[$key];
549 // extract the charset & language
550 $charset = substr($value,0,strpos($value,"'"));
551 $value = substr($value,strlen($charset)+1);
552 $language = substr($value,0,strpos($value,"'"));
553 $value = substr($value,strlen($charset)+1);
554 // FIX ME What's the status of charset decode with language information ????
555 $value = charset_decode($charset,$value);
556 $aResults[$key] = $value;
557 }
558 return $aResults;
559 }
560
561 function parseProperties($value) {
562 $propArray = explode(';', $value);
563 $propResultArray = array();
564 foreach ($propArray as $prop) {
565 $prop = trim($prop);
566 $pos = strpos($prop, '=');
567 if ($pos > 0) {
568 $key = trim(substr($prop, 0, $pos));
569 $val = trim(substr($prop, $pos+1));
570 if (strlen($val) > 0 && $val{0} == '"') {
571 $val = substr($val, 1, -1);
572 }
573 $propResultArray[$key] = $val;
574 }
575 }
576 return $this->processParameters($propResultArray);
577 }
578
579 function parseDisposition($value) {
580 $pos = strpos($value, ';');
581 $props = '';
582 if ($pos > 0) {
583 $name = trim(substr($value, 0, $pos));
584 $props = trim(substr($value, $pos+1));
585 } else {
586 $name = $value;
587 }
588 $props_a = $this->parseProperties($props);
589 $disp = new Disposition($name);
590 $disp->properties = $props_a;
591 $this->disposition = $disp;
592 }
593
594 function mlist($field, $value) {
595 $res_a = array();
596 $value_a = explode(',', $value);
597 foreach ($value_a as $val) {
598 $val = trim($val);
599 if ($val{0} == '<') {
600 $val = substr($val, 1, -1);
601 }
602 if (substr($val, 0, 7) == 'mailto:') {
603 $res_a['mailto'] = substr($val, 7);
604 } else {
605 $res_a['href'] = $val;
606 }
607 }
608 $this->mlist[$field] = $res_a;
609 }
610
611 /*
612 * function to get the addres strings out of the header.
613 * Arguments: string or array of strings !
614 * example1: header->getAddr_s('to').
615 * example2: header->getAddr_s(array('to', 'cc', 'bcc'))
616 */
617 function getAddr_s($arr, $separator = ',',$encoded=false) {
618 $s = '';
619
620 if (is_array($arr)) {
621 foreach($arr as $arg) {
622 if ($this->getAddr_s($arg, $separator, $encoded)) {
623 $s .= $separator;
624 }
625 }
626 $s = ($s ? substr($s, 2) : $s);
627 } else {
628 $addr = $this->{$arr};
629 if (is_array($addr)) {
630 foreach ($addr as $addr_o) {
631 if (is_object($addr_o)) {
632 if ($encoded) {
633 $s .= $addr_o->getEncodedAddress() . $separator;
634 } else {
635 $s .= $addr_o->getAddress() . $separator;
636 }
637 }
638 }
639 $s = substr($s, 0, -strlen($separator));
640 } else {
641 if (is_object($addr)) {
642 if ($encoded) {
643 $s .= $addr->getEncodedAddress();
644 } else {
645 $s .= $addr->getAddress();
646 }
647 }
648 }
649 }
650 return $s;
651 }
652
653 function getAddr_a($arg, $excl_arr = array(), $arr = array()) {
654 if (is_array($arg)) {
655 foreach($arg as $argument) {
656 $arr = $this->getAddr_a($argument, $excl_arr, $arr);
657 }
658 } else {
659 $addr = $this->{$arg};
660 if (is_array($addr)) {
661 foreach ($addr as $next_addr) {
662 if (is_object($next_addr)) {
663 if (isset($next_addr->host) && ($next_addr->host != '')) {
664 $email = $next_addr->mailbox . '@' . $next_addr->host;
665 } else {
666 $email = $next_addr->mailbox;
667 }
668 $email = strtolower($email);
669 if ($email && !isset($arr[$email]) && !isset($excl_arr[$email])) {
670 $arr[$email] = $next_addr->personal;
671 }
672 }
673 }
674 } else {
675 if (is_object($addr)) {
676 $email = $addr->mailbox;
677 $email .= (isset($addr->host) ? '@' . $addr->host : '');
678 $email = strtolower($email);
679 if ($email && !isset($arr[$email]) && !isset($excl_arr[$email])) {
680 $arr[$email] = $addr->personal;
681 }
682 }
683 }
684 }
685 return $arr;
686 }
687
688 function findAddress($address, $recurs = false) {
689 $result = false;
690 if (is_array($address)) {
691 $i=0;
692 foreach($address as $argument) {
693 $match = $this->findAddress($argument, true);
694 if ($match[1]) {
695 return $i;
696 } else {
697 if (count($match[0]) && !$result) {
698 $result = $i;
699 }
700 }
701 ++$i;
702 }
703 } else {
704 if (!is_array($this->cc)) $this->cc = array();
705 $srch_addr = $this->parseAddress($address);
706 $results = array();
707 foreach ($this->to as $to) {
708 if ($to->host == $srch_addr->host) {
709 if ($to->mailbox == $srch_addr->mailbox) {
710 $results[] = $srch_addr;
711 if ($to->personal == $srch_addr->personal) {
712 if ($recurs) {
713 return array($results, true);
714 } else {
715 return true;
716 }
717 }
718 }
719 }
720 }
721 foreach ($this->cc as $cc) {
722 if ($cc->host == $srch_addr->host) {
723 if ($cc->mailbox == $srch_addr->mailbox) {
724 $results[] = $srch_addr;
725 if ($cc->personal == $srch_addr->personal) {
726 if ($recurs) {
727 return array($results, true);
728 } else {
729 return true;
730 }
731 }
732 }
733 }
734 }
735 if ($recurs) {
736 return array($results, false);
737 } elseif (count($result)) {
738 return true;
739 } else {
740 return false;
741 }
742 }
743 //exit;
744 return $result;
745 }
746
747 function getContentType($type0, $type1) {
748 $type0 = $this->content_type->type0;
749 $type1 = $this->content_type->type1;
750 return $this->content_type->properties;
751 }
752}
753
754?>