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