From 5f17657e82abfae6bce0027e243e22a4183174a6 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Thu, 14 Aug 2014 12:19:55 -0400
Subject: [PATCH] Support contacts import in GMail CSV format

---
 CHANGELOG                                 |    1 
 tests/src/Csv2vcard/gmail.csv             |    0 
 tests/src/Csv2vcard/gmail.vcf             |   25 ++++++
 program/lib/Roundcube/rcube_csv2vcard.php |  175 +++++++++++++++++++++++++++++++++++++++++--
 tests/Framework/Csv2vcard.php             |   18 ++++
 5 files changed, 210 insertions(+), 9 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index aecd0f4..b4269c6 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 06bc387..b7d1591 100644
--- a/program/lib/Roundcube/rcube_csv2vcard.php
+++ b/program/lib/Roundcube/rcube_csv2vcard.php
@@ -149,6 +149,13 @@
 
         // 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 @@
         '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 @@
     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 @@
             }
             // 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 @@
                 $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 @@
         }
 
         $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 @@
             }
         }
 
+        // 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 @@
             $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 5d52efc..4f48dfa 100644
--- a/tests/Framework/Csv2vcard.php
+++ b/tests/Framework/Csv2vcard.php
@@ -55,4 +55,22 @@
         $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 0000000..9f67fe9
--- /dev/null
+++ b/tests/src/Csv2vcard/gmail.csv
Binary files differ
diff --git a/tests/src/Csv2vcard/gmail.vcf b/tests/src/Csv2vcard/gmail.vcf
new file mode 100644
index 0000000..5337d7e
--- /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

--
Gitblit v1.9.1