From 0501b637a3177cce441166b5fcfe27c9bd9fbe0f Mon Sep 17 00:00:00 2001
From: thomascube <thomas@roundcube.net>
Date: Tue, 18 Jan 2011 13:00:57 -0500
Subject: [PATCH] Merge branch devel-addressbook (r4193:4382) back into trunk

---
 program/steps/addressbook/list.inc       |    2 
 skins/default/common.css                 |   20 
 skins/default/templates/contact.html     |    9 
 program/include/main.inc                 |   43 
 program/steps/mail/compose.inc           |    2 
 program/steps/addressbook/mailto.inc     |    6 
 skins/default/mail.css                   |   14 
 program/steps/addressbook/delete.inc     |    4 
 skins/default/addressbook.css            |  163 +++
 skins/default/iehacks.css                |    6 
 program/include/rcube_plugin.php         |   15 
 program/include/rcube_shared.inc         |   19 
 skins/default/templates/contactadd.html  |   22 
 program/steps/addressbook/show.inc       |  135 ++
 program/steps/addressbook/export.inc     |   25 
 program/steps/addressbook/copy.inc       |    2 
 program/include/html.php                 |    7 
 program/include/rcube_plugin_api.php     |  118 +-
 skins/default/templates/contactedit.html |   22 
 program/steps/addressbook/edit.inc       |  120 ++
 program/include/rcube_ldap.php           |  126 ++
 program/steps/addressbook/save.inc       |  168 +++
 program/steps/addressbook/func.inc       |  343 +++++++
 config/main.inc.php.dist                 |   36 
 program/include/rcube_browser.php        |    1 
 skins/default/images/contactpic.png      |    0 
 program/steps/addressbook/groups.inc     |    3 
 program/include/rcube_addressbook.php    |  117 ++
 program/steps/addressbook/import.inc     |    9 
 program/steps/mail/autocomplete.inc      |   20 
 program/include/rcmail.php               |  143 +++
 program/localization/en_US/messages.inc  |    3 
 program/include/rcube_vcard.php          |  172 +++
 program/localization/en_US/labels.inc    |   37 
 program/include/rcube_template.php       |    5 
 skins/default/functions.js               |   21 
 program/js/app.js                        |  332 ++++++-
 program/include/rcube_contacts.php       |  140 ++
 program/steps/addressbook/search.inc     |    8 
 39 files changed, 2,014 insertions(+), 424 deletions(-)

diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist
index 85f3a06..150b70d 100644
--- a/config/main.inc.php.dist
+++ b/config/main.inc.php.dist
@@ -308,6 +308,15 @@
 // mime magic database
 $rcmail_config['mime_magic'] = '/usr/share/misc/magic';
 
+// path to imagemagick identify binary
+$rcmail_config['im_identify_path'] = null;
+
+// path to imagemagick convert binary
+$rcmail_config['im_convert_path'] = null;
+
+// maximum size of uploaded contact photos in pixel
+$rcmail_config['contact_photo_size'] = 160;
+
 // Enable DNS checking for e-mail address validation
 $rcmail_config['email_dns_check'] = false;
 
@@ -345,6 +354,9 @@
 
 // use this format for today's date display (date or strftime format)
 $rcmail_config['date_today'] = 'H:i';
+
+// use this format for date display without time (date or strftime format)
+$rcmail_config['date_format'] = 'Y-m-d';
 
 // store draft message is this mailbox
 // leave blank if draft messages should not be stored
@@ -458,7 +470,6 @@
   // The login name is used to search for the DN to bind with
   'search_base_dn' => '',
   'search_filter'  => '',   // e.g. '(&(objectClass=posixAccount)(uid=%u))'
-
   'writable'      => false,   // Indicates if we can write to the LDAP directory or not.
   // If writable is true then these fields need to be populated:
   // LDAP_Object_Classes, required_fields, LDAP_rdn
@@ -467,10 +478,21 @@
   'LDAP_rdn'      => 'mail', // The RDN field that is used for new entries, this field needs to be one of the search_fields, the base of base_dn is appended to the RDN to insert into the LDAP directory.
   'ldap_version'  => 3,       // using LDAPv3
   'search_fields' => array('mail', 'cn'),  // fields to search in
-  'name_field'    => 'cn',    // this field represents the contact's name
-  'email_field'   => 'mail',  // this field represents the contact's e-mail
-  'surname_field' => 'sn',    // this field represents the contact's last name
-  'firstname_field' => 'gn',  // this field represents the contact's first name
+  'fieldmap' => array(      // mapping of contact fields to directory attributes
+    // Roundcube  => LDAP
+    'name'        => 'cn',
+    'surname'     => 'sn',
+    'firstname'   => 'givenName',
+    'email'       => 'mail',
+    'phone:home'  => 'homePhone',
+    'phone:work'  => 'telephoneNumber',
+    'phone:mobile' => 'mobile',
+    'street'      => 'street',
+    'zipcode'     => 'postalCode',
+    'locality'    => 'l',
+    'country'     => 'c',
+    'organization' => 'o',
+  ),
   'sort'          => 'cn',    // The field to sort the listing by.
   'scope'         => 'sub',   // search mode: sub|base|list
   'filter'        => '',      // used for basic listing (if not empty) and will be &'d with search queries. example: status=act
@@ -489,6 +511,10 @@
 // may need to do lengthy results building given overly-broad searches
 $rcmail_config['autocomplete_min_length'] = 1;
 
+// show address fields in this order
+// available placeholders: {street}, {locality}, {zipcode}, {country}, {region}
+$rcmail_config['address_template'] = '{street}<br/>{locality} {zipcode}<br/>{country} {region}';
+
 // ----------------------------------
 // USER PREFERENCES
 // ----------------------------------
diff --git a/program/include/html.php b/program/include/html.php
index a7599cd..ef7314e 100644
--- a/program/include/html.php
+++ b/program/include/html.php
@@ -71,6 +71,9 @@
      */
     public static function tag($tagname, $attrib = array(), $content = null, $allowed_attrib = null)
     {
+        if (is_string($attrib))
+            $attrib = array('class' => $attrib);
+
         $inline_tags = array('a','span','img');
         $suffix = $attrib['nl'] || ($content && $attrib['nl'] !== false && !in_array($tagname, $inline_tags)) ? "\n" : '';
 
@@ -147,7 +150,7 @@
             $attr = array('href' => $attr);
         }
         return self::tag('a', $attr, $cont, array_merge(self::$common_attrib,
-	    array('href','target','name','onclick','onmouseover','onmouseout','onmousedown','onmouseup')));
+	    array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup')));
     }
 
     /**
@@ -501,7 +504,7 @@
     protected $tagname = 'select';
     protected $options = array();
     protected $allowed = array('name','size','tabindex','autocomplete',
-	'multiple','onchange','disabled');
+	'multiple','onchange','disabled','rel');
     
     /**
      * Add a new option to this drop-down
diff --git a/program/include/main.inc b/program/include/main.inc
index 1ddb5f9..0815c25 100644
--- a/program/include/main.inc
+++ b/program/include/main.inc
@@ -799,7 +799,7 @@
 
       // format each col
       foreach ($a_show_cols as $col)
-        $table->add($col, Q($row_data[$col]));
+        $table->add($col, Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
         
       $c++;
     }
@@ -819,32 +819,43 @@
  * @return string HTML field definition
  */
 function rcmail_get_edit_field($col, $value, $attrib, $type='text')
-  {
-  $fname = '_'.$col;
-  $attrib['name'] = $fname;
+{
+  static $colcounts = array();
   
-  if ($type=='checkbox')
-    {
+  $fname = '_'.$col;
+  $attrib['name'] = $fname . ($attrib['array'] ? '[]' : '');
+  $attrib['class'] = trim($attrib['class'] . ' ff_' . $col);
+  
+  if ($type == 'checkbox') {
     $attrib['value'] = '1';
     $input = new html_checkbox($attrib);
-    }
-  else if ($type=='textarea')
-    {
+  }
+  else if ($type == 'textarea') {
     $attrib['cols'] = $attrib['size'];
     $input = new html_textarea($attrib);
-    }
-  else
+  }
+  else if ($type == 'select') {
+    $input = new html_select($attrib);
+    $input->add('---', '');
+    $input->add(array_values($attrib['options']), array_keys($attrib['options']));
+  }
+  else {
+    if ($attrib['type'] != 'text' && $attrib['type'] != 'hidden')
+        $attrib['type'] = 'text';
     $input = new html_inputfield($attrib);
+  }
 
   // use value from post
-  if (!empty($_POST[$fname]))
-    $value = get_input_value($fname, RCUBE_INPUT_POST,
-	    $type == 'textarea' && strpos($attrib['class'], 'mce_editor')!==false ? true : false);
+  if (isset($_POST[$fname])) {
+    $postvalue = get_input_value($fname, RCUBE_INPUT_POST,
+      $type == 'textarea' && strpos($attrib['class'], 'mce_editor')!==false ? true : false);
+    $value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
+  }
 
   $out = $input->show($value);
-         
+
   return $out;
-  }
+}
 
 
 /**
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index cdf959f..56181a7 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -114,7 +114,7 @@
   public $comm_path = './';
 
   private $texts;
-  private $books = array();
+  private $address_books = array();
   private $action_map = array();
 
 
@@ -331,6 +331,10 @@
     if ($plugin['instance'] instanceof rcube_addressbook) {
       $contacts = $plugin['instance'];
     }
+    // use existing instance
+    else if (isset($this->address_books[$id]) && is_a($this->address_books[$id], 'rcube_addressbook') && (!$writeable || !$this->address_books[$id]->readonly)) {
+      $contacts = $this->address_books[$id];
+    }
     else if ($id && $ldap_config[$id]) {
       $contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['imap_host']));
     }
@@ -351,8 +355,8 @@
     }
 
     // add to the 'books' array for shutdown function
-    if (!in_array($contacts, $this->books))
-      $this->books[] = $contacts;
+    if (!isset($this->address_books[$id]))
+      $this->address_books[$id] = $contacts;
 
     return $contacts;
   }
@@ -373,11 +377,12 @@
 
     // We are using the DB address book
     if ($abook_type != 'ldap') {
-      $contacts = new rcube_contacts($this->db, null);
+      if (!isset($this->address_books['0']))
+        $this->address_books['0'] = new rcube_contacts($this->db, $this->user->ID);
       $list['0'] = array(
-        'id' => 0,
+        'id' => '0',
         'name' => rcube_label('personaladrbook'),
-        'groups' => $contacts->groups,
+        'groups' => $this->address_books['0']->groups,
         'readonly' => false,
         'autocomplete' => in_array('sql', $autocomplete)
       );
@@ -398,14 +403,15 @@
     $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
     $list = $plugin['sources'];
 
-    if ($writeable && !empty($list)) {
-      foreach ($list as $idx => $item) {
-        if ($item['readonly']) {
+    foreach ($list as $idx => $item) {
+      // register source for shutdown function
+      if (!is_object($this->address_books[$item['id']]))
+        $this->address_books[$item['id']] = $item;
+      // remove from list if not writeable as requested
+      if ($writeable && $item['readonly'])
           unset($list[$idx]);
-        }
-      }
     }
-
+    
     return $list;
   }
 
@@ -1078,9 +1084,12 @@
     if (is_object($this->smtp))
       $this->smtp->disconnect();
 
-    foreach ($this->books as $book)
-      if (is_object($book))
+    foreach ($this->address_books as $book) {
+      if (!is_object($book))  // maybe an address book instance wasn't fetched using get_address_book() yet
+        $book = $this->get_address_book($book['id']);
+      if (is_a($book, 'rcube_addressbook'))
         $book->close();
+    }
 
     // before closing the database connection, write session data
     if ($_SERVER['REMOTE_ADDR'])
@@ -1307,6 +1316,112 @@
 
 
   /**
+   * Use imagemagick or GD lib to read image properties
+   *
+   * @param string Absolute file path
+   * @return mixed Hash array with image props like type, width, height or False on error
+   */
+  public static function imageprops($filepath)
+  {
+    $rcmail = rcmail::get_instance();
+    if ($cmd = $rcmail->config->get('im_identify_path', false)) {
+      list(, $type, $size) = explode(' ', strtolower(rcmail::exec($cmd. ' 2>/dev/null {in}', array('in' => $filepath))));
+      if ($size)
+        list($width, $height) = explode('x', $size);
+    }
+    else if (function_exists('getimagesize')) {
+      $imsize = @getimagesize($filepath);
+      $width = $imsize[0];
+      $height = $imsize[1];
+      $type = preg_replace('!image/!', '', $imsize['mime']);
+    }
+
+    return $type ? array('type' => $type, 'width' => $width, 'height' => $height) : false;
+  }
+
+
+  /**
+   * Convert an image to a given size and type using imagemagick (ensures input is an image)
+   *
+   * @param $p['in']  Input filename (mandatory)
+   * @param $p['out'] Output filename (mandatory)
+   * @param $p['size']  Width x height of resulting image, e.g. "160x60"
+   * @param $p['type']  Output file type, e.g. "jpg"
+   * @param $p['-opts'] Custom command line options to ImageMagick convert
+   * @return Success of convert as true/false
+   */
+  public static function imageconvert($p)
+  {
+    $result = false;
+    $rcmail = rcmail::get_instance();
+    $convert  = $rcmail->config->get('im_convert_path', false);
+    $identify = $rcmail->config->get('im_identify_path', false);
+    
+    // imagemagick is required for this
+    if (!$convert)
+        return false;
+
+    if (!(($imagetype = @exif_imagetype($p['in'])) && ($type = image_type_to_extension($imagetype, false))))
+      list(, $type) = explode(' ', strtolower(rcmail::exec($identify . ' 2>/dev/null {in}', $p))); # for things like eps
+
+    $type = strtr($type, array("jpeg" => "jpg", "tiff" => "tif", "ps" => "eps", "ept" => "eps"));
+    $p += array('type' => $type, 'types' => "bmp,eps,gif,jp2,jpg,png,svg,tif", 'quality' => 75);
+    $p['-opts'] = array('-resize' => $p['size'].'>') + (array)$p['-opts'];
+
+    if (in_array($type, explode(',', $p['types']))) # Valid type?
+      $result = rcmail::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace RGB -quality {quality} {-opts} {in} {type}:{out}', $p) === "";
+
+    return $result;
+  }
+
+
+  /**
+   * Construct shell command, execute it and return output as string.
+   * Keywords {keyword} are replaced with arguments
+   *
+   * @param $cmd Format string with {keywords} to be replaced
+   * @param $values (zero, one or more arrays can be passed)
+   * @return output of command. shell errors not detectable
+   */
+  public static function exec(/* $cmd, $values1 = array(), ... */)
+  {
+    $args = func_get_args();
+    $cmd = array_shift($args);
+    $values = $replacements = array();
+
+    // merge values into one array
+    foreach ($args as $arg)
+      $values += (array)$arg;
+
+    preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
+    foreach ($matches as $tags) {
+      list(, $tag, $option, $key) = $tags;
+      $parts = array();
+
+      if ($option) {
+        foreach ((array)$values["-$key"] as $key => $value) {
+          if ($value === true || $value === false || $value === null)
+            $parts[] = $value ? $key : "";
+          else foreach ((array)$value as $val)
+            $parts[] = "$key " . escapeshellarg($val);
+        }
+      }
+      else {
+        foreach ((array)$values[$key] as $value)
+          $parts[] = escapeshellarg($value);
+      }
+
+      $replacements[$tag] = join(" ", $parts);
+    }
+
+    // use strtr behaviour of going through source string once
+    $cmd = strtr($cmd, $replacements);
+    
+    return (string)shell_exec($cmd);
+  }
+
+
+  /**
    * Helper method to set a cookie with the current path and host settings
    *
    * @param string Cookie name
diff --git a/program/include/rcube_addressbook.php b/program/include/rcube_addressbook.php
index ae2a286..b9a31cc 100644
--- a/program/include/rcube_addressbook.php
+++ b/program/include/rcube_addressbook.php
@@ -5,7 +5,7 @@
  | program/include/rcube_addressbook.php                                 |
  |                                                                       |
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2006-2009, The Roundcube Dev Team                       |
+ | Copyright (C) 2006-2011, The Roundcube Dev Team                       |
  | Licensed under the GNU GPL                                            |
  |                                                                       |
  | PURPOSE:                                                              |
@@ -27,13 +27,22 @@
  */
 abstract class rcube_addressbook
 {
-    /** public properties */
-    var $primary_key;
-    var $groups = false;
-    var $readonly = true;
-    var $ready = false;
-    var $list_page = 1;
-    var $page_size = 10;
+    /** constants for error reporting **/
+    const ERROR_READ_ONLY = 1;
+    const ERROR_NO_CONNECTION = 2;
+    const ERROR_INCOMPLETE = 3;
+    const ERROR_SAVING = 4;
+    
+    /** public properties (mandatory) */
+    public $primary_key;
+    public $groups = false;
+    public $readonly = true;
+    public $ready = false;
+    public $list_page = 1;
+    public $page_size = 10;
+    public $coltypes = array('name' => array('limit'=>1), 'firstname' => array('limit'=>1), 'surname' => array('limit'=>1), 'email' => array('limit'=>1));
+    
+    protected $error;
 
     /**
      * Save a search string for future listings
@@ -55,6 +64,16 @@
     abstract function reset();
 
     /**
+     * Refresh saved search set after data has changed
+     *
+     * @return mixed New search set
+     */
+    function refresh_search()
+    {
+        return $this->get_search_set();
+    }
+
+    /**
      * List the current set of contact records
      *
      * @param  array  List of cols to show
@@ -69,9 +88,11 @@
      * @param array   List of fields to search in
      * @param string  Search value
      * @param boolean True if results are requested, False if count only
-     * @return Indexed list of contact records and 'count' value
+     * @param boolean True to skip the count query (select only)
+     * @param array   List of fields that cannot be empty
+     * @return object rcube_result_set List of contact records and 'count' value
      */
-    abstract function search($fields, $value, $strict=false, $select=true);
+    abstract function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array());
 
     /**
      * Count number of available contacts in database
@@ -96,6 +117,27 @@
      * @return mixed Result object with all record fields or False if not found
      */
     abstract function get_record($id, $assoc=false);
+
+    /**
+     * Returns the last error occured (e.g. when updating/inserting failed)
+     *
+     * @return array Hash array with the following fields: type, message
+     */
+    function get_error()
+    {
+      return $this->error;
+    }
+    
+    /**
+     * Setter for errors for internal use
+     *
+     * @param int Error type (one of this class' error constants)
+     * @param string Error message (name of a text label)
+     */
+    protected function set_error($type, $message)
+    {
+      $this->error = array('type' => $type, 'message' => $message);
+    }
 
     /**
      * Close connection to source
@@ -129,6 +171,8 @@
      * Create a new contact record
      *
      * @param array Assoziative array with save data
+     *  Keys:   Field name with optional section in the form FIELD:SECTION
+     *  Values: Field value. Can be either a string or an array of strings for multiple values
      * @param boolean True to check for duplicates first
      * @return mixed The created record ID on success, False on error
      */
@@ -138,10 +182,31 @@
     }
 
     /**
+     * Create new contact records for every item in the record set
+     *
+     * @param object rcube_result_set Recordset to insert
+     * @param boolean True to check for duplicates first
+     * @return array List of created record IDs
+     */
+    function insertMultiple($recset, $check=false)
+    {
+        $ids = array();
+        if (is_object($recset) && is_a($recset, rcube_result_set)) {
+            while ($row = $recset->next()) {
+                if ($insert = $this->insert($row, $check))
+                    $ids[] = $insert;
+            }
+        }
+        return $ids;
+    }
+
+    /**
      * Update a specific contact record
      *
      * @param mixed Record identifier
      * @param array Assoziative array with save data
+     *  Keys:   Field name with optional section in the form FIELD:SECTION
+     *  Values: Field value. Can be either a string or an array of strings for multiple values
      * @return boolean True on success, False on error
      */
     function update($id, $save_cols)
@@ -176,9 +241,10 @@
     /**
      * List all active contact groups of this source
      *
+     * @param string  Optional search string to match group name
      * @return array  Indexed list of contact groups, each a hash array
      */
-    function list_groups()
+    function list_groups($search = null)
     {
         /* empty for address books don't supporting groups */
         return array();
@@ -260,5 +326,34 @@
         /* empty for address books don't supporting groups */
         return array();
     }
+
+
+    /**
+     * Utility function to return all values of a certain data column
+     * either as flat list or grouped by subtype
+     *
+     * @param string Col name
+     * @param array  Record data array as used for saving
+     * @param boolean True to return one array with all values, False for hash array with values grouped by type
+     * @return array List of column values
+     */
+    function get_col_values($col, $data, $flat = false)
+    {
+        $out = array();
+        foreach ($data as $c => $values) {
+            if (strpos($c, $col) === 0) {
+                if ($flat) {
+                    $out = array_merge($out, (array)$values);
+                }
+                else {
+                    list($f, $type) = explode(':', $c);
+                    $out[$type] = array_merge((array)$out[$type], (array)$values);
+                }
+            }
+        }
+      
+        return $out;
+    }
+    
 }
 
diff --git a/program/include/rcube_browser.php b/program/include/rcube_browser.php
index 581284a..c5f1b8a 100644
--- a/program/include/rcube_browser.php
+++ b/program/include/rcube_browser.php
@@ -68,6 +68,7 @@
         $this->dom = ($this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7));
         $this->pngalpha = $this->mz || $this->safari || ($this->ie && $this->ver>=5.5) ||
             ($this->ie && $this->ver>=5 && $this->mac) || ($this->opera && $this->ver>=7) ? true : false;
+        $this->imgdata = !$this->ie;
     }
 }
 
diff --git a/program/include/rcube_contacts.php b/program/include/rcube_contacts.php
index 9761e68..4a4c1e2 100644
--- a/program/include/rcube_contacts.php
+++ b/program/include/rcube_contacts.php
@@ -47,13 +47,17 @@
     private $table_cols = array('name', 'email', 'firstname', 'surname', 'vcard');
 
     // public properties
-    var $primary_key = 'contact_id';
-    var $readonly = false;
-    var $groups = true;
-    var $list_page = 1;
-    var $page_size = 10;
-    var $group_id = 0;
-    var $ready = false;
+    public $primary_key = 'contact_id';
+    public $readonly = false;
+    public $groups = true;
+    public $list_page = 1;
+    public $page_size = 10;
+    public $group_id = 0;
+    public $ready = false;
+    public $coltypes = array('name', 'firstname', 'surname', 'middlename', 'prefix', 'suffix', 'nickname',
+      'jobtitle', 'organization', 'department', 'assistant', 'manager',
+      'gender', 'maidenname', 'spouse', 'email', 'phone', 'address',
+      'birthday', 'anniversary', 'website', 'im', 'notes', 'photo');
 
 
     /**
@@ -152,7 +156,7 @@
     /**
      * List the current set of contact records
      *
-     * @param  array   List of cols to show
+     * @param  array   List of cols to show, Null means all
      * @param  int     Only return this number of records, use negative values for tail
      * @param  boolean True to skip the count query (select only)
      * @return array  Indexed list of contact records, each a hash array
@@ -187,11 +191,21 @@
             $this->user_id,
             $this->group_id);
 
+        // determine whether we have to parse the vcard or if only db cols are requested
+        $read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols);
+        
         while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
             $sql_arr['ID'] = $sql_arr[$this->primary_key];
+
+            if ($read_vcard)
+                $sql_arr = $this->convert_db_data($sql_arr);
+            else
+                $sql_arr['email'] = preg_split('/,\s*/', $sql_arr['email']);
+            
             // make sure we have a name to display
             if (empty($sql_arr['name']))
-                $sql_arr['name'] = $sql_arr['email'];
+                $sql_arr['name'] = $sql_arr['email'][0];
+
             $this->result->add($sql_arr);
         }
 
@@ -222,7 +236,7 @@
      * @param boolean True if results are requested, False if count only
      * @param boolean True to skip the count query (select only)
      * @param array   List of fields that cannot be empty
-     * @return Indexed list of contact records and 'count' value
+     * @return object rcube_result_set Contact records and 'count' value
      */
     function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
     {
@@ -345,12 +359,12 @@
         );
 
         if ($sql_arr = $this->db->fetch_assoc()) {
-            $sql_arr['ID'] = $sql_arr[$this->primary_key];
+            $record = $this->convert_db_data($sql_arr);
             $this->result = new rcube_result_set(1);
-            $this->result->add($sql_arr);
+            $this->result->add($record);
         }
 
-        return $assoc && $sql_arr ? $sql_arr : $this->result;
+        return $assoc && $record ? $record : $this->result;
     }
 
 
@@ -389,21 +403,29 @@
      */
     function insert($save_data, $check=false)
     {
-        if (is_object($save_data) && is_a($save_data, rcube_result_set))
-            return $this->insert_recset($save_data, $check);
+        if (!is_array($save_data))
+            return false;
 
         $insert_id = $existing = false;
 
-        if ($check)
-            $existing = $this->search('email', $save_data['email'], true, false);
+        if ($check) {
+            foreach ($save_data as $col => $values) {
+                if (strpos($col, 'email') === 0) {
+                    foreach ((array)$values as $email) {
+                        if ($existing = $this->search('email', $email, true, false))
+                            break 2;
+                    }
+                }
+            }
+        }
 
+        $save_data = $this->convert_save_data($save_data);
         $a_insert_cols = $a_insert_values = array();
 
-        foreach ($this->table_cols as $col)
-            if (isset($save_data[$col])) {
-                $a_insert_cols[]   = $this->db->quoteIdentifier($col);
-                $a_insert_values[] = $this->db->quote($save_data[$col]);
-            }
+        foreach ($save_data as $col => $value) {
+            $a_insert_cols[]   = $this->db->quoteIdentifier($col);
+            $a_insert_values[] = $this->db->quote($value);
+        }
 
         if (!$existing->count && !empty($a_insert_cols)) {
             $this->db->query(
@@ -426,20 +448,6 @@
 
 
     /**
-     * Insert new contacts for each row in set
-     */
-    function insert_recset($result, $check=false)
-    {
-        $ids = array();
-        while ($row = $result->next()) {
-            if ($insert = $this->insert($row, $check))
-                $ids[] = $insert;
-        }
-        return $ids;
-    }
-
-
-    /**
      * Update a specific contact record
      *
      * @param mixed Record identifier
@@ -450,11 +458,12 @@
     {
         $updated = false;
         $write_sql = array();
+        $record = $this->get_record($id, true);
+        $save_cols = $this->convert_save_data($save_cols, $record);
 
-        foreach ($this->table_cols as $col)
-            if (isset($save_cols[$col]))
-                $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col),
-                    $this->db->quote($save_cols[$col]));
+        foreach ($save_cols as $col => $value) {
+            $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value));
+        }
 
         if (!empty($write_sql)) {
             $this->db->query(
@@ -468,10 +477,61 @@
             );
 
             $updated = $this->db->affected_rows();
+            $this->result = null;  // clear current result (from get_record())
         }
 
         return $updated;
     }
+    
+    
+    private function convert_db_data($sql_arr)
+    {
+        $record = array();
+        $record['ID'] = $sql_arr[$this->primary_key];
+        
+        if ($sql_arr['vcard']) {
+            unset($sql_arr['email']);
+            $vcard = new rcube_vcard($sql_arr['vcard']);
+            $record += $vcard->get_assoc() + $sql_arr;
+        }
+        else {
+            $record += $sql_arr;
+            $record['email'] = preg_split('/,\s*/', $record['email']);
+        }
+        
+        return $record;
+    }
+
+
+    private function convert_save_data($save_data, $record = array())
+    {
+        $out = array();
+
+        // copy values into vcard object
+        $vcard = new rcube_vcard($record['vcard'] ? $record['vcard'] : $save_data['vcard']);
+        $vcard->reset();
+        foreach ($save_data as $key => $values) {
+            list($field, $section) = explode(':', $key);
+            foreach ((array)$values as $value) {
+                if (isset($value))
+                    $vcard->set($field, $value, $section);
+            }
+        }
+        $out['vcard'] = $vcard->export();
+
+        foreach ($this->table_cols as $col) {
+            $key = $col;
+            if (!isset($save_data[$key]))
+                $key .= ':home';
+            if (isset($save_data[$key]))
+                $out[$col] = is_array($save_data[$key]) ? join(',', $save_data[$key]) : $save_data[$key];
+        }
+
+        // save all e-mails in database column
+        $out['email'] = join(", ", $vcard->email);
+
+        return $out;
+    }
 
 
     /**
diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php
index 17d049e..7ea22eb 100644
--- a/program/include/rcube_ldap.php
+++ b/program/include/rcube_ldap.php
@@ -26,23 +26,24 @@
  */
 class rcube_ldap extends rcube_addressbook
 {
-  var $conn;
-  var $prop = array();
-  var $fieldmap = array();
+  protected $conn;
+  protected $prop = array();
+  protected $fieldmap = array();
 
-  var $filter = '';
-  var $result = null;
-  var $ldap_result = null;
-  var $sort_col = '';
-  var $mail_domain = '';
-  var $debug = false;
+  protected $filter = '';
+  protected $result = null;
+  protected $ldap_result = null;
+  protected $sort_col = '';
+  protected $mail_domain = '';
+  protected $debug = false;
 
   /** public properties */
-  var $primary_key = 'ID';
-  var $readonly = true;
-  var $list_page = 1;
-  var $page_size = 10;
-  var $ready = false;
+  public $primary_key = 'ID';
+  public $readonly = true;
+  public $list_page = 1;
+  public $page_size = 10;
+  public $ready = false;
+  public $coltypes = array();
 
 
   /**
@@ -57,9 +58,37 @@
   {
     $this->prop = $p;
 
-    foreach ($p as $prop => $value)
-      if (preg_match('/^(.+)_field$/', $prop, $matches))
-        $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
+    // fieldmap property is given
+    if (is_array($p['fieldmap'])) {
+      foreach ($p['fieldmap'] as $rf => $lf)
+        $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
+    }
+    else {
+      // read deprecated *_field properties to remain backwards compatible
+      foreach ($p as $prop => $value)
+        if (preg_match('/^(.+)_field$/', $prop, $matches))
+          $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
+    }
+    
+    // use fieldmap to advertise supported coltypes to the application
+    foreach ($this->fieldmap as $col => $lf) {
+      list($col, $type) = explode(':', $col);
+      if (!is_array($this->coltypes[$col])) {
+        $subtypes = $type ? array($type) : null;
+        $this->coltypes[$col] = array('limit' => 2, 'subtypes' => $subtypes);
+      }
+      else if ($type) {
+        $this->coltypes[$col]['subtypes'][] = $type;
+        $this->coltypes[$col]['limit']++;
+      }
+      if ($type && !$this->fieldmap[$col])
+        $this->fieldmap[$col] = $lf;
+    }
+    
+    if ($this->fieldmap['street'] && $this->fieldmap['locality'])
+      $this->coltypes['address'] = array('limit' => 1);
+    else if ($this->coltypes['address'])
+      $this->coltypes['address'] = array('type' => 'textarea', 'childs' => null, 'limit' => 1, 'size' => 40);
 
     // make sure 'required_fields' is an array
     if (!is_array($this->prop['required_fields']))
@@ -455,7 +484,7 @@
 
       if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
       {
-        $this->_debug("S: OK");
+        $this->_debug("S: OK"/* . print_r($rec, true)*/);
 
         $rec = array_change_key_case($rec, CASE_LOWER);
 
@@ -482,8 +511,10 @@
     // Map out the column names to their LDAP ones to build the new entry.
     $newentry = array();
     $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
-    foreach ($save_cols as $col => $val) {
-      $fld = $this->_map_field($col);
+    foreach ($this->fieldmap as $col => $fld) {
+      $val = $save_cols[$col];
+      if (is_array($val))
+        $val = array_filter($val);  // remove empty entries
       if ($fld && $val) {
         // The field does exist, add it to the entry.
         $newentry[$fld] = $val;
@@ -491,23 +522,29 @@
     } // end foreach
 
     // Verify that the required fields are set.
-    // We know that the email address is required as a default of rcube, so
-    // we will default its value into any unfilled required fields.
     foreach ($this->prop['required_fields'] as $fld) {
+      $missing = null;
       if (!isset($newentry[$fld])) {
-        $newentry[$fld] = $newentry[$this->_map_field('email')];
-      } // end if
-    } // end foreach
+        $missing[] = $fld;
+      }
+    }
+    
+    // abort process if requiered fields are missing
+    // TODO: generate message saying which fields are missing
+    if ($missing) {
+      $this->set_error(self::ERROR_INCOMPLETE, 'formincomplete');
+      return false;
+    }
 
     // Build the new entries DN.
-    $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap::quote_string($newentry[$this->prop['LDAP_rdn']], true)
-      .','.$this->prop['base_dn'];
+    $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->prop['base_dn'];
 
     $this->_debug("C: Add [dn: $dn]: ".print_r($newentry, true));
 
     $res = ldap_add($this->conn, $dn, $newentry);
     if ($res === FALSE) {
       $this->_debug("S: ".ldap_error($this->conn));
+      $this->set_error(self::ERROR_SAVING, 'errorsaving');
       return false;
     } // end if
 
@@ -533,8 +570,8 @@
     $newdata = array();
     $replacedata = array();
     $deletedata = array();
-    foreach ($save_cols as $col => $val) {
-      $fld = $this->_map_field($col);
+    foreach ($this->fieldmap as $col => $fld) {
+      $val = $save_cols[$col];
       if ($fld) {
         // The field does exist compare it to the ldap record.
         if ($record[$col] != $val) {
@@ -566,6 +603,7 @@
       $this->_debug("C: Delete [dn: $dn]: ".print_r($deletedata, true));
       if (!ldap_mod_del($this->conn, $dn, $deletedata)) {
         $this->_debug("S: ".ldap_error($this->conn));
+        $this->set_error(self::ERROR_SAVING, 'errorsaving');
         return false;
       }
       $this->_debug("S: OK");
@@ -575,11 +613,11 @@
       // Handle RDN change
       if ($replacedata[$this->prop['LDAP_rdn']]) {
         $newdn = $this->prop['LDAP_rdn'].'='
-	  .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
-	  .','.$this->prop['base_dn']; 
+          .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
+          .','.$this->prop['base_dn'];
         if ($dn != $newdn) {
           $newrdn = $this->prop['LDAP_rdn'].'='
-	    .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
+            .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
           unset($replacedata[$this->prop['LDAP_rdn']]);
         }
       }
@@ -589,7 +627,7 @@
         if (!ldap_mod_replace($this->conn, $dn, $replacedata)) {
           $this->_debug("S: ".ldap_error($this->conn));
           return false;
-	}
+        }
         $this->_debug("S: OK");
       } // end if
     } // end if
@@ -599,6 +637,7 @@
       $this->_debug("C: Add [dn: $dn]: ".print_r($newdata, true));
       if (!ldap_mod_add($this->conn, $dn, $newdata)) {
         $this->_debug("S: ".ldap_error($this->conn));
+        $this->set_error(self::ERROR_SAVING, 'errorsaving');
         return false;
       }
       $this->_debug("S: OK");
@@ -638,6 +677,7 @@
       $res = ldap_delete($this->conn, $dn);
       if ($res === FALSE) {
         $this->_debug("S: ".ldap_error($this->conn));
+        $this->set_error(self::ERROR_SAVING, 'errorsaving');
         return false;
       } // end if
       $this->_debug("S: OK");
@@ -679,8 +719,6 @@
    */
   private function _ldap2result($rec)
   {
-    global $RCMAIL;
-
     $out = array();
     
     if ($rec['dn'])
@@ -688,11 +726,17 @@
     
     foreach ($this->fieldmap as $rf => $lf)
     {
-      if ($rec[$lf]['count']) {
-        if ($rf == 'email' && $this->mail_domain && !strpos($rec[$lf][0], '@'))
-          $out[$rf] = sprintf('%s@%s', $rec[$lf][0], $this->mail_domain);
+      for ($i=0; $i < $rec[$lf]['count']; $i++) {
+        if (!($value = $rec[$lf][$i]))
+          continue;
+        if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
+          $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
+        else if (in_array($rf, array('street','zipcode','locality','country','region')))
+          $out['address'][$i][$rf] = $value;
+        else if ($rec[$lf]['count'] > 1)
+          $out[$rf][] = $value;
         else
-          $out[$rf] = $rec[$lf][0];
+          $out[$rf] = $value;
       }
     }
     
@@ -741,6 +785,10 @@
    */
   function quote_string($str, $dn=false)
   {
+    // take firt entry if array given
+    if (is_array($str))
+      $str = reset($str);
+    
     if ($dn)
       $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
         '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
diff --git a/program/include/rcube_plugin.php b/program/include/rcube_plugin.php
index 85ed774..0979137 100644
--- a/program/include/rcube_plugin.php
+++ b/program/include/rcube_plugin.php
@@ -83,7 +83,20 @@
    * Initialization method, needs to be implemented by the plugin itself
    */
   abstract function init();
-  
+
+
+  /**
+   * Attempt to load the given plugin which is required for the current plugin
+   *
+   * @param string Plugin name
+   * @return boolean True on success, false on failure
+   */
+  public function require_plugin($plugin_name)
+  {
+    return $this->api->load_plugin($plugin_name);
+  }
+
+
   /**
    * Load local config file from plugins directory.
    * The loaded values are patched over the global configuration.
diff --git a/program/include/rcube_plugin_api.php b/program/include/rcube_plugin_api.php
index 0f7ab3f..54a9a8b 100644
--- a/program/include/rcube_plugin_api.php
+++ b/program/include/rcube_plugin_api.php
@@ -109,42 +109,9 @@
     $this->output = $rcmail->output;
     $this->config = $rcmail->config;
 
-    $plugins_dir = dir($this->dir);
-    $plugins_dir = unslashify($plugins_dir->path);
     $plugins_enabled = (array)$rcmail->config->get('plugins', array());
-
     foreach ($plugins_enabled as $plugin_name) {
-      $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
-
-      if (file_exists($fn)) {
-        include($fn);
-
-        // instantiate class if exists
-        if (class_exists($plugin_name, false)) {
-          $plugin = new $plugin_name($this);
-          // check inheritance...
-          if (is_subclass_of($plugin, 'rcube_plugin')) {
-            // ... task, request type and framed mode
-            if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $rcmail->task))
-                && (!$plugin->noajax || is_a($this->output, 'rcube_template'))
-                && (!$plugin->noframe || empty($_REQUEST['_framed']))
-            ) {
-              $plugin->init();
-              $this->plugins[] = $plugin;
-            }
-          }
-        }
-        else {
-          raise_error(array('code' => 520, 'type' => 'php',
-	    'file' => __FILE__, 'line' => __LINE__,
-	    'message' => "No plugin class $plugin_name found in $fn"), true, false);
-        }
-      }
-      else {
-        raise_error(array('code' => 520, 'type' => 'php',
-	  'file' => __FILE__, 'line' => __LINE__,
-	  'message' => "Failed to load plugin file $fn"), true, false);
-      }
+      $this->load_plugin($plugin_name);
     }
     
     // check existance of all required core plugins
@@ -158,31 +125,14 @@
       }
       
       // load required core plugin if no derivate was found
-      if (!$loaded) {
-        $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
+      if (!$loaded)
+        $loaded = $this->load_plugin($plugin_name);
 
-        if (file_exists($fn)) {
-          include_once($fn);
-          
-          if (class_exists($plugin_name, false)) {
-            $plugin = new $plugin_name($this);
-            // check inheritance
-            if (is_subclass_of($plugin, 'rcube_plugin')) {
-	      if (!$plugin->task || preg_match('/('.$plugin->task.')/i', $rcmail->task)) {
-                $plugin->init();
-                $this->plugins[] = $plugin;
-              }
-	      $loaded = true;
-            }
-          }
-        }
-      }
-      
       // trigger fatal error if still not loaded
       if (!$loaded) {
         raise_error(array('code' => 520, 'type' => 'php',
-	  'file' => __FILE__, 'line' => __LINE__,
-	  'message' => "Requried plugin $plugin_name was not loaded"), true, true);
+          'file' => __FILE__, 'line' => __LINE__,
+          'message' => "Requried plugin $plugin_name was not loaded"), true, true);
       }
     }
 
@@ -191,6 +141,64 @@
     
     // maybe also register a shudown function which triggers shutdown functions of all plugin objects
   }
+
+
+  /**
+   * Load the specified plugin
+   *
+   * @param string Plugin name
+   * @return boolean True on success, false if not loaded or failure
+   */
+  public function load_plugin($plugin_name)
+  {
+    static $plugins_dir;
+    
+    $rcmail = rcmail::get_instance();
+    
+    if (!$plugins_dir) {
+      $dir = dir($this->dir);
+      $plugins_dir = unslashify($dir->path);
+    }
+    
+    // plugin already loaded
+    if ($this->plugins[$plugin_name] || class_exists($plugin_name, false))
+      return true;
+    
+    $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
+
+    if (file_exists($fn)) {
+      include($fn);
+
+      // instantiate class if exists
+      if (class_exists($plugin_name, false)) {
+        $plugin = new $plugin_name($this);
+        // check inheritance...
+        if (is_subclass_of($plugin, 'rcube_plugin')) {
+          // ... task, request type and framed mode
+          if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $rcmail->task)) /*
+              && (!$plugin->noajax || is_a($rcmail->output, 'rcube_template'))
+              && (!$plugin->noframe || empty($_REQUEST['_framed']))*/
+          ) {
+            $plugin->init();
+            $this->plugins[$plugin_name] = $plugin;
+          }
+          return true;
+        }
+      }
+      else {
+        raise_error(array('code' => 520, 'type' => 'php',
+          'file' => __FILE__, 'line' => __LINE__,
+          'message' => "No plugin class $plugin_name found in $fn"), true, false);
+      }
+    }
+    else {
+      raise_error(array('code' => 520, 'type' => 'php',
+        'file' => __FILE__, 'line' => __LINE__,
+        'message' => "Failed to load plugin file $fn"), true, false);
+    }
+    
+    return false;
+  }
   
   
   /**
diff --git a/program/include/rcube_shared.inc b/program/include/rcube_shared.inc
index 36c832e..83eefd6 100644
--- a/program/include/rcube_shared.inc
+++ b/program/include/rcube_shared.inc
@@ -485,6 +485,25 @@
     return $mime_type;
 }
 
+
+/**
+ * Detect image type of the given binary data by checking magic numbers
+ *
+ * @param string  Binary file content
+ * @return string Detected mime-type or jpeg as fallback
+ */
+function rc_image_content_type($data)
+{
+    $type = 'jpeg';
+    if      (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png';
+    else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif';
+    else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
+//  else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';
+
+    return 'image/' . $type;
+}
+
+
 /**
  * A method to guess encoding of a string.
  *
diff --git a/program/include/rcube_template.php b/program/include/rcube_template.php
index 2aa16f0..1d1a95b 100755
--- a/program/include/rcube_template.php
+++ b/program/include/rcube_template.php
@@ -996,8 +996,11 @@
         $attrib['action'] = './';
 
         // we already have a <form> tag
-        if ($attrib['form'])
+        if ($attrib['form']) {
+            if ($this->framed || !empty($_REQUEST['_framed']))
+                $hidden->add(array('name' => '_framed', 'value' => '1'));
             return $hidden->show() . $content;
+        }
         else
             return $this->form_tag($attrib, $hidden->show() . $content);
     }
diff --git a/program/include/rcube_vcard.php b/program/include/rcube_vcard.php
index 70ea55c..1187224 100644
--- a/program/include/rcube_vcard.php
+++ b/program/include/rcube_vcard.php
@@ -5,7 +5,7 @@
  | program/include/rcube_vcard.php                                       |
  |                                                                       |
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2008-2009, The Roundcube Dev Team                       |
+ | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
  | Licensed under the GNU GPL                                            |
  |                                                                       |
  | PURPOSE:                                                              |
@@ -33,6 +33,24 @@
     'FN' => array(),
     'N' => array(array('','','','','')),
   );
+  private $fieldmap = array(
+    'phone'    => 'TEL',
+    'birthday' => 'BDAY',
+    'website'  => 'URL',
+    'notes'    => 'NOTE',
+    'email'    => 'EMAIL',
+    'address'  => 'ADR',
+    'gender'      => 'X-GENDER',
+    'maidenname'  => 'X-MAIDENNAME',
+    'anniversary' => 'X-ANNIVERSARY',
+    'assistant'   => 'X-ASSISTANT',
+    'manager'     => 'X-MANAGER',
+    'spouse'      => 'X-SPOUSE',
+  );
+  private $typemap = array('iPhone' => 'mobile', 'CELL' => 'mobile');
+  private $phonetypemap = array('HOME1' => 'HOME', 'BUSINESS1' => 'WORK', 'BUSINESS2' => 'WORK2', 'WORKFAX' => 'BUSINESSFAX');
+  private $addresstypemap = array('BUSINESS' => 'WORK');
+  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');
 
   public $business = false;
   public $displayname;
@@ -107,11 +125,91 @@
 
 
   /**
+   * Return vCard data as associative array to be unsed in Roundcube address books
+   *
+   * @return array Hash array with key-value pairs
+   */
+  public function get_assoc()
+  {
+    $out = array('name' => $this->displayname);
+    $typemap = $this->typemap;
+    
+    // copy name fields to output array
+    foreach (array('firstname','surname','middlename','nickname','organization') as $col)
+      $out[$col] = $this->$col;
+    
+    $out['prefix'] = $this->raw['N'][0][3];
+    $out['suffix'] = $this->raw['N'][0][4];
+    
+    // convert from raw vcard data into associative data for Roundcube
+    foreach (array_flip($this->fieldmap) as $tag => $col) {
+      foreach ((array)$this->raw[$tag] as $i => $raw) {
+        if (is_array($raw)) {
+          $k = -1;
+          $key = $col;
+          $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]);
+          while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref'))
+            $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]);
+          if ($subtype)
+            $key .= ':' . $subtype;
+
+          // split ADR values into assoc array
+          if ($tag == 'ADR') {
+            list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw;
+            $out[$key][] = $value;
+          }
+          else
+            $out[$key][] = $raw[0];
+        }
+        else {
+          $out[$col][] = $raw;
+        }
+      }
+    }
+    
+    // handle special IM fields as used by Apple
+    foreach ($this->immap as $tag => $type) {
+      foreach ((array)$this->raw[$tag] as $i => $raw) {
+        $out['im:'.$type][] = $raw[0];
+      }
+    }
+    
+    // copy photo data
+    if ($this->raw['PHOTO'])
+      $out['photo'] = $this->raw['PHOTO'][0][0];
+    
+    return $out;
+  }
+
+
+  /**
    * Convert the data structure into a vcard 3.0 string
    */
   public function export()
   {
     return self::rfc2425_fold(self::vcard_encode($this->raw));
+  }
+  
+  
+  /**
+   * Clear the given fields in the loaded vcard data
+   *
+   * @param array List of field names to be reset
+   */
+  public function reset($fields = null)
+  {
+    if (!$fields)
+      $fields = array_merge(array_values($this->fieldmap), array_keys($this->immap), array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY'));
+    
+    foreach ($fields as $f)
+      unset($this->raw[$f]);
+
+    if (!$this->raw['N'])
+      $this->raw['N'] = array(array('','','','',''));
+    if (!$this->raw['FN'])
+      $this->raw['FN'] = array();
+      
+    $this->email = array();
   }
 
 
@@ -120,24 +218,40 @@
    *
    * @param string Field name
    * @param string Field value
-   * @param string Section name
+   * @param string Type/section name
    */
-  public function set($field, $value, $section = 'HOME')
+  public function set($field, $value, $type = 'HOME')
   {
+    $field = strtolower($field);
+    $type = strtoupper($type);
+    $typemap = array_flip($this->typemap);
+    
     switch ($field) {
       case 'name':
       case 'displayname':
         $this->raw['FN'][0][0] = $value;
         break;
         
+      case 'surname':
+        $this->raw['N'][0][0] = $value;
+        break;
+        
       case 'firstname':
         $this->raw['N'][0][1] = $value;
         break;
         
-      case 'surname':
-        $this->raw['N'][0][0] = $value;
+      case 'middlename':
+        $this->raw['N'][0][2] = $value;
         break;
-      
+        
+      case 'prefix':
+        $this->raw['N'][0][3] = $value;
+        break;
+        
+      case 'suffix':
+        $this->raw['N'][0][4] = $value;
+        break;
+        
       case 'nickname':
         $this->raw['NICKNAME'][0][0] = $value;
         break;
@@ -146,13 +260,47 @@
         $this->raw['ORG'][0][0] = $value;
         break;
         
+      case 'photo':
+        $encoded = !preg_match('![^a-z0-9/=+-]!i', $value);
+        $this->raw['PHOTO'][0] = array(0 => $encoded ? $value : base64_encode($value), 'BASE64' => true);
+        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;
+        $this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type)));
+        $this->email[] = $value;
+        break;
+        
+      case 'im':
+        // save IM subtypes into extension fields
+        $typemap = array_flip($this->immap);
+        if ($field = $typemap[strtolower($type)])
+          $this->raw[$field][] = array(0 => $value);
+        break;
+
+      case 'birthday':
+        if ($val = @strtotime($value))
+          $this->raw['BDAY'][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));
+        break;
+
+      case 'address':
+        if ($this->addresstypemap[$type])
+          $type = $this->addresstypemap[$type];
+
+        $value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']);
+
+        // fall through if not empty
+        if (!strlen(join('', $value)))
+          break;
+
+      default:
+        if ($field == 'phone' && $this->phonetypemap[$type])
+          $type = $this->phonetypemap[$type];
+
+        if (($tag = $this->fieldmap[$field]) && (is_array($value) || strlen($value))) {
+          $index = count($this->raw[$tag]);
+          $this->raw[$tag][$index] = (array)$value;
+          if ($type)
+            $this->raw[$tag][$index]['type'] = array(($typemap[$type] ? $typemap[$type] : $type));
         }
         break;
     }
diff --git a/program/js/app.js b/program/js/app.js
index b21435e..e7af013 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -319,7 +319,20 @@
 
         if ((this.env.action=='add' || this.env.action=='edit') && this.gui_objects.editform) {
           this.enable_command('save', true);
-          $("input[type='text']").first().select();
+          this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
+          this.enable_command('delete-photo', this.env.coltypes.photo && this.env.action == 'edit');
+
+          for (var col in this.env.coltypes)
+            this.init_edit_field(col, null);
+
+          $('.contactfieldgroup .row a.deletebutton').click(function(){ ref.delete_edit_field(this); return false });
+
+          $('select.addfieldmenu').change(function(e){
+            ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
+            this.selectedIndex = 0;
+          });
+
+          $("input[type='text']").first().focus();
         }
         else if (this.gui_objects.qsearchbox) {
           this.enable_command('search', 'reset-search', 'moveto', true);
@@ -639,6 +652,9 @@
               input_email.focus();
               break;
             }
+            
+            // clear empty input fields
+            $('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; });
           }
 
           this.gui_objects.editform.submit();
@@ -996,12 +1012,16 @@
 
       case 'export':
         if (this.contact_list.rowcount > 0) {
-          var add_url = (this.env.source ? '_source='+urlencode(this.env.source)+'&' : '');
-          if (this.env.search_request)
-            add_url += '_search='+this.env.search_request;
-
-          this.goto_url('export', add_url);
+          this.goto_url('export', { _source:this.env.source, _gid:this.env.group, _search:this.env.search_request });
         }
+        break;
+        
+      case 'upload-photo':
+        this.upload_contact_photo(props);
+        break;
+
+      case 'delete-photo':
+        this.replace_contact_photo('-del-');
         break;
 
       // user settings commands
@@ -1158,7 +1178,7 @@
 
   this.is_framed = function()
   {
-    return (this.env.framed && parent.rcmail);
+    return (this.env.framed && parent.rcmail && parent.rcmail != this && parent.rcmail.command);
   };
 
 
@@ -3177,27 +3197,7 @@
 
     // create hidden iframe and post upload form
     if (send) {
-      var ts = new Date().getTime();
-      var frame_name = 'rcmupload'+ts;
-
-      // have to do it this way for IE
-      // otherwise the form will be posted to a new window
-      if (document.all) {
-        var html = '<iframe name="'+frame_name+'" src="program/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>';
-        document.body.insertAdjacentHTML('BeforeEnd',html);
-      }
-      else { // for standards-compilant browsers
-        var frame = document.createElement('iframe');
-        frame.name = frame_name;
-        frame.style.border = 'none';
-        frame.style.width = 0;
-        frame.style.height = 0;
-        frame.style.visibility = 'hidden';
-        document.body.appendChild(frame);
-      }
-
-      // handle upload errors, parsing iframe content in onload
-      $(frame_name).bind('load', {ts:ts}, function(e) {
+      this.async_upload_form(form, 'upload', function(e) {
         var d, content = '';
         try {
           if (this.contentDocument) {
@@ -3217,11 +3217,6 @@
         if (bw.opera)
           rcmail.env.uploadframe = e.data.ts;
       });
-
-      form.target = frame_name;
-      form.action = this.env.comm_path+'&_action=upload&_uploadid='+ts;
-      form.setAttribute('enctype', 'multipart/form-data');
-      form.submit();
 
       // display upload indicator and cancel button
       var content = this.get_label('uploading');
@@ -3979,6 +3974,165 @@
   };
 
 
+  this.init_edit_field = function(col, elem)
+  {
+    if (!elem)
+      elem = $('.ff_' + col);
+    
+    elem.focus(function(){ ref.focus_textfield(this); })
+      .blur(function(){ ref.blur_textfield(this); })
+      .each(function(){ this._placeholder = ref.env.coltypes[col].label; ref.blur_textfield(this); });
+  };
+
+  this.insert_edit_field = function(col, section, menu)
+  {
+    // just make pre-defined input field visible
+    var elem = $('#ff_'+col);
+    if (elem.length) {
+      elem.show().focus();
+      $(menu).children('option[value="'+col+'"]').attr('disabled', true);
+    }
+    else {
+      var lastelem = $('.ff_'+col),
+        appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
+      
+      if (!appendcontainer.length)
+        appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col).insertAfter($('#contactsection'+section+' .contactfieldgroup').last());
+
+      if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
+        var input, colprop = this.env.coltypes[col],
+          row = $('<div>').addClass('row'),
+          cell = $('<div>').addClass('contactfieldcontent data'),
+          label = $('<div>').addClass('contactfieldlabel label');
+          
+        if (colprop.subtypes_select)
+          label.html(colprop.subtypes_select);
+        else
+          label.html(colprop.label);
+
+        var name_suffix = colprop.limit != 1 ? '[]' : '';
+        if (colprop.type == 'text' || colprop.type == 'date') {
+          input = $('<input>')
+            .addClass('ff_'+col)
+            .attr('type', 'text')
+            .attr('name', '_'+col+name_suffix)
+            .attr('size', colprop.size)
+            .appendTo(cell);
+
+          this.init_edit_field(col, input);
+        }
+        else if (colprop.type == 'composite') {
+          var childcol, cp, first;
+          for (var childcol in colprop.childs) {
+            cp = colprop.childs[childcol];
+            input = $('<input>')
+              .addClass('ff_'+childcol)
+              .attr('type', 'text')
+              .attr('name', '_'+childcol+name_suffix)
+              .attr('size', cp.size)
+              .appendTo(cell);
+            cell.append(" ");
+            this.init_edit_field(childcol, input);
+            if (!first) first = input;
+          }
+          input = first;  // set focus to the first of this composite fields
+        }
+        else if (colprop.type == 'select') {
+          input = $('<select>')
+            .addClass('ff_'+col)
+            .attr('name', '_'+col+name_suffix)
+            .appendTo(cell);
+          
+          var options = input.attr('options');
+          options[options.length] = new Option('---', '');
+          if (colprop.options)
+            $.each(colprop.options, function(i, val){ options[options.length] = new Option(val, i); });
+        }
+
+        if (input) {
+          var delbutton = $('<a href="#del"></a>')
+            .addClass('contactfieldbutton deletebutton')
+            .attr('title', this.get_label('delete'))
+            .attr('rel', col)
+            .html(this.env.delbutton)
+            .click(function(){ ref.delete_edit_field(this); return false })
+            .appendTo(cell);
+          
+          row.append(label).append(cell).appendTo(appendcontainer.show());
+          input.first().focus();
+          
+          // disable option if limit reached
+          if (!colprop.count) colprop.count = 0;
+          if (++colprop.count == colprop.limit && colprop.limit)
+            $(menu).children('option[value="'+col+'"]').attr('disabled', true);
+        }
+      }
+    }
+  };
+
+  this.delete_edit_field = function(elem)
+  {
+    var col = $(elem).attr('rel'),
+      colprop = this.env.coltypes[col],
+      fieldset = $(elem).parents('fieldset.contactfieldgroup'),
+      addmenu = fieldset.parent().find('select.addfieldmenu');
+    
+    // just clear input but don't hide the last field
+    if (--colprop.count <= 0 && colprop.visible)
+      $(elem).parent().children('input').val('').blur();
+    else {
+      $(elem).parents('div.row').remove();
+      // hide entire fieldset if no more rows
+      if (!fieldset.children('div.row').length)
+        fieldset.hide();
+    }
+    
+    // enable option in add-field selector or insert it if necessary
+    if (addmenu.length) {
+      var option = addmenu.children('option[value="'+col+'"]');
+      if (option.length)
+        option.attr('disabled', false);
+      else
+        option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
+      addmenu.show();
+    }
+  };
+
+
+  this.upload_contact_photo = function(form)
+  {
+    if (form && form.elements._photo.value) {
+      this.async_upload_form(form, 'upload-photo', function(e) {
+        rcmail.set_busy(false, null, rcmail.photo_upload_id);
+      });
+
+      // display upload indicator
+      this.photo_upload_id = this.set_busy(true, 'uploading');
+    }
+  };
+  
+  this.replace_contact_photo = function(id)
+  {
+    $('#ff_photo').val(id);
+    
+    var buttons = this.buttons['upload-photo'];
+    for (var n=0; n < buttons.length; n++)
+      $('#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
+    
+    var img_src = id == '-del-' ? this.env.photo_placeholder :
+      this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + this.env.cid + '&_photo=' + id;
+    $(this.gui_objects.contactphoto).children('img').attr('src', img_src);
+    
+    this.enable_command('delete-photo', id != '-del-');
+  };
+  
+  this.photo_upload_end = function()
+  {
+    this.set_busy(false, null, this.photo_upload_id);
+    delete this.photo_upload_id;
+  };
+
+
   /*********************************************************/
   /*********        user settings methods          *********/
   /*********************************************************/
@@ -4505,6 +4659,23 @@
     }
   };
 
+
+  this.focus_textfield = function(elem)
+  {
+    elem._hasfocus = true;
+    var $elem = $(elem);
+    if ($elem.hasClass('placeholder') || $elem.val() == elem._placeholder)
+      $elem.val('').removeClass('placeholder').attr('spellcheck', true);
+  };
+
+  this.blur_textfield = function(elem)
+  {
+    elem._hasfocus = false;
+    var $elem = $(elem);
+    if (elem._placeholder && (!$elem.val() || $elem.val() == elem._placeholder))
+      $elem.addClass('placeholder').attr('spellcheck', false).val(elem._placeholder);
+  };
+  
   // write to the document/window title
   this.set_pagetitle = function(title)
   {
@@ -4951,6 +5122,39 @@
   /********************************************************/
   /*********        remote request methods        *********/
   /********************************************************/
+  
+  // compose a valid url with the given parameters
+  this.url = function(action, query)
+  {
+    var querystring = typeof(query) == 'string' ? '&' + query : '';
+    
+    if (typeof action != 'string')
+      query = action;
+    else if (!query || typeof(query) != 'object')
+      query = {};
+    
+    if (action)
+      query._action = action;
+    else
+      query._action = this.env.action;
+    
+    var base = this.env.comm_path;
+
+    // overwrite task name
+    if (query._action.match(/([a-z]+)\/([a-z-_]+)/)) {
+      query._action = RegExp.$2;
+      base = base.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
+    }
+    
+    // remove undefined values
+    var param = {};
+    for (var k in query) {
+      if (typeof(query[k]) != 'undefined' && query[k] !== null)
+        param[k] = query[k];
+    }
+    
+    return base + '&' + $.param(param) + querystring;
+  };
 
   this.redirect = function(url, lock)
   {
@@ -4965,28 +5169,13 @@
 
   this.goto_url = function(action, query, lock)
   {
-    var url = this.env.comm_path,
-     querystring = query ? '&'+query : '';
-
-    // overwrite task name
-    if (action.match(/([a-z]+)\/([a-z-_]+)/)) {
-      action = RegExp.$2;
-      url = url.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
-    }
-
-    this.redirect(url+'&_action='+action+querystring, lock);
+    this.redirect(this.url(action, query));
   };
 
   // send a http request to the server
   this.http_request = function(action, query, lock)
   {
-    var url = this.env.comm_path;
-
-    // overwrite task name
-    if (action.match(/([a-z]+)\/([a-z-_]+)/)) {
-      action = RegExp.$2;
-      url = url.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
-    }
+    var url = this.url(action, query);
 
     // trigger plugin hook
     var result = this.triggerEvent('request'+action, query);
@@ -4999,7 +5188,7 @@
         query = result;
     }
 
-    url += '&_remote=1&_action=' + action + (query ? '&' : '') + query;
+    url += '&_remote=1';
 
     // send request
     console.log('HTTP GET: ' + url);
@@ -5013,15 +5202,7 @@
   // send a http POST request to the server
   this.http_post = function(action, postdata, lock)
   {
-    var url = this.env.comm_path;
-
-    // overwrite task name
-    if (action.match(/([a-z]+)\/([a-z-_]+)/)) {
-      action = RegExp.$2;
-      url = url.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
-    }
-
-    url += '&_action=' + action;
+    var url = this.url(action);
 
     if (postdata && typeof(postdata) == 'object') {
       postdata._remote = 1;
@@ -5168,6 +5349,37 @@
       this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
   };
 
+  // post the given form to a hidden iframe
+  this.async_upload_form = function(form, action, onload)
+  {
+    var ts = new Date().getTime();
+    var frame_name = 'rcmupload'+ts;
+
+    // have to do it this way for IE
+    // otherwise the form will be posted to a new window
+    if (document.all) {
+      var html = '<iframe name="'+frame_name+'" src="program/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>';
+      document.body.insertAdjacentHTML('BeforeEnd', html);
+    }
+    else { // for standards-compilant browsers
+      var frame = document.createElement('iframe');
+      frame.name = frame_name;
+      frame.style.border = 'none';
+      frame.style.width = 0;
+      frame.style.height = 0;
+      frame.style.visibility = 'hidden';
+      document.body.appendChild(frame);
+    }
+
+    // handle upload errors, parsing iframe content in onload
+    $(frame_name).bind('load', {ts:ts}, onload);
+
+    form.target = frame_name;
+    form.action = this.url(action, { _uploadid:ts });
+    form.setAttribute('enctype', 'multipart/form-data');
+    form.submit();
+  };
+  
   // starts interval for keep-alive/check-recent signal
   this.start_keepalive = function()
   {
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index e2e23c4..59e396f 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -243,11 +243,38 @@
 $labels['receiptnote'] = 'Note: This receipt only acknowledges that the message was displayed on the recipient\'s computer. There is no guarantee that the recipient has read or understood the message contents.';
 
 // address boook
-$labels['name']      = 'Display name';
-$labels['firstname'] = 'First name';
-$labels['surname']   = 'Last name';
-$labels['email']     = 'E-Mail';
+$labels['name']         = 'Display name';
+$labels['firstname']    = 'First name';
+$labels['surname']      = 'Last name';
+$labels['middlename']   = 'Middle name';
+$labels['nameprefix']   = 'Prefix';
+$labels['namesuffix']   = 'Suffix';
+$labels['nickname']     = 'Nickname';
+$labels['jobtitle']     = 'Job title';
+$labels['organization'] = 'Company';
+$labels['department']   = 'Department';
+$labels['gender']       = 'Gender';
+$labels['maidenname']   = 'Maiden name';
+$labels['email']        = 'E-Mail';
+$labels['phone']        = 'Phone';
+$labels['address']      = 'Address';
+$labels['street']       = 'Street';
+$labels['locality']     = 'City';
+$labels['zipcode']      = 'Zip code';
+$labels['region']       = 'Region';
+$labels['country']      = 'Country';
+$labels['birthday']     = 'Birthday';
+$labels['anniversary']  = 'Anniversary';
+$labels['website']      = 'Website';
+$labels['instantmessenger'] = 'IM';
+$labels['notes'] = 'Notes';
+$labels['male']   = 'male';
+$labels['female'] = 'female';
+$labels['manager'] = 'Manager';
+$labels['assistant'] = 'Assistant';
+$labels['spouse'] = 'Spouse';
 
+$labels['addfield'] = 'Add field...';
 $labels['addcontact'] = 'Add new contact';
 $labels['editcontact'] = 'Edit contact';
 $labels['contacts'] = 'Contacts';
@@ -258,6 +285,8 @@
 $labels['save']   = 'Save';
 $labels['delete'] = 'Delete';
 $labels['rename'] = 'Rename';
+$labels['addphoto'] = 'Add';
+$labels['replacephoto'] = 'Replace';
 
 $labels['newcontact']     = 'Create new contact card';
 $labels['deletecontact']  = 'Delete selected contacts';
diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc
index cca6e91..df347d1 100644
--- a/program/localization/en_US/messages.inc
+++ b/program/localization/en_US/messages.inc
@@ -104,7 +104,7 @@
 $messages['selectimportfile'] = 'Please select a file to upload';
 $messages['addresswriterror'] = 'The selected address book is not writeable';
 $messages['contactaddedtogroup'] = 'Successfully added the contacts to this group';
-$messages['contactremovedfromgroup'] = 'Successfully remove contacts from this group';
+$messages['contactremovedfromgroup'] = 'Successfully removed contacts from this group';
 $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>';
@@ -137,5 +137,6 @@
 $messages['nametoolong'] = 'Name is too long';
 $messages['folderupdated'] = 'Folder updated successfully';
 $messages['foldercreated'] = 'Folder created successfully';
+$messages['invalidimageformat'] = 'Not a valid image format';
 
 ?>
diff --git a/program/steps/addressbook/copy.inc b/program/steps/addressbook/copy.inc
index 152add2..b891e01 100644
--- a/program/steps/addressbook/copy.inc
+++ b/program/steps/addressbook/copy.inc
@@ -48,7 +48,7 @@
           'record' => $a_record, 'source' => $target, 'group' => $target_group));
 
         if (!$plugin['abort']) {
-          if ($insert_id = $TARGET->insert($a_record, false)) {
+          if ($insert_id = $TARGET->insert($plugin['record'], false)) {
             $ids[] = $insert_id;
             $success++;
           }
diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc
index ea67746..1cd4f35 100644
--- a/program/steps/addressbook/delete.inc
+++ b/program/steps/addressbook/delete.inc
@@ -38,6 +38,10 @@
         // count contacts for this user
         $result = $CONTACTS->count();
 
+        // update saved search after data changed
+        if (($search_request = $_REQUEST['_search']) && isset($_SESSION['search'][$search_request]))
+            $_SESSION['search'][$search_request] = $CONTACTS->refresh_search();
+
         // update message count display
         $OUTPUT->set_env('pagecount', ceil($result->count / $CONTACTS->page_size));
         $OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result->count));
diff --git a/program/steps/addressbook/edit.inc b/program/steps/addressbook/edit.inc
index a65eafe..747e12a 100644
--- a/program/steps/addressbook/edit.inc
+++ b/program/steps/addressbook/edit.inc
@@ -31,45 +31,140 @@
 }
 
 
-function rcmail_contact_editform($attrib)
+function rcmail_contact_edithead($attrib)
 {
-   global $RCMAIL, $CONTACTS, $OUTPUT;
+   global $RCMAIL, $CONTACTS;
 
     // check if we have a valid result
     if ($RCMAIL->action != 'add'
         && !(($result = $CONTACTS->get_result()) && ($record = $result->first()))
     ) {
-        $OUTPUT->show_message('contactnotfound');
+        $RCMAIL->output->show_message('contactnotfound');
+        return false;
+    }
+    
+    $i_size = !empty($attrib['size']) ? $attrib['size'] : 20;
+
+    $form = array(
+        'head' => array(
+            'content' => array(
+                'prefix' => array('size' => $i_size),
+                'firstname' => array('size' => $i_size, 'visible' => true),
+                'middlename' => array('size' => $i_size),
+                'surname' => array('size' => $i_size, 'visible' => true),
+                'suffix' => array('size' => $i_size),
+                'name' => array('size' => 2*$i_size),
+                'nickname' => array('size' => 2*$i_size),
+                'company' => array('size' => $i_size),
+                'department' => array('size' => $i_size),
+                'jobtitle' => array('size' => $i_size),
+            )
+        )
+    );
+
+    list($form_start, $form_end) = get_form_tags($attrib);
+    unset($attrib['form'], $attrib['name'], $attrib['size']);
+
+    // return the address edit form
+    $out = rcmail_contact_form($form, $record, $attrib);
+
+    return $form_start . $out . $form_end;
+}
+
+
+function rcmail_contact_editform($attrib)
+{
+   global $RCMAIL, $CONTACTS, $CONTACT_COLTYPES;
+
+    // check if we have a valid result
+    if ($RCMAIL->action != 'add'
+        && !(($result = $CONTACTS->get_result()) && ($record = $result->first()))
+    ) {
+        $RCMAIL->output->show_message('contactnotfound');
         return false;
     }
 
     // add some labels to client
-    $OUTPUT->add_label('noemailwarning', 'nonamewarning');
+    $RCMAIL->output->add_label('noemailwarning', 'nonamewarning');
 
     $i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
-    $t_rows = !empty($attrib['textarearows']) ? $attrib['textarearows'] : 6;
+    $t_rows = !empty($attrib['textarearows']) ? $attrib['textarearows'] : 10;
     $t_cols = !empty($attrib['textareacols']) ? $attrib['textareacols'] : 40;
 
     $form = array(
         'info' => array(
             'name'    => rcube_label('contactproperties'),
             'content' => array(
-                'name' => array('type' => 'text', 'size' => $i_size),
-                'firstname' => array('type' => 'text', 'size' => $i_size),
-                'surname' => array('type' => 'text', 'size' => $i_size),
-                'email' => array('type' => 'text', 'size' => $i_size),
+                'gender' => array('visible' => false),
+                'maidenname' => array('size' => $i_size),
+                'email' => array('size' => $i_size, 'visible' => true),
+                'phone' => array('size' => $i_size, 'visible' => true),
+                'address' => array('visible' => true),
+                'birthday' => array('size' => 12),
+                'anniversary' => array('size' => $i_size),
+                'website' => array('size' => $i_size),
+                'im' => array('size' => $i_size),
+                'manager' => array('size' => $i_size),
+                'assistant' => array('size' => $i_size),
+                'spouse' => array('size' => $i_size),
             ),
         ),
     );
-
+    
+    if (isset($CONTACT_COLTYPES['notes'])) {
+        $form['notes'] = array(
+            'name'    => rcube_label('notes'),
+            'content' => array(
+                'notes' => array('size' => $t_cols, 'rows' => $t_rows, 'label' => false, 'visible' => true, 'limit' => 1),
+            ),
+            'single' => true,
+        );
+    }
 
     list($form_start, $form_end) = get_form_tags($attrib);
     unset($attrib['form']);
 
     // return the complete address edit form as table
-    $out = rcmail_contact_form($form, $record);
+    $out = rcmail_contact_form($form, $record, $attrib);
 
     return $form_start . $out . $form_end;
+}
+
+
+function rcmail_upload_photo_form($attrib)
+{
+  global $OUTPUT;
+
+  // add ID if not given
+  if (!$attrib['id'])
+    $attrib['id'] = 'rcmUploadbox';
+
+  // find max filesize value
+  $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
+  $max_postsize = parse_bytes(ini_get('post_max_size'));
+  if ($max_postsize && $max_postsize < $max_filesize)
+    $max_filesize = $max_postsize;
+  $max_filesize = show_bytes($max_filesize);
+  
+  $hidden = new html_hiddenfield(array('name' => '_cid', 'value' => $GLOBALS['cid']));
+  $input = new html_inputfield(array('type' => 'file', 'name' => '_photo', 'size' => $attrib['size']));
+  $button = new html_inputfield(array('type' => 'button'));
+  
+  $out = html::div($attrib,
+    $OUTPUT->form_tag(array('name' => 'uploadform', 'method' => 'post', 'enctype' => 'multipart/form-data'),
+      $hidden->show() .
+      html::div(null, $input->show()) .
+      html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))) .
+      html::div('buttons',
+        $button->show(rcube_label('close'), array('class' => 'button', 'onclick' => "$('#$attrib[id]').hide()")) . ' ' .
+        $button->show(rcube_label('upload'), array('class' => 'button mainaction', 'onclick' => JS_OBJECT_NAME . ".command('upload-photo', this.form)"))
+      )
+    )
+  );
+  
+  $OUTPUT->add_label('addphoto','replacephoto');
+  $OUTPUT->add_gui_object('uploadbox', $attrib['id']);
+  return $out;
 }
 
 
@@ -103,7 +198,10 @@
 }
 
 
+$OUTPUT->add_handler('contactedithead', 'rcmail_contact_edithead');
 $OUTPUT->add_handler('contacteditform', 'rcmail_contact_editform');
+$OUTPUT->add_handler('contactphoto',    'rcmail_contact_photo');
+$OUTPUT->add_handler('photouploadform', 'rcmail_upload_photo_form');
 
 if (!$CONTACTS->get_result() && $OUTPUT->template_exists('contactadd'))
     $OUTPUT->send('contactadd');
diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc
index 1b2e029..509be59 100644
--- a/program/steps/addressbook/export.inc
+++ b/program/steps/addressbook/export.inc
@@ -30,13 +30,24 @@
 header('Content-Disposition: attachment; filename="rcube_contacts.vcf"');
 
 while ($result && ($row = $result->next())) {
-  $vcard = new rcube_vcard($row['vcard']);
-  $vcard->set('displayname', $row['name']);
-  $vcard->set('firstname', $row['firstname']);
-  $vcard->set('surname', $row['surname']);
-  $vcard->set('email', $row['email']);
-  
-  echo $vcard->export();
+  // we already have a vcard record
+  if ($row['vcard']) {
+    echo $row['vcard'];
+  }
+  // copy values into vcard object
+  else {
+    $vcard = new rcube_vcard($row['vcard']);
+    $vcard->reset();
+    foreach ($row as $key => $values) {
+      list($field, $section) = explode(':', $key);
+      foreach ((array)$values as $value) {
+        if (is_array($value) || strlen($value))
+          $vcard->set($field, $value, strtoupper($section));
+      }
+    }
+    
+    echo $vcard->export();
+  }
 }
 
 exit;
diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc
index e8b05ed..e9b3dc8 100644
--- a/program/steps/addressbook/func.inc
+++ b/program/steps/addressbook/func.inc
@@ -56,6 +56,56 @@
 }
 
 
+// general definition of contact coltypes
+$CONTACT_COLTYPES = array(
+  'name'         => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('name')),
+  'firstname'    => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('firstname')),
+  'surname'      => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('surname')),
+  'middlename'   => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('middlename')),
+  'prefix'       => array('type' => 'text', 'size' => 8,  'limit' => 1, 'label' => rcube_label('nameprefix')),
+  'suffix'       => array('type' => 'text', 'size' => 8,  'limit' => 1, 'label' => rcube_label('namesuffix')),
+  'nickname'     => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('nickname')),
+  'jobtitle'     => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('jobtitle')),
+  'organization' => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('organization')),
+  'department'   => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('department')),
+  'gender'       => array('type' => 'select', 'limit' => 1, 'label' => rcube_label('gender'), 'options' => array('male' => rcube_label('male'), 'female' => rcube_label('female'))),
+  'maidenname'   => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('maidenname')),
+  'email'        => array('type' => 'text', 'size' => 40, 'label' => rcube_label('email'), 'subtypes' => array('home','work','other')),
+  'phone'        => array('type' => 'text', 'size' => 40, 'label' => rcube_label('phone'), 'subtypes' => array('home','home2','work','work2','mobile','main','homefax','workfax','car','pager','video','assistant','other')),
+  'address'      => array('type' => 'composite', 'label' => rcube_label('address'), 'subtypes' => array('home','work','other'), 'childs' => array(
+    'street'     => array('type' => 'text', 'size' => 40, 'label' => rcube_label('street')),
+    'locality'   => array('type' => 'text', 'size' => 28, 'label' => rcube_label('locality')),
+    'zipcode'    => array('type' => 'text', 'size' => 8, 'label' => rcube_label('zipcode')),
+    'region'     => array('type' => 'text', 'size' => 12, 'label' => rcube_label('region')),
+    'country'    => array('type' => 'text', 'size' => 40, 'label' => rcube_label('country')),
+  )),
+  'birthday'     => array('type' => 'date', 'size' => 12, 'label' => rcube_label('birthday'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col'),
+  'anniversary'  => array('type' => 'date', 'size' => 12, 'label' => rcube_label('anniversary'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col'),
+  'website'      => array('type' => 'text', 'size' => 40, 'label' => rcube_label('website'), 'subtypes' => array('homepage','work','blog','other')),
+  'im'           => array('type' => 'text', 'size' => 40, 'label' => rcube_label('instantmessenger'), 'subtypes' => array('aim','icq','msn','yahoo','jabber','skype','other')),
+  'notes'        => array('type' => 'textarea', 'size' => 40, 'rows' => 15, 'label' => rcube_label('notes'), 'limit' => 1),
+  'photo'        => array('type' => 'image', 'limit' => 1),
+  'assistant'    => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('assistant')),
+  'manager'      => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('manager')),
+  'spouse'       => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('spouse')),
+  // TODO: define fields for vcards like GEO, KEY
+);
+
+// reduce/extend $CONTACT_COLTYPES with specification from the current $CONTACT object
+if (is_array($CONTACTS->coltypes)) {
+    // remove cols not listed by the backend class
+    $contact_cols = $CONTACTS->coltypes[0] ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes;
+    $CONTACT_COLTYPES = array_intersect_key($CONTACT_COLTYPES, $contact_cols);
+    // add associative coltypes definition
+    if (!$CONTACTS->coltypes[0]) {
+        foreach ($CONTACTS->coltypes as $col => $colprop)
+            $CONTACT_COLTYPES[$col] = $CONTACT_COLTYPES[$col] ? array_merge($CONTACT_COLTYPES[$col], $colprop) : $colprop;
+    }
+}
+
+$OUTPUT->set_env('photocol', is_array($CONTACT_COLTYPES['photo']));
+
+
 function rcmail_directory_list($attrib)
 {
     global $RCMAIL, $OUTPUT;
@@ -72,23 +122,21 @@
         html::a(array('href' => '%s',
             'onclick' => "return ".JS_OBJECT_NAME.".command('list','%s',this)"), '%s'));
 
-    if (!$current && strtolower($RCMAIL->config->get('address_book_type', 'sql')) != 'ldap') {
-        $current = '0';
-    }
-    else if (!$current) {
-        // DB address book not used, see if a source is set, if not use the
-        // first LDAP directory.
-        $current = key((array)$RCMAIL->config->get('ldap_public', array()));
-    }
+    // currently selected is the first address source in the list
+    if (!isset($current))
+        $current = strval(key((array)$OUTPUT->env['address_sources']));
 
     foreach ((array)$OUTPUT->env['address_sources'] as $j => $source) {
-        $id = $source['id'] ? $source['id'] : $j;
+        $id = strval($source['id'] ? $source['id'] : $j);
         $js_id = JQ($id);
-        $dom_id = preg_replace('/[^a-z0-9\-_]/i', '', $id);
-        $out .= sprintf($line_templ, $dom_id, ($current == $id ? 'selected' : ''),
+        $dom_id = preg_replace('/[^a-z0-9\-_]/i', '_', $id);
+        $out .= sprintf($line_templ, $dom_id, ($current === $id ? 'selected' : ''),
             Q(rcmail_url(null, array('_source' => $id))),
             $js_id, (!empty($source['name']) ? Q($source['name']) : Q($id)));
-        $groupdata = rcmail_contact_groups(array('out' => $out, 'jsdata' => $jsdata, 'source' => $id));
+
+        $groupdata = array('out' => $out, 'jsdata' => $jsdata, 'source' => $id);
+        if ($source['groups'])
+            $groupdata = rcmail_contact_groups($groupdata);
         $jsdata = $groupdata['jsdata'];
         $out = $groupdata['out'];
     }
@@ -130,15 +178,15 @@
 {
     global $CONTACTS, $OUTPUT;
 
+    // define list of cols to be displayed
+    $a_show_cols = array('name');
+
     // count contacts for this user
-    $result = $CONTACTS->list_records();
+    $result = $CONTACTS->list_records($a_show_cols);
 
     // add id to message list table if not specified
     if (!strlen($attrib['id']))
         $attrib['id'] = 'rcmAddressList';
-
-    // define list of cols to be displayed
-    $a_show_cols = array('name');
 
     // create XHTML table
     $out = rcube_table_output($attrib, $result->records, $a_show_cols, $CONTACTS->primary_key);
@@ -233,9 +281,9 @@
 }
 
 
-function rcmail_contact_form($form, $record)
+function rcmail_contact_form($form, $record, $attrib = null)
 {
-    global $RCMAIL;
+    global $RCMAIL, $CONFIG;
 
     // Allow plugins to modify contact form content
     $plugin = $RCMAIL->plugins->exec_hook('contact_form', array(
@@ -243,35 +291,222 @@
 
     $form = $plugin['form'];
     $record = $plugin['record'];
+    $edit_mode = $RCMAIL->action != 'show';
+    $del_button = $attrib['deleteicon'] ? html::img(array('src' => $CONFIG['skin_path'] . $attrib['deleteicon'], 'alt' => rcube_label('delete'))) : rcube_label('delete');
+    unset($attrib['deleteicon']);
     $out = '';
+    
+    // get default coltypes
+    $coltypes = $GLOBALS['CONTACT_COLTYPES'];
+    $coltype_lables = array();
+    
+    foreach ($coltypes as $col => $prop) {
+        if ($prop['subtypes']) {
+            $select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype'));
+            $select_subtype->add($prop['subtypes']);
+            $coltypes[$col]['subtypes_select'] = $select_subtype->show();
+        }
+        if ($prop['childs']) {
+            foreach ($prop['childs'] as $childcol => $cp)
+                $coltype_lables[$childcol] = array('label' => $cp['label']);
+        }
+    }
 
-    foreach ($form as $fieldset) {
+    foreach ($form as $section => $fieldset) {
+        // skip empty sections
         if (empty($fieldset['content']))
             continue;
 
+        $select_add = new html_select(array('class' => 'addfieldmenu', 'rel' => $section));
+        $select_add->add(rcube_label('addfield'), '');
+
+        // render head section with name fields (not a regular list of rows)
+        if ($section == 'head') {
+            $content = '';
+            
+            $names_arr = array($record['prefix'], $record['firstname'], $record['middlename'], $record['surname'], $record['suffix']);
+            if ($record['name'] == join(' ', array_filter($names_arr)))
+              unset($record['name']);
+
+            // group fields
+            $field_blocks = array(
+                'names'    => array('prefix','firstname','middlename','surname','suffix'),
+                'displayname' => array('name'),
+                'nickname' => array('nickname'),
+                'jobnames' => array('organization','department','jobtitle'),
+            );
+            foreach ($field_blocks as $blockname => $colnames) {
+                $fields = '';
+                foreach ($colnames as $col) {
+                    // skip cols unknown to the backend
+                    if (!$coltypes[$col])
+                        continue;
+
+                    if ($RCMAIL->action == 'show') {
+                        if (!empty($record[$col]))
+                            $fields .= html::span('namefield ' . $col, Q($record[$col])) . " ";
+                    }
+                    else {
+                        $colprop = (array)$fieldset['content'][$col] + (array)$coltypes[$col];
+                        $colprop['id'] = 'ff_'.$col;
+                        if (empty($record[$col]) && !$colprop['visible']) {
+                            $colprop['style'] = 'display:none';
+                            $select_add->add($colprop['label'], $col);
+                        }
+                        $fields .= rcmail_get_edit_field($col, $record[$col], $colprop, $colprop['type']);
+                    }
+                }
+                $content .= html::div($blockname, $fields);
+            }
+            
+            if ($edit_mode)
+                $content .= html::p('addfield', $select_add->show(null));
+
+            $out .= html::tag('fieldset', $attrib, (!empty($fieldset['name']) ? html::tag('legend', null, Q($fieldset['name'])) : '') . $content) ."\n";
+            continue;
+        }
+
         $content = '';
         if (is_array($fieldset['content'])) {
-            $table = new html_table(array('cols' => 2));
-
             foreach ($fieldset['content'] as $col => $colprop) {
-                $colprop['id'] = 'rcmfd_'.$col;
+                // remove subtype part of col name
+                list($field, $subtype) = explode(':', $col);
+                if (!$subtype) $subtype = 'home';
+                $fullkey = $col.':'.$subtype;
 
-                $label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
+                // skip cols unknown to the backend
+                if (!$coltypes[$field])
+                    continue;
+
+                // merge colprop with global coltype configuration
+                $colprop += $coltypes[$field];
+                $label = isset($colprop['label']) ? $colprop['label'] : rcube_label($col);
+
+                // prepare subtype selector in edit mode
+                if ($edit_mode && is_array($colprop['subtypes'])) {
+                    $select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype'));
+                    $select_subtype->add($colprop['subtypes']);
+                }
+                else
+                    $select_subtype = null;
 
                 if (!empty($colprop['value'])) {
-                    $value = $colprop['value'];
-                }
-                else if ($RCMAIL->action == 'show') {
-                    $value = $record[$col];
+                    $values = (array)$colprop['value'];
                 }
                 else {
-                    $value = rcmail_get_edit_field($col, $record[$col], $colprop, $colprop['type']);
+                    // iterate over possible subtypes and collect values with their subtype
+                    if (is_array($colprop['subtypes'])) {
+                        $values = $subtypes = array();
+                        foreach ($colprop['subtypes'] as $i => $st) {
+                            $newval = false;
+                            if ($record[$field.':'.$st]) {
+                                $subtypes[count($values)] = $st;
+                                $newval = $record[$field.':'.$st];
+                            }
+                            else if ($i == 0 && $record[$field]) {
+                                $subtypes[count($values)] = $st;
+                                $newval = $record[$field];
+                            }
+                            if ($newval !== false) {
+                                if (is_array($newval) && isset($newval[0]))
+                                    $values = array_merge($values, $newval);
+                                else
+                                    $values[] = $newval;
+                            }
+                        }
+                    }
+                    else {
+                        $values = $record[$fullkey] ? $record[$fullkey] : $record[$field];
+                        $subtypes = null;
+                    }
                 }
 
-                $table->add('title', sprintf('<label for="%s">%s</label>', $colprop['id'], Q($label)));
-                $table->add(null, $value);
+                // hack: create empty values array to force this field to be displayed
+                if (empty($values) && $colprop['visible'])
+                    $values[] = '';
+
+                $rows = '';
+                foreach ((array)$values as $i => $val) {
+                    if ($subtypes[$i])
+                        $subtype = $subtypes[$i];
+
+                    // render composite field
+                    if ($colprop['type'] == 'composite') {
+                        $composite = array(); $j = 0;
+                        $template = $RCMAIL->config->get($col . '_template', '{'.join('} {', array_keys($colprop['childs'])).'}');
+                        foreach ($colprop['childs'] as $childcol => $cp) {
+                            $childvalue = $val[$childcol] ? $val[$childcol] : $val[$j];
+
+                            if ($edit_mode) {
+                                if ($colprop['subtypes'] || $colprop['limit'] != 1) $cp['array'] = true;
+                                $composite['{'.$childcol.'}'] = rcmail_get_edit_field($childcol, $childvalue, $cp, $cp['type']) . " ";
+                            }
+                            else {
+                                $childval = $cp['render_func'] ? call_user_func($cp['render_func'], $childvalue, $childcol) : Q($childvalue);
+                                $composite['{'.$childcol.'}'] = html::span('data ' . $childcol, $childval) . " ";
+                            }
+                            $j++;
+                        }
+
+                        $coltypes[$field] += (array)$colprop;
+                        $coltypes[$field]['count']++;
+                        $val = strtr($template, $composite);
+                    }
+                    else if ($edit_mode) {
+                        // call callback to render/format value
+                        if ($colprop['render_func'])
+                            $val = call_user_func($colprop['render_func'], $val, $col);
+
+                        $coltypes[$field] = (array)$colprop + $coltypes[$field];
+
+                        if ($colprop['subtypes'] || $colprop['limit'] != 1)
+                            $colprop['array'] = true;
+
+                        $val = rcmail_get_edit_field($col, $val, $colprop, $colprop['type']);
+                        $coltypes[$field]['count']++;
+                    }
+                    else if ($colprop['render_func'])
+                        $val = call_user_func($colprop['render_func'], $val, $col);
+                    else if (is_array($colprop['options']) && isset($colprop['options'][$val]))
+                        $val = $colprop['options'][$val];
+                    else
+                        $val = Q($val);
+
+                    // use subtype as label
+                    if ($colprop['subtypes'])
+                        $label = $subtype;
+
+                    // add delete button/link
+                    if ($edit_mode && !($colprop['visible'] && $colprop['limit'] == 1))
+                        $val .= html::a(array('href' => '#del', 'class' => 'contactfieldbutton deletebutton', 'title' => rcube_label('delete'), 'rel' => $col), $del_button);
+
+                    // display row with label
+                    if ($label) {
+                        $rows .= html::div('row',
+                            html::div('contactfieldlabel label', $select_subtype ? $select_subtype->show($subtype) : Q($label)) .
+                            html::div('contactfieldcontent '.$colprop['type'], $val));
+                    }
+                    else   // row without label
+                        $rows .= html::div('row', html::div('contactfield', $val));
+                }
+                
+                // add option to the add-field menu
+                if (!$colprop['limit'] || $coltypes[$field]['count'] < $colprop['limit']) {
+                    $select_add->add($colprop['label'], $col);
+                    $select_add->_count++;
+                }
+                
+                // wrap rows in fieldgroup container
+                $content .= html::tag('fieldset', array('class' => 'contactfieldgroup contactcontroller' . $col, 'style' => ($rows ? null : 'display:none')),
+                  ($colprop['subtypes'] ? html::tag('legend', null, Q($colprop['label'])) : ' ') .
+                  $rows);
             }
-            $content = $table->show();
+
+            // also render add-field selector
+            if ($edit_mode)
+                $content .= html::p('addfield', $select_add->show(null, array('style' => $select_add->_count ? null : 'display:none')));
+
+            $content = html::div(array('id' => 'contactsection' . $section), $content);
         }
         else {
             $content = $fieldset['content'];
@@ -279,8 +514,50 @@
 
         $out .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $content) ."\n";
     }
-  
+
+    if ($edit_mode) {
+      $RCMAIL->output->set_env('coltypes', $coltypes + $coltype_lables);
+      $RCMAIL->output->set_env('delbutton', $del_button);
+      $RCMAIL->output->add_label('delete');
+    }
+
     return $out;
+}
+
+
+function rcmail_contact_photo($attrib)
+{
+    global $CONTACTS, $CONTACT_COLTYPES, $RCMAIL, $CONFIG;
+    
+    if ($result = $CONTACTS->get_result())
+        $record = $result->first();
+    
+    $photo_img = $attrib['placeholder'] ? $CONFIG['skin_path'] . $attrib['placeholder'] : 'program/blank.gif';
+    unset($attrib['placeholder']);
+    
+    if ($CONTACT_COLTYPES['photo']) {
+        $RCMAIL->output->set_env('photo_placeholder', $photo_img);
+        
+        if ($record['photo'])
+            $photo_img = $RCMAIL->url(array('_action' => 'photo', '_cid' => $record['ID'], '_source' => $_REQUEST['_source']));
+        $img = html::img(array('src' => $photo_img, 'border' => 1, 'alt' => ''));
+        $content = html::div($attrib, $img);
+      
+        if ($RCMAIL->action == 'edit' || $RCMAIL->action == 'add') {
+            $RCMAIL->output->add_gui_object('contactphoto', $attrib['id']);
+            $hidden = new html_hiddenfield(array('name' => '_photo', 'id' => 'ff_photo'));
+            $content .= $hidden->show();
+        }
+  }
+  
+  return $content;
+}
+
+
+function rcmail_format_date_col($val)
+{
+    global $RCMAIL;
+    return format_date($val, $RCMAIL->config->get('date_format', 'Y-m-d'));
 }
 
 
@@ -297,6 +574,8 @@
 // register action aliases
 $RCMAIL->register_action_map(array(
     'add' => 'edit.inc',
+    'photo' => 'show.inc',
+    'upload-photo' => 'save.inc',
     'group-create' => 'groups.inc',
     'group-rename' => 'groups.inc',
     'group-delete' => 'groups.inc',
diff --git a/program/steps/addressbook/groups.inc b/program/steps/addressbook/groups.inc
index b7fdb2f..b70bbf2 100644
--- a/program/steps/addressbook/groups.inc
+++ b/program/steps/addressbook/groups.inc
@@ -79,8 +79,7 @@
 
   if ($created && $OUTPUT->ajax_call) {
     $OUTPUT->show_message('groupcreated', 'confirmation');
-    $OUTPUT->command('insert_contact_group', array(
-      'source' => $source, 'id' => $created['id'], 'name' => $created['name']));
+    $OUTPUT->command('insert_contact_group', array('source' => $source) + $created);
   }
   else if (!$created) {
     $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'errorsaving', 'error');
diff --git a/program/steps/addressbook/import.inc b/program/steps/addressbook/import.inc
index 61b757f..532afdb 100644
--- a/program/steps/addressbook/import.inc
+++ b/program/steps/addressbook/import.inc
@@ -150,13 +150,8 @@
         }
       }
       
-      $a_record = array(
-        'name' => $vcard->displayname,
-        'firstname' => $vcard->firstname,
-        'surname' => $vcard->surname,
-        'email' => $email,
-        'vcard' => $vcard->export(),
-      );
+      $a_record = $vcard->get_assoc();
+      $a_record['vcard'] = $vcard->export();
       
       $plugin = $RCMAIL->plugins->exec_hook('contact_create', array('record' => $a_record, 'source' => null));
       $a_record = $plugin['record'];
diff --git a/program/steps/addressbook/list.inc b/program/steps/addressbook/list.inc
index 018f6e2..234e1a6 100644
--- a/program/steps/addressbook/list.inc
+++ b/program/steps/addressbook/list.inc
@@ -20,7 +20,7 @@
 */
 
 // get contacts for this user
-$result = $CONTACTS->list_records();
+$result = $CONTACTS->list_records(array('name'));
 
 // update message count display
 $OUTPUT->set_env('pagecount', ceil($result->count / $CONTACTS->page_size));
diff --git a/program/steps/addressbook/mailto.inc b/program/steps/addressbook/mailto.inc
index d38ae9e..702e1a6 100644
--- a/program/steps/addressbook/mailto.inc
+++ b/program/steps/addressbook/mailto.inc
@@ -29,8 +29,10 @@
   $CONTACTS->set_pagesize(100);
   $recipients = $CONTACTS->search($CONTACTS->primary_key, $cid);
 
-  while (is_object($recipients) && ($rec = $recipients->iterate()))
-    $mailto[] = format_email_recipient($rec['email'], $rec['name']);
+  while (is_object($recipients) && ($rec = $recipients->iterate())) {
+    $emails = $CONTACTS->get_col_values('email', $rec, true);
+    $mailto[] = format_email_recipient($emails[0], $rec['name']);
+  }
 }
 
 if (!empty($mailto))
diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc
index add8ef4..7d29b6f 100644
--- a/program/steps/addressbook/save.inc
+++ b/program/steps/addressbook/save.inc
@@ -5,7 +5,7 @@
  | program/steps/addressbook/save.inc                                    |
  |                                                                       |
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2005-2009, The Roundcube Dev Team                       |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
  | Licensed under the GNU GPL                                            |
  |                                                                       |
  | PURPOSE:                                                              |
@@ -29,31 +29,145 @@
   return;
 }
 
-// Basic input checks
-if ((!get_input_value('_name', RCUBE_INPUT_POST) || !get_input_value('_email', RCUBE_INPUT_POST))) {
+
+// handle photo upload for contacts
+if ($RCMAIL->action == 'upload-photo') {
+    // clear all stored output properties (like scripts and env vars)
+    $OUTPUT->reset();
+
+    if ($filepath = $_FILES['_photo']['tmp_name']) {
+        // check file type and resize image
+        $imageprop = rcmail::imageprops($_FILES['_photo']['tmp_name']);
+        
+        if ($imageprop['width'] && $imageprop['height']) {
+            $maxsize = intval($RCMAIL->config->get('contact_photo_size', 160));
+            $tmpfname = tempnam($RCMAIL->config->get('temp_dir'), 'rcmImgConvert');
+            $save_hook = 'attachment_upload';
+            
+            // scale image to a maximum size
+            if (($imageprop['width'] > $maxsize || $imageprop['height'] > $maxsize) &&
+                  (rcmail::imageconvert(array('in' => $filepath, 'out' => $tmpfname, 'size' => $maxsize.'x'.$maxsize, 'type' => $imageprop['type'])) !== false)) {
+                $filepath = $tmpfname;
+                $save_hook = 'attachment_save';
+            }
+            
+            // save uploaded file in storage backend
+            $attachment = $RCMAIL->plugins->exec_hook($save_hook, array(
+                'path' => $filepath,
+                'size' => $_FILES['_photo']['size'],
+                'name' => $_FILES['_photo']['name'],
+                'mimetype' => 'image/' . $imageprop['type'],
+            ));
+        }
+        else
+            $attachment['error'] = rcube_label('invalidimageformat');
+
+        if ($attachment['status'] && !$attachment['abort']) {
+            $file_id = $attachment['id'];
+            $_SESSION['contacts']['files'][$file_id] = $attachment;
+            $OUTPUT->command('replace_contact_photo', $file_id);
+        }
+        else {  // upload failed
+            $err = $_FILES['_photo']['error'];
+            if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE)
+                $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array('size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
+            else if ($attachment['error'])
+                $msg = $attachment['error'];
+            else
+                $msg = rcube_label('fileuploaderror');
+            
+            $OUTPUT->command('display_message', $msg, 'error');
+        }
+    }
+    else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+        // if filesize exceeds post_max_size then $_FILES array is empty,
+        // show filesizeerror instead of fileuploaderror
+        if ($maxsize = ini_get('post_max_size'))
+            $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array('size' => show_bytes(parse_bytes($maxsize)))));
+        else
+            $msg = rcube_label('fileuploaderror');
+            
+        $OUTPUT->command('display_message', $msg, 'error');
+    }
+    
+    $OUTPUT->command('photo_upload_end');
+    $OUTPUT->send('iframe');
+}
+
+
+// read POST values into hash array
+$a_record = array();
+foreach ($GLOBALS['CONTACT_COLTYPES'] as $col => $colprop) {
+  $fname = '_'.$col;
+  if ($colprop['composite'])
+    continue;
+  // gather form data of composite fields
+  if ($colprop['childs']) {
+    $values = array();
+    foreach ($colprop['childs'] as $childcol => $cp) {
+      $vals = get_input_value('_'.$childcol, RCUBE_INPUT_POST);
+      foreach ((array)$vals as $i => $val)
+        $values[$i][$childcol] = $val;
+    }
+    $subtypes = get_input_value('_subtype_' . $col, RCUBE_INPUT_POST);
+    foreach ($subtypes as $i => $subtype)
+      if ($values[$i])
+        $a_record[$col.':'.$subtype][] = $values[$i];
+  }
+  // assign values and subtypes
+  else if (is_array($_POST[$fname])) {
+    $values = get_input_value($fname, RCUBE_INPUT_POST);
+    $subtypes = get_input_value('_subtype_' . $col, RCUBE_INPUT_POST);
+    foreach ($values as $i => $val) {
+      $subtype = $subtypes[$i] ? ':'.$subtypes[$i] : '';
+      $a_record[$col.$subtype][] = $val;
+    }
+  }
+  else if (isset($_POST[$fname])) {
+    $a_record[$col] = get_input_value($fname, RCUBE_INPUT_POST);
+  }
+}
+
+if (empty($a_record['name']))
+  $a_record['name'] = join(' ', array_filter(array($a_record['prefix'], $a_record['firstname'], $a_record['middlename'], $a_record['surname'], $a_record['suffix'],)));
+
+#var_dump($a_record);
+
+// Basic input checks (TODO: delegate to $CONTACTS instance)
+if (empty($a_record['name'])/* || empty($a_record['email'])*/) {
   $OUTPUT->show_message('formincomplete', 'warning');
   rcmail_overwrite_action($return_action);
   return;
 }
 
-
-// setup some vars we need
-$a_save_cols = array('name', 'firstname', 'surname', 'email');
-$a_record = array();
-
-// read POST values into hash array
-foreach ($a_save_cols as $col) {
-  $fname = '_'.$col;
-  if (isset($_POST[$fname]))
-    $a_record[$col] = get_input_value($fname, RCUBE_INPUT_POST);
+// Validity checks
+foreach ($CONTACTS->get_col_values('email', $a_record, true) as $email) {
+  if (strlen($email)) {
+    $_email = idn_to_ascii($email);
+    if (!check_email($_email, false)) {
+      $OUTPUT->show_message('emailformaterror', 'warning', array('email' => $email));
+      rcmail_overwrite_action($return_action);
+      return;
+    }
+  }
 }
 
-// Validity checks
-$_email = idn_to_ascii($a_record['email']);
-if (!check_email($_email, false)) {
-  $OUTPUT->show_message('emailformaterror', 'warning', array('email' => $_email));
-  rcmail_overwrite_action($return_action);
-  return;
+// get raw photo data if changed
+if (isset($a_record['photo'])) {
+    if ($a_record['photo'] == '-del-') {
+        $a_record['photo'] = '';
+    }
+    else if ($tempfile = $_SESSION['contacts']['files'][$a_record['photo']]) {
+        $tempfile = $RCMAIL->plugins->exec_hook('attachment_get', $tempfile);
+        if ($tempfile['status'])
+            $a_record['photo'] = $tempfile['data'] ? $tempfile['data'] : @file_get_contents($tempfile['path']);
+    }
+    else
+        unset($a_record['photo']);
+    
+    // cleanup session data
+    $RCMAIL->plugins->exec_hook('attachments_cleanup', array());
+    $RCMAIL->session->remove('contacts');
 }
 
 // update an existing contact
@@ -92,7 +206,8 @@
   }
   else {
     // show error message
-    $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'errorsaving', 'error', null, false);
+    $err = $CONTACTS->get_error();
+    $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : ($err['message'] ? $err['message'] : 'errorsaving'), 'error', null, false);
     rcmail_overwrite_action('show');
   }
 }
@@ -100,10 +215,16 @@
 // insert a new contact
 else {
   // check for existing contacts
-  $existing = $CONTACTS->search('email', $a_record['email'], true, false);
+  $existing = false;
+  foreach ($CONTACTS->get_col_values('email', $a_record, true) as $email) {
+      if (($res = $CONTACTS->search('email', $email, true, false)) && $res->count) {
+          $existing = true;
+          break;
+      }
+  }
 
   // show warning message
-  if ($existing->count) {
+  if ($existing) {
     $OUTPUT->show_message('contactexists', 'warning', null, false);
     rcmail_overwrite_action('add');
     return;
@@ -138,7 +259,8 @@
   }
   else {
     // show error message
-    $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'errorsaving', 'error', null, false);
+    $err = $CONTACTS->get_error();
+    $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : ($err['message'] ? $err['message'] : 'errorsaving'), 'error', null, false);
     rcmail_overwrite_action('add');
   }
 }
diff --git a/program/steps/addressbook/search.inc b/program/steps/addressbook/search.inc
index 8cacbd9..9e40aba 100644
--- a/program/steps/addressbook/search.inc
+++ b/program/steps/addressbook/search.inc
@@ -5,7 +5,7 @@
  | program/steps/addressbook/search.inc                                  |
  |                                                                       |
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2005-2007, The Roundcube Dev Team                       |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
  | Licensed under the GNU GPL                                            |
  |                                                                       |
  | PURPOSE:                                                              |
@@ -28,11 +28,11 @@
 // get contacts for this user
 $result = $CONTACTS->search(array('name','email'), $search);
 
+// save search settings in session
+$_SESSION['search'][$search_request] = $CONTACTS->get_search_set();
+
 if ($result->count > 0)
 {
-  // save search settings in session
-  $_SESSION['search'][$search_request] = $CONTACTS->get_search_set();
-
   // create javascript list
   rcmail_js_contacts_list($result);
 }
diff --git a/program/steps/addressbook/show.inc b/program/steps/addressbook/show.inc
index 43ded2a..eb26450 100644
--- a/program/steps/addressbook/show.inc
+++ b/program/steps/addressbook/show.inc
@@ -25,8 +25,30 @@
     $OUTPUT->set_env('cid', $record['ID']);
 }
 
+// return raw photo of the given contact
+if ($RCMAIL->action == 'photo') {
+    if (($file_id = get_input_value('_photo', RCUBE_INPUT_GPC)) && ($tempfile = $_SESSION['contacts']['files'][$file_id])) {
+        $tempfile = $RCMAIL->plugins->exec_hook('attachment_display', $tempfile);
+        if ($tempfile['status']) {
+            if ($tempfile['data'])
+                $data = $tempfile['data'];
+            else if ($tempfile['path'])
+                $data = file_get_contents($tempfile['path']);
+        }
+    }
+    else if ($record['photo']) {
+        $data = is_array($record['photo']) ? $record['photo'][0] : $record['photo'];
+        if (!preg_match('![^a-z0-9/=+-]!i', $data))
+            $data = base64_decode($data, true);
+    }
+    
+    header('Content-Type: ' . rc_image_content_type($data));
+    echo $data ? $data : file_get_contents('program/blank.gif');
+    exit;
+}
 
-function rcmail_contact_details($attrib)
+
+function rcmail_contact_head($attrib)
 {
     global $CONTACTS, $RCMAIL;
 
@@ -36,51 +58,96 @@
         return false;
     }
 
-    $i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
-    $t_rows = !empty($attrib['textarearows']) ? $attrib['textarearows'] : 6;
-    $t_cols = !empty($attrib['textareacols']) ? $attrib['textareacols'] : 40;
-
     $microformats = array('name' => 'fn', 'email' => 'email');
+
+    $form = array(
+        'head' => array(  // section 'head' is magic!
+            'content' => array(
+                'prefix' => array('type' => 'text'),
+                'firstname' => array('type' => 'text'),
+                'middlename' => array('type' => 'text'),
+                'surname' => array('type' => 'text'),
+                'suffix' => array('type' => 'text'),
+            ),
+        ),
+    );
+
+    unset($attrib['name']);
+    return rcmail_contact_form($form, $record, $attrib);
+}
+
+
+function rcmail_contact_details($attrib)
+{
+    global $CONTACTS, $RCMAIL, $CONTACT_COLTYPES;
+
+    // check if we have a valid result
+    if (!(($result = $CONTACTS->get_result()) && ($record = $result->first()))) {
+        //$RCMAIL->output->show_message('contactnotfound');
+        return false;
+    }
+
+    $i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
 
     $form = array(
         'info' => array(
             'name'    => rcube_label('contactproperties'),
             'content' => array(
-                'name' => array('type' => 'text', 'size' => $i_size),
-                'firstname' => array('type' => 'text', 'size' => $i_size),
-                'surname' => array('type' => 'text', 'size' => $i_size),
-                'email' => array('type' => 'text', 'size' => $i_size),
+              'gender' => array('size' => $i_size),
+              'maidenname' => array('size' => $i_size),
+              'email' => array('size' => $i_size, 'render_func' => 'rcmail_render_email_value'),
+              'phone' => array('size' => $i_size),
+              'address' => array(),
+              'birthday' => array('size' => $i_size),
+              'anniversary' => array('size' => $i_size),
+              'website' => array('size' => $i_size, 'render_func' => 'rcmail_render_url_value'),
+              'im' => array('size' => $i_size),
+              'manager' => array('size' => $i_size),
+              'assistant' => array('size' => $i_size),
+              'spouse' => array('size' => $i_size),
             ),
         ),
-        'groups' => array(
-            'name'    => rcube_label('groups'),
-            'content' => '',
-        ),
     );
-
-    // Get content of groups fieldset
-    if ($groups = rcmail_contact_record_groups($record['ID'])) {
-        $form['groups']['content'] = $groups;    
+    
+    if (isset($CONTACT_COLTYPES['notes'])) {
+        $form['notes'] = array(
+            'name'    => rcube_label('notes'),
+            'content' => array(
+                'notes' => array('type' => 'textarea', 'label' => false),
+            ),
+        );
     }
-    else {
-        unset($form['groups']);
-    }
-
-    if (!empty($record['email'])) {
-        $form['info']['content']['email']['value'] = html::a(array(
-            'href' => 'mailto:' . $record['email'],
-            'onclick' => sprintf("return %s.command('compose','%s',this)", JS_OBJECT_NAME, JQ($record['email'])),
-            'title' => rcube_label('composeto'),
-            'class' => $microformats['email'],
-        ), Q($record['email']));
-    }
-    foreach (array('name', 'firstname', 'surname') as $col) {
-        if ($record[$col]) {
-            $form['info']['content'][$col]['value'] = html::span($microformats[$col], Q($record[$col]));
-        }
+    
+    if ($CONTACTS->groups) {
+        $form['groups'] = array(
+            'name'    => rcube_label('groups'),
+            'content' => rcmail_contact_record_groups($record['ID']),
+        );
     }
 
     return rcmail_contact_form($form, $record);
+}
+
+
+function rcmail_render_email_value($email, $col)
+{
+    return html::a(array(
+        'href' => 'mailto:' . $email,
+        'onclick' => sprintf("return %s.command('compose','%s',this)", JS_OBJECT_NAME, JQ($email)),
+        'title' => rcube_label('composeto'),
+        'class' => 'email',
+    ), Q($email));
+}
+
+
+function rcmail_render_url_value($url, $col)
+{
+    $prefix = preg_match('![htfps]+://!', $url) ? '' : 'http://';
+    return html::a(array(
+        'href' => $prefix . $url,
+        'target' => '_blank',
+        'class' => 'url',
+    ), Q($url));
 }
 
 
@@ -124,6 +191,8 @@
 
 
 //$OUTPUT->framed = $_framed;
+$OUTPUT->add_handler('contacthead', 'rcmail_contact_head');
 $OUTPUT->add_handler('contactdetails', 'rcmail_contact_details');
+$OUTPUT->add_handler('contactphoto', 'rcmail_contact_photo');
 
 $OUTPUT->send('contact');
diff --git a/program/steps/mail/autocomplete.inc b/program/steps/mail/autocomplete.inc
index b42ebf8..36542ca 100644
--- a/program/steps/mail/autocomplete.inc
+++ b/program/steps/mail/autocomplete.inc
@@ -29,8 +29,10 @@
     $abook->set_group($gid);
     $abook->set_pagesize(1000);  // TODO: limit number of group members by config
     $result = $abook->list_records(array('email','name'));
-    while ($result && ($sql_arr = $result->iterate()))
-      $members[] = format_email_recipient($sql_arr['email'], $sql_arr['name']);
+    while ($result && ($sql_arr = $result->iterate())) {
+      foreach ((array)$sql_arr['email'] as $email)
+        $members[] = format_email_recipient($email, $sql_arr['name']);
+    }
 
     $OUTPUT->command('replace_group_recipients', $gid, join(', ', $members));
   }
@@ -45,12 +47,14 @@
 
     if ($result = $abook->search(array('email','name'), $search, false, true, true, 'email')) {
       while ($sql_arr = $result->iterate()) {
-        $contact = format_email_recipient($sql_arr['email'], $sql_arr['name']);
-        // when we've got more than one book, we need to skip duplicates
-        if ($books_num == 1 || !in_array($contact, $contacts)) {
-          $contacts[] = $contact;
-          if (count($contacts) >= $MAXNUM)
-            break 2;
+        foreach ((array)$abook->get_col_values('email', $sql_arr, true) as $email) {
+          $contact = format_email_recipient($email, $sql_arr['name']);
+          // when we've got more than one book, we need to skip duplicates
+          if ($books_num == 1 || !in_array($contact, $contacts)) {
+            $contacts[] = $contact;
+            if (count($contacts) >= $MAXNUM)
+              break 2;
+          }
         }
       }
     }
diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
index 2b342a9..335945c 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -1133,7 +1133,7 @@
   
   $out = html::div($attrib,
     $OUTPUT->form_tag(array('name' => 'uploadform', 'method' => 'post', 'enctype' => 'multipart/form-data'),
-      html::div(null, rcmail_compose_attachment_field(array('size' => $attrib[attachmentfieldsize]))) .
+      html::div(null, rcmail_compose_attachment_field(array('size' => $attrib['attachmentfieldsize']))) .
       html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))) .
       html::div('buttons',
         $button->show(rcube_label('close'), array('class' => 'button', 'onclick' => "$('#$attrib[id]').hide()")) . ' ' .
diff --git a/skins/default/addressbook.css b/skins/default/addressbook.css
index a90dcf1..6e07281 100644
--- a/skins/default/addressbook.css
+++ b/skins/default/addressbook.css
@@ -214,3 +214,166 @@
   text-align: right;
 }
 
+#contacttabs
+{
+	position: relative;
+	padding-bottom: 22px;
+}
+
+#contacttabs div.tabsbar {
+	top: 0;
+	left: 2px;
+}
+
+#contacttabs fieldset.tabbed {
+	position: relative;
+	top: 22px;
+	min-height: 5em;
+}
+
+#contacthead
+{
+	margin-bottom: 1em;
+	border: 0;
+	padding: 0;
+}
+
+#contacthead .names span.namefield,
+#contacthead .names input
+{
+	font-size: 140%;
+}
+
+#contacthead .displayname span.namefield
+{
+	font-size: 120%;
+}
+
+#contacthead span.nickname:before,
+#contacthead span.nickname:after,
+#contacthead input.ff_nickname:before,
+#contacthead input.ff_nickname:after
+{
+	content: '"';
+}
+
+#contacthead input
+{
+	margin-right: 6px;
+	margin-bottom: 0.2em;
+}
+
+#contacthead .names input,
+#contacthead .addnames input,
+#contacthead .jobnames input
+{
+	width: 180px;
+}
+
+#contacthead input.ff_prefix,
+#contacthead input.ff_suffix
+{
+	width: 90px;
+}
+
+#contacthead .addnames input.ff_name
+{
+	width: 374px;
+}
+
+#contactphoto
+{
+	float: right;
+	width: 60px;
+	margin-left: 3em;
+	margin-right: 4px;
+}
+
+#contactpic
+{
+	width: 60px;
+	min-height: 60px;
+	border: 1px solid #ccc;
+	background: white;
+}
+
+#contactpic img {
+	width: 60px;
+}
+
+#contactphoto .formlinks
+{
+	margin-top: 0.5em;
+	text-align: center;
+}
+
+fieldset.contactfieldgroup
+{
+	border: 0;
+	margin: 0.5em 0;
+	padding: 0.5em 2px;
+}
+
+fieldset.contactfieldgroup legend
+{
+	font-size: 0.9em;
+}
+
+.contactfieldgroup .row
+{
+	position: relative;
+	margin-bottom: 0.4em;
+}
+
+.contactfieldgroup .contactfieldlabel
+{
+	position: absolute;
+	top: 0;
+	left: 2px;
+	width: 90px;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	color: #666;
+	font-weight: bold;
+}
+
+.contactfieldgroup .contactfieldlabel select
+{
+	width: 78px;
+	background: none;
+	border: 0;
+	color: #666;
+	font-weight: bold;
+	padding-left: 0;
+}
+
+.contactfieldgroup .contactfieldcontent
+{
+	padding-left: 100px;
+	min-height: 1em;
+	line-height: 1.3em;
+}
+
+.contactfieldgroup .contactfield {
+	line-height: 1.3em;
+}
+
+.contactcontrolleraddress .contactfieldcontent input {
+	margin-bottom: 0.1em;
+}
+
+.contactfieldcontent .contactfieldbutton {
+	vertical-align: middle;
+	margin-left: 0.5em;
+}
+
+#upload-form
+{
+	padding: 6px;
+}
+
+#upload-form div
+{
+	padding: 2px;
+}
diff --git a/skins/default/common.css b/skins/default/common.css
index e052552..0d9b307 100644
--- a/skins/default/common.css
+++ b/skins/default/common.css
@@ -76,6 +76,12 @@
   padding: 1px 3px;
 }
 
+input.placeholder,
+textarea.placeholder
+{
+  color: #aaa;
+}
+
 input.button
 {
   height: 20px;
@@ -114,6 +120,20 @@
   font-size: 11px;
 }
 
+.formlinks a,
+.formlinks a:visited
+{
+  color: #CC0000;
+  font-size: 11px;
+  text-decoration: none;
+}
+
+.formlinks a.disabled,
+.formlinks a.disabled:visited
+{
+  color: #999999;
+}
+
 /** common user interface objects */
 
 #mainscreen
diff --git a/skins/default/functions.js b/skins/default/functions.js
index 00e97fd..62e4783 100644
--- a/skins/default/functions.js
+++ b/skins/default/functions.js
@@ -25,9 +25,8 @@
 // Warning: don't place "caller" <script> inside page element (id)
 function rcube_init_tabs(id, current)
 {
-  var content = document.getElementById(id),
-    // get fieldsets of the higher-level (skip nested fieldsets)
-    fs = $('fieldset', content).not('fieldset > fieldset');
+  var content = $('#'+id),
+    fs = content.children('fieldset');
 
   if (!fs.length)
     return;
@@ -42,9 +41,7 @@
 
   // convert fildsets into tabs
   fs.each(function(idx) {
-    var tab, a, elm = $(this),
-      // get first legend element
-      legend = $(elm).children('legend');
+    var tab, a, elm = $(this), legend = elm.children('legend');
 
     // create a tab
     a   = $('<a>').text(legend.text()).attr('href', '#');
@@ -66,8 +63,7 @@
 
 function rcube_show_tab(id, index)
 {
-  var content = document.getElementById(id),
-    fs = $('fieldset', content).not('fieldset > fieldset');
+  var fs = $('#'+id).children('fieldset');
 
   fs.each(function(idx) {
     // Show/hide fieldset (tab content)
@@ -94,7 +90,8 @@
     mailboxmenu:    {id:'mailboxoptionsmenu', above:1},
     composemenu:    {id:'composeoptionsmenu', editable:1},
     // toggle: #1486823, #1486930
-    uploadmenu:     {id:'attachment-form', editable:1, above:1, toggle:!bw.ie&&!bw.linux }
+    uploadmenu:     {id:'attachment-form', editable:1, above:1, toggle:!bw.ie&&!bw.linux },
+    uploadform:     {id:'upload-form', editable:1, toggle:!bw.ie&&!bw.linux }
   };
 
   var obj;
@@ -135,6 +132,9 @@
 
     if (!above && pos.top + ref.offsetHeight + obj.height() > window.innerHeight)
       above = true;
+
+    if (pos.left + obj.width() > window.innerWidth)
+      pos.left = window.innerWidth - obj.width() - 30;
 
     obj.css({ left:pos.left, top:(pos.top + (above ? -obj.height() : ref.offsetHeight)) });
   }
@@ -500,6 +500,9 @@
     if (rcmail.env.action == 'compose')
       rcmail_ui.init_compose_form();
   }
+  else if (rcmail.env.task == 'addressbook') {
+    rcmail.addEventListener('afterupload-photo', function(){ rcmail_ui.show_popup('uploadform', false); });
+  }
 }
 
 // Events handling in iframes (eg. preview pane)
diff --git a/skins/default/iehacks.css b/skins/default/iehacks.css
index 29ab8cb..4c0816a 100644
--- a/skins/default/iehacks.css
+++ b/skins/default/iehacks.css
@@ -236,3 +236,9 @@
 {
   margin-top: 2px;
 }
+
+.contactfieldgroup legend
+{
+	padding: 0 0 0.5em 0;
+	margin-left: -4px;
+}
diff --git a/skins/default/images/contactpic.png b/skins/default/images/contactpic.png
new file mode 100644
index 0000000..bdb6cdc
--- /dev/null
+++ b/skins/default/images/contactpic.png
Binary files differ
diff --git a/skins/default/mail.css b/skins/default/mail.css
index 7bb308c..3057229 100644
--- a/skins/default/mail.css
+++ b/skins/default/mail.css
@@ -1342,20 +1342,6 @@
   display: none;
 }
 
-.formlinks a,
-.formlinks a:visited
-{
-  color: #999999;
-  font-size: 11px;
-  text-decoration: none;
-}
-
-.formlinks a,
-.formlinks a:visited
-{
-  color: #CC0000;
-}
-
 #compose-editorfooter
 {
   position: absolute;
diff --git a/skins/default/templates/contact.html b/skins/default/templates/contact.html
index 06d0fbe..084664e 100644
--- a/skins/default/templates/contact.html
+++ b/skins/default/templates/contact.html
@@ -9,12 +9,17 @@
 
 <div id="contact-title" class="boxtitle"><roundcube:label name="contactproperties" /></div>
 <div id="contact-details" class="boxcontent">
-  <roundcube:object name="contactdetails" />
+  <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div>
+  <roundcube:object name="contacthead" id="contacthead" />
+  <div style="clear:both"></div>
+  <div id="contacttabs">
+    <roundcube:object name="contactdetails" />
+  </div>
   <p>
     <roundcube:button command="edit" type="input" class="button" label="editcontact" condition="!ENV:readonly" />
   </p>
 </div>
-<script type="text/javascript">rcube_init_tabs('contact-details')</script>
+<script type="text/javascript">rcube_init_tabs('contacttabs')</script>
 
 </body>
 </html>
diff --git a/skins/default/templates/contactadd.html b/skins/default/templates/contactadd.html
index 1a10f10..b5fd056 100644
--- a/skins/default/templates/contactadd.html
+++ b/skins/default/templates/contactadd.html
@@ -5,18 +5,34 @@
 <roundcube:include file="/includes/links.html" />
 <script type="text/javascript" src="/functions.js"></script>
 </head>
-<body class="iframe">
+<body class="iframe" onload="rcube_init_mail_ui()">
 
 <div id="contact-title" class="boxtitle"><roundcube:label name="addcontact" /></div>
 <div id="contact-details" class="boxcontent">
-  <roundcube:object name="contacteditform" size="40" />
+<form name="editform" method="post" action="./">
+  <div id="contactphoto">
+    <roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" />
+    <div class="formlinks">
+      <roundcube:button command="upload-photo" id="uploadformlink" type="link" label="addphoto" class="disabled" classAct="active" onclick="rcmail_ui.show_popup('uploadform', true);return false" condition="env:photocol" /><br/>
+      <roundcube:button command="delete-photo" type="link" label="delete" class="disabled" classAct="active" condition="env:photocol" />
+    </div>
+  </div>
+  <roundcube:object name="contactedithead" id="contacthead" size="16" form="editform" />
+  <div style="clear:both"></div>
+  
+  <div id="contacttabs">
+    <roundcube:object name="contacteditform" size="40" textareacols="60" deleteIcon="/images/icons/delete.png" form="editform" />
+  </div>
   <p>
     <input type="button" value="<roundcube:label name="cancel" />" class="button" onclick="history.back()" />&nbsp;
     <roundcube:button command="save" type="input" class="button mainaction" label="save" />
   </p>
 </form>
 </div>
-<script type="text/javascript">rcube_init_tabs('contact-details')</script>
+
+<roundcube:object name="photoUploadForm" id="upload-form" size="30" class="popupmenu" />
+
+<script type="text/javascript">rcube_init_tabs('contacttabs')</script>
 
 </body>
 </html>
diff --git a/skins/default/templates/contactedit.html b/skins/default/templates/contactedit.html
index a15aaf2..681201c 100644
--- a/skins/default/templates/contactedit.html
+++ b/skins/default/templates/contactedit.html
@@ -5,18 +5,34 @@
 <roundcube:include file="/includes/links.html" />
 <script type="text/javascript" src="/functions.js"></script>
 </head>
-<body class="iframe">
+<body class="iframe" onload="rcube_init_mail_ui()">
 
 <div id="contact-title" class="boxtitle"><roundcube:label name="editcontact" /></div>
 <div id="contact-details" class="boxcontent">
-  <roundcube:object name="contacteditform" size="40" />
+<form name="editform" method="post" action="./">
+  <div id="contactphoto">
+    <roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" />
+    <div class="formlinks">
+      <roundcube:button command="upload-photo" id="uploadformlink" type="link" label="replacephoto" class="disabled" classAct="active" onclick="rcmail_ui.show_popup('uploadform', true);return false" condition="env:photocol" /><br/>
+      <roundcube:button command="delete-photo" type="link" label="delete" class="disabled" classAct="active" condition="env:photocol" />
+    </div>
+  </div>
+  <roundcube:object name="contactedithead" id="contacthead" size="16" form="editform" />
+  <div style="clear:both"></div>
+  
+  <div id="contacttabs">
+    <roundcube:object name="contacteditform" size="40" textareacols="60" deleteIcon="/images/icons/delete.png" form="editform" />
+  </div>
   <p>
     <roundcube:button command="show" type="input" class="button" label="cancel" />&nbsp;
     <roundcube:button command="save" type="input" class="button mainaction" label="save" />
   </p>
 </form>
 </div>
-<script type="text/javascript">rcube_init_tabs('contact-details')</script>
+
+<roundcube:object name="photoUploadForm" id="upload-form" size="30" class="popupmenu" />
+
+<script type="text/javascript">rcube_init_tabs('contacttabs')</script>
 
 </body>
 </html>

--
Gitblit v1.9.1