thomascube
2011-01-29 6039aae3878aa5880415290cbc41af4bac4fdcb5
commit | author | age
ed132e 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_vcard.php                                       |
6  |                                                                       |
e019f2 7  | This file is part of the Roundcube Webmail client                     |
0501b6 8  | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
ed132e 9  | Licensed under the GNU GPL                                            |
T 10  |                                                                       |
11  | PURPOSE:                                                              |
12  |   Logical representation of a vcard address record                    |
13  +-----------------------------------------------------------------------+
14  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
15  +-----------------------------------------------------------------------+
16
1d786c 17  $Id$
ed132e 18
T 19 */
20
21
22 /**
23  * Logical representation of a vcard-based address record
24  * Provides functions to parse and export vCard data format
25  *
26  * @package    Addressbook
27  * @author     Thomas Bruederli <roundcube@gmail.com>
28  */
29 class rcube_vcard
30 {
6bdb61 31   private static $values_decoded = false;
0dbac3 32   private $raw = array(
T 33     'FN' => array(),
34     'N' => array(array('','','','','')),
35   );
0501b6 36   private $fieldmap = array(
T 37     'phone'    => 'TEL',
38     'birthday' => 'BDAY',
39     'website'  => 'URL',
40     'notes'    => 'NOTE',
41     'email'    => 'EMAIL',
42     'address'  => 'ADR',
43     'gender'      => 'X-GENDER',
44     'maidenname'  => 'X-MAIDENNAME',
45     'anniversary' => 'X-ANNIVERSARY',
46     'assistant'   => 'X-ASSISTANT',
47     'manager'     => 'X-MANAGER',
48     'spouse'      => 'X-SPOUSE',
49   );
50   private $typemap = array('iPhone' => 'mobile', 'CELL' => 'mobile');
51   private $phonetypemap = array('HOME1' => 'HOME', 'BUSINESS1' => 'WORK', 'BUSINESS2' => 'WORK2', 'WORKFAX' => 'BUSINESSFAX');
52   private $addresstypemap = array('BUSINESS' => 'WORK');
53   private $immap = array('X-JABBER' => 'jabber', 'X-ICQ' => 'icq', 'X-MSN' => 'msn', 'X-AIM' => 'aim', 'X-YAHOO' => 'yahoo', 'X-SKYPE' => 'skype', 'X-SKYPE-USERNAME' => 'skype');
ed132e 54
T 55   public $business = false;
56   public $displayname;
57   public $surname;
58   public $firstname;
59   public $middlename;
60   public $nickname;
61   public $organization;
62   public $notes;
63   public $email = array();
64
65
66   /**
67    * Constructor
68    */
6bdb61 69   public function __construct($vcard = null, $charset = RCMAIL_CHARSET, $detect = false)
ed132e 70   {
T 71     if (!empty($vcard))
6bdb61 72       $this->load($vcard, $charset, $detect);
ed132e 73   }
T 74
75
76   /**
77    * Load record from (internal, unfolded) vcard 3.0 format
78    *
79    * @param string vCard string to parse
6bdb61 80    * @param string Charset of string values
T 81    * @param boolean True if loading a 'foreign' vcard and extra heuristics for charset detection is required
ed132e 82    */
6bdb61 83   public function load($vcard, $charset = RCMAIL_CHARSET, $detect = false)
ed132e 84   {
6bdb61 85     self::$values_decoded = false;
ed132e 86     $this->raw = self::vcard_decode($vcard);
6bdb61 87
5570ad 88     // resolve charset parameters
6bdb61 89     if ($charset == null) {
T 90       $this->raw = self::charset_convert($this->raw);
91     }
92     // vcard has encoded values and charset should be detected
93     else if ($detect && self::$values_decoded &&
94       ($detected_charset = self::detect_encoding(self::vcard_encode($this->raw))) && $detected_charset != RCMAIL_CHARSET) {
95         $this->raw = self::charset_convert($this->raw, $detected_charset);
96     }
ed132e 97
T 98     // find well-known address fields
5570ad 99     $this->displayname = $this->raw['FN'][0][0];
ed132e 100     $this->surname = $this->raw['N'][0][0];
T 101     $this->firstname = $this->raw['N'][0][1];
102     $this->middlename = $this->raw['N'][0][2];
5570ad 103     $this->nickname = $this->raw['NICKNAME'][0][0];
T 104     $this->organization = $this->raw['ORG'][0][0];
105     $this->business = ($this->raw['X-ABSHOWAS'][0][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization));
ed132e 106     
T 107     foreach ((array)$this->raw['EMAIL'] as $i => $raw_email)
bb8781 108       $this->email[$i] = is_array($raw_email) ? $raw_email[0] : $raw_email;
ed132e 109     
T 110     // make the pref e-mail address the first entry in $this->email
111     $pref_index = $this->get_type_index('EMAIL', 'pref');
112     if ($pref_index > 0) {
113       $tmp = $this->email[0];
114       $this->email[0] = $this->email[$pref_index];
115       $this->email[$pref_index] = $tmp;
116     }
c56f1f 117
A 118     // make sure displayname is not empty (required by RFC2426)
119     if (!strlen($this->displayname)) {
120       // the same method is used in steps/mail/addcontact.inc
121       $this->displayname = ucfirst(preg_replace('/[\.\-]/', ' ',
122         substr($this->email[0], 0, strpos($this->email[0], '@'))));
123     }
ed132e 124   }
T 125
126
127   /**
0501b6 128    * Return vCard data as associative array to be unsed in Roundcube address books
T 129    *
130    * @return array Hash array with key-value pairs
131    */
132   public function get_assoc()
133   {
134     $out = array('name' => $this->displayname);
135     $typemap = $this->typemap;
136     
137     // copy name fields to output array
138     foreach (array('firstname','surname','middlename','nickname','organization') as $col)
139       $out[$col] = $this->$col;
140     
141     $out['prefix'] = $this->raw['N'][0][3];
142     $out['suffix'] = $this->raw['N'][0][4];
143     
144     // convert from raw vcard data into associative data for Roundcube
145     foreach (array_flip($this->fieldmap) as $tag => $col) {
146       foreach ((array)$this->raw[$tag] as $i => $raw) {
147         if (is_array($raw)) {
148           $k = -1;
149           $key = $col;
e9aa8c 150           
0501b6 151           $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]);
T 152           while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref'))
153             $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]);
e9aa8c 154           
T 155           // read vcard 2.1 subtype
156           if (!$subtype) {
157             foreach ($raw as $k => $v) {
158               if (!is_numeric($k) && $v === true && !in_array(strtolower($k), array('pref','internet','voice','base64'))) {
159                 $subtype = $typemap[$k] ? $typemap[$k] : strtolower($k);
160                 break;
161               }
162             }
163           }
164           
0501b6 165           if ($subtype)
T 166             $key .= ':' . $subtype;
167
168           // split ADR values into assoc array
169           if ($tag == 'ADR') {
170             list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw;
171             $out[$key][] = $value;
172           }
173           else
174             $out[$key][] = $raw[0];
175         }
176         else {
177           $out[$col][] = $raw;
178         }
179       }
180     }
181     
182     // handle special IM fields as used by Apple
183     foreach ($this->immap as $tag => $type) {
184       foreach ((array)$this->raw[$tag] as $i => $raw) {
185         $out['im:'.$type][] = $raw[0];
186       }
187     }
188     
189     // copy photo data
190     if ($this->raw['PHOTO'])
191       $out['photo'] = $this->raw['PHOTO'][0][0];
192     
193     return $out;
194   }
195
196
197   /**
ed132e 198    * Convert the data structure into a vcard 3.0 string
T 199    */
200   public function export()
201   {
202     return self::rfc2425_fold(self::vcard_encode($this->raw));
0501b6 203   }
T 204   
205   
206   /**
207    * Clear the given fields in the loaded vcard data
208    *
209    * @param array List of field names to be reset
210    */
211   public function reset($fields = null)
212   {
213     if (!$fields)
214       $fields = array_merge(array_values($this->fieldmap), array_keys($this->immap), array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY'));
215     
216     foreach ($fields as $f)
217       unset($this->raw[$f]);
218
219     if (!$this->raw['N'])
220       $this->raw['N'] = array(array('','','','',''));
221     if (!$this->raw['FN'])
222       $this->raw['FN'] = array();
223       
224     $this->email = array();
ed132e 225   }
T 226
227
228   /**
229    * Setter for address record fields
230    *
231    * @param string Field name
232    * @param string Field value
0501b6 233    * @param string Type/section name
ed132e 234    */
0501b6 235   public function set($field, $value, $type = 'HOME')
ed132e 236   {
0501b6 237     $field = strtolower($field);
T 238     $type = strtoupper($type);
239     $typemap = array_flip($this->typemap);
240     
ed132e 241     switch ($field) {
T 242       case 'name':
243       case 'displayname':
5570ad 244         $this->raw['FN'][0][0] = $value;
ed132e 245         break;
T 246         
0501b6 247       case 'surname':
T 248         $this->raw['N'][0][0] = $value;
249         break;
250         
ed132e 251       case 'firstname':
T 252         $this->raw['N'][0][1] = $value;
253         break;
254         
0501b6 255       case 'middlename':
T 256         $this->raw['N'][0][2] = $value;
ed132e 257         break;
0501b6 258         
T 259       case 'prefix':
260         $this->raw['N'][0][3] = $value;
261         break;
262         
263       case 'suffix':
264         $this->raw['N'][0][4] = $value;
265         break;
266         
ed132e 267       case 'nickname':
5570ad 268         $this->raw['NICKNAME'][0][0] = $value;
ed132e 269         break;
T 270         
271       case 'organization':
5570ad 272         $this->raw['ORG'][0][0] = $value;
ed132e 273         break;
T 274         
0501b6 275       case 'photo':
T 276         $encoded = !preg_match('![^a-z0-9/=+-]!i', $value);
277         $this->raw['PHOTO'][0] = array(0 => $encoded ? $value : base64_encode($value), 'BASE64' => true);
278         break;
279         
ed132e 280       case 'email':
0501b6 281         $this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type)));
T 282         $this->email[] = $value;
283         break;
284         
285       case 'im':
286         // save IM subtypes into extension fields
287         $typemap = array_flip($this->immap);
288         if ($field = $typemap[strtolower($type)])
289           $this->raw[$field][] = array(0 => $value);
290         break;
291
292       case 'birthday':
e9aa8c 293         if ($val = rcube_strtotime($value))
0501b6 294           $this->raw['BDAY'][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));
T 295         break;
296
297       case 'address':
298         if ($this->addresstypemap[$type])
299           $type = $this->addresstypemap[$type];
300
301         $value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']);
302
303         // fall through if not empty
304         if (!strlen(join('', $value)))
305           break;
306
307       default:
308         if ($field == 'phone' && $this->phonetypemap[$type])
309           $type = $this->phonetypemap[$type];
310
311         if (($tag = $this->fieldmap[$field]) && (is_array($value) || strlen($value))) {
312           $index = count($this->raw[$tag]);
313           $this->raw[$tag][$index] = (array)$value;
314           if ($type)
315             $this->raw[$tag][$index]['type'] = array(($typemap[$type] ? $typemap[$type] : $type));
ed132e 316         }
T 317         break;
318     }
319   }
320
321
322   /**
323    * Find index with the '$type' attribute
324    *
325    * @param string Field name
326    * @return int Field index having $type set
327    */
328   private function get_type_index($field, $type = 'pref')
329   {
330     $result = 0;
331     if ($this->raw[$field]) {
332       foreach ($this->raw[$field] as $i => $data) {
333         if (is_array($data['type']) && in_array_nocase('pref', $data['type']))
334           $result = $i;
335       }
336     }
337     
338     return $result;
339   }
5570ad 340   
T 341   
342   /**
343    * Convert a whole vcard (array) to UTF-8.
6bdb61 344    * If $force_charset is null, each member value that has a charset parameter will be converted
5570ad 345    */
6bdb61 346   private static function charset_convert($card, $force_charset = null)
5570ad 347   {
T 348     foreach ($card as $key => $node) {
349       foreach ($node as $i => $subnode) {
6bdb61 350         if (is_array($subnode) && (($charset = $force_charset) || ($subnode['charset'] && ($charset = $subnode['charset'][0])))) {
5570ad 351           foreach ($subnode as $j => $value) {
T 352             if (is_numeric($j) && is_string($value))
353               $card[$key][$i][$j] = rcube_charset_convert($value, $charset);
354           }
355           unset($card[$key][$i]['charset']);
356         }
357       }
358     }
359
360     return $card;
361   }
ed132e 362
T 363
364   /**
365    * Factory method to import a vcard file
366    *
367    * @param string vCard file content
368    * @return array List of rcube_vcard objects
369    */
370   public static function import($data)
371   {
372     $out = array();
373
5570ad 374     // check if charsets are specified (usually vcard version < 3.0 but this is not reliable)
T 375     if (preg_match('/charset=/i', substr($data, 0, 2048)))
376       $charset = null;
ed132e 377     // detect charset and convert to utf-8
5570ad 378     else if (($charset = self::detect_encoding($data)) && $charset != RCMAIL_CHARSET) {
T 379       $data = rcube_charset_convert($data, $charset);
6fa87f 380       $data = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $data); // also remove BOM
5570ad 381       $charset = RCMAIL_CHARSET;
ed132e 382     }
T 383
384     $vcard_block = '';
385     $in_vcard_block = false;
386
387     foreach (preg_split("/[\r\n]+/", $data) as $i => $line) {
388       if ($in_vcard_block && !empty($line))
389         $vcard_block .= $line . "\n";
390
928bca 391       $line = trim($line);
A 392
393       if (preg_match('/^END:VCARD$/i', $line)) {
ed132e 394         // parse vcard
6bdb61 395         $obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true);
ed132e 396         if (!empty($obj->displayname))
T 397           $out[] = $obj;
398
399         $in_vcard_block = false;
400       }
928bca 401       else if (preg_match('/^BEGIN:VCARD$/i', $line)) {
ed132e 402         $vcard_block = $line . "\n";
T 403         $in_vcard_block = true;
404       }
405     }
406
407     return $out;
408   }
409
410
411   /**
412    * Normalize vcard data for better parsing
413    *
414    * @param string vCard block
415    * @return string Cleaned vcard block
416    */
417   private static function cleanup($vcard)
418   {
419     // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
420     $vcard = preg_replace(
421       '/item(\d+)\.(TEL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
422       '\2;type=\5\3:\4',
423       $vcard);
424
425     // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines
426     $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard);
e9aa8c 427     
T 428     // convert X-WAB-GENDER to X-GENDER
429     if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) {
430       $value = $matches[1] == '2' ? 'male' : 'female';
431       $vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard);
432     }
ed132e 433
b58f11 434     // if N doesn't have any semicolons, add some 
T 435     $vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard);
ed132e 436
T 437     return $vcard;
438   }
439
478c7c 440   private static function rfc2425_fold_callback($matches)
A 441   {
442     return ":\n  ".rtrim(chunk_split($matches[1], 72, "\n  "));
443   }
ed132e 444
T 445   private static function rfc2425_fold($val)
446   {
5e6815 447     return preg_replace_callback('/:([^\n]{72,})/', array('self', 'rfc2425_fold_callback'), $val) . "\n";
ed132e 448   }
T 449
450
451   /**
452    * Decodes a vcard block (vcard 3.0 format, unfolded)
453    * into an array structure
454    *
455    * @param string vCard block to parse
456    * @return array Raw data structure
457    */
458   private static function vcard_decode($vcard)
459   {
460     // Perform RFC2425 line unfolding
461     $vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard);
462     
b58f11 463     $lines = preg_split('/\r?\n/', $vcard);
ed132e 464     $data = array();
b58f11 465     
T 466     for ($i=0; $i < count($lines); $i++) {
467       if (!preg_match('/^([^\\:]*):(.+)$/', $lines[$i], $line))
468           continue;
ed132e 469
b58f11 470       // convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:"
T 471       if (($data['VERSION'][0] == "2.1") && preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2) && !preg_match('/^TYPE=/i', $regs2[2])) {
472         $line[1] = $regs2[1];
473         foreach (explode(';', $regs2[2]) as $prop)
474           $line[1] .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop);
ed132e 475       }
T 476
cc97ea 477       if (!preg_match('/^(BEGIN|END)$/i', $line[1]) && preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) {
93af15 478         $entry = array();
cc97ea 479         $field = strtoupper($regs2[1][0]);
b58f11 480
T 481         foreach($regs2[1] as $attrid => $attr) {
482           if ((list($key, $value) = explode('=', $attr)) && $value) {
5570ad 483             $value = trim($value);
b58f11 484             if ($key == 'ENCODING') {
93af15 485               // add next line(s) to value string if QP line end detected
23a2ee 486               while ($value == 'QUOTED-PRINTABLE' && preg_match('/=$/', $lines[$i]))
b58f11 487                   $line[2] .= "\n" . $lines[++$i];
T 488               
489               $line[2] = self::decode_value($line[2], $value);
490             }
491             else
492               $entry[strtolower($key)] = array_merge((array)$entry[strtolower($key)], (array)self::vcard_unquote($value, ','));
493           }
494           else if ($attrid > 0) {
93af15 495             $entry[$key] = true;  // true means attr without =value
b58f11 496           }
T 497         }
498
93af15 499         $entry = array_merge($entry, (array)self::vcard_unquote($line[2]));
5570ad 500         $data[$field][] = $entry;
b58f11 501       }
ed132e 502     }
b58f11 503
T 504     unset($data['VERSION']);
ed132e 505     return $data;
T 506   }
507
508
509   /**
510    * Split quoted string
511    *
512    * @param string vCard string to split
513    * @param string Separator char/string
514    * @return array List with splitted values
515    */
516   private static function vcard_unquote($s, $sep = ';')
517   {
518     // break string into parts separated by $sep, but leave escaped $sep alone
519     if (count($parts = explode($sep, strtr($s, array("\\$sep" => "\007")))) > 1) {
520       foreach($parts as $s) {
521         $result[] = self::vcard_unquote(strtr($s, array("\007" => "\\$sep")), $sep);
522       }
523       return $result;
524     }
525     else {
526       return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\,' => ',', '\;' => ';', '\:' => ':'));
bb8781 527     }
T 528   }
529
530
531   /**
532    * Decode a given string with the encoding rule from ENCODING attributes
533    *
534    * @param string String to decode
535    * @param string Encoding type (quoted-printable and base64 supported)
536    * @return string Decoded 8bit value
537    */
538   private static function decode_value($value, $encoding)
539   {
540     switch (strtolower($encoding)) {
541       case 'quoted-printable':
6bdb61 542         self::$values_decoded = true;
bb8781 543         return quoted_printable_decode($value);
T 544
545       case 'base64':
6bdb61 546         self::$values_decoded = true;
bb8781 547         return base64_decode($value);
T 548
549       default:
550         return $value;
ed132e 551     }
T 552   }
553
554
555   /**
556    * Encodes an entry for storage in our database (vcard 3.0 format, unfolded)
557    *
558    * @param array Raw data structure to encode
559    * @return string vCard encoded string
560    */
561   static function vcard_encode($data)
562   {
563     foreach((array)$data as $type => $entries) {
564       /* valid N has 5 properties */
b58f11 565       while ($type == "N" && is_array($entries[0]) && count($entries[0]) < 5)
ed132e 566         $entries[0][] = "";
T 567
568       foreach((array)$entries as $entry) {
569         $attr = '';
570         if (is_array($entry)) {
571           $value = array();
572           foreach($entry as $attrname => $attrvalues) {
573             if (is_int($attrname))
574               $value[] = $attrvalues;
575             elseif ($attrvalues === true)
93af15 576               $attr .= ";$attrname";    // true means just tag, not tag=value, as in PHOTO;BASE64:...
ed132e 577             else {
T 578               foreach((array)$attrvalues as $attrvalue)
579                 $attr .= ";$attrname=" . self::vcard_quote($attrvalue, ',');
580             }
581           }
582         }
583         else {
584           $value = $entry;
585         }
586
587         $vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . "\n";
588       }
589     }
590
0dbac3 591     return "BEGIN:VCARD\nVERSION:3.0\n{$vcard}END:VCARD";
ed132e 592   }
T 593
594
595   /**
596    * Join indexed data array to a vcard quoted string
597    *
598    * @param array Field data
599    * @param string Separator
600    * @return string Joined and quoted string
601    */
602   private static function vcard_quote($s, $sep = ';')
603   {
604     if (is_array($s)) {
605       foreach($s as $part) {
606         $r[] = self::vcard_quote($part, $sep);
607       }
608       return(implode($sep, (array)$r));
609     }
610     else {
611       return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ';' => '\;', ':' => '\:'));
612     }
613   }
614
615
616   /**
617    * Returns UNICODE type based on BOM (Byte Order Mark)
618    *
619    * @param string Input string to test
620    * @return string Detected encoding
621    */
622   private static function detect_encoding($string)
623   {
624     if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE';  // Big Endian
625     if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE';  // Little Endian
626     if (substr($string, 0, 2) == "\xFE\xFF")     return 'UTF-16BE';  // Big Endian
627     if (substr($string, 0, 2) == "\xFF\xFE")     return 'UTF-16LE';  // Little Endian
628     if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8';
629
6fa87f 630     // use mb_detect_encoding()
T 631     $encodings = array('UTF-8', 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3',
632       'ISO-8859-4', 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9',
633       'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16',
634       'WINDOWS-1252', 'WINDOWS-1251', 'BIG5', 'GB2312');
635
ae9124 636     if (function_exists('mb_detect_encoding') && ($enc = mb_detect_encoding($string, $encodings)))
abdc58 637       return $enc;
A 638
ed132e 639     // No match, check for UTF-8
T 640     // from http://w3.org/International/questions/qa-forms-utf-8.html
641     if (preg_match('/\A(
642         [\x09\x0A\x0D\x20-\x7E]
643         | [\xC2-\xDF][\x80-\xBF]
644         | \xE0[\xA0-\xBF][\x80-\xBF]
645         | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}
646         | \xED[\x80-\x9F][\x80-\xBF]
647         | \xF0[\x90-\xBF][\x80-\xBF]{2}
648         | [\xF1-\xF3][\x80-\xBF]{3}
649         | \xF4[\x80-\x8F][\x80-\xBF]{2}
650         )*\z/xs', substr($string, 0, 2048)))
651       return 'UTF-8';
652
ae9124 653     return rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1'); # fallback to Latin-1
ed132e 654   }
T 655
656 }
657
658