diff --git a/CHANGELOG b/CHANGELOG index aecd0f422..b4269c68d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Support contacts import in GMail CSV format - Added namespace filter in Folder Manager - Added folder searching in Folder Manager - Added config option 'imap_log_session' to enable Roundcube <-> IMAP session ID logging diff --git a/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php index 06bc387d5..b7d159178 100644 --- a/program/lib/Roundcube/rcube_csv2vcard.php +++ b/program/lib/Roundcube/rcube_csv2vcard.php @@ -149,6 +149,13 @@ class rcube_csv2vcard // GMail 'groups' => 'groups', + 'group_membership' => 'groups', + 'given_name' => 'firstname', + 'additional_name' => 'middlename', + 'family_name' => 'surname', + 'name' => 'displayname', + 'name_prefix' => 'prefix', + 'name_suffix' => 'suffix', ); /** @@ -272,12 +279,95 @@ class rcube_csv2vcard 'work_mobile' => "Work Mobile", 'work_title' => "Work Title", 'work_zip' => "Work Zip", - 'groups' => "Group", + 'group' => "Group", + + // GMail + 'groups' => "Groups", + 'group_membership' => "Group Membership", + 'given_name' => "Given Name", + 'additional_name' => "Additional Name", + 'family_name' => "Family Name", + 'name' => "Name", + 'name_prefix' => "Name Prefix", + 'name_suffix' => "Name Suffix", + ); + + /** + * Special fields map for GMail format + * + * @var array + */ + protected $gmail_label_map = array( + 'E-mail' => array( + 'Value' => array( + 'home' => 'email:home', + 'work' => 'email:work', + ), + ), + 'Phone' => array( + 'Value' => array( + 'home' => 'phone:home', + 'homefax' => 'phone:homefax', + 'main' => 'phone:pref', + 'pager' => 'phone:pager', + 'mobile' => 'phone:cell', + 'work' => 'phone:work', + 'workfax' => 'phone:workfax', + ), + ), + 'Relation' => array( + 'Value' => array( + 'spouse' => 'spouse', + ), + ), + 'Website' => array( + 'Value' => array( + 'profile' => 'website:profile', + 'blog' => 'website:blog', + 'homepage' => 'website:homepage', + 'work' => 'website:work', + ), + ), + 'Address' => array( + 'Street' => array( + 'home' => 'street:home', + 'work' => 'street:work', + ), + 'City' => array( + 'home' => 'locality:home', + 'work' => 'locality:work', + ), + 'Region' => array( + 'home' => 'region:home', + 'work' => 'region:work', + ), + 'Postal Code' => array( + 'home' => 'zipcode:home', + 'work' => 'zipcode:work', + ), + 'Country' => array( + 'home' => 'country:home', + 'work' => 'country:work', + ), + ), + 'Organization' => array( + 'Name' => array( + '' => 'organization', + ), + 'Title' => array( + '' => 'jobtitle', + ), + 'Department' => array( + '' => 'department', + ), + ), ); + protected $local_label_map = array(); - protected $vcards = array(); - protected $map = array(); + protected $vcards = array(); + protected $map = array(); + protected $gmail_map = array(); /** @@ -308,16 +398,24 @@ class rcube_csv2vcard public function import($csv) { // convert to UTF-8 - $head = substr($csv, 0, 4096); - $charset = rcube_charset::detect($head, RCUBE_CHARSET); - $csv = rcube_charset::convert($csv, $charset); - $head = ''; + $head = substr($csv, 0, 4096); + $charset = rcube_charset::detect($head, RCUBE_CHARSET); + $csv = rcube_charset::convert($csv, $charset); + $csv = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $csv); // also remove BOM + $head = ''; + $prev_line = false; - $this->map = array(); + $this->map = array(); + $this->gmail_map = array(); // Parse file foreach (preg_split("/[\r\n]+/", $csv) as $line) { + if (!empty($prev_line)) { + $line = '"' . $line; + } + $elements = $this->parse_line($line); + if (empty($elements)) { continue; } @@ -331,7 +429,28 @@ class rcube_csv2vcard } // Parse data row else { + // handle multiline elements (e.g. Gmail) + if (!empty($prev_line)) { + $first = array_shift($elements); + + if ($first[0] == '"') { + $prev_line[count($prev_line)-1] = '"' . $prev_line[count($prev_line)-1] . "\n" . substr($first, 1); + } + else { + $prev_line[count($prev_line)-1] .= "\n" . $first; + } + + $elements = array_merge($prev_line, $elements); + } + + $last_element = $elements[count($elements)-1]; + if ($last_element[0] == '"') { + $elements[count($elements)-1] = substr($last_element, 1); + $prev_line = $elements; + continue; + } $this->csv_to_vcard($elements); + $prev_line = false; } } } @@ -389,6 +508,7 @@ class rcube_csv2vcard $map1[$i] = $this->csv2vcard_map[$label]; } } + // check localized labels if (!empty($this->local_label_map)) { for ($i = 0; $i < $size; $i++) { @@ -406,6 +526,22 @@ class rcube_csv2vcard } $this->map = count($map1) >= count($map2) ? $map1 : $map2; + + // support special Gmail format + foreach ($this->gmail_label_map as $key => $items) { + $num = 1; + while (($_key = "$key $num - Type") && ($found = array_search($_key, $elements)) !== false) { + $this->gmail_map["$key:$num"] = array('_key' => $key, '_idx' => $found); + foreach (array_keys($items) as $item_key) { + $_key = "$key $num - $item_key"; + if (($found = array_search($_key, $elements)) !== false) { + $this->gmail_map["$key:$num"][$item_key] = $found; + } + } + + $num++; + } + } } /** @@ -421,6 +557,22 @@ class rcube_csv2vcard } } + // Gmail format support + foreach ($this->gmail_map as $idx => $item) { + $type = preg_replace('/[^a-z]/', '', strtolower($data[$item['_idx']])); + $key = $item['_key']; + + unset($item['_idx']); + unset($item['_key']); + + foreach ($item as $item_key => $item_idx) { + $value = $data[$item_idx]; + if ($value !== null && $value !== '' && ($data_idx = $this->gmail_label_map[$key][$item_key][$type])) { + $contact[$data_idx] = $value; + } + } + } + if (empty($contact)) { return; } @@ -430,9 +582,14 @@ class rcube_csv2vcard $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d']; } - // categories/groups separator in vCard is ',' not ';' if (!empty($contact['groups'])) { + // categories/groups separator in vCard is ',' not ';' $contact['groups'] = str_replace(';', ',', $contact['groups']); + + // remove "* " added by GMail + if (!empty($this->gmail_map)) { + $contact['groups'] = str_replace('* ', '', $contact['groups']); + } } // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00" diff --git a/tests/Framework/Csv2vcard.php b/tests/Framework/Csv2vcard.php index 5d52efc58..4f48dfaa2 100644 --- a/tests/Framework/Csv2vcard.php +++ b/tests/Framework/Csv2vcard.php @@ -55,4 +55,22 @@ class Framework_Csv2vcard extends PHPUnit_Framework_TestCase $vcard = trim(str_replace("\r\n", "\n", $vcard)); $this->assertEquals($vcf_text, $vcard); } + + function test_import_gmail() + { + $csv_text = file_get_contents(TESTS_DIR . '/src/Csv2vcard/gmail.csv'); + $vcf_text = file_get_contents(TESTS_DIR . '/src/Csv2vcard/gmail.vcf'); + + $csv = new rcube_csv2vcard; + $csv->import($csv_text); + $result = $csv->export(); + $vcard = $result[0]->export(false); + + $this->assertCount(1, $result); + + $vcf_text = trim(str_replace("\r\n", "\n", $vcf_text)); + $vcard = trim(str_replace("\r\n", "\n", $vcard)); + + $this->assertEquals($vcf_text, $vcard); + } } diff --git a/tests/src/Csv2vcard/gmail.csv b/tests/src/Csv2vcard/gmail.csv new file mode 100644 index 000000000..9f67fe9f5 Binary files /dev/null and b/tests/src/Csv2vcard/gmail.csv differ diff --git a/tests/src/Csv2vcard/gmail.vcf b/tests/src/Csv2vcard/gmail.vcf new file mode 100644 index 000000000..5337d7e63 --- /dev/null +++ b/tests/src/Csv2vcard/gmail.vcf @@ -0,0 +1,25 @@ +BEGIN:VCARD +VERSION:3.0 +FN:Prefix Firstname Middle Lastname Suffix +N:Lastname;Firstname;Middle;Prefix;Suffix +NICKNAME:nick +BDAY;VALUE=date:1975-12-12 +NOTE:note"note +CATEGORIES:My Contacts +EMAIL;TYPE=INTERNET;TYPE=HOME:home@aaa.pl +EMAIL;TYPE=INTERNET;TYPE=WORK:work@email.pl +TEL;TYPE=pager:pager +TEL;TYPE=pref:mainphone +TEL;TYPE=home:homephone +TEL;TYPE=homefax:homefax +TEL;TYPE=cell:mobile +TEL;TYPE=work:workphone +TEL;TYPE=workfax:workfax +X-SPOUSE:spouse +URL;TYPE=profile:test.com +URL;TYPE=homepage:home.page.com +ORG:company +TITLE:jobtitle +ADR;TYPE=home:;;home_street;home_city;home_state;home_zip;home_country +ADR;TYPE=work:;;work_street;work_city;;work_zip;work_country +END:VCARD