From ed132eddea4a5a21f6f0c1454efa14c5ebe79520 Mon Sep 17 00:00:00 2001
From: thomascube <thomas@roundcube.net>
Date: Tue, 02 Sep 2008 13:22:00 -0400
Subject: [PATCH] New feature to import contacts from a vcard file + mark form buttons that provide the most obvious operation

---
 skins/default/common.css                        |    9 
 skins/default/templates/editidentity.html       |    2 
 CHANGELOG                                       |    5 
 program/include/main.inc                        |   13 
 program/localization/de_CH/messages.inc         |    5 
 skins/default/images/buttons/adr_import_act.png |    0 
 skins/default/templates/addcontact.html         |    2 
 program/localization/de_CH/labels.inc           |    6 
 skins/default/templates/editcontact.html        |    2 
 skins/default/templates/importcontacts.html     |   27 ++
 program/steps/addressbook/import.inc            |  179 ++++++++++++++
 program/localization/en_US/messages.inc         |    5 
 program/include/rcube_vcard.php                 |  386 ++++++++++++++++++++++++++++++++
 program/localization/en_US/labels.inc           |    6 
 skins/default/templates/login.html              |    2 
 skins/default/templates/addressbook.html        |    2 
 program/include/rcube_template.php              |    2 
 skins/default/images/buttons/adr_import_sel.png |    0 
 skins/default/templates/compose.html            |    2 
 program/js/app.js                               |   19 +
 skins/default/images/buttons/adr_import_pas.png |    0 
 skins/default/templates/settings.html           |    2 
 program/include/rcube_contacts.php              |    2 
 skins/default/addresses.css                     |   16 +
 24 files changed, 675 insertions(+), 19 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 01bac58..2726ffd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,11 @@
 CHANGELOG RoundCube Webmail
 ---------------------------
 
+2008/09/02 (thomasb)
+----------
+- Add feature to import contacts from vcard files (#1326103)
+- Mark form buttons that provide the most obvious operation (mainaction)
+
 2008/08/30 (alec)
 ----------
 - Improved HTML to TXT conversion by html2text class update
diff --git a/program/include/main.inc b/program/include/main.inc
index e337c4e..83ed8c0 100644
--- a/program/include/main.inc
+++ b/program/include/main.inc
@@ -792,19 +792,20 @@
  *
  * @param mixed Debug message or data
  */
-function console($msg)
+function console()
   {
-  if (!is_string($msg))
-    $msg = var_export($msg, true);
+  $msg = array();
+  foreach (func_get_args() as $arg)
+    $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
 
   if (!($GLOBALS['CONFIG']['debug_level'] & 4))
-    write_log('console', $msg);
+    write_log('console', join(";\n", $msg));
   else if ($GLOBALS['OUTPUT']->ajax_call)
-    print "/*\n $msg \n*/\n";
+    print "/*\n " . join(";\n", $msg) . " \n*/\n";
   else
     {
     print '<div style="background:#eee; border:1px solid #ccc; margin-bottom:3px; padding:6px"><pre>';
-    print $msg;
+    print join(";<br/>\n", $msg);
     print "</pre></div>\n";
     }
   }
diff --git a/program/include/rcube_contacts.php b/program/include/rcube_contacts.php
index 811ec4c..7ef2af1 100644
--- a/program/include/rcube_contacts.php
+++ b/program/include/rcube_contacts.php
@@ -34,7 +34,7 @@
   var $result = null;
   var $search_fields;
   var $search_string;
-  var $table_cols = array('name', 'email', 'firstname', 'surname');
+  var $table_cols = array('name', 'email', 'firstname', 'surname', 'vcard');
   
   /** public properties */
   var $primary_key = 'contact_id';
diff --git a/program/include/rcube_template.php b/program/include/rcube_template.php
index ba73539..db2275e 100755
--- a/program/include/rcube_template.php
+++ b/program/include/rcube_template.php
@@ -670,7 +670,7 @@
      * @todo   Remove all inline JS calls and use jQuery instead.
      * @todo   Remove all sprintf()'s - they are pretty, but also slow.
      */
-    private function button($attrib)
+    public function button($attrib)
     {
         static $sa_buttons = array();
         static $s_button_count = 100;
diff --git a/program/include/rcube_vcard.php b/program/include/rcube_vcard.php
new file mode 100644
index 0000000..560d37d
--- /dev/null
+++ b/program/include/rcube_vcard.php
@@ -0,0 +1,386 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_vcard.php                                       |
+ |                                                                       |
+ | This file is part of the RoundCube Webmail client                     |
+ | Copyright (C) 2008, RoundCube Dev. - Switzerland                      |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Logical representation of a vcard address record                    |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id: $
+
+*/
+
+
+/**
+ * Logical representation of a vcard-based address record
+ * Provides functions to parse and export vCard data format
+ *
+ * @package    Addressbook
+ * @author     Thomas Bruederli <roundcube@gmail.com>
+ */
+class rcube_vcard
+{
+  private $raw = array();
+
+  public $business = false;
+  public $displayname;
+  public $surname;
+  public $firstname;
+  public $middlename;
+  public $nickname;
+  public $organization;
+  public $notes;
+  public $email = array();
+
+
+  /**
+   * Constructor
+   */
+  public function __construct($vcard = null)
+  {
+    if (!empty($vcard))
+      $this->load($vcard);
+  }
+
+
+  /**
+   * Load record from (internal, unfolded) vcard 3.0 format
+   *
+   * @param string vCard string to parse
+   */
+  public function load($vcard)
+  {
+    $this->raw = self::vcard_decode($vcard);
+
+    // find well-known address fields
+    $this->displayname = $this->raw['FN'][0];
+    $this->surname = $this->raw['N'][0][0];
+    $this->firstname = $this->raw['N'][0][1];
+    $this->middlename = $this->raw['N'][0][2];
+    $this->nickname = $this->raw['NICKNAME'][0];
+    $this->organization = $this->raw['ORG'][0];
+    $this->business = ($this->raw['X-ABShowAs'][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization));
+    
+    foreach ((array)$this->raw['EMAIL'] as $i => $raw_email)
+      $this->email[$i] = $raw_email[0];
+    
+    // make the pref e-mail address the first entry in $this->email
+    $pref_index = $this->get_type_index('EMAIL', 'pref');
+    if ($pref_index > 0) {
+      $tmp = $this->email[0];
+      $this->email[0] = $this->email[$pref_index];
+      $this->email[$pref_index] = $tmp;
+    }
+  }
+
+
+  /**
+   * Convert the data structure into a vcard 3.0 string
+   */
+  public function export()
+  {
+    return self::rfc2425_fold(self::vcard_encode($this->raw));
+  }
+
+
+  /**
+   * Setter for address record fields
+   *
+   * @param string Field name
+   * @param string Field value
+   * @param string Section name
+   */
+  public function set($field, $value, $section = 'home')
+  {
+    switch ($field) {
+      case 'name':
+      case 'displayname':
+        $this->raw['FN'][0] = $value;
+        break;
+        
+      case 'firstname':
+        $this->raw['N'][0][1] = $value;
+        break;
+        
+      case 'surname':
+        $this->raw['N'][0][0] = $value;
+        break;
+      
+      case 'nickname':
+        $this->raw['NICKNAME'][0] = $value;
+        break;
+        
+      case 'organization':
+        $this->raw['ORG'][0] = $value;
+        break;
+        
+      case 'email':
+        $index = $this->get_type_index('EMAIL', $section);
+        if (!is_array($this->raw['EMAIL'][$index])) {
+          $this->raw['EMAIL'][$index] = array(0 => $value, 'type' => array('INTERNET', $section, 'pref'));
+        }
+        else {
+          $this->raw['EMAIL'][$index][0] = $value;
+        }
+        break;
+    }
+  }
+
+
+  /**
+   * Find index with the '$type' attribute
+   *
+   * @param string Field name
+   * @return int Field index having $type set
+   */
+  private function get_type_index($field, $type = 'pref')
+  {
+    $result = 0;
+    if ($this->raw[$field]) {
+      foreach ($this->raw[$field] as $i => $data) {
+        if (is_array($data['type']) && in_array_nocase('pref', $data['type']))
+          $result = $i;
+      }
+    }
+    
+    return $result;
+  }
+
+
+  /**
+   * Factory method to import a vcard file
+   *
+   * @param string vCard file content
+   * @return array List of rcube_vcard objects
+   */
+  public static function import($data)
+  {
+    $out = array();
+
+    // detect charset and convert to utf-8
+    $encoding = self::detect_encoding($data);
+    if ($encoding && $encoding != RCMAIL_CHARSET) {
+      $data = rcube_charset_convert($data, $encoding);
+    }
+
+    $vcard_block = '';
+    $in_vcard_block = false;
+
+    foreach (preg_split("/[\r\n]+/", $data) as $i => $line) {
+      if ($in_vcard_block && !empty($line))
+        $vcard_block .= $line . "\n";
+
+      if (trim($line) == 'END:VCARD') {
+        // parse vcard
+        $obj = new rcube_vcard(self::cleanup($vcard_block));
+        if (!empty($obj->displayname))
+          $out[] = $obj;
+
+        $in_vcard_block = false;
+      }
+      else if (trim($line) == 'BEGIN:VCARD') {
+        $vcard_block = $line . "\n";
+        $in_vcard_block = true;
+      }
+    }
+
+    return $out;
+  }
+
+
+  /**
+   * Normalize vcard data for better parsing
+   *
+   * @param string vCard block
+   * @return string Cleaned vcard block
+   */
+  private static function cleanup($vcard)
+  {
+    // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
+    $vcard = preg_replace(
+      '/item(\d+)\.(TEL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
+      '\2;type=\5\3:\4',
+      $vcard);
+
+    // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines
+    $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard);
+
+    // remove vcard 2.1 charset definitions
+    $vcard = preg_replace('/;CHARSET=[^:]+/', '', $vcard);
+
+    return $vcard;
+  }
+
+
+  private static function rfc2425_fold($val)
+  {
+    return preg_replace('/:([^\n]{72,})/e', '":\n  ".rtrim(chunk_split("\\1", 72, "\n  "))', $val) . "\n\n";
+  }
+
+
+  /**
+   * Decodes a vcard block (vcard 3.0 format, unfolded)
+   * into an array structure
+   *
+   * @param string vCard block to parse
+   * @return array Raw data structure
+   */
+  private static function vcard_decode($vcard)
+  {
+    // Perform RFC2425 line unfolding
+    $vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard);
+    
+    $data = array();
+    if (preg_match_all('/^([^\\:]*):(.+)$/m', $vcard, $regs, PREG_SET_ORDER)) {
+      foreach($regs as $line) {
+        // convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet,home:"
+        if(($data['VERSION'][0] == "2.1") && preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2) && !preg_match('/^TYPE=/i', $regs2[2])) {
+          $line[1] = $regs2[1] . ";TYPE=" . strtr($regs2[2], array(";" => ","));
+        }
+
+        if (!preg_match('/^(BEGIN|END)$/', $line[1]) && preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) {
+          $entry = array(self::vcard_unquote($line[2]));
+
+          foreach($regs2[1] as $attrid => $attr) {
+            if ((list($key, $value) = explode('=', $attr)) && $value)
+              $entry[strtolower($key)] = array_merge((array)$entry[strtolower($key)], (array)self::vcard_unquote($value, ','));
+            elseif ($attrid > 0)
+              $entry[$key] = true;  # true means attr without =value
+          }
+
+          $data[$regs2[1][0]][] = count($entry) > 1 ? $entry : $entry[0];
+        }
+      }
+
+      unset($data['VERSION']);
+    }
+
+    return $data;
+  }
+
+
+  /**
+   * Split quoted string
+   *
+   * @param string vCard string to split
+   * @param string Separator char/string
+   * @return array List with splitted values
+   */
+  private static function vcard_unquote($s, $sep = ';')
+  {
+    // break string into parts separated by $sep, but leave escaped $sep alone
+    if (count($parts = explode($sep, strtr($s, array("\\$sep" => "\007")))) > 1) {
+      foreach($parts as $s) {
+        $result[] = self::vcard_unquote(strtr($s, array("\007" => "\\$sep")), $sep);
+      }
+      return $result;
+    }
+    else {
+      return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\,' => ',', '\;' => ';', '\:' => ':'));
+    }
+  }
+
+
+  /**
+   * Encodes an entry for storage in our database (vcard 3.0 format, unfolded)
+   *
+   * @param array Raw data structure to encode
+   * @return string vCard encoded string
+   */
+  static function vcard_encode($data)
+  {
+    foreach((array)$data as $type => $entries) {
+      /* valid N has 5 properties */
+      while ($type == "N" && count($entries[0]) < 5)
+        $entries[0][] = "";
+
+      foreach((array)$entries as $entry) {
+        $attr = '';
+        if (is_array($entry)) {
+          $value = array();
+          foreach($entry as $attrname => $attrvalues) {
+            if (is_int($attrname))
+              $value[] = $attrvalues;
+            elseif ($attrvalues === true)
+              $attr .= ";$attrname";    # true means just tag, not tag=value, as in PHOTO;BASE64:...
+            else {
+              foreach((array)$attrvalues as $attrvalue)
+                $attr .= ";$attrname=" . self::vcard_quote($attrvalue, ',');
+            }
+          }
+        }
+        else {
+          $value = $entry;
+        }
+
+        $vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . "\n";
+      }
+    }
+
+    return "BEGIN:VCARD\nVERSION:3.0\n{$vcard}END:VCARD\n";
+  }
+
+
+  /**
+   * Join indexed data array to a vcard quoted string
+   *
+   * @param array Field data
+   * @param string Separator
+   * @return string Joined and quoted string
+   */
+  private static function vcard_quote($s, $sep = ';')
+  {
+    if (is_array($s)) {
+      foreach($s as $part) {
+        $r[] = self::vcard_quote($part, $sep);
+      }
+      return(implode($sep, (array)$r));
+    }
+    else {
+      return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ';' => '\;', ':' => '\:'));
+    }
+  }
+
+
+  /**
+   * Returns UNICODE type based on BOM (Byte Order Mark)
+   *
+   * @param string Input string to test
+   * @return string Detected encoding
+   */
+  private static function detect_encoding($string)
+  {
+    if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE';  // Big Endian
+    if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE';  // Little Endian
+    if (substr($string, 0, 2) == "\xFE\xFF")     return 'UTF-16BE';  // Big Endian
+    if (substr($string, 0, 2) == "\xFF\xFE")     return 'UTF-16LE';  // Little Endian
+    if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8';
+
+    // No match, check for UTF-8
+    // from http://w3.org/International/questions/qa-forms-utf-8.html
+    if (preg_match('/\A(
+        [\x09\x0A\x0D\x20-\x7E]
+        | [\xC2-\xDF][\x80-\xBF]
+        | \xE0[\xA0-\xBF][\x80-\xBF]
+        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}
+        | \xED[\x80-\x9F][\x80-\xBF]
+        | \xF0[\x90-\xBF][\x80-\xBF]{2}
+        | [\xF1-\xF3][\x80-\xBF]{3}
+        | \xF4[\x80-\x8F][\x80-\xBF]{2}
+        )*\z/xs', substr($string, 0, 2048)))
+      return 'UTF-8';
+
+    return null;
+  }
+
+}
+
+
diff --git a/program/js/app.js b/program/js/app.js
index d64b58c..ff6ff8c 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -281,7 +281,7 @@
         if ((this.env.action=='add' || this.env.action=='edit') && this.gui_objects.editform)
           this.enable_command('save', true);
         else
-          this.enable_command('search', 'reset-search', 'moveto', true);
+          this.enable_command('search', 'reset-search', 'moveto', 'import', true);
 
         this.enable_command('list', true);
         break;
@@ -972,7 +972,7 @@
           break;
         }
 
-      // reset quicksearch        
+      // reset quicksearch
       case 'reset-search':
         var s = this.env.search_request;
         this.reset_qsearch();
@@ -983,6 +983,21 @@
           this.list_contacts(this.env.source);
         break;
 
+      case 'import':
+        if (this.env.action == 'import' && this.gui_objects.importform) {
+          var file = document.getElementById('rcmimportfile');
+          if (file && !file.value) {
+            alert(this.get_label('selectimportfile'));
+            break;
+          }
+          this.gui_objects.importform.submit();
+          this.set_busy(true, 'importwait');
+          this.lock_form(this.gui_objects.importform, true);
+        }
+        else
+          this.goto_url('import');
+        break
+
       // collapse/expand folder
       case 'collapse-folder':
         if (props)
diff --git a/program/localization/de_CH/labels.inc b/program/localization/de_CH/labels.inc
index 3b32459..a5909b8 100644
--- a/program/localization/de_CH/labels.inc
+++ b/program/localization/de_CH/labels.inc
@@ -184,6 +184,12 @@
 $labels['lastpage'] = 'Letzte Seite';
 $labels['groups'] = 'Gruppen';
 $labels['personaladrbook'] = 'Persönliches Adressbuch';
+$labels['import'] = 'Importieren';
+$labels['importcontacts'] = 'Adressen importieren';
+$labels['importfromfile'] = 'Import aus Datei:';
+$labels['importreplace'] = 'Bestehendes Adressbuch komplett ersetzen';
+$labels['importtext'] = 'Sie können Kontakte aus einem bestehenden Adressbuch hochladen.<br/>Es können Adressbücher im <a href="http://de.wikipedia.org/wiki/VCard">vCard-Format</a> importiert werden.';
+$labels['done'] = 'Fertig';
 $labels['settingsfor'] = 'Einstellungen für';
 $labels['preferences'] = 'Einstellungen';
 $labels['userpreferences'] = 'Benutzereinstellungen';
diff --git a/program/localization/de_CH/messages.inc b/program/localization/de_CH/messages.inc
index 83817b5..201bf94 100644
--- a/program/localization/de_CH/messages.inc
+++ b/program/localization/de_CH/messages.inc
@@ -80,5 +80,10 @@
 $messages['nodeletelastidentity'] = 'Sie können diesen Absender nicht löschen';
 $messages['addsubfolderhint'] = 'Wird als Unterdornder des aktuell selektieren Ordners erstellt';
 $messages['forbiddencharacter'] = 'Der Ordnername enthält ein ungültiges Zeichen';
+$messages['selectimportfile'] = 'Bitte wählen Sie eine Datei zum Importieren aus';
+$messages['addresswriterror'] = 'Das gewählte Adressbuch kann nicht verändert werden';
+$messages['importwait'] = 'Daten werden importiert, bitte warten...';
+$messages['importerror'] = 'Import fehlgeschlagen! Die hochgeladene Datei ist nicht im vCard-Format.';
+$messages['importconfirm'] = '<b>Es wurden $inserted Adressen erfolgreich importiert und $skipped bestehende Einträge übersprungen</b>:<p><em>$names</em></p>';
 
 ?>
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index 43d91f9..b628799 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -232,6 +232,12 @@
 $labels['groups'] = 'Groups';
 $labels['personaladrbook'] = 'Personal Addresses';
 
+$labels['import'] = 'Import';
+$labels['importcontacts'] = 'Import contacts';
+$labels['importfromfile'] = 'Import from file:';
+$labels['importreplace'] = 'Replace the entire address book';
+$labels['importtext'] = 'You can upload contacts from an existing address book.<br/>We currently support importing addresses from the <a href="http://en.wikipedia.org/wiki/VCard">vCard</a> data format.';
+$labels['done'] = 'Done';
 
 // settings
 $labels['settingsfor']  = 'Settings for';
diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc
index 8777bb1..a2a9559 100644
--- a/program/localization/en_US/messages.inc
+++ b/program/localization/en_US/messages.inc
@@ -80,5 +80,10 @@
 $messages['nodeletelastidentity'] = 'You cannot delete this identity, it\'s your last one.';
 $messages['addsubfolderhint'] = 'This folder will be created as subfolder of the currently selected one';
 $messages['forbiddencharacter'] = 'Folder name contains a forbidden character';
+$messages['selectimportfile'] = 'Please select a file to upload';
+$messages['addresswriterror'] = 'The selected address book is not writeable';
+$messages['importwait'] = 'Importing, please wait...';
+$messages['importerror'] = 'Import failed! The uploaded file is not a valid vCard file.';
+$messages['importconfirm'] = '<b>Successfully imported $inserted contacts, $skipped existing entries skipped</b>:<p><em>$names</em></p>';
 
 ?>
\ No newline at end of file
diff --git a/program/steps/addressbook/import.inc b/program/steps/addressbook/import.inc
new file mode 100644
index 0000000..2291349
--- /dev/null
+++ b/program/steps/addressbook/import.inc
@@ -0,0 +1,179 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/steps/addressbook/import.inc                                  |
+ |                                                                       |
+ | This file is part of the RoundCube Webmail client                     |
+ | Copyright (C) 2008, RoundCube Dev. - Switzerland                      |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Import contacts from a vCard or CSV file                            |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id: $
+
+*/
+
+/**
+ * Handler function to display the import/upload form
+ */
+function rcmail_import_form($attrib)
+{
+  global $RCMAIL, $OUTPUT;
+  
+  $attrib += array('id' => "rcmImportForm");
+  
+  $upload = new html_inputfield(array('type' => 'file', 'name' => '_file', 'id' => 'rcmimportfile', 'size' => 40));
+  $form = html::p(null, html::label('rcmimportfile', rcube_label('importfromfile')) . html::br() . $upload->show());
+  
+  $check_replace = new html_checkbox(array('name' => '_replace', 'value' => 1, 'id' => 'rcmimportreplace'));
+  $form .= html::p(null, $check_replace->show(get_input_value('_replace', RCUBE_INPUT_GPC)) .
+    html::label('rcmimportreplace', rcube_label('importreplace')));
+  
+  $OUTPUT->add_label('selectimportfile','importwait');
+  $OUTPUT->add_gui_object('importform', $attrib['id']);
+  
+  $out = html::p(null, Q(rcube_label('importtext'), 'show'));
+  
+  $out .= $OUTPUT->form_tag(array(
+      'action' => $RCMAIL->url('import'),
+      'method' => 'post',
+      'enctype' => 'multipart/form-data') + $attrib,
+    $form);
+  
+  return $out;
+}
+
+
+/**
+ * Render the confirmation page for the import process
+ */
+function rcmail_import_confirm($attrib)
+{
+  global $IMPORT_STATS;
+  
+  $vars = get_object_vars($IMPORT_STATS);
+  $vars['names'] = join(', ', $IMPORT_STATS->names);
+  
+  return html::p($attrib, Q(rcube_label(array(
+    'name' => 'importconfirm',
+    'nr' => $IMORT_STATS->inserted,
+    'vars' => $vars,
+  )), 'show'));
+}
+
+
+/**
+ * Create navigation buttons for the current import step
+ */
+function rcmail_import_buttons($attrib)
+{
+  global $IMPORT_STATS, $OUTPUT;
+  
+  $attrib += array('type' => "input");
+  unset($attrib['name']);
+  
+  if (is_object($IMPORT_STATS)) {
+    $attrib['class'] = trim($attrib['class'] . ' mainaction');
+    $out = $OUTPUT->button(array('command' => "list", 'label' => "done") + $attrib);
+  }
+  else {
+    $out = $OUTPUT->button(array('command' => "list", 'label' => "cancel") + $attrib);
+    $out .= '&nbsp;';
+    $attrib['class'] = trim($attrib['class'] . ' mainaction');
+    $out .= $OUTPUT->button(array('command' => "import", 'label' => "import") + $attrib);
+  }
+  
+  return $out;
+}
+
+
+/** The import process **/
+
+$importstep = 'rcmail_import_form';
+
+if ($_FILES['_file']['tmp_name'] && is_uploaded_file($_FILES['_file']['tmp_name'])) {
+
+  $replace = (bool)get_input_value('_replace', RCUBE_INPUT_GPC);
+  $CONTACTS = $RCMAIL->get_address_book(null, true);
+
+  // let rcube_vcard do the hard work :-)
+  $vcards = rcube_vcard::import(file_get_contents($_FILES['_file']['tmp_name']));
+
+  // no vcards detected
+  if (!count($vcards)) {
+    $OUTPUT->show_message('importerror', 'error');
+  }
+  else if ($CONTACTS->readonly) {
+    $OUTPUT->show_message('addresswriterror', 'error');
+  }
+  else {
+    $IMPORT_STATS = new stdClass;
+    $IMPORT_STATS->names = array();
+    $IMPORT_STATS->count = count($vcards);
+    $IMPORT_STATS->inserted = $IMPORT_STATS->skipped = $IMPORT_STATS->errors = 0;
+    
+    if ($replace)
+      $CONTACTS->delete_all();
+    
+    foreach ($vcards as $vcard) {
+      $email = $vcard->email[0];
+      
+      if (!$replace) {
+        // compare e-mail address
+        $existing = $CONTACTS->search('email', $email, false, false);
+        if (!$existing->count) {  // compare display name
+          $existing = $CONTACTS->search('name', $vcard->displayname, false, false);
+        }
+        if ($existing->count) {
+          $IMPORT_STATS->skipped++;
+          continue;
+        }
+      }
+      
+      $success = $CONTACTS->insert(array(
+        'name' => $vcard->displayname,
+        'firstname' => $vcard->firstname,
+        'surname' => $vcard->surname,
+        'email' => $email,
+        'vcard' => $vcard->export(),
+      ));
+      
+      if ($success) {
+        $IMPORT_STATS->inserted++;
+        $IMPORT_STATS->names[] = $vcard->displayname;
+      }
+      else {
+        $IMPORT_STATS->errors++;
+      }
+    }
+
+    $importstep = 'rcmail_import_confirm';
+  }
+}
+else if ($err = $_FILES['_file']['error']) {
+  if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
+    $OUTPUT->show_message('filesizeerror', 'error', array('size' => show_bytes(parse_bytes(ini_get('upload_max_filesize')))));
+  }
+  else {
+    $OUTPUT->show_message('fileuploaderror', 'error');
+  }
+}
+
+
+$OUTPUT->set_pagetitle(rcube_label('importcontacts'));
+
+$OUTPUT->add_handlers(array(
+  'importstep' => $importstep,
+  'importnav' => 'rcmail_import_buttons',
+));
+
+// render page
+$OUTPUT->send('importcontacts');
+
+?>
\ No newline at end of file
diff --git a/skins/default/addresses.css b/skins/default/addresses.css
index 8209c1f..27612c3 100644
--- a/skins/default/addresses.css
+++ b/skins/default/addresses.css
@@ -30,7 +30,7 @@
   color: #333333;
 }
 
-#directorylist, #addresslist
+#directorylist, #addresslist, #importbox
 {
   position: absolute;
   top: 85px;
@@ -54,6 +54,20 @@
   width: 340px;
 }
 
+#importbox
+{
+  left: 20px;
+  right: 40px;
+  height: auto;
+  bottom: auto;
+  padding-bottom: 4ex;
+}
+
+#importbox a
+{
+  color: blue;
+}
+
 #directorylist ul
 {
   list-style: none;
diff --git a/skins/default/common.css b/skins/default/common.css
index 22c6542..c9a641f 100644
--- a/skins/default/common.css
+++ b/skins/default/common.css
@@ -76,7 +76,8 @@
 }
 
 input[type="checkbox"],
-input[type="radio"]
+input[type="radio"],
+input[type="file"]
 {
   border: 0;
   padding: 0;
@@ -99,6 +100,12 @@
   color: black;
 }
 
+input.mainaction
+{
+  font-weight: bold;
+  border: 1px solid #999;
+}
+
 img
 {
   behavior: url('skins/default/pngbehavior.htc');
diff --git a/skins/default/images/buttons/adr_import_act.png b/skins/default/images/buttons/adr_import_act.png
new file mode 100644
index 0000000..c600c71
--- /dev/null
+++ b/skins/default/images/buttons/adr_import_act.png
Binary files differ
diff --git a/skins/default/images/buttons/adr_import_pas.png b/skins/default/images/buttons/adr_import_pas.png
new file mode 100644
index 0000000..265d1b4
--- /dev/null
+++ b/skins/default/images/buttons/adr_import_pas.png
Binary files differ
diff --git a/skins/default/images/buttons/adr_import_sel.png b/skins/default/images/buttons/adr_import_sel.png
new file mode 100644
index 0000000..d78bbfd
--- /dev/null
+++ b/skins/default/images/buttons/adr_import_sel.png
Binary files differ
diff --git a/skins/default/templates/addcontact.html b/skins/default/templates/addcontact.html
index e52b8c7..70a796a 100644
--- a/skins/default/templates/addcontact.html
+++ b/skins/default/templates/addcontact.html
@@ -14,7 +14,7 @@
 
 <p><br />
 <input type="button" value="<roundcube:label name="cancel" />" class="button" onclick="history.back()" />&nbsp;
-<roundcube:button command="save" type="input" class="button" label="save" />
+<roundcube:button command="save" type="input" class="button" label="save mainaction" />
 </p>
 
 </form>
diff --git a/skins/default/templates/addressbook.html b/skins/default/templates/addressbook.html
index a97f584..6083ac6 100644
--- a/skins/default/templates/addressbook.html
+++ b/skins/default/templates/addressbook.html
@@ -15,7 +15,7 @@
 <roundcube:button command="add" imageSel="/images/buttons/add_contact_sel.png" imageAct="/images/buttons/add_contact_act.png" imagePas="/images/buttons/add_contact_pas.png" width="32" height="32" title="newcontact" />
 <roundcube:button command="delete" imageSel="/images/buttons/delete_sel.png" imageAct="/images/buttons/delete_act.png" imagePas="/images/buttons/delete_pas.png" width="32" height="32" title="deletecontact" />
 <roundcube:button command="compose" imageSel="/images/buttons/compose_sel.png" imageAct="/images/buttons/compose_act.png" imagePas="/images/buttons/compose_pas.png" width="32" height="32" title="composeto" />
-<roundcube:button command="print" imageSel="/images/buttons/print_sel.png" imageAct="/images/buttons/print_act.png" imagePas="/images/buttons/print_pas.png" width="32" height="32" title="print" />
+<roundcube:button command="import" imageSel="/images/buttons/download_sel.png" imageAct="/images/buttons/adr_import_act.png" imagePas="/images/buttons/adr_import_pas.png" width="32" height="32" title="importcontacts" />
 <roundcube:button command="export" imageSel="/images/buttons/download_sel.png" imageAct="/images/buttons/download_act.png" imagePas="/images/buttons/download_pas.png" width="32" height="32" title="export" />
 </div>
 
diff --git a/skins/default/templates/compose.html b/skins/default/templates/compose.html
index f3cc927..da8b457 100644
--- a/skins/default/templates/compose.html
+++ b/skins/default/templates/compose.html
@@ -133,7 +133,7 @@
 <table border="0" cellspacing="0" width="100%" summary=""><tbody>
 <tr>
 <td style="white-space: nowrap">
-<roundcube:button type="input" command="send" class="button" label="sendmessage" tabindex="8" />
+<roundcube:button type="input" command="send" class="button mainaction" label="sendmessage" tabindex="8" />
 <roundcube:button type="input" command="list" class="button" label="cancel" tabindex="9" />
 </td>
 <td style="text-align:center; white-space: nowrap">
diff --git a/skins/default/templates/editcontact.html b/skins/default/templates/editcontact.html
index bff81df..b92f709 100644
--- a/skins/default/templates/editcontact.html
+++ b/skins/default/templates/editcontact.html
@@ -14,7 +14,7 @@
 
 <p><br />
 <roundcube:button command="show" type="input" class="button" label="cancel" />&nbsp;
-<roundcube:button command="save" type="input" class="button" label="save" />
+<roundcube:button command="save" type="input" class="button mainaction" label="save" />
 </p>
 
 </form>
diff --git a/skins/default/templates/editidentity.html b/skins/default/templates/editidentity.html
index 04a3e8b..94c70e6 100644
--- a/skins/default/templates/editidentity.html
+++ b/skins/default/templates/editidentity.html
@@ -25,7 +25,7 @@
 
 <p><br />
 <roundcube:button command="delete" type="input" class="button" label="delete" />&nbsp;
-<roundcube:button command="save" type="input" class="button" label="save" />
+<roundcube:button command="save" type="input" class="button mainaction" label="save" />
 </p>
 </div>
 </div>
diff --git a/skins/default/templates/importcontacts.html b/skins/default/templates/importcontacts.html
new file mode 100644
index 0000000..35a8e41
--- /dev/null
+++ b/skins/default/templates/importcontacts.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+<link rel="stylesheet" type="text/css" href="/addresses.css" />
+</head>
+<body>
+
+<roundcube:include file="/includes/taskbar.html" />
+<roundcube:include file="/includes/header.html" />
+
+<div id="importbox">
+<div class="boxtitle"><roundcube:label name="importcontacts" /></div>
+
+<div style="padding-left:20px; width:48em">
+<roundcube:object name="importstep" />
+
+<p><br />
+<roundcube:object name="importnav" class="button" />
+</p>
+
+</div>
+</div>
+
+</body>
+</html>
diff --git a/skins/default/templates/login.html b/skins/default/templates/login.html
index a3089d7..9e1cc4a 100644
--- a/skins/default/templates/login.html
+++ b/skins/default/templates/login.html
@@ -39,7 +39,7 @@
 <form name="form" action="./" method="post">
 <roundcube:object name="loginform" form="form" autocomplete="off" />
 
-<p style="text-align:center;"><input type="submit" class="button" value="<roundcube:label name='login' />" /></p>
+<p style="text-align:center;"><input type="submit" class="button mainaction" value="<roundcube:label name='login' />" /></p>
 
 </form>
 </div>
diff --git a/skins/default/templates/settings.html b/skins/default/templates/settings.html
index dfb67e0..490d35d 100644
--- a/skins/default/templates/settings.html
+++ b/skins/default/templates/settings.html
@@ -17,7 +17,7 @@
 <div style="padding:15px">
 <roundcube:object name="userprefs">
 
-<p><roundcube:button command="save" type="input" class="button" label="save" /></p>
+<p><roundcube:button command="save" type="input" class="button mainaction" label="save" /></p>
 </div>
 </div>
 

--
Gitblit v1.9.1