Index: /branches/devel-composer/.htaccess
===================================================================
--- /branches/devel-composer/.htaccess	(revision 5386)
+++ /branches/devel-composer/.htaccess	(revision 5386)
@@ -0,0 +1,52 @@
+# AddDefaultCharset	UTF-8
+AddType text/x-component .htc
+
+<IfModule mod_php5.c>
+php_flag	display_errors	Off
+php_flag	log_errors	On
+# php_value	error_log	logs/errors
+
+php_value	upload_max_filesize	5M
+php_value	post_max_size		6M
+php_value	memory_limit		64M
+
+php_flag	zlib.output_compression		Off
+php_flag	magic_quotes_gpc		Off
+php_flag	magic_quotes_runtime		Off
+php_flag	zend.ze1_compatibility_mode	Off
+php_flag 	suhosin.session.encrypt 	Off
+
+#php_value	session.cookie_path		/
+php_value	session.auto_start	0
+php_value	session.gc_maxlifetime	21600
+php_value	session.gc_divisor	500
+php_value	session.gc_probability	1
+
+# http://bugs.php.net/bug.php?id=30766
+php_value	mbstring.func_overload	0
+</IfModule>
+
+<IfModule mod_rewrite.c>
+RewriteEngine On
+RewriteRule ^favicon.ico$ skins/default/images/favicon.ico
+# security rules
+RewriteRule .svn/ - [F]
+RewriteRule ^README|INSTALL|LICENSE|SQL|bin|CHANGELOG$ - [F]
+</IfModule>
+
+<IfModule mod_deflate.c>
+SetOutputFilter DEFLATE
+</IfModule>
+
+<IfModule mod_headers.c>
+# replace 'append' with 'merge' for Apache version 2.2.9 and later
+#Header append Cache-Control public env=!NO_CACHE
+</IfModule>
+
+<IfModule mod_expires.c>
+ExpiresActive On
+ExpiresDefault "access plus 1 month"
+</IfModule>
+
+FileETag MTime Size
+Options -Indexes
Index: /branches/devel-composer/CHANGELOG
===================================================================
--- /branches/devel-composer/CHANGELOG	(revision 5386)
+++ /branches/devel-composer/CHANGELOG	(revision 5386)
@@ -0,0 +1,1190 @@
+CHANGELOG Roundcube Webmail
+===========================
+
+- TinyMCE:
+  - updated to 3.4.6
+  - security issue: removed moxieplayer (embedding flv and mp4 is not supported anymore)
+- Fix IDN address validation issue (#1488137)
+- Fix JS error when dst_active checkbox doesn't exist (#1488133)
+- Autocomplete LDAP records when adding contacts from mail (#1488073)
+- Plugin API: added 'ready' hook (#1488063)
+- Ignore DSN request when it isn't supported by SMTP server (#1487800)
+- Make sure LDAP name fields aren't arrays (#1488108)
+- Fixed imap test to non-default port when using ssl (#1488118)
+- Force all files to be overwritten when updating (#1488117)
+- Fix issue where it wasn't possible to change list view mode in folder manager for INBOX (#1488107)
+- Fix namespace handling in special folders settings (#1488112)
+- Disable time limit for CLI scripts (#1488109)
+- Fix misleading display when chaning editor type (#1488104)
+- Add loading indicator on contact delete
+- Fix bug where after delete message rows can be added to the list of another folder (#1487752)
+- Add notice on autocompletion that not all records were displayed
+- Add option 'searchonly' for LDAP address books
+- Add Priority filter to the messages list
+- Cache synchronization using QRESYNC/CONDSTORE
+- Trigger 'new_messages' hook for all checked folders (#1488083)
+- Make date/time format user configurable; drop 'date_today' config option
+- Fix setting title for truncated subject in IE (#1487128)
+- Fix displaying multipart/alternative messages with only one part (#1487938)
+- Rewritten messages caching:
+  Indexes are stored in a separate table, so there's no need to store all messages in a folder
+  Added threads data caching
+  Flags are stored separately, so flag change doesn't cause DELETE+INSERT, just UPDATE
+- Improved FETCH response handling
+- Improvements in response tokenization method
+- Use 'From' and 'To' labels instead of 'Sender' and 'Recipient'
+- Fix username case-insensitivity issue in MySQL (#1488021)
+- Addressbook Saved Searches
+- Added spellchecker exceptions dictionary (shared or per-user)
+- Added possibility to ignore words containing caps, numbers, symbols (spellcheck_ignore_* options)
+- Added 'priority' column on messages list (#1486782)
+- Localize forwarded message header (#1488058)
+
+RELEASE 0.6
+-----------
+- Fix bug where the last identity is used on reply (#1488101)
+- Fix locked folder rename option on servers supporting RFC2086 only (#1488089)
+- Fix session race conditions when composing new messages
+- Fix encoding of LDAP contacts identifiers (#1488079)
+- jQuery 1.6.4
+- Fix handling of binary attachments encoded with quoted-printable (#1488065)
+- Fix text-overflow:ellipsis issues on messages list in FF7 and Webkit (#1488061)
+- Fix handling of links with IP address
+- Fix compacting folder resets message list filter (#1488076)
+
+RELEASE 0.6-rc
+----------------
+- Send X-Frame-Options headers to protect from clickjacking (#1487037)
+- Fallback to mail_domain in LDAP variable replacements; added 'host' to 'user_create' hook arguments (#1488024)
+- Fixed wrong vCard type parameter mobile (#1488067)
+- Fixed vCard WORKFAX issue (#1488046)
+- Add vCard's Profile URL support (#1488062)
+- jQuery 1.6.3
+- Fix imap_cache setting to values other than 'db' (#1488060)
+- Fix handling of attachments inside message/rfc822 parts (#1488026)
+- Make list of mimetypes that open in preview window configurable (#1487625)
+- Added plugin hook 'message_part_get' for attachment downloads
+- Added unique connection identifier to IMAP debug messages
+- Fix image type check for contact photo uploads
+
+RELEASE 0.6-beta
+----------------
+- Fixed selecting identity on reply/forward (#1487981)
+- Add option to hide selected LDAP addressbook on the list
+- Add client-side checking of uploaded files size
+- Add newlines between organization, department, jobtitle (#1488028)
+- Recalculate date when replying to a message and localize the cite header (#1487675)
+- Fix handling of email addresses with quoted local part (#1487939)
+- Fix EOL character in vCard exports (#1487873)
+- Added optional "multithreading" autocomplete feature
+- Plugin API: Added 'config_get' hook
+- Fixed new_user_identity plugin to work with updated rcube_ldap class (#1487994)
+- Plugin API: added folder_delete and folder_rename hooks
+- Added possibility to undo last contact delete operation
+- Fix sorting of contact groups after group create (#1487747)
+- Add optional textual upload progress indicator (#1486039)
+- Fix parsing URLs containing commas (#1487970)
+- Added vertical splitter for books/groups list in addressbook (#1487923)
+- Improved namespace roots handling in folder manager
+- Added searching in all addressbook sources
+- Added addressbook source selection in contacts import
+- Implement LDAPv3 Virtual List View (VLV) for paged results listing
+- Use 'address_template' config option when adding a new address block (#1487944)
+- Added addressbook advanced search
+- Add popup with basic fields selection for addressbook search
+- Case-insensitive matching in autocompletion (#1487933)
+- Added option to force spellchecking before sending a message (#1485458)
+- Fix handling of "<" character in contact data, search fields and folder names (#1487864)
+- Fix saving "<" character in identity name and organization fields (#1487864)
+- Added option to specify to which address book add new contacts
+- Added plugin hook for keep-alive requests
+- Store user preferences in session when write-master is not available and session is stored in memcache, write them later
+- Improve performence of folder manager operations
+- Fix default_port option handling in Installer when config.inc.php file exists (#1487925)
+- Removed option focus_on_new_message, added newmail_notifier plugin
+- Added general rcube_cache class with Memcache and APC support
+- Improved caching performance by skipping writes of unchanged data
+- Option enable_caching replaced by imap_cache and messages_cache options
+- Fix WORKFAX saving in address book (#1487910)
+- Add forward-as-attachment feature
+- jQuery-1.6.2 (#1487913, #1487144)
+- Improve display name composition when saving contacts (#1487143)
+- Fix problems with subfolders of INBOX folder on some IMAP servers (#1487725)
+- Fix handling of folders that doesn't belong to any namespace (#1487637)
+- Enable multiselection for attachments uploading in capable browsers (#1485969)
+- Add possibility to change HTML editor configuration by skin
+- Fix a bug where selecting too many contacts would produce too large URI request (#1487892)
+- Improve performance by including files with absolute path (#1487849)
+- Move folder name truncation to client/skin (#1485412)
+- Added plugin hook for request token creation
+- Replace LDAP vars in group queries (#1487837)
+- Fix vcard folding with uncode characters (#1487868)
+- Keep all submitted data if contact form validation fails (#1487865)
+- Handle uncode strings in rcube_addressbook::normalize_string() (#1487866)
+- Fix handling of debug_level=4 in ajax requests (#1487831)
+- Enable TinyMCE's contextmenu (#1487014)
+- Allow multiple concurrent compose sessions
+- New config option for custom logo
+- Allow skins to define/override texts with <roundcube:label />
+- Add simple ACL rights/namespace handling in folder manager
+- Force IE to send referers (#1487806)
+- Better display of vcard import results (#1485457)
+- Improved vcard import
+- Interactive update script with improved DB schema check
+- Fix problem with contactgroupmembers table creation on MySQL 4.x, add index on contact_id column
+- Add LDAP SASL bind and proxy authentication (#1486692)
+- Replying to a sent message puts the old recipient as the new recipient (#1487074)
+- Fulltext search over (almost) all data for contacts
+- Extend address book with rich contact information
+
+RELEASE 0.5.4
+-------------
+- Fix XSS vulnerability in UI messages (#1488030)
+
+RELEASE 0.5.3
+-------------
+- Fix identities "reply-to" and "bcc" fields have a bogus value when left empty (#1487943)
+- Fix issue which cases IMAP disconnection when encrypt() method was used (#1487900)
+- Fix some CSS issues in Settings for Internet Explorer 
+- Fixed handling of folder with name "0" in folder selector
+- Fix bug where messages were deleted instead moved to trash folder after Shift key was used (#1487902)
+- Fix relative URLs handling according to a <base> in HTML (#1487889)
+- Fix handling of top-level domains with more than 5 chars or unicode chars (#1487883)
+- Fix usage of non-standard HTTP error codes (#1487797)
+- Fix PHP warning on mistaken in_array() usage (#1487901)
+
+RELEASE 0.5.2
+-------------
+- TinyMCE 3.4.2 now compatible with IE9
+- PEAR::Net_SMTP 1.5.2, fixed timeout issue (#1487843)
+- Fix bug where template name without plugin prefix was used in render_page hook
+- Support 'abort' and 'result' response in 'preferences_save' hook, add error handling
+- Fix bug where some content would cause hang on html2text conversion (#1487863)
+- Improve space-stuffing handling in format=flowed messages (#1487861)
+- Fix bug where some dates would produce SQL error in MySQL (#1487856)
+- Added workaround for some IMAP server with broken STATUS response (#1487859)
+- Fix bug where default_charset was not used for text messages (#1487836)
+- Stateless request tokens. No keep-alive necessary on login page (#1487829)
+- Force names of unique constraints in PostgreSQL DDL
+- Add code for prevention from IMAP connection hangs when server closes socket unexpectedly
+- Remove redundant DELETE query (for old session deletion) on login
+- Get around unreliable rand() and mt_rand() in session ID generation (#1486281)
+- Fix some emails are not shown using Cyrus IMAP (#1487820)
+- Fix handling of mime-encoded words with non-integral number of octets in a word (#1487801)
+- Fix parsing links with non-printable characters inside (#1487805)
+- Fixed de_CH Localization bugs (#1487773)
+- Add variable for 'Today' label in date_today option (#1486120)
+- Fix dont_override setting does not override existing user preferences (#1487664)
+- Use only one from IMAP authentication methods to prevent login delays (1487784)
+- Support strftime format in date_today option
+- Fix SQL query in rcube_user::query() so it uses index on MySQL again
+- Removed redundant </form> tags from contact add/edit pages
+- Fix CSS error in contact details screen on IE7 (#1487775)
+
+RELEASE 0.5.1
+-------------
+- Fix handling of attachments with invalid content type (#1487767)
+- Add workaround for DBMail's bug http://www.dbmail.org/mantis/view.php?id=881 (#1487766)
+- Use IMAP's ID extension (RFC2971) to print more info into debug log
+- Security: add optional referer check to prevent CSRF in GET requests
+- Fix email_dns_check setting not used for identities/contacts (#1487740)
+- Fix ICANN example addresses doesn't validate (#1487742)
+- Security: protect login form submission from CSRF
+- Security: prevent from relaying malicious requests through modcss.inc
+- Fix handling of non-image attachments in multipart/related messages (#1487750)
+- Fix IDNA support when IDN/INTL modules are in use (#1487742)
+- Fix handling of invalid HTML comments in messages (#1487759)
+- Fix parsing FETCH response for very long headers (#1487753)
+- Fix add/remove columns in message list when message_sort_order isn't set (#1487751)
+- Check mime headers before attempt to parse them (#1487745)
+- Quote header values in show_additional_headers plugin (#1487744)
+- Fix settings UI on IE 6 (#1487724)
+- Remove double borders in folder listing (#1487713)
+- Separate full message headers UI element from headers table (#1487715)
+- Add part MIME ID to message_part_* hooks (#1487718) 
+- Improve parsing of MS Outlook vCards (#1487716)
+- Updated PEAR::Net_Socket to 1.0.10
+- Updated PEAR::Net_IDNA2 to 0.1.1
+- Fix handling of comments inside an email address spec. (#1487673)
+- Show full mail subject as title when hovering a cut subject link (#1487128)
+- Fix randomly disappearing folders list in IE (#1487704)
+- Fix list column add/removal in IE (#1487703)
+- Fix login redirect issues (#1487686)
+- Require PHP 5.2.1 or greater
+- Fix %h/%z variables in username_domain option (#1487701)
+- Workaround for setting charset in case of malformed bodystructure response (#1487700)
+- Fix impossible to subscribe to protected folders (#1487656)
+- Fix setting timezone in Preferences (#1487705)
+
+RELEASE 0.5
+-----------
+- Fix double-login/session issue (#1487104)
+- Wrap HTML parts with <html><body> and add Doctype declaration (#1487098)
+- Make rcube_autoload silently skip unknown classes (#1487109)
+- Fix charset detection in vcards with encoded values (#1485542)
+- Better CSS cursors for splitters (#1486874)
+- Show the same message only once (#1487641)
+- Fix namespaces handling (#1487649)
+- Add handling of multifolder METADATA/ANNOTATION responses
+- Fix handling of INBOX when personal namespace prefix is non-empty (#1487657)
+- Fix handling square brackets in links (#1487672)
+- Add description of 'use_https' option in main.inc.php.dist file
+
+RELEASE 0.5-RC
+--------------
+- Plugin API: Add 'pass' argument in 'authenticate' hook (#1487134)
+- Fix attachments of type message/rfc822 are not listed on attachments list
+- Add 'login_lc' config option for case-insensitive authentication (#1487113)
+- Fix window is blur'ed in IE when selecting a message (#1487316)
+- Fix cursor position on compose form in Webkit browsers (#1486674)
+- Fix setting charset of attachment filenames (#1487122)
+- Allow setting autocomplete attribute for all inputs separately (#1487313)
+- New Folder Manager UI
+- Fix invalid Request when creating a folder (#1487443)
+- Add folder size and quota indicator in folder manager (#1485780)
+- Add possibility to move a subfolder into root folder (#1486791)
+- Fix copying all messages in a folder copies only messages from current page
+- Improve performance of moving or copying of all messages in a folder
+- Fix plaintext versions of HTML messages don't contain placeholders for emotions (#1485206)
+- Improve performance of folder rename and delete actions
+- Better support for READ-ONLY and NOPERM responses handling (#1487083)
+- Add confirmation message on purge/expunge command response
+- Fix handling of untagged responses for AUTHENTICATE command (#1487450)
+- Add username and IP address to log message on unsuccessful login (#1487626)
+- Improved Mail-Followup-To and Mail-Reply-To headers handling
+- Fix charset conversion for text attachments without charset specification (#1487634)
+
+RELEASE 0.5-BETA
+----------------
+- Make session data storage more robust against garbage session data (#1487136)
+- Config option for autocomplete on login screen
+- Allow plugin templates to include local files (#1487133)
+- List groups in address detail view and allow to subscribe/unsubscribe from there (#1486753)
+- Messages caching: performance improvements, fixed syncing, fixes related with #1486748
+- Add link to identities in compose window (#1486729)
+- Add Internationalized Domain Name (IDNA) support (#1483894)
+- Add option to automatically send read notifications for known senders (#1485883)
+- Add option to "Return receipt" will be always checked (#1486352)
+- Fix HTML to plain text conversion doesn't handle citation blocks (#1486921)
+- Use custom sorting when SORT is disabled by IMAP admin (#1486959)
+- Allow setting some washtml options from plugin (#1486578)
+- Add option do bind for an individual LDAP address book (#1486997)
+- Change reply prefix to display email address only if sender name doesn't exist (#1486550)
+- Plugin API: improved 'abort' flag handling, added 'result' item in some hooks (#1486914)
+- Fix mailto optional params in plain text messages aren't handled (#1487026)
+- Add Reply-to-List feature (#1484252)
+- Add Mail-Followup-To/Mail-Reply-To support (#1485547)
+- Fix confirmation message isn't displayed after sending mail on Chrome (#1486177)
+- Fix keyboard doesn't work with autocomplete list with Chrome (#1487029)
+- Improve tabs to fixed width and add tabs in identities info (#1486974)
+- Add unique index on users.username+users.mail_host
+- Make htmleditor option more consistent and add option to use HTML on reply to HTML message (#1485840)
+- Use empty envelope sender address for message disposition notifications (RFC 2298.3)
+- Support SMTP Delivery Status Notifications - RFC 3461 (#1486142)
+- Use css sprite image for messages list
+- Add (different) attachment icon for messages of type multipart/report (#1486165)
+- Prevent from inserting empty link when composing HTML message (#1486944)
+- Add caching support in id2uid and uid2id functions (#1487019)
+- Add SASL proxy authentication for SMTP (#1486693)
+- Improve displaying of UI messages (#1486977)
+- Fix double e-mail filed in identity form (#1487054)
+- Display IMAP errors for LIST/THREAD/SEARCH commands (#1486905)
+- Add LITERAL+ (IMAP4 non-synchronizing literals) support (RFC 2088)
+- Add separate column for message status icon (#1486665)
+- Add ACL extension support into IMAP classes (RFC 4314)
+- Add ANNOTATEMORE extension support into IMAP classes (draft-daboo-imap-annotatemore)
+- Add METADATA extension support into IMAP classes (RFC 5464)
+- Fix decoding of e-mail address strings in message headers (#1487068)
+- Fix handling of attachments when Content-Disposition is not inline nor attachment (#1487051)
+- Improve performance of unseen messages counting (#1487058)
+- Improve performance of messages counting using ESEARCH extension (RFC4731)
+- Add LIST-STATUS support in rcube_imap_generic class (RFC 5819)
+- Add SASL-IR support in IMAP (RFC 4959)
+- Add LOGINDISABLED support (RFC 2595)
+- Add support for AUTH=PLAIN in IMAP authentication
+- Re-implemented SMTP proxy authentication support
+- Add support for IMAP proxy authentication (#1486690)
+- Add support for AUTH=DIGEST-MD5 in IMAP (RFC 2831)
+- Fix parent folder with unread subfolder not bold when message is open (#1487078)
+- Add basic IMAP LIST's \Noselect option support
+- Add support for selection options from LIST-EXTENDED extension (RFC 5258)
+- Don't list subscribed but non-existent folders (#1486225)
+- Fix handling of URLs with tilde (~) or semicolon (;) character (#1487087, #1487088)
+- Plugin API: added 'contact_form' hook
+- Add SORT=DISPLAY support (RFC 5957)
+- Plugin API: add possibility to disable plugin in AJAX mode, 'noajax' property
+- Plugin API: add possibility to disable plugin in framed mode, 'noframe' property
+- Improve performance of setting IMAP flags using .SILENT suffix
+- Improve performance of message cache status checking with skip_disabled=true
+- Support contact's email addresses up to 255 characters long (#1487095)
+- Add option to place replies in the folder of the message being replied to (#1485945)
+- Add missing confirmation/error messages on contact/group/message actions (#1486845)
+- Add 'loading' message on message move/copy/delete/mark actions
+- Improve responsiveness of messages displaying (#1486986)
+- Add option for minimum length of autocomplete's string (#1486428)
+- Fix operations on messages in unsubscribed folder (#1487107)
+- Add support for shared folders (#1403507)
+- Fix handling of folders with name "0" (#1487119)
+- Fix handling of folders with "<>" characters in name
+- jQuery 1.4.4
+- Fix handling of HTML entity strings in plain text messages
+- Fix focused elements aren't unfocused when clicking on the list (#1487123)
+- Fix error in MSSQL DDL scripts (#1487112)
+- Lock submit button in onsubmit event on login page (#1487036)
+- Don't set attachment's charset in Content-type header (#1487122)
+- Fix handling of message bodies (quoted-printable encoded) with NULL characters (#1486189)
+- Add workaround for MSOE's multipart/related messages with non-related attachments
+
+RELEASE 0.4.2
+-------------
+- Fix handling of backslash as IMAP delimiter
+- Fix charset replacement in HTML message bodies (#1487021)
+- Fix: contact group input is empty when using rename action more than once on the same group record
+- Fix "Server Error! (Not Found)" when using utils/save-pref action (#1487023)
+- Fix handling of Thunderbird's vCards (#1487024)
+
+RELEASE 0.4.1
+-------------
+- Fix space-stuffing in format=flowed messages (#1487018)
+- Fix msgexport.sh now using the new imap wrapper
+- Avoid displaying password on shell (#1486947)
+- Only lower-case user name if first login attempt failed (#1486393)
+- Make alias setting in squirrelmail_usercopy plugin configurable (patch by pommi, #1487007)
+- Prevent from saving a non-existing skin path in user prefs (#1486936)
+- Improve handling of single-part messages with bogus BODYSTRUCTURE (#1486898)
+- Fix path to SQL files when using pgsql/mysqli/sqlsrv drivers (#1486902)
+- Fix upgrade script for SQLite (#1486903)
+- Fixes in SQL init script + added update script for MSSQL database
+- Remove redundant date in syslog messages (#1486945)
+- Fix contacts list page controls when a group is selected (#1486946)
+- Fix SMTP test in Installer (#1486952)
+- Fix "Select all" causes message to be opened in folder with exactly one message (#1486913)
+- Fix Tab key doesn't work in HTML editor in Google Chrome (#1486925)
+- Fix TinyMCE uses zh_CN when zh_TW locale is set (#1486929)
+- Fix TinyMCE buttons are hidden in Opera (#1486922)
+- Fix JS error on IE when trying to send HTML message with enabled spellchecker (#1486940)
+- Display inline images with known extensions and non-image content-type (#1486934)
+- Fix "Threaded" checkbox after subfolder creation (#1486928)
+- Fix timezone string in sent mail (#1486961)
+- Show disabled checkboxes for protected folders instead of dots (#1485498)
+- Added fieldsets in Identity form, added 'identity_form' hook
+- Re-added 'Close' button in upload form (#1486930, #1486823)
+- Fix handling of charsets with LATIN-* label
+- Fix messages background image handling in some cases (#1486990)
+- Fix format=flowed handling (#1486989)
+- Fix when IMAP connection fails in 'get' action session shouldn't be destroyed (#1486995)
+- Fix list_cols is not updated after column dragging (#1486999)
+- Support %z variable in host configuration options (#1487003)
+
+RELEASE 0.4
+-----------
+- Fix disapearing upload form disapears when user selects a file on Safari (#1486823)
+- Don't replace error messages with loading info (#1486300)
+- Fix JS errors on compose mode switch (#1486870)
+- Fix message structure parsing when it lacks optional fields (#1486881)
+- Include all recipients in sendmail log
+- Support HTTP_X_FORWARDED_PROTO header for HTTPS detecting (#1486866)
+- Fix default IMAP port configuration (#1486864)
+- Create Sent folder when starting to compose a new message (#1486802)
+- Fix handling of messages with Content-Type: application/* and no filename (#1484050)
+- Improved compose screen: resizable body and attachments list, vertical splitter, options menu
+- Fix RC forgets search results (#1483883)
+- TinyMCE 3.3.7
+- Improve parsing of styled empty tags in HTML messages (#1486812)
+- Add %dc variable support in base_dn/bind_dn config (#1486779)
+- Add button to hide/unhide the preview pane (#1484215)
+- Fix no-cache headers on https to prevent content caching by proxies (#1486798)
+- Fix attachment filenames broken with TNEF decoder using long filenames (#1486795)
+- Use user's timezone in Date header, not server's timezone (#1486119)
+- Add option to set separate footer for HTML messages (#1486660)
+- Add real SMTP error description to displayed error messages (#1485927)
+- Fix some IMAP errors handling when opening the message (#1485443)
+- Fix related parts aren't displayed when got mimetype other than image/* (#1486432)
+- Multiple identity and database support for squirrelmail_usercopy plugin (#1486517)
+- Support dynamic hostname (%d/%n) variables in configuration options (#1485438)
+- Add 'messages_list' hook (#1486266)
+- Add request* event triggers in http_post/http_request (#1486054)
+- Fix use RFC-compliant line-delimiter when saving messages on IMAP (#1486712)
+- Add 'imap_timeout' option (#1486760)
+- Fix forwarding of messages with winmail attachments
+- Fix handling of uuencoded attachments in message body (#1485839)
+- Added list_mailboxes hook in rcube_imap::list_unsubscribed() (#1486668)
+- Fix wrong message on file upload error (#1486725)
+- Add support for data URI scheme [RFC2397] (#1486740)
+- Added 'actionbefore', 'actionafter', 'responsebefore', 'responseafter' events
+- Fix double-addition of e-mail domain to content ID in HTML images
+- Read and send messages with format=flowed (#1484370), fixes word wrapping issues (#1486543)
+- Fix duplicated attachments when forwarding a message (#1486487)
+- Fix message/rfc822 attachments containing only attachments are not parsed properly (#1486743)
+- Fix %00 character in winmail.dat attachments names (#1486738)
+- Fix handling errors of folder deletion (#1486705)
+- Parse untagged CAPABILITY response for LOGIN command (#1486742)
+- Renamed all php-cli scripts to use .sh extension
+- Some files from /bin + spellchecking actions moved to the new 'utils' task
+- Added thread tree icons
+- Extend contact groups support (#1486682)
+- Fix check-recent action issues and performance (#1486526)
+- Fix messages order after checking for recent (#1484664)
+- Fix autocomplete shows entries without email (#1486452)
+- Fix listupdate event doesn't trigger on search response (#1486708)
+- Fix select_all_mode value after selecting a message (#1486720)
+- Set focus to editor on reply in HTML mode (#1486632)
+- Fix composing in HTML jumps cursor to body instead of recipients (#1486674)
+- Allow columns order change per user - drag&drop (#1485795)
+- Add References header in read receipt (#1486681)
+- Fix database constraint violation when opening a message (#1486696)
+- Add 'loading' message while login is in progress (#1486667)
+- Fix quota_zero_as_unlimited (#1486662)
+- Fix folder subscription checking (#1486684)
+- Fix INBOX appears (sometimes) twice in mailbox list (#1486672)
+- Fix listing of attachments of some types e.g. "x-epoc/x-sisx-app" (#1486653)
+- Fix DB Schema checking when some db_table_* options are not set (#1486654)
+
+RELEASE 0.4-beta
+----------------
+- Add sizelimit and timelimit variables in LDAP config (#1486544)
+- Hide IMAP host dropdown when single host is defined (#1486326)
+- Add images pre-loading on login page (#1451160)
+- Add HTTP_X_REAL_IP and HTTP_X_FORWARDED_FOR to successful logins log (#1486441)
+- Fix setting spellcheck languages with extended codes (#1486605)
+- Fix messages list scrolling in FF3.6 (#1486472)
+- Fix quicksearch input focus (#1486637)
+- Always set changed date when flagging a DB record as deleted + provide a cleanup script
+- Fix address book/group selection (#1486619)
+- Assign newly created contacts to the active group (#1486626)
+- Added option not to mark messages as read when viewed in preview pane (#1485012)
+- Allow plugins modify the Sent folder when composing (#1486548)
+- Added optional (max_recipients) support to restrict total number of recipients per message (#1484542)
+- Re-organize editor buttons, add blockquote and search buttons
+- Make possible to write inside or after a quoted html message (#1485476)
+- Fix bugs on unexpected IMAP connection close (#1486190, #1486270)
+- Iloha's imap.inc rewritten into rcube_imap_generic class
+- Added contact groups in address book (not finished yet)
+- Added PageUp/PageDown/Home/End keys support on lists (#1486430)
+- Added possibility to select all messages in a folder (#1484756)
+- Added 'imap_force_caps' option for after-login CAPABILITY checking (#1485750)
+- Password: Support dovecotpw encryption
+- TinyMCE 3.3.1
+- Implemented messages copying using drag&drop + SHIFT (#1484086)
+- Improved performance of folders operations (#1486525)
+- Fix blocked.gif attachment is not attached to the message (#1486516)
+- Managesieve: import from Horde-INGO
+- Managesieve: support for more than one match (#1486078)
+- Managesieve: support for selectively disabling rules within a single sieve script (#1485882)
+- Threaded message listing now available
+- Added sorting by ARRIVAL and CC
+- Message list columns configurable by the user
+- Removed 'index_sort' option, now we're using empty 'message_sort_col' for this
+- virtuser_query: support other identity data (#1486148)
+- Options virtuser_* replaced with virtuser_* plugins
+- Plugin API: Implemented 'email2user' and 'user2email' hooks
+- Fix forwarding message omits CC header (#1486305)
+- Add 'default_charset' option to user preferences (#1485451)
+- Add 'delete_always' option to user preferences
+- Support/Require tls:// prefix in 'smtp_server' option for TLS connections
+- Fix inconsistent behaviour of 'delete_always' option (#1486299)
+- Fix deleting all messages from last list page (#1486293)
+- Flag original messages when sending a draft (#1486203)
+- Changed signature separator when top-posting (#1486330)
+- Let the admin define defaults for search modifiers (#1485897)
+- Fix long e-mail addresses validation (#1486453)
+- Remember search modifiers in user prefs (#1486146)
+- Added force_7bit option to force MIME encoding of plain/text messages (#1486510)
+- Use case sensitive check when checking for default folders (#1486346)
+- Fix checking for new mail: now checks unseen count of inbox (#1485794)
+- Improve performance by avoiding unnecessary updates to the session table (#1486325)
+- Fix invalid <font> tags which cause HTML message rendering problems (#1486521)
+- Fix CVE-2010-0464: Disable DNS prefetching (#1486449)
+- Fix Received headers to behave better with SpamAssassin (#1486513)
+- Password: Make passwords encoding consistent with core, add 'password_charset' global option (#1486473)
+- Fix adding contacts SQL error on mysql (#1486459)
+- Squirrelmail_usercopy: support reply-to field (#1486506)
+- Fix IE spellcheck suggestion popup issue (#1486471)
+- Fix email address auto-completion shows regexp pattern (#1486258)
+- Fix merging of configuration parameters: user prefs always survive (#1486368) 
+- Fix quota indicator value after folder purge/expunge (#1486488)
+- Fix external mailto links support for use as protocol handler (#1486037)
+- Fix attachment excessive memory use, support messages of any size (#1484660)
+- Fix setting task name according to auth state
+- Password: fix vpopmaild driver (#1486478)
+- Add workaround for MySQL bug [http://bugs.mysql.com/bug.php?id=46293] (#1486474)
+- Fix quoted text wrapping when replying to an HTML email in plain text (#1484141)
+- Fix handling of extended mailto links (with params) (#1486354)
+- Fix sorting by date of messages without date header on servers without SORT (#1486286)
+- Fix inconsistency when not using default table names (#1486467)
+- Fix folder rename/delete buttons do not appear on creation of first folder (#1486468)
+- Fix character set conversion fails on systems where iconv doesn't accept //IGNORE (#1486375)
+- Log in performance: Create default folders on first login only
+- Import contacts into the selected address book (by Phil Weir)
+- Add support for MDB2's 'sqlsrv' driver (#1486395)
+- Use jQuery-1.4
+- Removed problematic browser-caching of messages
+- Fix incompatybility with suhosin.executor.disable_emodifier (#1486321)
+- Use PLAIN auth when CRAM fails and imap_auth_type='check' (#1486371)
+- Fix removal of <title> tag from HTML messages (#1486432)
+- Fix 'force_https' to specified port when URL contains a port number (#1486411)
+- Fix to-text converting of HTML entities inside b/strong/th/hX tags (#1486422)
+- Bug in spellchecker suggestions when server charset != UTF8 (#1486406)
+- Managesieve: Fix requires generation for multiple actions (#1486397)
+- Fix LDAP problem with special characters in RDN (#1486320)
+- Improved handling of message parts of type message/rfc822
+- Plugin API: added 'quota' hook
+- Fix parsing conditional comments in HTML messages (#1486350)
+- Use built-in json_encode() for proper JSON format in AJAX replies
+- Allow setting only selected params in 'message_compose' hook (#1486312)
+- Plugin API: added 'message_compose_body' hook (#1486285)
+- Fix counters of all folders are checked in 'getunread' action  with check_all_folders disabled (#1486128)
+- Fix displaying alternative parts in messages of type message/rfc822 (#1486246)
+- Fix possible messages exposure when using Roundcube behind a proxy (#1486281)
+- Fix unicode para and line separators in javascript response (#1486310)
+- Additional_message_headers: allow unsetting headers, support plugin's config file (#1486268)
+- Fix displaying of hidden directories in skins list (#1486301)
+- Fix open_basedir restriction error when reading skins list (#1486304)
+- Fix pasting from Office apps into html editor (#1486271)
+- Fix empty <a> tags parsing (#1486272)
+- Don't cut off attachment names when using non-RFC2231 encoding (#1485515)
+- Allow inserting signatures above replied message body (#1484272)
+- Managesieve 2.0: multi-script support
+- Fix imap_auth_type regression (#1486263)
+
+RELEASE 0.3.1
+------------------
+- Specify toolbar container in compose template (#1486247)
+- Fix $_SERVER['HTTPS'] check for SSL forcing on IIS (#1486243)
+- Avoid unnecessary page loads for selected tab (#1486032)
+- Fix quota indicator issues by content generation on client-size (#1486197, #1486220)
+- Don't display disabled sections in Settings (#1486099)
+- Added server-side e-mail address validation with 'email_dns_check' option (#1485857)
+- Fix login page loading into an iframe when session expires (#1485952)
+- Allow setting port number in 'force_https' option (#1486091)
+- Option 'force_https' replaced by 'force_https' plugin
+- Fix IE issue with non-UTF-8 characters in AJAX response (#1486159)
+- Partially fixed "empty body" issue by showing raw body of malformed message (#1486166)
+- Fix importing/sending to email address with whitespace (#1486214)
+- Added XIMSS (CommuniGate) driver for Password plugin
+- Fix newly attached files are not saved in drafts w/o editing any text (#1486202)
+- Added attachment upload indicator with parallel upload (#1486058)
+- Use default_charset for bodies of messages without charset definition (#1486187)
+- Password: added cPanel driver
+- Fix return to first page from e-mail screen (#1486105)
+- Fix handling HTML comments in HTML messages (#1486189)
+- Fix folder/messagelist controls alignment - icons used (#1486072)
+- Fix LDAP addressbook shows 'Contact not found' error sometimes (#1486178)
+- Fix cache status checking + improve cache operations performance (#1486104)
+- Prevent from setting INBOX as any of special folders (#1486114)
+- Fix regular expression for e-mail address (#1486152)
+- Fix Received header format
+- Implemented sorting by message index - added 'index_sort' option (#1485936)
+- Fix dl() use in installer (#1486150)
+- Added 'ldap_debug' option
+- Fix "Empty startup greeting" bug (#1486085)
+- Fix setting user name in 'new_user_identity' plugin (#1486137)
+- Fix incorrect count of new messages in folder list when using multiple IMAP clients (#1485995)
+- Fix all folders checking for new messages with disabled caching (#1486128)
+- Support skins in 'archive' and 'markasjunk' plugins
+- Added 'html_editor' hook (#1486068)
+- Fix DB constraint violation when populating messages cache (#1486052)
+- Password: added password strength options (#1486062)
+- Fix LDAP partial result warning (#1485536)
+- Fix delete in message view deletes permanently with flag_for_deletion=true (#1486101)
+- Use faster/secure mt_rand() (#1486094)
+- Fix roundcube hangs on empty inbox with bincimapd (#1486093)
+- Fix wrong headers for IE on servers without $_SERVER['HTTPS'] (#1485926)
+- Force IE style headers for attachments in non-HTTPS session, 'use_https' option (#1485655)
+- Check 'post_max_size' for upload max filesize (#1486089) 
+- Password Plugin: Fix %d inserts username instead of domain (#1486088)
+- Fix rcube_mdb2::affected_rows() (#1486082)
+
+RELEASE 0.3-stable
+------------------
+- Fix gn and givenName should be synonymous in LDAP addressbook (#1485892)
+- Add mail_domain to LDAP email entries without @ sign (#1485201)
+- Fix saving empty values in LDAP contact data (#1485781)
+- Fix LDAP contact update when RDN field is changed (#1485788)
+- Fix LDAP attributes case senitivity problems (#1485830)
+- Fix LDAP addressbook browsing when only one directory is used (#1486022)
+- Fix endless loop on error response for APPEND command (#1486060)
+- Don't require date.timezone setting in installer (#1485989)
+- Fix date sorting problem with Courier IMAP server (#1486065)
+- Unselect pressed buttons on mouse up (#1485987)
+- Don't set php_value error_log in .htaccess but mention in INSTALL (#1485924)
+- Fix too small status/flag/attachment columns in Safari 4 (#1486063)
+- Fix selection disabling while dragging splitter in webkit browsers (#1486056)
+- Added 'new_messages' plugin hook (#1486005)
+- Added 'logout_after' plugin hook (#1486042)
+- Added 'message_compose' hook
+- Added 'imap_connect' hook (#1485956)
+- Fix vcard_attachments plugin (#1486035)
+- Updated PEAR::Auth_SASL to 1.0.3 version
+- Use sequence names only with PostgreSQL (#1486018)
+- Re-designed User Preferences interface 
+- Fix MS SQL DDL (#1486020)
+- Fix rcube_mdb2.php: call to setCharset not implemented in mssql driver (#1486019)
+- Added 'display_next' option
+- Fix rcube_mdb2::unixtimestamp for MS SQL (#1486015)
+- Fix HTML washing to respect character encoding
+- Fix endless loop in iil_C_Login() with Courier IMAP (#1486010)
+- Fix #messagemenu display on IE (#1486006)
+- Speedup UI by using sprites for (toolbar) buttons
+- Fix charset names with X- prefix handling
+- Fix displaying of HTML messages with unknown/malformed tags (#1486003)
+
+RELEASE 0.3-RC1
+---------------
+- Fix import of vCard entries with params (#1485453)
+- Fix HTML messages output with empty block elements (#1485974)
+- Use request tokens to protect POST requests from CSRF
+- Added hook when killing a session
+- Added hook to write_log function (#1485971)
+- Performance improvements by use UID commands (#1485690)
+- Fix HTML editor tabIndex setting (#1485972)
+- Added 'imap_debug' and 'smtp_debug' options
+- Support strftime's format modifiers in date_* options (#1484806)
+- Support %h variable in 'smtp_server' option (#1485766)
+- Show SMTP errors in browser (#1485927)
+- Allow WBR tag in HTML message (#1485960)
+- Use spl_autoload_register() instead of __autoload (#1485947)
+- Add hook for identities listing (#1485958)
+- Trigger hook 'smtp_connect' when opening an SMTP connection (#1485954)
+- Added config option to enforce HTTPS connections
+- Fix non-unicode characters caching in unicode database (#1484608)
+- Performance improvements of messages caching
+- Fix empty Date header issue (#1485923)
+- Open collapsed folders during drag & drop (#1485914)
+- Fixed link text replacements (#1485789)
+- Also trigger 'insertrow' events on page load (#1485826)
+- No link on subject in IE browsers (#1484913)
+- Fixed filename encoding according to RFC2231 (#1485875)
+- Added message Edit feature (#1483891, #1484440)
+- Fix message Etag generation for counter issues (#1485623)
+- Fix messages searching on MailEnable IMAP (#1485762)
+- Fixed many 'skip_deleted' issues (#1485634)
+- Fixed messages list sorting on servers without SORT capability
+- Colorized signatures in plain text messages
+- Reviewed/fixed skip_deleted/read_when_deleted/flag_for_deletion options handling in UI
+- Fix displaying of big maximum upload filesize (#1485889)
+- Added possibility to invert messages selection
+- After move/delete from 'show' action display next message instead of messages list (#1485887)
+- Fixed problem with double quote at the end of folder name (#1485884)
+- Speedup UI by using CSS sprites and etags/expires/deflate in Apache config (#1484858,#1485800)
+- Support UID EXPUNGE: remove only moved/deleted messages
+- Add drag cancelling with ESC key (#1484344)
+- Support initial identity name from virtuser_query (#1484003)
+- Added message menu, removed Print and Source buttons
+- Added possibility to save message as .eml file (#1485861)
+- Added 1 minute interval in autosave options (#1485854)
+- Support UTF-7 encoding in messages (#1485832)
+- Better support for malformed character names (#1485758)
+
+RELEASE 0.3-BETA
+----------------
+- Plugin API + jQuery engine
+- Added possibility to encrypt received header, option 'http_received_header_encrypt',
+  added some more logic in encrypt/decrypt functions for security
+- Fix Answered/Forwarded flag setting for messages in subfolders
+- Fix autocomplete problem with capital letters (#1485792)
+- Support UUencode content encoding (#1485839)
+- Minimize chance of race condition in session handling (#1485659, #1484678)
+- Fix session handling on non-session SQL query error (#1485734)
+- Fix html editor mode setting when reopening draft message (#1485834)
+- Added quick search box menu (#1484304)
+- Fix wrong column sort order icons (#1485823)
+- Updated TinyMCE to 3.2.3 version
+- Fix attachment names encoding when charset isn't specified in attachment part (#1484969)
+- Fix message normal priority problem (#1485820)
+- Fix autocomplete spinning wheel does not disappear (#1485804)
+- Added log_date_format option (#1485709)
+- Fix text wrapping in HTML editor after switching from plain text to HTML (#1485521)
+- Fix auto-complete function hangs with plus sign (#1485815)
+- Fix AJAX requests errors handler (#1485000)
+- Speed up message list displaying on IE
+- Fix read/write database recognition (#1485811)
+
+RELEASE 0.2.2
+-------------
+- Fix quicksearchbox look in Chrome and Konqueror (#1484841)
+- Fix UTF-8 byte-order mark removing (#1485514)
+- Fix folders subscribtions on Konqueror (#1484841) 
+- Fix debug console on Konqueror and Safari
+- Fix messagelist focus issue when modifying status of selected messages (#1485807)
+- Support STARTTLS in IMAP connection (#1485284)
+- Fix DEL key problem in search boxes (#1485528)
+- Support several e-mail addresses per user from virtuser_file (#1485678)
+- Fix drag&drop with scrolling on IE (#1485786)
+- Fix adding signature separator in html mode (#1485350)
+- Fix opening attachment marks message as read (#1485803)
+- Fix 'temp_dir' does not support relative path under Windows (#1484529)
+- Fix "Initialize Database" button missing from installer (#1485802)
+- Fix compose window doesn't fit 1024x768 window (#1485396)
+- Fix service not available error when pressing back from compose dialog (#1485552)
+- Fix using mail() on Windows (#1485779)
+- Fix word wrapping in message-part's <PRE>s for printing (#1485787)
+- Fix incorrect word wrapping in outgoing plaintext multibyte messages (#1485714)
+- Fix double footer in HTML message with embedded images
+- Fix TNEF implementation bug (#1485773)
+- Fix incorrect row id parsing for LDAP contacts list (#1485784) 
+- Fix 'mode' parameter in sqlite DSN (#1485772)
+
+RELEASE 0.2.1
+------------------
+- Use US-ASCII as failover when Unicode searching fails (#1485762)
+- Fix errors handling in IMAP command continuations (#1485762)
+- Fix FETCH result parsing for servers returning flags at the end of result (#1485763)
+- Fix datetime columns defaults in mysql's DDL (#1485641)
+- Fix attaching more than nine inline images (#1485759)
+- Support 'UNICODE-1-1-UTF-7' alias for UTF-7 encoding (#1485758)
+- Fix mime-type detection using a hard-coded map (#1485311)
+- Don't return empty string if charset conversion failed (#1485757)
+- Disable concurrent autocomplete query results display (#1485743)
+- Fix new lines stripped from message footer (#1485751)
+- Fix IE problem with mouse click autocomplete (#1485739)
+- Fix html body washing on reply/forward + fix attachments handling (#1485676)
+- Fix multiple recipients input parsing (#1485733)
+- Fix replying to message with html attachment (#1485676)
+- Use default_charset for messages without specified charset (#1485661, #1484961)
+- Support non-standard "GMT-XXXX" literal in date header (#1485729)
+- Added TNEF support to decode MS Outlook attachments (winmail.dat)
+- Fix "value continuation" MIME headers by adding required semicolon (#1485727)
+- Fix pressing select all/unread multiple times (#1485723)
+- Fix selecting all unread does not honor new messages (#1485724)
+- Fix some base64 encoded attachments handling (#1485725)
+- Support NGINX as IMAP backend: better BAD response handling (#1485720)
+- Performance fix: don't fetch attachment parts headers twice to parse filename
+- Fix checking for recent messages on various IMAP servers (#1485702)
+- Performance fix: Don't fetch quota and recent messages in "message view" mode
+- Fix displaying of alternative-inside-alternative messages (#1485713)
+- Fix MDNSent flag checking, use arbitrary keywords (asterisk) flag (#1485706)
+- Fix creation of folders with '&' sign in name
+- Fix parsing of email addresses without angle brackets (#1485693)
+- Save spellcheck corrections when switching from plain to html editor (and spellchecking is on)
+- Fix large search results on server without SORT capability (#1485668)
+- Get rid of preg_replace() with eval modifier and create_function usage (#1485686)
+- Bring back <base> and <link> tags in HTML messages
+- Fix XSS vulnerability through background attributes as reported by Julien Cayssol
+- Fix problems with backslash as IMAP hierarchy delimiter (#1484467)
+- Secure vcard export by getting rid of preg's 'e' modifier use (#1485689)
+- Fix authentication when submitting form with existing session (#1485679)
+- Allow absolute URLs to images in HTML messages/sigs (#1485666)
+- Fix message body which contains both inline attachments and emotions
+- Fix SQL query execution errors handling in rcube_mdb2 class (#1485509)
+- Fix address names with '@' sign handling (#1485654)
+- Improve messages display performance
+- Fix messages searching with 'to:' modifier
+
+RELEASE 0.2-STABLE
+------------------
+- Fix mark popup in IE 7 (#1485369)
+- Fix line-break issue when copy & paste in Firefox (#1485425)
+- Fix autocomplete "unknown server error" (#1485637)
+- Fix STARTTLS before AUTH in SMTP connection (#1484883)
+- Support multiple quota values in QUOTAROOT resonse (#1485626)
+- Only abbreviate file name for IE < 7 browsers (#1485063)
+- Performance: allow setting imap rootdir and delimiter before connect (#1485172)
+- Fix sorting of folders with more than 2 levels (#1485569)
+- Fix search results page jumps in LDAP addressbook (#1485253)
+- Fix empty line before the signature in IE (#1485351)
+- Fix horizontal scrollbar in preview pane on IE (#1484633)
+- Add Robots meta tag in login page and installer (#1484846)
+- Added 'show_images' option, removed 'addrbook_show_images' (#1485597)
+- Option to check for new mails in all folders (#1484374)
+- Don't set client busy when checking for new messages (#1485276)
+- Allow UTF-8 folder names in config (#1485579)
+- Add junk_mbox option configuration in installer (#1485579)
+- Do serverside addressbook queries for autocompletion (#1485531)
+- Allow setting attachment col position in 'list_cols' option
+- Allow override 'list_cols' via skin (#1485577)
+- Fix 'cache' table cleanup on session destroy (#1485516)
+- Increase speed of session destroy and garbage clean up
+- Fix session timeout when DB server got clock skew (#1485490)
+- Fix handling of some malformed messages (#1484438)
+- Speed up raw message body handling
+- Better HTML entities conversion in html2text (#1485519)
+- Fix big memory consumption and speed up searching on servers without SORT capability
+- Fix setting locale to tr_TR, ku and az_AZ (#1485470) 
+- Use SORT for searching on servers with SORT capability
+- Added message status filter
+- Fix empty file sending (#1485389)
+- Improved searching with many criterias (calling one SEARCH command)
+- Fix HTML editor initialization on IE (#1485304)
+- Add warning when switching editor mode from html to plain (#1485488)
+- Make identities list scrollable (#1485538)
+- Fix problem with numeric folder names (#1485527)
+- Added BYE response simple support to prevent from endless loops in imap.inc (#1483956)
+- Fix unread message unintentionally marked as read if read_when_deleted=true (#1485409)
+- Remove port number from SERVER_NAME in smtp_helo_host (#1485518)
+- Don't send disposition notification receipts for messages marked as 'read' (#1485523)
+- Added 'keep_alive' and 'min_keep_alive' options (#1485360)
+- Added option 'identities_level', removed 'multiple_identities'
+- Allow deleting identities when multiple_identities=false (#1485435)
+- Added option focus_on_new_message (#1485374)
+- Fix html2text class autoloading on Windows (#1485505)
+- Fix html signature formatting when identity save error occured (#1485426)
+- Add feedback and set busy when moving folder (#1485497)
+- Fix 'Empty' link visibility for some languages e.g. Slovak (#1485489)
+- Fix messages count bar overlapping (#1485270)
+- Fix adding signature in drafts compose mode (#1485484)
+- Fix iil_C_Sort() to support very long and/or divided responses (#1485283)
+- Fix matching case sensitivity when setting identity on reply (#1485480)
+- Prefer default identity on reply
+- Fix imap searching on ISMail server (#1485466)
+- Add css class for flagged messages (#1485464)
+- Write username instead of id in sendmail log (#1485477)
+- Fix htmlspecialchars() use for PHP version < 5.2.3 (#1485475)
+- Fix js keywords escaping in json_serialize() for IE/Opera (#1485472)
+- Added bin/killcache.php script (#1485434)
+- Add support for SJIS, GB2312, BIG5 in rc_detect_encoding()
+- Fix vCard file encoding detection for non-UTF-8 strings (#1485410)
+- Add 'skip_deleted' option in User Preferences (#1485445)
+- Minimize "inline" javascript scripts use (#1485433)
+- Fix css class setting for folders with names matching defined classes names (#1485355)
+- Fix race conditions when changing mailbox
+- Fix spellchecking when switching to html editor (#1485362)
+- Fix compose window width/height (#1485396)
+- Allow calling msgimport.sh/msgexport.sh from any directory (#1485431)
+- Localized filesize units (#1485340)
+- Better handling of "no identity" and "no email in identity" situations (#1485117)
+- Added 'mime_param_folding' option with possibility to choose long/non-ascii attachment names encoding eg. to be readable in MS Outlook/OE (#1485320)
+- Added "advanced options" feature in User Preferences
+- Fix unread counter when displaying cached massage in preview panel (#1485290)
+- Fix htmleditor spellchecking on MS Windows (#1485397)
+- Fix problem with non-ascii attachment names in Mail_mime (#1485267, #1485096)
+- Fix language autodetection (#1485401)
+- Fix button label in folders management (#1485405)
+- Fix collapsed folder not indicating unread msgs count of all subfolders (#1485403)
+- Fix handling of apostrophes in filenames decoded according to rfc2231
+
+RELEASE 0.2-BETA
+----------------
+- Made config files location configurable (#1485215)
+- Reduced memory footprint when forwarding attachments (#1485345)
+- Allow and use spellcheck attribute for input/textarea fields (#1485060)
+- Added icons for forwarded/forwarded+replied messages (#1485257)
+- Added Reply-To to forwarded emails (#1485315)
+- Display progress message for folders create/delete/rename (#1485357)
+- Smart Tags and NOBR tag support in html messages (#1485363, #1485327)
+- Redesign of the identities settings (#1484042)
+- Add config option to disable creation/deletion of identities (#1484498)
+- Added 'sendmail_delay' option to restrict messages sending interval (#1484491)
+- Added vertical splitter for folders list resizing
+- Added possibility to view all headers in message view
+- Fixed splitter drag/resize on Opera (#1485170)
+- Fixed quota img height/width setting from template (#1484857)
+- Refactor drag & drop functionality. Don't rely on browser events anymore (#1484453)
+- Insert "virtual" folders in subscription list (#1484779)
+- Added link to open message in new window
+- Enable export of address book contacts as vCard
+- Add feature to import contacts from vcard files (#1326103)
+- Respect Content-Location headers in multipart/related messages according to RFC2110 (#1484946)
+- Allowed max. attachment size now indicated in compose screen (#1485030)
+- Also capture backspace key in list mode (#1484566)
+- Allow application/pgp parts to be displayed (#1484753)
+- Correctly handle options in mailto-links (#1485228)
+- Immediately save sort_col/sort_order in user prefs (#1485265)
+- Truncate very long (above 50 characters) attachment filenames when displaying
+- Allow to auto-detect client language if none set (#1484434)
+- Auto-detect the client timezone (user configurable)
+- Add RFC2231 header value continuations support for attachment filenames + hack for servers that not support that feature
+- Fix Reply-To header displaying (#1485314)
+- Mark form buttons that provide the most obvious operation (mainaction)
+- Added option 'quota_zero_as_unlimited' (#1484604)
+- Added PRE handling in html2text class (#1484740)
+- Added folder hierarchy collapsing
+- Added options to use syslog instead of log file (#1484850)
+- Added Logging & Debugging section in Installer
+- Fix In-Reply-To and References headers when composing saved draft message (#1485288)
+- Fix html message charset conversion for charsets with underline (#1485287) 
+- Fix buttons status after contacts deletion (#1485233)
+- Fix escaping of To: and From: fields when building message body for reply or forward in the HTML editor (#1484904)
+- Use current mailbox name in template (#1485256)
+- Better fix for skipping untagged responses (#1485261)
+- Added pspell support patch by Kris Steinhoff (#1483960)
+- Enable spellchecker for HTML editor (#1485114)
+- Respect spellcheck_uri in tinyMCE spellchecker (#1484196)
+- Case insensitive contacts searching using PostgreSQL (#1485259)
+- Make default imap folders configurable for each user (#1485075)
+- Save outgoing mail to selectable folder (#1324581)
+- Fix hiding of mark menu when clicking th button again (#1484944)
+- Use long date format in print mode (#1485191)
+- Updated TinyMCE to version 3.1.0.1
+- Re-enable autocomplete attribute for login form (#1485211)
+- Check PERMANENTFLAGS before saving $MDNSent flag (#1484963, #1485163)
+- Added flag column on messages list (#1484623)
+- Patched Mail/MimePart.php (http://pear.php.net/bugs/bug.php?id=14232)
+- Allow trash/junk subfolders to be purged (#1485085)
+- Store compose parameters in session and redirect to a unique URL
+- Fixed CRAM-MD5 authentication (#1484819)
+- Fixed forwarding messages with one HTML attachment (#1484442)
+- Fixed encoding of message/rfc822 attachments and image/pjpeg handling (#1484914)
+- Added option to select skin in user preferences
+- Added option to configure displaying of attached images below the message body
+- Added option to display images in messages from known senders (#1484601)
+- User preferences grouped in more fieldsets
+- Fix corrupted MIME headers of messages in Sent folder (#1485111)
+- Fixed bug in MDB2 package: http://pear.php.net/bugs/bug.php?id=14124
+- Use keypress instead of keydown to select list's row (#1484816)
+- Don't call expunge and don't remove message row after message move if flag_for_deletion is set to true (#1485002)
+
+RELEASE 0.2-ALPHA
+-----------------
+- Added option to disable autocompletion from selected LDAP address books (#1484922)
+- TLS support in LDAP connections: 'use_tls' property (#1485104)
+- Fixed removing messages from search set after deleting them (#1485106)
+- imap.inc: Fixed iil_C_FetchStructureString() to handle many 
+  literal strings in response (#1484969)
+- Support for subfolders in default/protected folders (#1484665)
+- Disallowed delimiter in folder name (#1484803)
+- Support " and \ in folder names
+- Escape \ in login (#1484614)
+- Better HTML sanitization with the DOM-based washtml script (#1484701)
+- Fixed sorting of folders with non-ascii characters
+- Fixed Mysql DDL for default identities creation (#1485070)
+- In Preferences added possibility to configure 'read_when_deleted',
+  'mdn_requests', 'flag_for_deletion' options
+- Made IMAP auth type configurable (#1483825)
+- Fixed empty values with FROM_UNIXTIME() in rcube_mdb2 (#1485055)
+- Fixed attachment list on IE 6/7 (#1484807)
+- Fixed JavaScript in compose.html that shows cc/bcc fields if populated
+- Make password input fields of type password in installer (#1484886)
+- Don't attempt to delete cache entries if enable_caching is FALSE (#1485051)
+- Optimized messages sorting on servers without sort capability (#1485049)
+- Corrected message headers decoding when charset isn't specified and improved
+  support for native languages (#1485050, #1485048)
+- Expanded LDAP configuration options to support LDAP server writes.
+- Installer: encode special characters in DB username/password (#1485042)
+- Fixed management of folders with national characters in names (#1485036, #1485001)
+- Fixed identities saving when using MDB2 pgsql driver (#1485032)
+- Fixed BCC header reset (#1484997)
+- Improved messages list performance - patch from Justin Heesemann
+- Append skin_path to images location only when it starts with '/' sign (#1484859)
+- Fix IMAP response in message body when message has no body (#1484964)
+- Fixed non-RFC dates formatting (#1484901)
+- Fixed typo in set_charset() (#1484991)
+- Decode entities when inserting HTML signature to plain text message (#1484990)
+- HTML editing is now working with PHP5 updates and TinyMCE v3.0.6
+- Fixed signature loading on Windows (#1484545)
+- Added language support to HTML editing (#1484862)
+- Fixed remove signature when replying (#1333167)
+- Fixed problem with line with a space at the end (#1484916)
+- Fixed <!DOCTYPE> tag filtering (#1484391)
+- Fixed <?xml> tag filtering (#1484403)
+- Added sections (fieldset+label) in Settings interface
+- Mark as read in one action with message preview (#1484972)
+- Deleted redundant quota reads (#1484972)
+- Added options for empty trash and expunge inbox on logout (#1483863)
+- Removed lines wrapping when displaying message
+- Fixed month localization
+- Changed codebase to PHP5 with autoloader
+
+RELEASE 0.1.1
+-------------
+- Clear selection when selecting single item (#1484942)
+- Remove hard-coded image size in skin templates (#1484893)
+- Database schema improvements (dropped unnecessary indexes)
+- Fixed creating a new folder with a comma in its name (#1484681)
+- Fixed sorting of messages when default mailbox is empty (#1484317)
+- Improve message previewpane - less loading (#1484316)
+- Fixed login form autoompletion (#1484839)
+- Fixed virtuser_query option for mdb2 backend (#1484874)
+- Fixed attachment resoting from Drafts when message body was empty (#1484506)
+- Fixed usage of ob_gzhandler (#1484851)
+- Fixed message part window in IE6 (#1484610)
+- Fixed decoding of mime-encoded strings (#1484191)
+- Fixed some iconv/mb_string problems (#1484598)
+- Correctly quote mailbox name when using in URL (#1484313)
+- Fixed "headers already sent" errors (#1484860)
+
+RELEASE 0.1-STABLE
+------------------
+- Added interactive installer script
+- Fix folder adding/renaming inspired by #1484800
+- Localize folder name in page title (#1484785)
+- Fix code using wrong variable name (#1484018)
+- Allow to send mail with BCC recipients only
+- condense TinyMCE toolbar down to one line, removing table buttons (#1484747)
+- Add function to mark the selected messages as read/unread (#1457360)
+- Also do charset decoding as suggested in RFC 2231 (fix #1484321)
+- Show message count in folder list and hint when creating a subfolder
+- Distinguish ssl and tls for imap connections (#1484667)
+- Added some charset aliases to fix typical mis-labelling (#1484565)
+- Remember decision to display images for a certain message during session (#1484754)
+- Truncate attachment filenames to 55 characters due to an IE bug (#1484757)
+- Make sending of read receipts configurable
+- Respect config when localize folder names (#1484707)
+- Also respect receipt and priority settings when re-opening a draft message
+- Remember search results (closes #1483883), patch by the_glu
+- Add Received header on outgoing mail
+- Upgrade to TinyMCE 2.1.3
+- Allow inserting image attachments into HTML messages while composing (#1484557)
+- Implement Message-Disposition-Notification (Receipts)
+- Fix overriding of session vars when register_globals is on (#1484670)
+- Fix bug with case-sensitive folder names (#1484245)
+- Don't create default folders by default
+- Fixed some potential security risks (audited by Andris)
+- Only show new messages if they match the current search (#1484176)
+- Switch to/from when searcing in Sent folder (#1484555)
+- Correctly read the References header (#1484646)
+- Unset old cookie before sending a new value (#1484639)
+- Correctly decode attachments when downloading them (#1484645 and #1484642)
+- Suppress IE errors when clearing attachments form (#1484356)
+- Log error when login fails due to auto_create_user turned off
+- Filter linked/imported CSS files (closes #1484056)
+- Improve message compose screen (closes #1484383)
+- Select next row after removing one from list (#1484387)
+
+RELEASE 0.1-RC2
+---------------
+- Enable drag-&-dropping of folders to a new parent and allow to create subfolders (#1457344)
+- Suppress IE errors when clearing attachments form (#1484356)
+- Set preferences field in user table to NULL (#1484386)
+- Log error when login fails due to auto_create_user turned off
+- Filter linked/imported CSS files (closes #1484056)
+- Improve message compose screen (closes #1484383)
+- Select next row after removing one from list (#1484387)
+- Make smtp HELO/EHLO hostname configurable (#1484067)
+- IPv6 Compatability (#1484322), Patch #1484373
+- Unlock interface when message sending fails (#1484570)
+- Eval PHP code in template includes (if configured)
+- Show message when folder is empty. Mo more static text in table (#1484395)
+- Only display unread count in page title when new messages arrived
+- Fixed wrong delete button tooltip (#1483965)
+- Fixed charset encoding bug (#1484429)
+- Applied patch for LDAP version (#1484552)
+- Improved XHTML validation
+- Fix message list selection (#1484550)
+- Better fix lowercased usernames (#1484473)
+- Update pngbehavior Script as suggested in #1484490
+- Fixed moving/deleting messages when more than 1 is selected
+- Applied patch for LDAP contacts listing by Glen Ogilvie
+- Applied patch for more address fields in LDAP contacts (#1484402)
+- Add alternative for getallheaders() (fix #1484508)
+- Identify mailboxes case-sensitive
+- Sort mailbox list case-insensitive (closes #1484338)
+- Fix display of multipart messages from Apple Mail (closes #1484027)
+- Protect AJAX request from being fetched by a foreign site (XSS)
+- Make autocomplete for loginform configurable by the skin template
+- Fix compose function from address book (closes #1484426)
+- Added //IGNORE to iconv call (patch #1484420, closes #1484023)
+- Check if mbstring supports charset (#1484290 and #1484292)
+- Prefer iconv over mbstring (as suggested in #1484292)
+- Check filesize of template includes (#1484409)
+- Fixed bug with buttons not dimming/enabling properly after switching folders
+- Fixed compose window becoming unresponsive after saving a draft (#1484487)
+- Re-enabled "Back" button in compose window now that bug #1484487 is fixed
+- Fixed unresponsive interface issue when downloading attachments (#1484496)
+- Lowered status message time from 5 to 3 seconds to improve responsiveness
+- Raised .htaccess upload_max_filesize from 2M to 5M to differ from default php.ini
+- Increased "mailboxcontrols" mail.css width from 160 to 170px to fix non-english languages (#1484499)
+- Fix status message bug #1484464 with regard to #1484353
+- Fix address adding bug reported by David Koblas
+- Applied socket error patch by Thomas Mangin
+- Pass-by-reference workarround for PHP5 in sendmail.inc
+- Fixed buggy imap_root settings (closes #1484379)
+- Prevent default events on subject links (#1484399)
+- Use HTTP-POST requests for actions that change state
+
+RELEASE 0.1-RC1
+---------------
+- Use global filters and bind username/ for Ldap searches (#1484159)
+- Hide quota display if imap server does not support it
+- Hide address groups if no LDAP servers configured
+- Add link to message subjects (closes #1484257)
+- Better SQL query for contact listing/search (closes #1484369)
+- Fixed marking as read in preview pane (closes #1484364)
+- CSS hack to display attachments correctly in IE6
+- Wrap message body text (closes #1484148)
+- LDAP access is back in address book (closes #1484087)
+- Added search function for contacts
+- New Template parsing and output encoding
+- Fixed bugs #1484119 and #1483978
+- Fixed message moving procedure (closes #1484308)
+- Fixed display of multiple attachments (closes #1466563)
+- Fixed check for new messages (closes #1484310)
+- List attachments without filename
+- New session authentication: Change sessid cookie when login, authentication with sessauth cookie is now configurable.
+  Should close bugs #1483951 and #1484299
+- Correctly translate mailbox names (closes #1484276)
+- Quote e-mail address links (closes #1484300)
+- Updated PEAR::Mail_mime package
+- Accept single quotes for HTML attributes when modifying message body (thanks Jason)
+- Sanitize input for new users/identities (thanks Colin Alston)
+- Don't download HTML message parts
+- Convert HTML parts to plaintext if 'prefer_html' is off
+- Correctly parse message/rfc822 parts (closes #1484045)
+- Also use user_id for unique key in messages table (closes #1484074)
+- Hide contacts drop down on blur (closes #1484203)
+- Make entries in contacts drop down clickable
+- Turn off browser autocompletion on login page
+- Quote <? in text/html message parts
+- Hide border around radio buttons
+- Applied patch for attachment download by crichardson (closes #1484198)
+- Fixed bug in Postgres DB handling (closes #1484068)
+- Fixed bug of invalid calls to fetchRow() in rcube_db.inc (closes #1484280)
+- Fixed array_merge bug (closes #1484281)
+- Fixed flag for deletion in list view (closes #1484264)
+- Finally support semicolons as recipient separator (closes ##1484251)
+- Fixed message headers (subject) encoding
+- check if safe mode is on or not (closes #1484269)
+- Show "no subject" in message list if subject is missing (closes #1484243)
+- Solved page caching of message preview (closes #1484153)
+- Only use gzip compression if configured (closes #1484236)
+- Fixed priority selector issue (#1484150)
+- Fixed some CSS issues in default skin (closes #1484210 and #1484161)
+- Prevent from double quoting of numeric HTML character references (closes #1484253)
+- Fixed display of HTML message attachments (closes #1484178)
+- Applied patch for preview caching (closes #1484186)
+- Added error handling for attachment uploads
+- Use multibyte safe string functions where necessary (closes #1483988)
+- Applied security patch to validate the submitted host value (by Kees Cook)
+- Applied security patch to validate input values when deleting contacts (by Kees Cook)
+- Applied security patch that sanitizes emoticon paths when attaching them (by Kees Cook)
+- Applied a patch to more aggressively sanitize a HTML message
+- Visualize blocked images in HTML messages
+- Fixed wrong message listing when showing search results (closes #1484131)
+- Show remote images when opening HTML message part as attachment
+- Improve memory usage when sending mail (closes #1484098)
+- Mark messages as read once the preview is loaded (closes #1484132)
+- Include smtp final response in log (closes #1484081)
+- Corrected date string in sent message header (closes #1484125)
+- Correclty choose "To" column in sent and draft mailboxes (closes #1483943)
+- Changed srong tooltips for message browse buttons (closes #1483930)
+- Fixed signature delimeter character to be standard (Bug #1484035)
+- Fixed XSS vulnerability (Bug #1484109)
+- Remove newlines from mail headers (Bug #1484031)
+- Selection issues when moving/deleting (Bug #1484044)
+- Applied patch of Clement Moulin for imap host auto-selection
+- ISO-encode IMAP password for plaintext login (Bugs #1483977 & #1483886)
+- Fixed folder name encoding in subscription list (Bug #1484113)
+- Fixed JS errors in identity list (Bug #1484120)
+- Translate foldernames in folder form (closes #1484113)
+- Added first and last buttons to message list, address book
+  and message detail
+- Pressing Shift-Del bypasses Trash folder
+- Enable purge command for Junk folder
+- Fetch all aliases if virtuser_query is used instead
+- Re-enabled multi select of contacts (Bug #1484017)
+- Enable contact editing right after creation (Bug #1459641)
+- Correct UTF-7 to UTF-8 conversion if mbstring is not available
+- Fixed IMAP fetch of message body (Bug #1484019)
+- Fixed safe_mode problems (Bug #1418381)
+- Fixed wrong header encoding (Bug #1483976)
+- Made automatic draft saving configurable
+- Fixed JS bug when renaming folders (Bug #1483989)
+- Added quota display as image (by Brett Patterson)
+- Corrected creation of a message-id
+- New indentation for quoted message text
+- Improved HTML validity
+- Fixed URL character set (Ticket #1445501)
+- Fixed saving of contact into MySQL from LDAP query results (Ticket #1483820)
+- Fixed folder renaming: unsubscribe before rename (Bug #1483920)
+- Finalized new message parsing (+ chaching)
+- Fixed wrong usage of mbstring (Bug #1462439)
+- Set default spelling language (Ticket #1483938)
+- Added support for Nox Spell Server
+- Re-built message parsing (Bug #1327068)
+  Now based on the message structure delivered by the IMAP server.
+- Fixed some XSS and SQL injection issues
+- Fixed charset problems with folder renaming
+
+
+
+
Index: /branches/devel-composer/INSTALL
===================================================================
--- /branches/devel-composer/INSTALL	(revision 5386)
+++ /branches/devel-composer/INSTALL	(revision 5386)
@@ -0,0 +1,233 @@
+INTRODUCTION
+============
+
+This file describes the basic steps to install Roundcube Webmail on your
+web server. For additional information, please also consult the project's
+wiki page at http://trac.roundcube.net/wiki
+
+
+REQUIREMENTS
+============
+
+* The Apache, Lighttpd, Cherokee or Hiawatha web server
+* .htaccess support allowing overrides for DirectoryIndex
+* PHP Version 5.2.1 or greater including
+   - PCRE, DOM, JSON, XML, Session, Sockets (required)
+   - libiconv (recommended)
+   - mbstring, fileinfo, mcrypt (optional)
+* PEAR packages distributed with Roundcube or external:
+   - MDB2 2.5.0 or newer
+   - Mail_Mime 1.8.1 or newer
+   - Net_SMTP 1.4.2 or newer
+   - Net_IDNA2 0.1.1 or newer
+   - Auth_SASL 1.0.3 or newer
+* php.ini options (see .htaccess file):
+   - error_reporting E_ALL & ~E_NOTICE (or lower)
+   - memory_limit > 16MB (increase as suitable to support large attachments)
+   - file_uploads enabled (for attachment upload features)
+   - session.auto_start disabled
+   - zend.ze1_compatibility_mode disabled
+   - suhosin.session.encrypt disabled
+   - mbstring.func_overload disabled
+   - magic_quotes_runtime disabled
+* PHP compiled with OpenSSL to connect to IMAPS and to use the spell checker
+* A MySQL (4.0.8 or newer), PostgreSQL, MSSQL database engine
+  or the SQLite extension for PHP
+* One of the above databases with permission to create tables
+* An SMTP server (recommended) or PHP configured for mail delivery
+
+
+INSTALLATION
+============
+
+1. Decompress and put this folder somewhere inside your document root
+2. Make sure that the following directories (and the files within)
+   are writable by the webserver
+   - /temp
+   - /logs
+3. Create a new database and a database user for Roundcube (see DATABASE SETUP)
+4. Point your browser to http://url-to-roundcube/installer/
+5. Follow the instructions of the install script (or see MANUAL CONFIGURATION)
+6. After creating and testing the configuration, remove the installer directory
+7. Done!
+
+
+CONFIGURATION HINTS
+===================
+
+Roundcube writes internal errors to the 'errors' log file located in the logs
+directory which can be configured in config/main.inc.php. If you want ordinary
+PHP errors to be logged there as well, enable the 'php_value error_log' line
+in the .htaccess file and set the path to the log file accordingly.
+
+By default the session_path settings of PHP are not modified by Roundcube.
+However if you want to limit the session cookies to the directory where
+Roundcube resides you can uncomment and configure the according line
+in the .htaccess file.
+
+
+DATABASE SETUP
+==============
+
+Note: Database for Roundcube must use UTF-8 character set.
+
+* MySQL
+-------
+Setting up the mysql database can be done by creating an empty database,
+importing the table layout and granting the proper permissions to the
+roundcube user. Here is an example of that procedure:
+
+# mysql
+> CREATE DATABASE roundcubemail /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+> GRANT ALL PRIVILEGES ON roundcubemail.* TO roundcube@localhost
+    IDENTIFIED BY 'password';
+> quit
+
+# mysql roundcubemail < SQL/mysql.initial.sql
+
+Note 1: 'password' is the master password for the roundcube user. It is strongly
+recommended you replace this with a more secure password. Please keep in
+mind: You need to specify this password later in 'config/db.inc.php'.
+
+
+* SQLite
+--------
+You need sqlite 2 (preferably 2.8) to setup the sqlite db 
+(sqlite 3.x also doesn't work at the moment). Here is
+an example how you can setup the sqlite.db for roundcube:
+
+# sqlite -init SQL/sqlite.initial.sql sqlite.db
+Loading resources from SQL/sqlite.initial.sql
+SQLite version 2.8.16
+Enter ".help" for instructions
+sqlite> .exit
+# chmod o+rw sqlite.db
+
+Make sure your configuration points to the sqlite.db file and that the
+webserver can write to the file and the directory containing the file.
+
+
+* PostgreSQL
+------------
+To use Roundcube with PostgreSQL support you have to follow these
+simple steps, which have to be done as the postgres system user (or
+which ever is the database superuser):
+
+$ createuser roundcube
+$ createdb -O roundcube -E UNICODE roundcubemail
+$ psql roundcubemail
+
+roundcubemail =# ALTER USER roundcube WITH PASSWORD 'the_new_password';
+roundcubemail =# \c - roundcube
+roundcubemail => \i SQL/postgres.initial.sql
+
+All this has been tested with PostgreSQL 8.x and 7.4.x. Older
+versions don't have a -O option for the createdb, so if you are
+using that version you'll have to change ownership of the DB later.
+
+
+Database cleaning
+-----------------
+Do keep your database slick and clean we recommend to periodically execute
+bin/cleandb.sh which finally removes all records that are marked as deleted.
+Best solution is to install a cronjob running this script daily.
+
+
+
+MANUAL CONFIGURATION
+====================
+
+First of all, rename the files config/*.inc.php.dist to config/*.inc.php.
+You can then change these files according to your environment and your needs.
+Details about the config parameters can be found in the config files.
+See http://trac.roundcube.net/wiki/Howto_Install for even more guidance.
+
+You can also modify the default .htaccess file. This is necessary to
+increase the allowed size of file attachments, for example:
+	php_value       upload_max_filesize     2M
+
+
+UPGRADING
+=========
+
+If you already have a previous version of Roundcube installed,
+please refer to the instructions in UPGRADING guide.
+
+
+OPTIMISING
+==========
+
+There are two forms of optimisation here, compression and caching, both aimed
+at increasing an end user's experience using Roundcube Webmail. Compression
+allows the static web pages to be delivered with less bandwidth. The index.php
+of Roundcube Webmail already enables compression on its output. The settings
+below allow compression to occur for all static files. Caching sets HTTP 
+response headers that enable a user's web client to understand what is static
+and how to cache it.
+
+The caching directives used are:
+ * Etags - sets at tag so the client can request is the page has changed
+ * Cache-control - defines the age of the page and that the page is 'public'
+   This enables clients to cache javascript files that don't have private 
+   information between sessions even if using HTTPS. It also allows proxies
+   to share the same cached page between users.
+ * Expires - provides another hint to increase the lifetime of static pages.
+
+For more information refer to RFC 2616.
+
+Side effects:
+-------------
+These directives are designed for production use. If you are using this in
+a development environment you may get horribly confused if your webclient
+is caching stuff that you changed on the server. Disabling the expires 
+parts below should save you some grief.
+
+If you are changing the skins, it is recommended that you copy content to 
+a different directory apart from 'default'.
+
+Apache:
+-------
+To enable these features in apache the following modules need to be enabled:
+ * mod_deflate
+ * mod_expires
+ * mod_headers
+
+The optimisation is already included in the .htaccess file in the top 
+directory of your installation.
+
+If you are using Apache version 2.2.9 and later, in the .htaccess file
+change the 'append' word to 'merge' for a more correct response. Keeping
+as 'append' shouldn't cause any problems though changing to merge will 
+eliminate the possibility of duplicate 'public' headers in Cache-control.
+
+Lighttpd:
+---------
+With Lightty the addition of Expire: tags by mod_expire is incompatible with
+the addition of "Cache-control: public". Using Cache-control 'public' is 
+used below as it is assumed to give a better caching result.
+
+Enable modules in server.modules:
+    "mod_setenv"
+    "mod_compress"
+
+Mod_compress is a server side cache of compressed files to improve its performance.
+
+$HTTP["host"] == "www.example.com" {
+
+    static-file.etags = "enable"
+    # http://redmine.lighttpd.net/projects/lighttpd/wiki/Etag.use-mtimeDetails
+    etag.use-mtime = "enable"
+
+    # http://redmine.lighttpd.net/projects/lighttpd/wiki/Docs:ModSetEnv
+    $HTTP["url"] =~ "^/roundcubemail/(plugins|skins|program)" {
+        setenv.add-response-header  = ( "Cache-Control" => "public, max-age=2592000")
+    }
+
+    # http://redmine.lighttpd.net/projects/lighttpd/wiki/Docs:ModCompress
+    # set compress.cache-dir to somewhere outside the docroot.
+    compress.cache-dir   = var.statedir + "/cache/compress"
+
+    compress.filetype = ("text/plain", "text/html", "text/javascript", "text/css", "text/xml", "image/gif", "image/png")
+}
+
+
Index: /branches/devel-composer/LICENSE
===================================================================
--- /branches/devel-composer/LICENSE	(revision 5386)
+++ /branches/devel-composer/LICENSE	(revision 5386)
@@ -0,0 +1,339 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
Index: /branches/devel-composer/README
===================================================================
--- /branches/devel-composer/README	(revision 5386)
+++ /branches/devel-composer/README	(revision 5386)
@@ -0,0 +1,67 @@
+Roundcube Webmail (http://roundcube.net)
+
+
+ATTENTION
+---------
+This is just a snapshot of the current SVN repository and is NOT A STABLE
+version of Roundcube. It's not recommended to replace an existing installation
+of Roundcube with this version. Also using a separate database for this
+installation is highly recommended.
+
+
+Introduction:
+-------------
+Roundcube Webmail is a browser-based multilingual IMAP client with an
+application-like user interface. It provides full functionality you expect
+from an e-mail client, including MIME support, address book, folder management,
+message searching and spell checking. Roundcube Webmail is written in PHP and
+requires the MySQL, PostgreSQL or SQLite database. With its plugin API it is
+easily extendable and the user interface is fully customizable using skins
+which are pure XHTML and CSS 2.
+
+This project includes other open-source classes/libraries from PEAR
+(http://pear.php.net), an IMAP library derived from IlohaMail
+the TinyMCE rich text editor, Googiespell library for spell checking
+or the HTML sanitizer by Frederic Motte.
+
+The current default skin uses icons designed by Stephen Horlander and Kevin 
+Gerich for Mozilla.org.
+
+
+Installation:
+-------------
+For detailed instructions on how to install Roundcube webmail on your server,
+please refer to the INSTALL document in the same directory as this document.
+
+If you're updating an older version of Roundcube please follow the steps
+described in the UPGRADING file.
+
+
+Licensing:
+----------
+This product is distributed under the GNU General Public License Version 2.
+Please read through the file LICENSE for more information about our license.
+
+Even if skins might contain some programming work, they are not considered
+as a linked part of the application and therefore skins DO NOT fall under
+the provisions of the GPL license. See the README file located in the skins
+folder for details on the skin license.
+
+
+Contribution:
+-------------
+Want to help make Roundcube the best webmail solution ever?
+Roundcube is open source software. Our developers and contributors all
+are volunteers and we're always looking for new additions and resources.
+For more information visit http://roundcube.net/contribute
+
+
+Contact:
+--------
+For any bug reports or feature requests please refer to the tracking system
+at trac.roundcube.net (http://trac.roundcube.net/wiki/Howto_ReportIssues) or 
+subscribe to our mailing list. See http://roundcube.net/support
+for details.
+
+You're always welcome to send a message to the project admin:
+hello@roundcube.net
Index: /branches/devel-composer/SQL/mssql.initial.sql
===================================================================
--- /branches/devel-composer/SQL/mssql.initial.sql	(revision 5386)
+++ /branches/devel-composer/SQL/mssql.initial.sql	(revision 5386)
@@ -0,0 +1,371 @@
+CREATE TABLE [dbo].[cache] (
+	[cache_id] [int] IDENTITY (1, 1) NOT NULL ,
+	[user_id] [int] NOT NULL ,
+	[cache_key] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[created] [datetime] NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[cache_index] (
+	[user_id] [int] NOT NULL ,
+	[mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[valid] [char] (1) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[cache_thread] (
+	[user_id] [int] NOT NULL ,
+	[mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[cache_messages] (
+	[user_id] [int] NOT NULL ,
+	[mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[uid] [int] NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+	[flags] [int](1) NOT NULL ,
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[contacts] (
+	[contact_id] [int] IDENTITY (1, 1) NOT NULL ,
+	[user_id] [int] NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[del] [char] (1) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[name] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[email] [varchar] (255) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[firstname] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[surname] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[vcard] [text] COLLATE Latin1_General_CI_AI NULL ,
+	[words] [text] COLLATE Latin1_General_CI_AI NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[contactgroups] (
+	[contactgroup_id] [int] IDENTITY (1, 1) NOT NULL ,
+	[user_id] [int] NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[del] [char] (1) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[name] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL
+) ON [PRIMARY] 
+GO
+
+CREATE TABLE [dbo].[contactgroupmembers] (
+	[contactgroup_id] [int] NOT NULL ,
+	[contact_id] [int] NOT NULL ,
+	[created] [datetime] NOT NULL
+) ON [PRIMARY] 
+GO
+
+CREATE TABLE [dbo].[identities] (
+	[identity_id] [int] IDENTITY (1, 1) NOT NULL ,
+	[user_id] [int] NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[del] [char] (1) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[standard] [char] (1) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[name] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[organization] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[email] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[reply-to] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[bcc] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[signature] [text] COLLATE Latin1_General_CI_AI NULL, 
+	[html_signature] [char] (1) COLLATE Latin1_General_CI_AI NOT NULL
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[session] (
+	[sess_id] [varchar] (32) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[created] [datetime] NOT NULL ,
+	[changed] [datetime] NULL ,
+	[ip] [varchar] (40) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[vars] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[users] (
+	[user_id] [int] IDENTITY (1, 1) NOT NULL ,
+	[username] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[mail_host] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[alias] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[created] [datetime] NOT NULL ,
+	[last_login] [datetime] NULL ,
+	[language] [varchar] (5) COLLATE Latin1_General_CI_AI NULL ,
+	[preferences] [text] COLLATE Latin1_General_CI_AI NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[dictionary] (
+	[user_id] [int] ,
+	[language] [varchar] (5) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[searches] (
+	[search_id] [int] IDENTITY (1, 1) NOT NULL ,
+	[user_id] [int] NOT NULL ,
+	[type] [tinyint] NOT NULL ,
+	[name] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[cache] WITH NOCHECK ADD 
+	 PRIMARY KEY  CLUSTERED 
+	(
+		[cache_id]
+	)  ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[cache_index] WITH NOCHECK ADD 
+	 PRIMARY KEY CLUSTERED 
+	(
+		[user_id],[mailbox]
+	) ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[cache_thread] WITH NOCHECK ADD 
+	 PRIMARY KEY CLUSTERED 
+	(
+		[user_id],[mailbox]
+	) ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[cache_messages] WITH NOCHECK ADD 
+	 PRIMARY KEY CLUSTERED 
+	(
+		[user_id],[mailbox],[uid]
+	) ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[contacts] WITH NOCHECK ADD 
+	CONSTRAINT [PK_contacts_contact_id] PRIMARY KEY  CLUSTERED 
+	(
+		[contact_id]
+	)  ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[contactgroups] WITH NOCHECK ADD 
+	CONSTRAINT [PK_contactgroups_contactgroup_id] PRIMARY KEY CLUSTERED 
+	(
+		[contactgroup_id]
+	)  ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[contactgroupmembers] WITH NOCHECK ADD 
+	CONSTRAINT [PK_contactgroupmembers_id] PRIMARY KEY CLUSTERED 
+	(
+		[contactgroup_id], [contact_id]
+	)  ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[identities] WITH NOCHECK ADD 
+	 PRIMARY KEY  CLUSTERED 
+	(
+		[identity_id]
+	)  ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[session] WITH NOCHECK ADD 
+	CONSTRAINT [PK_session_sess_id] PRIMARY KEY  CLUSTERED 
+	(
+		[sess_id]
+	)  ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[users] WITH NOCHECK ADD 
+	CONSTRAINT [PK_users_user_id] PRIMARY KEY  CLUSTERED 
+	(
+		[user_id]
+	)  ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[searches] WITH NOCHECK ADD 
+	CONSTRAINT [PK_searches_search_id] PRIMARY KEY CLUSTERED 
+	(
+		[search_id]
+	) ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[cache] ADD 
+	CONSTRAINT [DF_cache_user_id] DEFAULT ('0') FOR [user_id],
+	CONSTRAINT [DF_cache_cache_key] DEFAULT ('') FOR [cache_key],
+	CONSTRAINT [DF_cache_created] DEFAULT (getdate()) FOR [created]
+GO
+
+CREATE  INDEX [IX_cache_user_id] ON [dbo].[cache]([user_id]) ON [PRIMARY]
+GO
+
+CREATE  INDEX [IX_cache_cache_key] ON [dbo].[cache]([cache_key]) ON [PRIMARY]
+GO
+
+CREATE  INDEX [IX_cache_created] ON [dbo].[cache]([created]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[cache_index] ADD 
+	CONSTRAINT [DF_cache_index_changed] DEFAULT (getdate()) FOR [changed],
+	CONSTRAINT [DF_cache_index_valid] DEFAULT ('0') FOR [valid]
+GO
+
+CREATE  INDEX [IX_cache_index_user_id] ON [dbo].[cache_index]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[cache_thread] ADD 
+	CONSTRAINT [DF_cache_thread_changed] DEFAULT (getdate()) FOR [changed]
+GO
+
+CREATE  INDEX [IX_cache_thread_user_id] ON [dbo].[cache_thread]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[cache_messages] ADD 
+	CONSTRAINT [DF_cache_messages_changed] DEFAULT (getdate()) FOR [changed],
+	CONSTRAINT [DF_cache_messages_flags] DEFAULT (0) FOR [flags],
+GO
+
+CREATE  INDEX [IX_cache_messages_user_id] ON [dbo].[cache_messages]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[contacts] ADD 
+	CONSTRAINT [DF_contacts_user_id] DEFAULT (0) FOR [user_id],
+	CONSTRAINT [DF_contacts_changed] DEFAULT (getdate()) FOR [changed],
+	CONSTRAINT [DF_contacts_del] DEFAULT ('0') FOR [del],
+	CONSTRAINT [DF_contacts_name] DEFAULT ('') FOR [name],
+	CONSTRAINT [DF_contacts_email] DEFAULT ('') FOR [email],
+	CONSTRAINT [DF_contacts_firstname] DEFAULT ('') FOR [firstname],
+	CONSTRAINT [DF_contacts_surname] DEFAULT ('') FOR [surname],
+	CONSTRAINT [CK_contacts_del] CHECK ([del] = '1' or [del] = '0')
+GO
+
+CREATE  INDEX [IX_contacts_user_id] ON [dbo].[contacts]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[contactgroups] ADD 
+	CONSTRAINT [DF_contactgroups_user_id] DEFAULT (0) FOR [user_id],
+	CONSTRAINT [DF_contactgroups_changed] DEFAULT (getdate()) FOR [changed],
+	CONSTRAINT [DF_contactgroups_del] DEFAULT ('0') FOR [del],
+	CONSTRAINT [DF_contactgroups_name] DEFAULT ('') FOR [name],
+	CONSTRAINT [CK_contactgroups_del] CHECK ([del] = '1' or [del] = '0')
+GO
+
+CREATE  INDEX [IX_contactgroups_user_id] ON [dbo].[contacts]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[contactgroupmembers] ADD 
+	CONSTRAINT [DF_contactgroupmembers_contactgroup_id] DEFAULT (0) FOR [contactgroup_id],
+	CONSTRAINT [DF_contactgroupmembers_contact_id] DEFAULT (0) FOR [contact_id],
+	CONSTRAINT [DF_contactgroupmembers_created] DEFAULT (getdate()) FOR [created]
+GO
+
+CREATE  INDEX [IX_contactgroupmembers_contact_id] ON [dbo].[contactgroupmembers]([contact_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[identities] ADD 
+	CONSTRAINT [DF_identities_user] DEFAULT ('0') FOR [user_id],
+	CONSTRAINT [DF_identities_del] DEFAULT ('0') FOR [del],
+	CONSTRAINT [DF_identities_standard] DEFAULT ('0') FOR [standard],
+	CONSTRAINT [DF_identities_name] DEFAULT ('') FOR [name],
+	CONSTRAINT [DF_identities_organization] DEFAULT ('') FOR [organization],
+	CONSTRAINT [DF_identities_email] DEFAULT ('') FOR [email],
+	CONSTRAINT [DF_identities_reply] DEFAULT ('') FOR [reply-to],
+	CONSTRAINT [DF_identities_bcc] DEFAULT ('') FOR [bcc],
+	CONSTRAINT [DF_identities_html_signature] DEFAULT ('0') FOR [html_signature],
+	 CHECK ([standard] = '1' or [standard] = '0'),
+	 CHECK ([del] = '1' or [del] = '0')
+GO
+
+CREATE  INDEX [IX_identities_user_id] ON [dbo].[identities]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[session] ADD 
+	CONSTRAINT [DF_session_sess_id] DEFAULT ('') FOR [sess_id],
+	CONSTRAINT [DF_session_created] DEFAULT (getdate()) FOR [created],
+	CONSTRAINT [DF_session_ip] DEFAULT ('') FOR [ip]
+GO
+
+CREATE  INDEX [IX_session_changed] ON [dbo].[session]([changed]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[users] ADD 
+	CONSTRAINT [DF_users_username] DEFAULT ('') FOR [username],
+	CONSTRAINT [DF_users_mail_host] DEFAULT ('') FOR [mail_host],
+	CONSTRAINT [DF_users_alias] DEFAULT ('') FOR [alias],
+	CONSTRAINT [DF_users_created] DEFAULT (getdate()) FOR [created]
+GO
+
+CREATE  UNIQUE INDEX [IX_users_username] ON [dbo].[users]([username],[mail_host]) ON [PRIMARY]
+GO
+
+CREATE  INDEX [IX_users_alias] ON [dbo].[users]([alias]) ON [PRIMARY]
+GO
+
+CREATE  UNIQUE INDEX [IX_dictionary_user_language] ON [dbo].[dictionary]([user_id],[language]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[searches] ADD 
+	CONSTRAINT [DF_searches_user] DEFAULT (0) FOR [user_id],
+	CONSTRAINT [DF_searches_type] DEFAULT (0) FOR [type],
+GO
+
+CREATE UNIQUE INDEX [IX_searches_user_type_name] ON [dbo].[searches]([user_id],[type],[name]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[identities] ADD CONSTRAINT [FK_identities_user_id] 
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[contacts] ADD CONSTRAINT [FK_contacts_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[contactgroups] ADD CONSTRAINT [FK_contactgroups_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[cache] ADD CONSTRAINT [FK_cache_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[cache_index] ADD CONSTRAINT [FK_cache_index_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[cache_thread] ADD CONSTRAINT [FK_cache_thread_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[cache_messages] ADD CONSTRAINT [FK_cache_messages_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[contactgroupmembers] ADD CONSTRAINT [FK_contactgroupmembers_contactgroup_id]
+    FOREIGN KEY ([contactgroup_id]) REFERENCES [dbo].[contactgroups] ([contactgroup_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[searches] ADD CONSTRAINT [FK_searches_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+-- Use trigger instead of foreign key (#1487112)
+-- "Introducing FOREIGN KEY constraint ... may cause cycles or multiple cascade paths."
+CREATE TRIGGER [contact_delete_member] ON [dbo].[contacts]
+    AFTER DELETE AS
+    DELETE FROM [dbo].[contactgroupmembers]
+    WHERE [contact_id] IN (SELECT [contact_id] FROM deleted)
+GO
+
Index: /branches/devel-composer/SQL/mssql.upgrade.sql
===================================================================
--- /branches/devel-composer/SQL/mssql.upgrade.sql	(revision 5386)
+++ /branches/devel-composer/SQL/mssql.upgrade.sql	(revision 5386)
@@ -0,0 +1,241 @@
+-- Roundcube Webmail update script for MSSQL databases
+
+-- Updates from version 0.3.1
+
+ALTER TABLE [dbo].[messages] ADD CONSTRAINT [FK_messages_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[cache] ADD CONSTRAINT [FK_cache_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[contacts] ADD CONSTRAINT [FK_contacts_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[identities] ADD CONSTRAINT [FK_identities_user_id] 
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[identities] ADD [changed] [datetime] NULL 
+GO
+
+CREATE TABLE [dbo].[contactgroups] (
+	[contactgroup_id] [int] IDENTITY (1, 1) NOT NULL ,
+	[user_id] [int] NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[del] [char] (1) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[name] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL
+) ON [PRIMARY] 
+GO
+
+CREATE TABLE [dbo].[contactgroupmembers] (
+	[contactgroup_id] [int] NOT NULL ,
+	[contact_id] [int] NOT NULL ,
+	[created] [datetime] NOT NULL
+) ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[contactgroups] WITH NOCHECK ADD 
+	CONSTRAINT [PK_contactgroups_contactgroup_id] PRIMARY KEY CLUSTERED 
+	(
+		[contactgroup_id]
+	)  ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[contactgroupmembers] WITH NOCHECK ADD 
+	CONSTRAINT [PK_contactgroupmembers_id] PRIMARY KEY CLUSTERED 
+	(
+		[contactgroup_id], [contact_id]
+	)  ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[contactgroups] ADD 
+	CONSTRAINT [DF_contactgroups_user_id] DEFAULT (0) FOR [user_id],
+	CONSTRAINT [DF_contactgroups_changed] DEFAULT (getdate()) FOR [changed],
+	CONSTRAINT [DF_contactgroups_del] DEFAULT ('0') FOR [del],
+	CONSTRAINT [DF_contactgroups_name] DEFAULT ('') FOR [name],
+	CONSTRAINT [CK_contactgroups_del] CHECK ([del] = '1' or [del] = '0')
+GO
+
+CREATE  INDEX [IX_contactgroups_user_id] ON [dbo].[contacts]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[contactgroupmembers] ADD 
+	CONSTRAINT [DF_contactgroupmembers_contactgroup_id] DEFAULT (0) FOR [contactgroup_id],
+	CONSTRAINT [DF_contactgroupmembers_contact_id] DEFAULT (0) FOR [contact_id],
+	CONSTRAINT [DF_contactgroupmembers_created] DEFAULT (getdate()) FOR [created]
+GO
+
+ALTER TABLE [dbo].[contactgroupmembers] ADD CONSTRAINT [FK_contactgroupmembers_contactgroup_id]
+    FOREIGN KEY ([contactgroup_id]) REFERENCES [dbo].[contactgroups] ([contactgroup_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+CREATE TRIGGER [contact_delete_member] ON [dbo].[contacts]
+    AFTER DELETE AS
+    DELETE FROM [dbo].[contactgroupmembers]
+    WHERE [contact_id] IN (SELECT [contact_id] FROM deleted)
+GO
+
+ALTER TABLE [dbo].[contactgroups] ADD CONSTRAINT [FK_contactgroups_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+-- Updates from version 0.4.2
+
+DROP INDEX [IX_users_username]
+GO
+CREATE UNIQUE INDEX [IX_users_username] ON [dbo].[users]([username],[mail_host]) ON [PRIMARY]
+GO
+ALTER TABLE [dbo].[contacts] ALTER COLUMN [email] [varchar] (255) COLLATE Latin1_General_CI_AI NOT NULL
+GO
+
+-- Updates from version 0.5.1
+-- Updates from version 0.5.2
+-- Updates from version 0.5.3
+-- Updates from version 0.5.4
+
+ALTER TABLE [dbo].[contacts] ADD [words] [text] COLLATE Latin1_General_CI_AI NULL 
+GO
+CREATE INDEX [IX_contactgroupmembers_contact_id] ON [dbo].[contactgroupmembers]([contact_id]) ON [PRIMARY]
+GO
+DELETE FROM [dbo].[messages]
+GO
+DELETE FROM [dbo].[cache]
+GO
+
+-- Updates from version 0.6
+
+CREATE TABLE [dbo].[dictionary] (
+    [user_id] [int] ,
+    [language] [varchar] (5) COLLATE Latin1_General_CI_AI NOT NULL ,
+    [data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+CREATE  UNIQUE INDEX [IX_dictionary_user_language] ON [dbo].[dictionary]([user_id],[language]) ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[searches] (
+	[search_id] [int] IDENTITY (1, 1) NOT NULL ,
+	[user_id] [int] NOT NULL ,
+	[type] [tinyint] NOT NULL ,
+	[name] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[searches] WITH NOCHECK ADD 
+	CONSTRAINT [PK_searches_search_id] PRIMARY KEY CLUSTERED 
+	(
+		[search_id]
+	) ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[searches] ADD 
+	CONSTRAINT [DF_searches_user] DEFAULT (0) FOR [user_id],
+	CONSTRAINT [DF_searches_type] DEFAULT (0) FOR [type],
+GO
+
+CREATE UNIQUE INDEX [IX_searches_user_type_name] ON [dbo].[searches]([user_id],[type],[name]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[searches] ADD CONSTRAINT [FK_searches_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+DROP TABLE [dbo].[messages]
+GO
+CREATE TABLE [dbo].[cache_index] (
+	[user_id] [int] NOT NULL ,
+	[mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[valid] [char] (1) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[cache_thread] (
+	[user_id] [int] NOT NULL ,
+	[mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+CREATE TABLE [dbo].[cache_messages] (
+	[user_id] [int] NOT NULL ,
+	[mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+	[uid] [int] NOT NULL ,
+	[changed] [datetime] NOT NULL ,
+	[data] [text] COLLATE Latin1_General_CI_AI NOT NULL 
+	[flags] [int] NOT NULL ,
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[cache_index] WITH NOCHECK ADD 
+	 PRIMARY KEY CLUSTERED 
+	(
+		[user_id],[mailbox]
+	) ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[cache_thread] WITH NOCHECK ADD 
+	 PRIMARY KEY CLUSTERED 
+	(
+		[user_id],[mailbox]
+	) ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[cache_messages] WITH NOCHECK ADD 
+	 PRIMARY KEY CLUSTERED 
+	(
+		[user_id],[mailbox],[uid]
+	) ON [PRIMARY] 
+GO
+
+ALTER TABLE [dbo].[cache_index] ADD 
+	CONSTRAINT [DF_cache_index_changed] DEFAULT (getdate()) FOR [changed],
+	CONSTRAINT [DF_cache_index_valid] DEFAULT ('0') FOR [valid]
+GO
+
+CREATE  INDEX [IX_cache_index_user_id] ON [dbo].[cache_index]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[cache_thread] ADD 
+	CONSTRAINT [DF_cache_thread_changed] DEFAULT (getdate()) FOR [changed]
+GO
+
+CREATE  INDEX [IX_cache_thread_user_id] ON [dbo].[cache_thread]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[cache_messages] ADD 
+	CONSTRAINT [DF_cache_messages_changed] DEFAULT (getdate()) FOR [changed],
+	CONSTRAINT [DF_cache_messages_flags] DEFAULT (0) FOR [flags]
+GO
+
+CREATE  INDEX [IX_cache_messages_user_id] ON [dbo].[cache_messages]([user_id]) ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[cache_index] ADD CONSTRAINT [FK_cache_index_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[cache_thread] ADD CONSTRAINT [FK_cache_thread_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
+ALTER TABLE [dbo].[cache_messages] ADD CONSTRAINT [FK_cache_messages_user_id]
+    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
+    ON DELETE CASCADE ON UPDATE CASCADE
+GO
+
Index: /branches/devel-composer/SQL/mysql.initial.sql
===================================================================
--- /branches/devel-composer/SQL/mysql.initial.sql	(revision 5386)
+++ /branches/devel-composer/SQL/mysql.initial.sql	(revision 5386)
@@ -0,0 +1,192 @@
+-- Roundcube Webmail initial database structure
+
+
+/*!40014  SET FOREIGN_KEY_CHECKS=0 */;
+
+-- Table structure for table `session`
+
+CREATE TABLE `session` (
+ `sess_id` varchar(40) NOT NULL,
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `ip` varchar(40) NOT NULL,
+ `vars` mediumtext NOT NULL,
+ PRIMARY KEY(`sess_id`),
+ INDEX `changed_index` (`changed`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+-- Table structure for table `users`
+
+CREATE TABLE `users` (
+ `user_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `username` varchar(128) BINARY NOT NULL,
+ `mail_host` varchar(128) NOT NULL,
+ `alias` varchar(128) BINARY NOT NULL,
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `last_login` datetime DEFAULT NULL,
+ `language` varchar(5),
+ `preferences` text,
+ PRIMARY KEY(`user_id`),
+ UNIQUE `username` (`username`, `mail_host`),
+ INDEX `alias_index` (`alias`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+-- Table structure for table `cache`
+
+CREATE TABLE `cache` (
+ `cache_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `cache_key` varchar(128) /*!40101 CHARACTER SET ascii COLLATE ascii_general_ci */ NOT NULL ,
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `data` longtext NOT NULL,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY(`cache_id`),
+ CONSTRAINT `user_id_fk_cache` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ INDEX `created_index` (`created`),
+ INDEX `user_cache_index` (`user_id`,`cache_key`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+-- Table structure for table `cache_index`
+
+CREATE TABLE `cache_index` (
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `mailbox` varchar(255) BINARY NOT NULL,
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `valid` tinyint(1) NOT NULL DEFAULT '0',
+ `data` longtext NOT NULL,
+ CONSTRAINT `user_id_fk_cache_index` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ INDEX `changed_index` (`changed`),
+ PRIMARY KEY (`user_id`, `mailbox`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+-- Table structure for table `cache_thread`
+
+CREATE TABLE `cache_thread` (
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `mailbox` varchar(255) BINARY NOT NULL,
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `data` longtext NOT NULL,
+ CONSTRAINT `user_id_fk_cache_thread` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ INDEX `changed_index` (`changed`),
+ PRIMARY KEY (`user_id`, `mailbox`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+-- Table structure for table `cache_messages`
+
+CREATE TABLE `cache_messages` (
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `mailbox` varchar(255) BINARY NOT NULL,
+ `uid` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `data` longtext NOT NULL,
+ `flags` int(11) NOT NULL DEFAULT '0',
+ CONSTRAINT `user_id_fk_cache_messages` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ INDEX `changed_index` (`changed`),
+ PRIMARY KEY (`user_id`, `mailbox`, `uid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+-- Table structure for table `contacts`
+
+CREATE TABLE `contacts` (
+ `contact_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `del` tinyint(1) NOT NULL DEFAULT '0',
+ `name` varchar(128) NOT NULL DEFAULT '',
+ `email` varchar(255) NOT NULL,
+ `firstname` varchar(128) NOT NULL DEFAULT '',
+ `surname` varchar(128) NOT NULL DEFAULT '',
+ `vcard` longtext NULL,
+ `words` text NULL,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY(`contact_id`),
+ CONSTRAINT `user_id_fk_contacts` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ INDEX `user_contacts_index` (`user_id`,`email`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+-- Table structure for table `contactgroups`
+
+CREATE TABLE `contactgroups` (
+  `contactgroup_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+  `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+  `del` tinyint(1) NOT NULL DEFAULT '0',
+  `name` varchar(128) NOT NULL DEFAULT '',
+  PRIMARY KEY(`contactgroup_id`),
+  CONSTRAINT `user_id_fk_contactgroups` FOREIGN KEY (`user_id`)
+    REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  INDEX `contactgroups_user_index` (`user_id`,`del`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE `contactgroupmembers` (
+  `contactgroup_id` int(10) UNSIGNED NOT NULL,
+  `contact_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+  `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+  PRIMARY KEY (`contactgroup_id`, `contact_id`),
+  CONSTRAINT `contactgroup_id_fk_contactgroups` FOREIGN KEY (`contactgroup_id`)
+    REFERENCES `contactgroups`(`contactgroup_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  CONSTRAINT `contact_id_fk_contacts` FOREIGN KEY (`contact_id`)
+    REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  INDEX `contactgroupmembers_contact_index` (`contact_id`)
+) /*!40000 ENGINE=INNODB */;
+
+
+-- Table structure for table `identities`
+
+CREATE TABLE `identities` (
+ `identity_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `del` tinyint(1) NOT NULL DEFAULT '0',
+ `standard` tinyint(1) NOT NULL DEFAULT '0',
+ `name` varchar(128) NOT NULL,
+ `organization` varchar(128) NOT NULL DEFAULT '',
+ `email` varchar(128) NOT NULL,
+ `reply-to` varchar(128) NOT NULL DEFAULT '',
+ `bcc` varchar(128) NOT NULL DEFAULT '',
+ `signature` text,
+ `html_signature` tinyint(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY(`identity_id`),
+ CONSTRAINT `user_id_fk_identities` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ INDEX `user_identities_index` (`user_id`, `del`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+-- Table structure for table `dictionary`
+
+CREATE TABLE `dictionary` (
+  `user_id` int(10) UNSIGNED DEFAULT NULL,
+  `language` varchar(5) NOT NULL,
+  `data` longtext NOT NULL,
+  CONSTRAINT `user_id_fk_dictionary` FOREIGN KEY (`user_id`)
+    REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  UNIQUE `uniqueness` (`user_id`, `language`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+-- Table structure for table `searches`
+
+CREATE TABLE `searches` (
+ `search_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `type` int(3) NOT NULL DEFAULT '0',
+ `name` varchar(128) NOT NULL,
+ `data` text,
+ PRIMARY KEY(`search_id`),
+ CONSTRAINT `user_id_fk_searches` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ UNIQUE `uniqueness` (`user_id`, `type`, `name`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+/*!40014 SET FOREIGN_KEY_CHECKS=1 */;
Index: /branches/devel-composer/SQL/mysql.update.sql
===================================================================
--- /branches/devel-composer/SQL/mysql.update.sql	(revision 5386)
+++ /branches/devel-composer/SQL/mysql.update.sql	(revision 5386)
@@ -0,0 +1,210 @@
+-- Roundcube Webmail update script for MySQL databases
+
+-- Updates from version 0.1-stable
+
+TRUNCATE TABLE `messages`;
+
+ALTER TABLE `messages`
+  DROP INDEX `idx`,
+  DROP INDEX `uid`;
+
+ALTER TABLE `cache`
+  DROP INDEX `cache_key`,
+  DROP INDEX `session_id`,
+  ADD INDEX `user_cache_index` (`user_id`,`cache_key`);
+
+ALTER TABLE `users`
+    ADD INDEX `username_index` (`username`),
+    ADD INDEX `alias_index` (`alias`);
+
+-- Updates from version 0.1.1
+
+ALTER TABLE `identities`
+    MODIFY `signature` text, 
+    MODIFY `bcc` varchar(128) NOT NULL DEFAULT '', 
+    MODIFY `reply-to` varchar(128) NOT NULL DEFAULT '', 
+    MODIFY `organization` varchar(128) NOT NULL DEFAULT '',
+    MODIFY `name` varchar(128) NOT NULL, 
+    MODIFY `email` varchar(128) NOT NULL; 
+
+-- Updates from version 0.2-alpha
+
+ALTER TABLE `messages`
+    ADD INDEX `created_index` (`created`);
+
+-- Updates from version 0.2-beta (InnoDB required)
+
+ALTER TABLE `cache`
+    DROP `session_id`;
+
+ALTER TABLE `session`
+    ADD INDEX `changed_index` (`changed`);
+
+ALTER TABLE `cache`
+    ADD INDEX `created_index` (`created`);
+
+ALTER TABLE `users`
+    CHANGE `language` `language` varchar(5);
+
+ALTER TABLE `cache` ENGINE=InnoDB;
+ALTER TABLE `session` ENGINE=InnoDB;
+ALTER TABLE `messages` ENGINE=InnoDB;
+ALTER TABLE `users` ENGINE=InnoDB;
+ALTER TABLE `contacts` ENGINE=InnoDB;
+ALTER TABLE `identities` ENGINE=InnoDB;
+
+-- Updates from version 0.3-stable
+
+TRUNCATE `messages`;
+
+ALTER TABLE `messages`
+    ADD INDEX `index_index` (`user_id`, `cache_key`, `idx`);
+
+ALTER TABLE `session` 
+    CHANGE `vars` `vars` MEDIUMTEXT NOT NULL;
+
+ALTER TABLE `contacts`
+    ADD INDEX `user_contacts_index` (`user_id`,`email`);
+
+-- Updates from version 0.3.1
+-- WARNING: Make sure that all tables are using InnoDB engine!!!
+--          If not, use: ALTER TABLE xxx ENGINE=InnoDB;
+
+/* MySQL bug workaround: http://bugs.mysql.com/bug.php?id=46293 */
+/*!40014 SET FOREIGN_KEY_CHECKS=0 */;
+
+ALTER TABLE `messages` DROP FOREIGN KEY `user_id_fk_messages`;
+ALTER TABLE `cache` DROP FOREIGN KEY `user_id_fk_cache`;
+ALTER TABLE `contacts` DROP FOREIGN KEY `user_id_fk_contacts`;
+ALTER TABLE `identities` DROP FOREIGN KEY `user_id_fk_identities`;
+
+ALTER TABLE `messages` ADD CONSTRAINT `user_id_fk_messages` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE `cache` ADD CONSTRAINT `user_id_fk_cache` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE `contacts` ADD CONSTRAINT `user_id_fk_contacts` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE `identities` ADD CONSTRAINT `user_id_fk_identities` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE `contacts` ALTER `name` SET DEFAULT '';
+ALTER TABLE `contacts` ALTER `firstname` SET DEFAULT '';
+ALTER TABLE `contacts` ALTER `surname` SET DEFAULT '';
+
+ALTER TABLE `identities` ADD INDEX `user_identities_index` (`user_id`, `del`);
+ALTER TABLE `identities` ADD `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00' AFTER `user_id`;
+
+CREATE TABLE `contactgroups` (
+  `contactgroup_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+  `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+  `del` tinyint(1) NOT NULL DEFAULT '0',
+  `name` varchar(128) NOT NULL DEFAULT '',
+  PRIMARY KEY(`contactgroup_id`),
+  CONSTRAINT `user_id_fk_contactgroups` FOREIGN KEY (`user_id`)
+    REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  INDEX `contactgroups_user_index` (`user_id`,`del`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE `contactgroupmembers` (
+  `contactgroup_id` int(10) UNSIGNED NOT NULL,
+  `contact_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+  `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+  PRIMARY KEY (`contactgroup_id`, `contact_id`),
+  CONSTRAINT `contactgroup_id_fk_contactgroups` FOREIGN KEY (`contactgroup_id`)
+    REFERENCES `contactgroups`(`contactgroup_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  CONSTRAINT `contact_id_fk_contacts` FOREIGN KEY (`contact_id`)
+    REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */;
+
+/*!40014 SET FOREIGN_KEY_CHECKS=1 */;
+
+-- Updates from version 0.4-beta
+
+ALTER TABLE `users` CHANGE `last_login` `last_login` datetime DEFAULT NULL;
+UPDATE `users` SET `last_login` = NULL WHERE `last_login` = '1000-01-01 00:00:00';
+
+-- Updates from version 0.4.2
+
+ALTER TABLE `users` DROP INDEX `username_index`;
+ALTER TABLE `users` ADD UNIQUE `username` (`username`, `mail_host`);
+
+ALTER TABLE `contacts` MODIFY `email` varchar(255) NOT NULL;
+
+TRUNCATE TABLE `messages`;
+
+-- Updates from version 0.5.1
+-- Updates from version 0.5.2
+-- Updates from version 0.5.3
+-- Updates from version 0.5.4
+
+ALTER TABLE `contacts` ADD `words` TEXT NULL AFTER `vcard`;
+ALTER TABLE `contacts` CHANGE `vcard` `vcard` LONGTEXT /*!40101 CHARACTER SET utf8 */ NULL DEFAULT NULL;
+ALTER TABLE `contactgroupmembers` ADD INDEX `contactgroupmembers_contact_index` (`contact_id`);
+
+TRUNCATE TABLE `messages`;
+TRUNCATE TABLE `cache`;
+
+-- Updates from version 0.6
+
+ALTER TABLE `users` CHANGE `alias` `alias` varchar(128) BINARY NOT NULL;
+ALTER TABLE `users` CHANGE `username` `username` varchar(128) BINARY NOT NULL;
+
+CREATE TABLE `dictionary` (
+  `user_id` int(10) UNSIGNED DEFAULT NULL,
+  `language` varchar(5) NOT NULL,
+  `data` longtext NOT NULL,
+  CONSTRAINT `user_id_fk_dictionary` FOREIGN KEY (`user_id`)
+    REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  UNIQUE `uniqueness` (`user_id`, `language`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE `searches` (
+  `search_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+  `type` int(3) NOT NULL DEFAULT '0',
+  `name` varchar(128) NOT NULL,
+  `data` text,
+  PRIMARY KEY(`search_id`),
+  CONSTRAINT `user_id_fk_searches` FOREIGN KEY (`user_id`)
+    REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  UNIQUE `uniqueness` (`user_id`, `type`, `name`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE `messages`;
+
+CREATE TABLE `cache_index` (
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `mailbox` varchar(255) BINARY NOT NULL,
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `valid` tinyint(1) NOT NULL DEFAULT '0',
+ `data` longtext NOT NULL,
+ CONSTRAINT `user_id_fk_cache_index` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ INDEX `changed_index` (`changed`),
+ PRIMARY KEY (`user_id`, `mailbox`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE `cache_thread` (
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `mailbox` varchar(255) BINARY NOT NULL,
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `data` longtext NOT NULL,
+ CONSTRAINT `user_id_fk_cache_thread` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ INDEX `changed_index` (`changed`),
+ PRIMARY KEY (`user_id`, `mailbox`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE `cache_messages` (
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `mailbox` varchar(255) BINARY NOT NULL,
+ `uid` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `data` longtext NOT NULL,
+ `flags` int(11) NOT NULL DEFAULT '0',
+ CONSTRAINT `user_id_fk_cache_messages` FOREIGN KEY (`user_id`)
+   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ INDEX `changed_index` (`changed`),
+ PRIMARY KEY (`user_id`, `mailbox`, `uid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
Index: /branches/devel-composer/SQL/postgres.initial.sql
===================================================================
--- /branches/devel-composer/SQL/postgres.initial.sql	(revision 5386)
+++ /branches/devel-composer/SQL/postgres.initial.sql	(revision 5386)
@@ -0,0 +1,278 @@
+-- Roundcube Webmail initial database structure
+
+--
+-- Sequence "user_ids"
+-- Name: user_ids; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE user_ids
+    INCREMENT BY 1
+    NO MAXVALUE
+    NO MINVALUE
+    CACHE 1;
+
+--
+-- Table "users"
+-- Name: users; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE users (
+    user_id integer DEFAULT nextval('user_ids'::text) PRIMARY KEY,
+    username varchar(128) DEFAULT '' NOT NULL,
+    mail_host varchar(128) DEFAULT '' NOT NULL,
+    alias varchar(128) DEFAULT '' NOT NULL,
+    created timestamp with time zone DEFAULT now() NOT NULL,
+    last_login timestamp with time zone DEFAULT NULL,
+    "language" varchar(5),
+    preferences text DEFAULT ''::text NOT NULL,
+    CONSTRAINT users_username_key UNIQUE (username, mail_host)
+);
+
+CREATE INDEX users_alias_id_idx ON users (alias);
+
+  
+--
+-- Table "session"
+-- Name: session; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE "session" (
+    sess_id varchar(40) DEFAULT '' PRIMARY KEY,
+    created timestamp with time zone DEFAULT now() NOT NULL,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    ip varchar(41) NOT NULL,
+    vars text NOT NULL
+);
+
+CREATE INDEX session_changed_idx ON session (changed);
+
+
+--
+-- Sequence "identity_ids"
+-- Name: identity_ids; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE identity_ids
+    START WITH 1
+    INCREMENT BY 1
+    NO MAXVALUE
+    NO MINVALUE
+    CACHE 1;
+
+--
+-- Table "identities"
+-- Name: identities; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE identities (
+    identity_id integer DEFAULT nextval('identity_ids'::text) PRIMARY KEY,
+    user_id integer NOT NULL
+        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    del smallint DEFAULT 0 NOT NULL,
+    standard smallint DEFAULT 0 NOT NULL,
+    name varchar(128) NOT NULL,
+    organization varchar(128),
+    email varchar(128) NOT NULL,
+    "reply-to" varchar(128),
+    bcc varchar(128),
+    signature text,
+    html_signature integer DEFAULT 0 NOT NULL
+);
+
+CREATE INDEX identities_user_id_idx ON identities (user_id, del);
+
+
+--
+-- Sequence "contact_ids"
+-- Name: contact_ids; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE contact_ids
+    START WITH 1
+    INCREMENT BY 1
+    NO MAXVALUE
+    NO MINVALUE
+    CACHE 1;
+
+--
+-- Table "contacts"
+-- Name: contacts; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE contacts (
+    contact_id integer DEFAULT nextval('contact_ids'::text) PRIMARY KEY,
+    user_id integer NOT NULL
+        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    del smallint DEFAULT 0 NOT NULL,
+    name varchar(128) DEFAULT '' NOT NULL,
+    email varchar(255) DEFAULT '' NOT NULL,
+    firstname varchar(128) DEFAULT '' NOT NULL,
+    surname varchar(128) DEFAULT '' NOT NULL,
+    vcard text,
+    words text
+);
+
+CREATE INDEX contacts_user_id_idx ON contacts (user_id, email);
+
+--
+-- Sequence "contactgroups_ids"
+-- Name: contactgroups_ids; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE contactgroups_ids
+    INCREMENT BY 1
+    NO MAXVALUE
+    NO MINVALUE
+    CACHE 1;
+
+--
+-- Table "contactgroups"
+-- Name: contactgroups; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE contactgroups (
+    contactgroup_id integer DEFAULT nextval('contactgroups_ids'::text) PRIMARY KEY,
+    user_id integer NOT NULL
+        REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    del smallint NOT NULL DEFAULT 0,
+    name varchar(128) NOT NULL DEFAULT ''
+);
+
+CREATE INDEX contactgroups_user_id_idx ON contactgroups (user_id, del);
+
+--
+-- Table "contactgroupmembers"
+-- Name: contactgroupmembers; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE contactgroupmembers (
+    contactgroup_id integer NOT NULL
+        REFERENCES contactgroups(contactgroup_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    contact_id integer NOT NULL
+        REFERENCES contacts(contact_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    created timestamp with time zone DEFAULT now() NOT NULL,
+    PRIMARY KEY (contactgroup_id, contact_id)
+);
+
+CREATE INDEX contactgroupmembers_contact_id_idx ON contactgroupmembers (contact_id);
+
+--
+-- Sequence "cache_ids"
+-- Name: cache_ids; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE cache_ids
+    INCREMENT BY 1
+    NO MAXVALUE
+    NO MINVALUE
+    CACHE 1;
+
+--
+-- Table "cache"
+-- Name: cache; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE "cache" (
+    cache_id integer DEFAULT nextval('cache_ids'::text) PRIMARY KEY,
+    user_id integer NOT NULL
+    	REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    cache_key varchar(128) DEFAULT '' NOT NULL,
+    created timestamp with time zone DEFAULT now() NOT NULL,
+    data text NOT NULL
+);
+
+CREATE INDEX cache_user_id_idx ON "cache" (user_id, cache_key);
+CREATE INDEX cache_created_idx ON "cache" (created);
+
+--
+-- Table "cache_index"
+-- Name: cache_index; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE cache_index (
+    user_id integer NOT NULL
+    	REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    mailbox varchar(255) NOT NULL,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    valid smallint NOT NULL DEFAULT 0,
+    data text NOT NULL,
+    PRIMARY KEY (user_id, mailbox)
+);
+
+CREATE INDEX cache_index_changed_idx ON cache_index (changed);
+
+--
+-- Table "cache_thread"
+-- Name: cache_thread; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE cache_thread (
+    user_id integer NOT NULL
+    	REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    mailbox varchar(255) NOT NULL,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    data text NOT NULL,
+    PRIMARY KEY (user_id, mailbox)
+);
+
+CREATE INDEX cache_thread_changed_idx ON cache_thread (changed);
+
+--
+-- Table "cache_messages"
+-- Name: cache_messages; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE cache_messages (
+    user_id integer NOT NULL
+    	REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    mailbox varchar(255) NOT NULL,
+    uid integer NOT NULL,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    data text NOT NULL,
+    flags integer NOT NULL DEFAULT 0,
+    PRIMARY KEY (user_id, mailbox, uid)
+);
+
+CREATE INDEX cache_messages_changed_idx ON cache_messages (changed);
+
+--
+-- Table "dictionary"
+-- Name: dictionary; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE dictionary (
+    user_id integer DEFAULT NULL
+    	REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+   "language" varchar(5) NOT NULL,
+    data text NOT NULL,
+    CONSTRAINT dictionary_user_id_language_key UNIQUE (user_id, "language")
+);
+
+--
+-- Sequence "searches_ids"
+-- Name: searches_ids; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE search_ids
+    INCREMENT BY 1
+    NO MAXVALUE
+    NO MINVALUE
+    CACHE 1;
+
+--
+-- Table "searches"
+-- Name: searches; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE searches (
+    search_id integer DEFAULT nextval('search_ids'::text) PRIMARY KEY,
+    user_id integer NOT NULL
+        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    "type" smallint DEFAULT 0 NOT NULL,
+    name varchar(128) NOT NULL,
+    data text NOT NULL,
+    CONSTRAINT searches_user_id_key UNIQUE (user_id, "type", name)
+);
Index: /branches/devel-composer/SQL/postgres.update.sql
===================================================================
--- /branches/devel-composer/SQL/postgres.update.sql	(revision 5386)
+++ /branches/devel-composer/SQL/postgres.update.sql	(revision 5386)
@@ -0,0 +1,167 @@
+-- Roundcube Webmail update script for Postgres databases
+-- Updates from version 0.1-stable to 0.1.1
+
+CREATE INDEX cache_user_id_idx ON cache (user_id, cache_key);
+CREATE INDEX contacts_user_id_idx ON contacts (user_id);
+CREATE INDEX identities_user_id_idx ON identities (user_id);
+
+CREATE INDEX users_username_id_idx ON users (username);
+CREATE INDEX users_alias_id_idx ON users (alias);
+
+-- added ON DELETE/UPDATE actions
+ALTER TABLE messages DROP CONSTRAINT messages_user_id_fkey;
+ALTER TABLE messages ADD FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE identities DROP CONSTRAINT identities_user_id_fkey;
+ALTER TABLE identities ADD FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE contacts DROP CONSTRAINT contacts_user_id_fkey;
+ALTER TABLE contacts ADD FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE cache DROP CONSTRAINT cache_user_id_fkey;
+ALTER TABLE cache ADD FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- Updates from version 0.2-alpha
+
+CREATE INDEX messages_created_idx ON messages (created);
+
+-- Updates from version 0.2-beta
+
+ALTER TABLE cache DROP session_id;
+
+CREATE INDEX session_changed_idx ON session (changed);
+CREATE INDEX cache_created_idx ON "cache" (created);
+
+ALTER TABLE users ALTER "language" DROP NOT NULL;
+ALTER TABLE users ALTER "language" DROP DEFAULT;
+
+ALTER TABLE identities ALTER del TYPE smallint;
+ALTER TABLE identities ALTER standard TYPE smallint;
+ALTER TABLE contacts ALTER del TYPE smallint;
+ALTER TABLE messages ALTER del TYPE smallint;
+
+-- Updates from version 0.3-stable
+
+TRUNCATE messages;
+CREATE INDEX messages_index_idx ON messages (user_id, cache_key, idx);
+DROP INDEX contacts_user_id_idx;
+CREATE INDEX contacts_user_id_idx ON contacts (user_id, email);
+
+-- Updates from version 0.3.1
+
+DROP INDEX identities_user_id_idx;
+CREATE INDEX identities_user_id_idx ON identities (user_id, del);
+
+ALTER TABLE identities ADD changed timestamp with time zone DEFAULT now() NOT NULL;
+
+CREATE SEQUENCE contactgroups_ids
+    INCREMENT BY 1
+    NO MAXVALUE
+    NO MINVALUE
+    CACHE 1;
+
+CREATE TABLE contactgroups (
+    contactgroup_id integer DEFAULT nextval('contactgroups_ids'::text) PRIMARY KEY,
+    user_id 	integer		NOT NULL
+        REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    changed 	timestamp with time zone DEFAULT now() NOT NULL,
+    del 	smallint 	NOT NULL DEFAULT 0,
+    name 	varchar(128) 	NOT NULL DEFAULT ''
+);
+
+CREATE INDEX contactgroups_user_id_idx ON contactgroups (user_id, del);
+
+CREATE TABLE contactgroupmembers (
+    contactgroup_id 	integer NOT NULL
+	REFERENCES contactgroups(contactgroup_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    contact_id 		integer NOT NULL
+	REFERENCES contacts(contact_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    created timestamp with time zone DEFAULT now() NOT NULL,
+    PRIMARY KEY (contactgroup_id, contact_id)
+);
+
+-- Updates from version 0.4-beta
+
+ALTER TABLE users ALTER last_login DROP NOT NULL;
+ALTER TABLE users ALTER last_login SET DEFAULT NULL;
+
+-- Updates from version 0.4.2
+
+DROP INDEX users_username_id_idx;
+ALTER TABLE users ADD CONSTRAINT users_username_key UNIQUE (username, mail_host);
+ALTER TABLE contacts ALTER email TYPE varchar(255);
+
+TRUNCATE messages;
+
+-- Updates from version 0.5.1
+-- Updates from version 0.5.2
+-- Updates from version 0.5.3
+-- Updates from version 0.5.4
+
+ALTER TABLE contacts ADD words TEXT NULL;
+CREATE INDEX contactgroupmembers_contact_id_idx ON contactgroupmembers (contact_id);
+
+TRUNCATE messages;
+TRUNCATE cache;
+
+-- Updates from version 0.6
+
+CREATE TABLE dictionary (
+    user_id integer DEFAULT NULL
+        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+   "language" varchar(5) NOT NULL,
+    data text NOT NULL,
+    CONSTRAINT dictionary_user_id_language_key UNIQUE (user_id, "language")
+);
+
+CREATE SEQUENCE search_ids
+    INCREMENT BY 1
+    NO MAXVALUE
+    NO MINVALUE
+    CACHE 1;
+
+CREATE TABLE searches (
+    search_id integer DEFAULT nextval('search_ids'::text) PRIMARY KEY,
+    user_id integer NOT NULL
+        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    "type" smallint DEFAULT 0 NOT NULL,
+    name varchar(128) NOT NULL,
+    data text NOT NULL,
+    CONSTRAINT searches_user_id_key UNIQUE (user_id, "type", name)
+);
+
+DROP SEQUENCE messages_ids;
+DROP TABLE messages;
+
+CREATE TABLE cache_index (
+    user_id integer NOT NULL
+    	REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    mailbox varchar(255) NOT NULL,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    valid smallint NOT NULL DEFAULT 0,
+    data text NOT NULL,
+    PRIMARY KEY (user_id, mailbox)
+);
+
+CREATE INDEX cache_index_changed_idx ON cache_index (changed);
+
+CREATE TABLE cache_thread (
+    user_id integer NOT NULL
+    	REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    mailbox varchar(255) NOT NULL,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    data text NOT NULL,
+    PRIMARY KEY (user_id, mailbox)
+);
+
+CREATE INDEX cache_thread_changed_idx ON cache_thread (changed);
+
+CREATE TABLE cache_messages (
+    user_id integer NOT NULL
+    	REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    mailbox varchar(255) NOT NULL,
+    uid integer NOT NULL,
+    changed timestamp with time zone DEFAULT now() NOT NULL,
+    data text NOT NULL,
+    flags integer NOT NULL DEFAULT 0,
+    PRIMARY KEY (user_id, mailbox, uid)
+);
+
+CREATE INDEX cache_messages_changed_idx ON cache_messages (changed);
Index: /branches/devel-composer/SQL/sqlite.initial.sql
===================================================================
--- /branches/devel-composer/SQL/sqlite.initial.sql	(revision 5386)
+++ /branches/devel-composer/SQL/sqlite.initial.sql	(revision 5386)
@@ -0,0 +1,201 @@
+-- Roundcube Webmail initial database structure
+
+-- 
+-- Table structure for table cache
+-- 
+
+CREATE TABLE cache (
+  cache_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL default 0,
+  cache_key varchar(128) NOT NULL default '',
+  created datetime NOT NULL default '0000-00-00 00:00:00',
+  data text NOT NULL
+);
+
+CREATE INDEX ix_cache_user_cache_key ON cache(user_id, cache_key);
+CREATE INDEX ix_cache_created ON cache(created);
+
+
+-- --------------------------------------------------------
+
+-- 
+-- Table structure for table contacts and related
+-- 
+
+CREATE TABLE contacts (
+  contact_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL default '0',
+  changed datetime NOT NULL default '0000-00-00 00:00:00',
+  del tinyint NOT NULL default '0',
+  name varchar(128) NOT NULL default '',
+  email varchar(255) NOT NULL default '',
+  firstname varchar(128) NOT NULL default '',
+  surname varchar(128) NOT NULL default '',
+  vcard text NOT NULL default '',
+  words text NOT NULL default ''
+);
+
+CREATE INDEX ix_contacts_user_id ON contacts(user_id, email);
+
+
+CREATE TABLE contactgroups (
+  contactgroup_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL default '0',
+  changed datetime NOT NULL default '0000-00-00 00:00:00',
+  del tinyint NOT NULL default '0',
+  name varchar(128) NOT NULL default ''
+);
+
+CREATE INDEX ix_contactgroups_user_id ON contactgroups(user_id, del);
+
+
+CREATE TABLE contactgroupmembers (
+  contactgroup_id integer NOT NULL,
+  contact_id integer NOT NULL default '0',
+  created datetime NOT NULL default '0000-00-00 00:00:00',
+  PRIMARY KEY (contactgroup_id, contact_id)
+);
+
+CREATE INDEX ix_contactgroupmembers_contact_id ON contactgroupmembers (contact_id);
+
+
+-- --------------------------------------------------------
+
+-- 
+-- Table structure for table identities
+-- 
+
+CREATE TABLE identities (
+  identity_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL default '0',
+  changed datetime NOT NULL default '0000-00-00 00:00:00',
+  del tinyint NOT NULL default '0',
+  standard tinyint NOT NULL default '0',
+  name varchar(128) NOT NULL default '',
+  organization varchar(128) default '',
+  email varchar(128) NOT NULL default '',
+  "reply-to" varchar(128) NOT NULL default '',
+  bcc varchar(128) NOT NULL default '',
+  signature text NOT NULL default '',
+  html_signature tinyint NOT NULL default '0'
+);
+
+CREATE INDEX ix_identities_user_id ON identities(user_id, del);
+
+
+-- --------------------------------------------------------
+
+-- 
+-- Table structure for table users
+-- 
+
+CREATE TABLE users (
+  user_id integer NOT NULL PRIMARY KEY,
+  username varchar(128) NOT NULL default '',
+  mail_host varchar(128) NOT NULL default '',
+  alias varchar(128) NOT NULL default '',
+  created datetime NOT NULL default '0000-00-00 00:00:00',
+  last_login datetime DEFAULT NULL,
+  language varchar(5),
+  preferences text NOT NULL default ''
+);
+
+CREATE UNIQUE INDEX ix_users_username ON users(username, mail_host);
+CREATE INDEX ix_users_alias ON users(alias);
+
+-- --------------------------------------------------------
+
+-- 
+-- Table structure for table session
+-- 
+
+CREATE TABLE session (
+  sess_id varchar(40) NOT NULL PRIMARY KEY,
+  created datetime NOT NULL default '0000-00-00 00:00:00',
+  changed datetime NOT NULL default '0000-00-00 00:00:00',
+  ip varchar(40) NOT NULL default '',
+  vars text NOT NULL
+);
+
+CREATE INDEX ix_session_changed ON session (changed);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table dictionary
+--
+
+CREATE TABLE dictionary (
+    user_id integer DEFAULT NULL,
+   "language" varchar(5) NOT NULL,
+    data text NOT NULL
+);
+
+CREATE UNIQUE INDEX ix_dictionary_user_language ON dictionary (user_id, "language");
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table searches
+--
+
+CREATE TABLE searches (
+  search_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL DEFAULT '0',
+  "type" smallint NOT NULL DEFAULT '0',
+  name varchar(128) NOT NULL,
+  data text NOT NULL
+);
+
+CREATE UNIQUE INDEX ix_searches_user_type_name (user_id, type, name);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table cache_index
+--
+
+CREATE TABLE cache_index (
+    user_id integer NOT NULL,
+    mailbox varchar(255) NOT NULL,
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    valid smallint NOT NULL DEFAULT '0',
+    data text NOT NULL,
+    PRIMARY KEY (user_id, mailbox)
+);
+
+CREATE INDEX ix_cache_index_changed ON cache_index (changed);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table cache_thread
+--
+
+CREATE TABLE cache_thread (
+    user_id integer NOT NULL,
+    mailbox varchar(255) NOT NULL,
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    data text NOT NULL,
+    PRIMARY KEY (user_id, mailbox)
+);
+
+CREATE INDEX ix_cache_thread_changed ON cache_thread (changed);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table cache_messages
+--
+
+CREATE TABLE cache_messages (
+    user_id integer NOT NULL,
+    mailbox varchar(255) NOT NULL,
+    uid integer NOT NULL,
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    data text NOT NULL,
+    flags integer NOT NULL DEFAULT '0',
+    PRIMARY KEY (user_id, mailbox, uid)
+);
+
+CREATE INDEX ix_cache_messages_changed ON cache_messages (changed);
Index: /branches/devel-composer/SQL/sqlite.update.sql
===================================================================
--- /branches/devel-composer/SQL/sqlite.update.sql	(revision 5386)
+++ /branches/devel-composer/SQL/sqlite.update.sql	(revision 5386)
@@ -0,0 +1,284 @@
+-- Roundcube Webmail update script for SQLite databases
+-- Updates from version 0.1-stable to 0.1.1
+
+DROP TABLE messages;
+
+CREATE TABLE messages (
+  message_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL default '0',
+  del tinyint NOT NULL default '0',
+  cache_key varchar(128) NOT NULL default '',
+  created datetime NOT NULL default '0000-00-00 00:00:00',
+  idx integer NOT NULL default '0',
+  uid integer NOT NULL default '0',
+  subject varchar(255) NOT NULL default '',
+  "from" varchar(255) NOT NULL default '',
+  "to" varchar(255) NOT NULL default '',
+  "cc" varchar(255) NOT NULL default '',
+  "date" datetime NOT NULL default '0000-00-00 00:00:00',
+  size integer NOT NULL default '0',
+  headers text NOT NULL,
+  structure text
+);
+
+CREATE INDEX ix_messages_user_cache_uid ON messages(user_id,cache_key,uid);
+CREATE INDEX ix_users_username ON users(username);
+CREATE INDEX ix_users_alias ON users(alias);
+
+-- Updates from version 0.2-alpha
+
+CREATE INDEX ix_messages_created ON messages (created);
+
+-- Updates from version 0.2-beta
+
+CREATE INDEX ix_session_changed ON session (changed);
+CREATE INDEX ix_cache_created ON cache (created);
+
+-- Updates from version 0.3-stable
+
+DELETE FROM messages;
+DROP INDEX ix_messages_user_cache_uid;
+CREATE UNIQUE INDEX ix_messages_user_cache_uid ON messages (user_id,cache_key,uid);
+CREATE INDEX ix_messages_index ON messages (user_id,cache_key,idx);
+DROP INDEX ix_contacts_user_id;
+CREATE INDEX ix_contacts_user_id ON contacts(user_id, email);
+
+-- Updates from version 0.3.1
+
+-- ALTER TABLE identities ADD COLUMN changed datetime NOT NULL default '0000-00-00 00:00:00'; --
+
+CREATE TABLE temp_identities (
+  identity_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL default '0',
+  standard tinyint NOT NULL default '0',
+  name varchar(128) NOT NULL default '',
+  organization varchar(128) default '',
+  email varchar(128) NOT NULL default '',
+  "reply-to" varchar(128) NOT NULL default '',
+  bcc varchar(128) NOT NULL default '',
+  signature text NOT NULL default '',
+  html_signature tinyint NOT NULL default '0'
+);
+INSERT INTO temp_identities (identity_id, user_id, standard, name, organization, email, "reply-to", bcc, signature, html_signature)
+  SELECT identity_id, user_id, standard, name, organization, email, "reply-to", bcc, signature, html_signature
+  FROM identities WHERE del=0;
+
+DROP INDEX ix_identities_user_id;
+DROP TABLE identities;
+
+CREATE TABLE identities (
+  identity_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL default '0',
+  changed datetime NOT NULL default '0000-00-00 00:00:00',
+  del tinyint NOT NULL default '0',
+  standard tinyint NOT NULL default '0',
+  name varchar(128) NOT NULL default '',
+  organization varchar(128) default '',
+  email varchar(128) NOT NULL default '',
+  "reply-to" varchar(128) NOT NULL default '',
+  bcc varchar(128) NOT NULL default '',
+  signature text NOT NULL default '',
+  html_signature tinyint NOT NULL default '0'
+);
+CREATE INDEX ix_identities_user_id ON identities(user_id, del);
+
+INSERT INTO identities (identity_id, user_id, standard, name, organization, email, "reply-to", bcc, signature, html_signature)
+  SELECT identity_id, user_id, standard, name, organization, email, "reply-to", bcc, signature, html_signature
+  FROM temp_identities;
+
+DROP TABLE temp_identities;
+
+CREATE TABLE contactgroups (
+  contactgroup_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL default '0',
+  changed datetime NOT NULL default '0000-00-00 00:00:00',
+  del tinyint NOT NULL default '0',
+  name varchar(128) NOT NULL default ''
+);
+
+CREATE INDEX ix_contactgroups_user_id ON contactgroups(user_id, del);
+
+CREATE TABLE contactgroupmembers (
+  contactgroup_id integer NOT NULL,
+  contact_id integer NOT NULL default '0',
+  created datetime NOT NULL default '0000-00-00 00:00:00',
+  PRIMARY KEY (contactgroup_id, contact_id)
+);
+
+-- Updates from version 0.3.1
+
+CREATE TABLE tmp_users (
+  user_id integer NOT NULL PRIMARY KEY,
+  username varchar(128) NOT NULL default '',
+  mail_host varchar(128) NOT NULL default '',
+  alias varchar(128) NOT NULL default '',
+  created datetime NOT NULL default '0000-00-00 00:00:00',
+  last_login datetime NOT NULL default '0000-00-00 00:00:00',
+  language varchar(5),
+  preferences text NOT NULL default ''
+);
+
+INSERT INTO tmp_users (user_id, username, mail_host, alias, created, last_login, language, preferences)
+    SELECT user_id, username, mail_host, alias, created, last_login, language, preferences FROM users;
+
+DROP TABLE users;
+
+CREATE TABLE users (
+  user_id integer NOT NULL PRIMARY KEY,
+  username varchar(128) NOT NULL default '',
+  mail_host varchar(128) NOT NULL default '',
+  alias varchar(128) NOT NULL default '',
+  created datetime NOT NULL default '0000-00-00 00:00:00',
+  last_login datetime DEFAULT NULL,
+  language varchar(5),
+  preferences text NOT NULL default ''
+);
+
+INSERT INTO users (user_id, username, mail_host, alias, created, last_login, language, preferences)
+    SELECT user_id, username, mail_host, alias, created, last_login, language, preferences FROM tmp_users;
+
+CREATE INDEX ix_users_username ON users(username);
+CREATE INDEX ix_users_alias ON users(alias);
+DROP TABLE tmp_users;
+
+-- Updates from version 0.4.2
+
+DROP INDEX ix_users_username;
+CREATE UNIQUE INDEX ix_users_username ON users(username, mail_host);
+
+CREATE TABLE contacts_tmp (
+    contact_id integer NOT NULL PRIMARY KEY,
+    user_id integer NOT NULL default '0',
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    del tinyint NOT NULL default '0',
+    name varchar(128) NOT NULL default '',
+    email varchar(255) NOT NULL default '',
+    firstname varchar(128) NOT NULL default '',
+    surname varchar(128) NOT NULL default '',
+    vcard text NOT NULL default ''
+);
+
+INSERT INTO contacts_tmp (contact_id, user_id, changed, del, name, email, firstname, surname, vcard)
+    SELECT contact_id, user_id, changed, del, name, email, firstname, surname, vcard FROM contacts;
+
+DROP TABLE contacts;
+CREATE TABLE contacts (
+    contact_id integer NOT NULL PRIMARY KEY,
+    user_id integer NOT NULL default '0',
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    del tinyint NOT NULL default '0',
+    name varchar(128) NOT NULL default '',
+    email varchar(255) NOT NULL default '',
+    firstname varchar(128) NOT NULL default '',
+    surname varchar(128) NOT NULL default '',
+    vcard text NOT NULL default ''
+);
+
+INSERT INTO contacts (contact_id, user_id, changed, del, name, email, firstname, surname, vcard)
+    SELECT contact_id, user_id, changed, del, name, email, firstname, surname, vcard FROM contacts_tmp;
+
+CREATE INDEX ix_contacts_user_id ON contacts(user_id, email);
+DROP TABLE contacts_tmp;
+
+DELETE FROM messages;
+
+
+-- Updates from version 0.5.1
+-- Updates from version 0.5.2
+-- Updates from version 0.5.3
+-- Updates from version 0.5.4
+
+CREATE TABLE contacts_tmp (
+    contact_id integer NOT NULL PRIMARY KEY,
+    user_id integer NOT NULL default '0',
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    del tinyint NOT NULL default '0',
+    name varchar(128) NOT NULL default '',
+    email varchar(255) NOT NULL default '',
+    firstname varchar(128) NOT NULL default '',
+    surname varchar(128) NOT NULL default '',
+    vcard text NOT NULL default ''
+);
+
+INSERT INTO contacts_tmp (contact_id, user_id, changed, del, name, email, firstname, surname, vcard)
+    SELECT contact_id, user_id, changed, del, name, email, firstname, surname, vcard FROM contacts;
+
+DROP TABLE contacts;
+CREATE TABLE contacts (
+    contact_id integer NOT NULL PRIMARY KEY,
+    user_id integer NOT NULL default '0',
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    del tinyint NOT NULL default '0',
+    name varchar(128) NOT NULL default '',
+    email varchar(255) NOT NULL default '',
+    firstname varchar(128) NOT NULL default '',
+    surname varchar(128) NOT NULL default '',
+    vcard text NOT NULL default '',
+    words text NOT NULL default ''
+);
+
+INSERT INTO contacts (contact_id, user_id, changed, del, name, email, firstname, surname, vcard)
+    SELECT contact_id, user_id, changed, del, name, email, firstname, surname, vcard FROM contacts_tmp;
+
+CREATE INDEX ix_contacts_user_id ON contacts(user_id, email);
+DROP TABLE contacts_tmp;
+
+
+DELETE FROM messages;
+DELETE FROM cache;
+CREATE INDEX ix_contactgroupmembers_contact_id ON contactgroupmembers (contact_id);
+
+-- Updates from version 0.6
+
+CREATE TABLE dictionary (
+    user_id integer DEFAULT NULL,
+   "language" varchar(5) NOT NULL,
+    data text NOT NULL
+);
+
+CREATE UNIQUE INDEX ix_dictionary_user_language ON dictionary (user_id, "language");
+
+CREATE TABLE searches (
+  search_id integer NOT NULL PRIMARY KEY,
+  user_id integer NOT NULL DEFAULT '0',
+  "type" smallint NOT NULL DEFAULT '0',
+  name varchar(128) NOT NULL,
+  data text NOT NULL
+);
+
+CREATE UNIQUE INDEX ix_searches_user_type_name (user_id, type, name);
+
+DROP TABLE messages;
+
+CREATE TABLE cache_index (
+    user_id integer NOT NULL,
+    mailbox varchar(255) NOT NULL,
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    valid smallint NOT NULL DEFAULT '0',
+    data text NOT NULL,
+    PRIMARY KEY (user_id, mailbox)
+);
+
+CREATE INDEX ix_cache_index_changed ON cache_index (changed);
+
+CREATE TABLE cache_thread (
+    user_id integer NOT NULL,
+    mailbox varchar(255) NOT NULL,
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    data text NOT NULL,
+    PRIMARY KEY (user_id, mailbox)
+);
+
+CREATE INDEX ix_cache_thread_changed ON cache_thread (changed);
+
+CREATE TABLE cache_messages (
+    user_id integer NOT NULL,
+    mailbox varchar(255) NOT NULL,
+    uid integer NOT NULL,
+    changed datetime NOT NULL default '0000-00-00 00:00:00',
+    data text NOT NULL,
+    flags integer NOT NULL DEFAULT '0',
+    PRIMARY KEY (user_id, mailbox, uid)
+);
+
+CREATE INDEX ix_cache_messages_changed ON cache_messages (changed);
Index: /branches/devel-composer/UPGRADING
===================================================================
--- /branches/devel-composer/UPGRADING	(revision 5386)
+++ /branches/devel-composer/UPGRADING	(revision 5386)
@@ -0,0 +1,54 @@
+UPGRADING instructions
+======================
+
+Follow these instructions if upgrading from a previous version
+of Roundcube Webmail. We recommend to carefully backup the existing
+installation as well as the database before going through the following steps.
+
+Using the update script
+-----------------------
+There is a shell script (for unix based systems) that does the job for you.
+To use it, unpack the archive of the new Roundcube version to a temporary location
+(don't replace the Roundcube installation you want to update)
+and cd into that directory. From there, run the following command in a shell:
+
+  ./bin/installto.sh <TARGET-FOLDER>
+
+For <TARGET-FOLDER> you specify the path to the Roundcube installation 
+which should be updated. The update script will then copy all new files to the 
+target location and check and update the configuration and database schema.
+After all is done, the temporary folder with the new Roundcube files can be 
+removed again.
+
+Please also see Post-Upgrade Activities section.
+
+
+Updating manually
+-----------------
+If you don't have shell access to the Roundcube installation or if not running 
+it on a unix system, you need to do the following operations by hand:
+
+1. Replace index.php and all files in
+   - ./bin/
+   - ./SQL/
+   - ./program/
+   - ./installer/
+   - ./skins/default/
+   - ./plugins/
+2. Run ./bin/update.sh from the commandline OR
+   open http://url-to-roundcube/installer/ in a browser and choose "3 Test config".
+   To enable the latter one, you have to temporary set 'enable_installer'
+   to true in your local config/main.inc.php file.
+3. Let the update script/installer check your configuration and
+   update your config files and database schema as suggested by the updater.
+4. Make sure 'enable_installer' is set to false again.
+5. See Post-Upgrade Activities section.
+
+
+Post-Upgrade Activities
+-----------------------
+1. Check .htaccess settings (some php settings could become required)
+2. If you're using build-in addressbook, run indexing script /bin/indexcontacts.sh.
+3. When upgrading from version older than 0.6-beta you should make sure
+   your folder settings contain namespace prefix. For example Courier users
+   should add INBOX. prefix to folder names in main configuration file.
Index: /branches/devel-composer/bin/cleandb.sh
===================================================================
--- /branches/devel-composer/bin/cleandb.sh	(revision 5386)
+++ /branches/devel-composer/bin/cleandb.sh	(revision 5386)
@@ -0,0 +1,78 @@
+#!/usr/bin/env php
+<?php
+/*
+
+ +-----------------------------------------------------------------------+
+ | bin/cleandb.sh                                                        |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2010, The Roundcube Dev Team                            |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Finally remove all db records marked as deleted some time ago       |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
+
+require INSTALL_PATH.'program/include/clisetup.php';
+
+// mapping for table name => primary key
+$primary_keys = array(
+    'contacts' => "contact_id",
+    'contactgroups' => "contactgroup_id",
+);
+
+// connect to DB
+$RCMAIL = rcmail::get_instance();
+$db = $RCMAIL->get_dbh();
+$db->db_connect('w');
+
+if (!$db->is_connected() || $db->is_error())
+    die("No DB connection\n");
+
+if (!empty($_SERVER['argv'][1]))
+    $days = intval($_SERVER['argv'][1]);
+else
+    $days = 7;
+
+// remove all deleted records older than two days
+$threshold = date('Y-m-d 00:00:00', time() - $days * 86400);
+
+foreach (array('contacts','contactgroups','identities') as $table) {
+
+    $sqltable = get_table_name($table);
+
+    // also delete linked records
+    // could be skipped for databases which respect foreign key constraints
+    if ($db->db_provider == 'sqlite'
+        && ($table == 'contacts' || $table == 'contactgroups')
+    ) {
+        $pk = $primary_keys[$table];
+        $memberstable = get_table_name('contactgroupmembers');
+
+        $db->query(
+            "DELETE FROM $memberstable".
+            " WHERE $pk IN (".
+                "SELECT $pk FROM $sqltable".
+                " WHERE del=1 AND changed < ?".
+            ")",
+            $threshold);
+
+        echo $db->affected_rows() . " records deleted from '$memberstable'\n";
+    }
+
+    // delete outdated records
+    $db->query("DELETE FROM $sqltable WHERE del=1 AND changed < ?", $threshold);
+
+    echo $db->affected_rows() . " records deleted from '$table'\n";
+}
+
+?>
Index: /branches/devel-composer/bin/decrypt.sh
===================================================================
--- /branches/devel-composer/bin/decrypt.sh	(revision 5386)
+++ /branches/devel-composer/bin/decrypt.sh	(revision 5386)
@@ -0,0 +1,67 @@
+#!/usr/bin/env php
+<?php
+/*
+
+ +-----------------------------------------------------------------------+
+ | bin/decrypt.sh                                                        |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2009, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Decrypt the encrypted parts of the HTTP Received: headers           |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Tomas Tevesz <ice@extreme.hu>                                 |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+*/
+
+/*-
+ * If http_received_header_encrypt is configured, the IP address and the
+ * host name of the added Received: header is encrypted with 3DES, to
+ * protect information that some could consider sensitve, yet their
+ * availability is a must in some circumstances.
+ *
+ * Such an encrypted Received: header might look like:
+ *
+ * Received: from DzgkvJBO5+bw+oje5JACeNIa/uSI4mRw2cy5YoPBba73eyBmjtyHnQ==
+ * 	[my0nUbjZXKtl7KVBZcsvWOxxtyVFxza4]
+ *	with HTTP/1.1 (POST); Thu, 14 May 2009 19:17:28 +0200
+ *
+ * In this example, the two encrypted components are the sender host name
+ * (DzgkvJBO5+bw+oje5JACeNIa/uSI4mRw2cy5YoPBba73eyBmjtyHnQ==) and the IP
+ * address (my0nUbjZXKtl7KVBZcsvWOxxtyVFxza4).
+ *
+ * Using this tool, they can be decrypted into plain text:
+ *
+ * $ bin/decrypt.sh 'my0nUbjZXKtl7KVBZcsvWOxxtyVFxza4' \
+ * > 'DzgkvJBO5+bw+oje5JACeNIa/uSI4mRw2cy5YoPBba73eyBmjtyHnQ=='
+ * 84.3.187.208
+ * 5403BBD0.catv.pool.telekom.hu
+ * $
+ *
+ * Thus it is known that this particular message was sent by 84.3.187.208,
+ * having, at the time of sending, the name of 5403BBD0.catv.pool.telekom.hu.
+ *
+ * If (most likely binary) junk is shown, then
+ *  - either the encryption password has, between the time the mail was sent
+ *    and `now', changed, or
+ *  - you are dealing with counterfeit header data.
+ */
+
+define('INSTALL_PATH', realpath(dirname(__FILE__).'/..') . '/');
+
+require INSTALL_PATH . 'program/include/clisetup.php';
+
+if ($argc < 2) {
+	die("Usage: " . basename($argv[0]) . " encrypted-hdr-part [encrypted-hdr-part ...]\n");
+}
+
+$RCMAIL = rcmail::get_instance();
+
+for ($i = 1; $i < $argc; $i++) {
+	printf("%s\n", $RCMAIL->decrypt($argv[$i]));
+};
Index: /branches/devel-composer/bin/dumpschema.sh
===================================================================
--- /branches/devel-composer/bin/dumpschema.sh	(revision 5386)
+++ /branches/devel-composer/bin/dumpschema.sh	(revision 5386)
@@ -0,0 +1,98 @@
+#!/usr/bin/env php
+<?php
+/*
+
+ +-----------------------------------------------------------------------+
+ | bin/dumpschema.sh                                                     |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2009, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Dumps database schema in XML format using MDB2_Schema               |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
+
+require INSTALL_PATH.'program/include/clisetup.php';
+
+/** callback function for schema dump **/
+function print_schema($dump)
+{
+	foreach ((array)$dump as $part)
+		echo $dump . "\n";
+}
+
+$config = new rcube_config();
+
+// don't allow public access if not in devel_mode
+if (!$config->get('devel_mode') && $_SERVER['REMOTE_ADDR']) {
+	header("HTTP/1.0 401 Access denied");
+	die("Access denied!");
+}
+
+$options = array(
+	'use_transactions' => false,
+	'log_line_break' => "\n",
+	'idxname_format' => '%s',
+	'debug' => false,
+	'quote_identifier' => true,
+	'force_defaults' => false,
+	'portability' => false,
+);
+
+$dsnw = $config->get('db_dsnw');
+$dsn_array = MDB2::parseDSN($dsnw);
+
+// set options for postgres databases
+if ($dsn_array['phptype'] == 'pgsql') {
+	$options['disable_smart_seqname'] = true;
+	$options['seqname_format'] = '%s';
+}
+
+$schema =& MDB2_Schema::factory($dsnw, $options);
+$schema->db->supported['transactions'] = false;
+
+
+// send as text/xml when opened in browser
+if ($_SERVER['REMOTE_ADDR'])
+	header('Content-Type: text/xml');
+
+
+if (PEAR::isError($schema)) {
+	$error = $schema->getMessage() . ' ' . $schema->getUserInfo();
+}
+else {
+	$dump_config = array(
+		// 'output_mode' => 'file',
+		'output' => 'print_schema',
+	);
+	
+	$definition = $schema->getDefinitionFromDatabase();
+	$definition['charset'] = 'utf8';
+
+	if (PEAR::isError($definition)) {
+		$error = $definition->getMessage() . ' ' . $definition->getUserInfo();
+	}
+	else {
+		$operation = $schema->dumpDatabase($definition, $dump_config, MDB2_SCHEMA_DUMP_STRUCTURE);
+		if (PEAR::isError($operation)) {
+			$error = $operation->getMessage() . ' ' . $operation->getUserInfo();
+		}
+	}
+}
+
+$schema->disconnect();
+
+if ($error && !$_SERVER['REMOTE_ADDR'])
+	fputs(STDERR, $error);
+
+?>
Index: /branches/devel-composer/bin/indexcontacts.sh
===================================================================
--- /branches/devel-composer/bin/indexcontacts.sh	(revision 5386)
+++ /branches/devel-composer/bin/indexcontacts.sh	(revision 5386)
@@ -0,0 +1,54 @@
+#!/usr/bin/env php
+<?php
+/*
+
+ +-----------------------------------------------------------------------+
+ | bin/indexcontacts.sh                                                  |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2011, The Roundcube Dev Team                            |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Update the fulltext index for all contacts of the internal          |
+ |   address book.                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
+
+require_once INSTALL_PATH.'program/include/clisetup.php';
+ini_set('memory_limit', -1);
+
+// connect to DB
+$RCMAIL = rcmail::get_instance();
+
+$db = $RCMAIL->get_dbh();
+$db->db_connect('w');
+
+if (!$db->is_connected() || $db->is_error())
+    die("No DB connection\n");
+
+// iterate over all users
+$sql_result = $db->query("SELECT user_id FROM " . $RCMAIL->config->get('db_table_users', 'users')." WHERE 1=1");
+while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
+    echo "Indexing contacts for user " . $sql_arr['user_id'] . "...";
+    
+    $contacts = new rcube_contacts($db, $sql_arr['user_id']);
+    $contacts->set_pagesize(9999);
+    
+    $result = $contacts->list_records();
+    while ($result->count && ($row = $result->next())) {
+        unset($row['words']);
+        $contacts->update($row['ID'], $row);
+    }
+
+    echo "done.\n";
+}
+
+?>
Index: /branches/devel-composer/bin/installto.sh
===================================================================
--- /branches/devel-composer/bin/installto.sh	(revision 5386)
+++ /branches/devel-composer/bin/installto.sh	(revision 5386)
@@ -0,0 +1,70 @@
+#!/usr/bin/env php
+<?php
+/*
+ +-----------------------------------------------------------------------+
+ | bin/installto.sh                                                      |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2011, The Roundcube Dev Team                            |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Update an existing Roundcube installation with files from           |
+ |   this version                                                        |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
+
+require_once INSTALL_PATH . 'program/include/clisetup.php';
+
+$target_dir = unslashify($_SERVER['argv'][1]);
+
+if (empty($target_dir) || !is_dir(realpath($target_dir)))
+  die("Invalid target: not a directory\nUsage: installto.sh <TARGET>\n");
+
+// read version from iniset.php
+$iniset = @file_get_contents($target_dir . '/program/include/iniset.php');
+if (!preg_match('/define\(.RCMAIL_VERSION.,\s*.([0-9.]+[a-z-]*)/', $iniset, $m))
+  die("No valid Roundcube installation found at $target_dir\n");
+
+$oldversion = $m[1];
+
+if (version_compare($oldversion, RCMAIL_VERSION, '>='))
+  die("Installation at target location is up-to-date!\n");
+
+echo "Upgrading from $oldversion. Do you want to continue? (y/N)\n";
+$input = trim(fgets(STDIN));
+
+if (strtolower($input) == 'y') {
+  $err = false;
+  echo "Copying files to target location...";
+  foreach (array('program','installer','bin','SQL','plugins','skins/default') as $dir) {
+    if (!system("rsync -avC " . INSTALL_PATH . "$dir/* $target_dir/$dir/")) {
+      $err = true;
+      break;
+    }
+  }
+  foreach (array('index.php','.htaccess','config/main.inc.php.dist','config/db.inc.php.dist','CHANGELOG','README','UPGRADING') as $file) {
+    if (!system("rsync -av " . INSTALL_PATH . "$file $target_dir/$file")) {
+      $err = true;
+      break;
+    }
+  }
+  echo "done.\n\n";
+  
+  if (!$err) {
+    echo "Running update script at target...\n";
+    system("cd $target_dir && bin/update.sh --version=$oldversion");
+    echo "All done.\n";
+  }
+}
+else
+  echo "Update cancelled. See ya!\n";
+
+?>
Index: /branches/devel-composer/bin/jsshrink.sh
===================================================================
--- /branches/devel-composer/bin/jsshrink.sh	(revision 5386)
+++ /branches/devel-composer/bin/jsshrink.sh	(revision 5386)
@@ -0,0 +1,48 @@
+#!/bin/sh
+JS_DIR=`dirname "$0"`/../program/js
+JAR_DIR='/tmp'
+CLOSURE_COMPILER_URL='http://closure-compiler.googlecode.com/files/compiler-latest.zip'
+
+do_shrink() {
+	rm -f "$2"
+	java -jar $JAR_DIR/compiler.jar --compilation_level=SIMPLE_OPTIMIZATIONS --js="$1" --js_output_file="$2"
+}
+
+if [ ! -d "$JS_DIR" ]; then
+	echo "Directory $JS_DIR not found."
+	exit 1
+fi
+
+if [ ! -w "$JAR_DIR" ]; then
+	JAR_DIR=`dirname "$0"`
+fi
+
+if java -version >/dev/null 2>&1; then
+	:
+else
+	echo "Java not found. Please ensure that the 'java' program is in your PATH."
+	exit 1
+fi
+
+if [ ! -r "$JAR_DIR/compiler.jar" ]; then
+	if which wget >/dev/null 2>&1 && which unzip >/dev/null 2>&1; then
+		wget "$CLOSURE_COMPILER_URL" -O "/tmp/$$.zip"
+	elif which curl >/dev/null 2>&1 && which unzip >/dev/null 2>&1; then
+		curl "$CLOSURE_COMPILER_URL" -o "/tmp/$$.zip"
+	else
+		echo "Please download $CLOSURE_COMPILER_URL and extract compiler.jar to $JAR_DIR/."
+		exit 1
+	fi
+	(cd $JAR_DIR && unzip "/tmp/$$.zip" "compiler.jar")
+	rm -f "/tmp/$$.zip"
+fi
+
+for fn in app common googiespell list; do
+	if [ -r "$JS_DIR/${fn}.js.src" ]; then
+		echo "$JS_DIR/${fn}.js.src already exists, not overwriting"
+	else
+		mv "$JS_DIR/${fn}.js" "$JS_DIR/${fn}.js.src"
+	fi
+	echo "Shrinking $JS_DIR/${fn}.js"
+	do_shrink "$JS_DIR/${fn}.js.src" "$JS_DIR/${fn}.js"
+done
Index: /branches/devel-composer/bin/jsunshrink.sh
===================================================================
--- /branches/devel-composer/bin/jsunshrink.sh	(revision 5386)
+++ /branches/devel-composer/bin/jsunshrink.sh	(revision 5386)
@@ -0,0 +1,14 @@
+#!/bin/sh
+JS_DIR=`dirname "$0"`/../program/js
+
+if [ ! -d "$JS_DIR" ]; then
+	echo "Directory $JS_DIR not found."
+	exit 1
+fi
+
+for fn in app common googiespell list; do
+	if [ -r "$JS_DIR/${fn}.js.src" ]; then
+		mv "$JS_DIR/${fn}.js.src" "$JS_DIR/${fn}.js"
+		echo "Reverted $JS_DIR/${fn}.js"
+	fi
+done
Index: /branches/devel-composer/bin/makedoc.sh
===================================================================
--- /branches/devel-composer/bin/makedoc.sh	(revision 5386)
+++ /branches/devel-composer/bin/makedoc.sh	(revision 5386)
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+TITLE="Roundcube Classes"
+PACKAGES="Core"
+
+INSTALL_PATH="`dirname $0`/.."
+PATH_PROJECT=$INSTALL_PATH/program/include
+PATH_DOCS=$INSTALL_PATH/doc/phpdoc
+BIN_PHPDOC="`/usr/bin/which phpdoc`"
+
+if [ ! -x "$BIN_PHPDOC" ]
+then
+  echo "phpdoc not found: $BIN_PHPDOC"
+  exit 1
+fi
+
+OUTPUTFORMAT=HTML
+CONVERTER=frames
+TEMPLATE=earthli
+PRIVATE=off
+
+# make documentation
+$BIN_PHPDOC -d $PATH_PROJECT -t $PATH_DOCS -ti "$TITLE" -dn $PACKAGES \
+-o $OUTPUTFORMAT:$CONVERTER:$TEMPLATE -pp $PRIVATE
+
Index: /branches/devel-composer/bin/msgexport.sh
===================================================================
--- /branches/devel-composer/bin/msgexport.sh	(revision 5386)
+++ /branches/devel-composer/bin/msgexport.sh	(revision 5386)
@@ -0,0 +1,139 @@
+#!/usr/bin/env php
+<?php
+
+define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
+ini_set('memory_limit', -1);
+
+require_once INSTALL_PATH.'program/include/clisetup.php';
+
+function print_usage()
+{
+	print "Usage:  msgexport -h imap-host -u user-name -m mailbox name\n";
+	print "--host   IMAP host\n";
+	print "--user   IMAP user name\n";
+	print "--mbox   Folder name, set to '*' for all\n";
+	print "--file   Output file\n";
+}
+
+function vputs($str)
+{
+	$out = $GLOBALS['args']['file'] ? STDOUT : STDERR;
+	fwrite($out, $str);
+}
+
+function progress_update($pos, $max)
+{
+	$percent = round(100 * $pos / $max);
+	vputs(sprintf("%3d%% [%-51s] %d/%d\033[K\r", $percent, @str_repeat('=', $percent / 2) . '>', $pos, $max));
+}
+
+function export_mailbox($mbox, $filename)
+{
+	global $IMAP;
+	
+	$IMAP->set_mailbox($mbox);
+	
+	vputs("Getting message list of {$mbox}...");
+	vputs($IMAP->messagecount()." messages\n");
+	
+	if ($filename)
+	{
+		if (!($out = fopen($filename, 'w')))
+		{
+			vputs("Cannot write to output file\n");
+			return;
+		}
+		vputs("Writing to $filename\n");
+	}
+	else
+		$out = STDOUT;
+	
+	for ($count = $IMAP->messagecount(), $i=1; $i <= $count; $i++)
+	{
+		$headers = $IMAP->get_headers($i, null, false);
+		$from = current($IMAP->decode_address_list($headers->from, 1, false));
+		
+		fwrite($out, sprintf("From %s %s UID %d\n", $from['mailto'], $headers->date, $headers->uid));
+		fwrite($out, $IMAP->conn->fetchPartHeader($mbox, $i));
+		fwrite($out, $IMAP->conn->handlePartBody($mbox, $i));
+		fwrite($out, "\n\n\n");
+		
+		progress_update($i, $count);
+	}
+	vputs("\ncomplete.\n");
+	
+	if ($filename)
+		fclose($out);
+}
+
+
+// get arguments
+$args = get_opt(array('h' => 'host', 'u' => 'user', 'p' => 'pass', 'm' => 'mbox', 'f' => 'file')) + array('host' => 'localhost', 'mbox' => 'INBOX');
+
+if ($_SERVER['argv'][1] == 'help')
+{
+	print_usage();
+	exit;
+}
+else if (!$args['host'])
+{
+	vputs("Missing required parameters.\n");
+	print_usage();
+	exit;
+}
+
+// prompt for username if not set
+if (empty($args['user']))
+{
+	vputs("IMAP user: ");
+	$args['user'] = trim(fgets(STDIN));
+}
+
+// prompt for password
+$args['pass'] = prompt_silent("Password: ");
+
+
+// parse $host URL
+$a_host = parse_url($args['host']);
+if ($a_host['host'])
+{
+	$host = $a_host['host'];
+	$imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? TRUE : FALSE;
+	$imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
+}
+else
+{
+	$host = $args['host'];
+	$imap_port = 143;
+}
+
+// instantiate IMAP class
+$IMAP = new rcube_imap(null);
+
+// try to connect to IMAP server
+if ($IMAP->connect($host, $args['user'], $args['pass'], $imap_port, $imap_ssl))
+{
+	vputs("IMAP login successful.\n");
+	
+	$filename = null;
+	$mailboxes = $args['mbox'] == '*' ? $IMAP->list_mailboxes(null) : array($args['mbox']);
+
+	foreach ($mailboxes as $mbox)
+	{
+		if ($args['file'])
+			$filename = preg_replace('/\.[a-z0-9]{3,4}$/i', '', $args['file']) . asciiwords($mbox) . '.mbox';
+		else if ($args['mbox'] == '*')
+			$filename = asciiwords($mbox) . '.mbox';
+			
+		if ($args['mbox'] == '*' && in_array(strtolower($mbox), array('junk','spam','trash')))
+			continue;
+
+		export_mailbox($mbox, $filename);
+	}
+}
+else
+{
+	vputs("IMAP login failed.\n");
+}
+
+?>
Index: /branches/devel-composer/bin/msgimport.sh
===================================================================
--- /branches/devel-composer/bin/msgimport.sh	(revision 5386)
+++ /branches/devel-composer/bin/msgimport.sh	(revision 5386)
@@ -0,0 +1,113 @@
+#!/usr/bin/env php
+<?php
+
+define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
+ini_set('memory_limit', -1);
+
+require_once INSTALL_PATH.'program/include/clisetup.php';
+
+function print_usage()
+{
+	print "Usage:  msgimport -h imap-host -u user-name -m mailbox -f message-file\n";
+	print "--host   IMAP host\n";
+	print "--user   IMAP user name\n";
+	print "--mbox   Target mailbox\n";
+	print "--file   Message file to upload\n";
+}
+
+
+// get arguments
+$args = get_opt(array('h' => 'host', 'u' => 'user', 'p' => 'pass', 'm' => 'mbox', 'f' => 'file')) + array('host' => 'localhost', 'mbox' => 'INBOX');
+
+if ($_SERVER['argv'][1] == 'help')
+{
+	print_usage();
+	exit;
+}
+else if (!($args['host'] && $args['file']))
+{
+	print "Missing required parameters.\n";
+	print_usage();
+	exit;
+}
+else if (!is_file($args['file']))
+{
+	print "Cannot read message file\n";
+	exit;
+}
+
+// prompt for username if not set
+if (empty($args['user']))
+{
+	//fwrite(STDOUT, "Please enter your name\n");
+	echo "IMAP user: ";
+	$args['user'] = trim(fgets(STDIN));
+}
+
+// prompt for password
+if (empty($args['pass']))
+{
+	$args['pass'] = prompt_silent("Password: ");
+}
+
+// parse $host URL
+$a_host = parse_url($args['host']);
+if ($a_host['host'])
+{
+	$host = $a_host['host'];
+	$imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? TRUE : FALSE;
+	$imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
+}
+else
+{
+	$host = $args['host'];
+	$imap_port = 143;
+}
+
+// instantiate IMAP class
+$IMAP = new rcube_imap(null);
+
+// try to connect to IMAP server
+if ($IMAP->connect($host, $args['user'], $args['pass'], $imap_port, $imap_ssl))
+{
+	print "IMAP login successful.\n";
+	print "Uploading messages...\n";
+	
+	$count = 0;
+	$message = $lastline = '';
+	
+	$fp = fopen($args['file'], 'r');
+	while (($line = fgets($fp)) !== false)
+	{
+		if (preg_match('/^From\s+-/', $line) && $lastline == '')
+		{
+			if (!empty($message))
+			{
+				if ($IMAP->save_message($args['mbox'], rtrim($message)))
+					$count++;
+				else
+					die("Failed to save message to {$args['mbox']}\n");
+				$message = '';
+			}
+			continue;
+		}
+
+		$message .= $line;
+		$lastline = rtrim($line);
+	}
+
+	if (!empty($message) && $IMAP->save_message($args['mbox'], rtrim($message)))
+		$count++;
+
+	// upload message from file
+	if ($count)
+		print "$count messages successfully added to {$args['mbox']}.\n";
+	else
+		print "Adding messages failed!\n";
+}
+else
+{
+	print "IMAP login failed.\n";
+}
+
+?>
Index: /branches/devel-composer/bin/update.sh
===================================================================
--- /branches/devel-composer/bin/update.sh	(revision 5386)
+++ /branches/devel-composer/bin/update.sh	(revision 5386)
@@ -0,0 +1,182 @@
+#!/usr/bin/env php
+<?php
+/*
+ +-----------------------------------------------------------------------+
+ | bin/update.sh                                                         |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2010-2011, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Check local configuration and database schema after upgrading       |
+ |   to a new version                                                    |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
+
+require_once INSTALL_PATH . 'program/include/clisetup.php';
+require_once INSTALL_PATH . 'installer/rcube_install.php';
+
+// get arguments
+$opts = get_opt(array('v' => 'version'));
+
+// ask user if no version is specified
+if (!$opts['version']) {
+  echo "What version are you upgrading from? Type '?' if you don't know.\n";
+  if (($input = trim(fgets(STDIN))) && preg_match('/^[0-9.]+[a-z-]*$/', $input))
+    $opts['version'] = $input;
+}
+
+if ($opts['version'] && version_compare($opts['version'], RCMAIL_VERSION, '>'))
+  die("Nothing to be done here. Bye!\n");
+
+
+$RCI = rcube_install::get_instance();
+$RCI->load_config();
+
+if ($RCI->configured) {
+  $success = true;
+  
+  if ($messages = $RCI->check_config()) {
+    $success = false;
+    $err = 0;
+
+    // list missing config options
+    if (is_array($messages['missing'])) {
+      echo "WARNING: Missing config options:\n";
+      echo "(These config options should be present in the current configuration)\n";
+
+      foreach ($messages['missing'] as $msg) {
+        echo "- '" . $msg['prop'] . ($msg['name'] ? "': " . $msg['name'] : "'") . "\n";
+        $err++;
+      }
+      echo "\n";
+    }
+
+    // list old/replaced config options
+    if (is_array($messages['replaced'])) {
+      echo "WARNING: Replaced config options:\n";
+      echo "(These config options have been replaced or renamed)\n";
+
+      foreach ($messages['replaced'] as $msg) {
+        echo "- '" . $msg['prop'] . "' was replaced by '" . $msg['replacement'] . "'\n";
+        $err++;
+      }
+      echo "\n";
+    }
+
+    // list obsolete config options (just a notice)
+    if (is_array($messages['obsolete'])) {
+      echo "NOTICE: Obsolete config options:\n";
+      echo "(You still have some obsolete or inexistent properties set. This isn't a problem but should be noticed)\n";
+
+      foreach ($messages['obsolete'] as $msg) {
+        echo "- '" . $msg['prop'] . ($msg['name'] ? "': " . $msg['name'] : "'") . "\n";
+        $err++;
+      }
+      echo "\n";
+    }
+
+    // ask user to update config files
+    if ($err) {
+      echo "Do you want me to fix your local configuration? (y/N)\n";
+      $input = trim(fgets(STDIN));
+
+      // positive: let's merge the local config with the defaults
+      if (strtolower($input) == 'y') {
+        $copy1 = $copy2 = $write1 = $write2 = false;
+        
+        // backup current config
+        echo ". backing up the current config files...\n";
+        $copy1 = copy(RCMAIL_CONFIG_DIR . '/main.inc.php', RCMAIL_CONFIG_DIR . '/main.old.php');
+        $copy2 = copy(RCMAIL_CONFIG_DIR . '/db.inc.php', RCMAIL_CONFIG_DIR . '/db.old.php');
+        
+        if ($copy1 && $copy2) {
+          $RCI->merge_config();
+        
+          echo ". writing " . RCMAIL_CONFIG_DIR . "/main.inc.php...\n";
+          $write1 = file_put_contents(RCMAIL_CONFIG_DIR . '/main.inc.php', $RCI->create_config('main', true));
+          echo ". writing " . RCMAIL_CONFIG_DIR . "/main.db.php...\n";
+          $write2 = file_put_contents(RCMAIL_CONFIG_DIR . '/db.inc.php', $RCI->create_config('db', true));
+        }
+        
+        // Success!
+        if ($write1 && $write2) {
+          echo "Done.\n";
+          echo "Your configuration files are now up-to-date!\n";
+        }
+        else {
+          echo "Failed to write config files!\n";
+          echo "Grant write privileges to the current user or update the files manually according to the above messages.\n";
+        }
+      }
+      else {
+        echo "Please update your config files manually according to the above messages.\n\n";
+      }
+    }
+
+    // check dependencies based on the current configuration
+    if (is_array($messages['dependencies'])) {
+      echo "WARNING: Dependency check failed!\n";
+      echo "(Some of your configuration settings require other options to be configured or additional PHP modules to be installed)\n";
+
+      foreach ($messages['dependencies'] as $msg) {
+        echo "- " . $msg['prop'] . ': ' . $msg['explain'] . "\n";
+      }
+      echo "Please fix your config files and run this script again!\n";
+      echo "See ya.\n";
+    }
+  }
+
+  // check database schema
+  if ($RCI->config['db_dsnw']) {
+    $DB = new rcube_mdb2($RCI->config['db_dsnw'], '', false);
+    $DB->db_connect('w');
+    if ($db_error_msg = $DB->is_error()) {
+      echo "Error connecting to database: $db_error_msg\n";
+      $success = false;
+    }
+    else if ($err = $RCI->db_schema_check($DB, false)) {
+      $updatefile = INSTALL_PATH . 'SQL/' . (isset($RCI->db_map[$DB->db_provider]) ? $RCI->db_map[$DB->db_provider] : $DB->db_provider) . '.update.sql';
+      echo "WARNING: Database schema needs to be updated!\n";
+      echo join("\n", $err) . "\n\n";
+      $success = false;
+      
+      if ($opts['version']) {
+        echo "Do you want to run the update queries to get the schmea fixed? (y/N)\n";
+        $input = trim(fgets(STDIN));
+        if (strtolower($input) == 'y') {
+          $success = $RCI->update_db($DB, $opts['version']);
+        }
+      }
+      
+      if (!$success)
+        echo "Open $updatefile and execute all queries below the comment with the currently installed version number.\n";
+    }
+  }
+  
+  // index contacts for fulltext searching
+  if (version_compare($opts['version'], '0.6', '<')) {
+    system(INSTALL_PATH . 'bin/indexcontacts.sh');
+  }
+  
+  if ($success) {
+    echo "This instance of Roundcube is up-to-date.\n";
+    echo "Have fun!\n";
+  }
+}
+else {
+  echo "This instance of Roundcube is not yet configured!\n";
+  echo "Open http://url-to-roundcube/installer/ in your browser and follow the instuctions.\n";
+}
+
+echo "\n";
+
+?>
Index: /branches/devel-composer/config/.htaccess
===================================================================
--- /branches/devel-composer/config/.htaccess	(revision 5386)
+++ /branches/devel-composer/config/.htaccess	(revision 5386)
@@ -0,0 +1,2 @@
+Order allow,deny
+Deny from all 
Index: /branches/devel-composer/config/db.inc.php.dist
===================================================================
--- /branches/devel-composer/config/db.inc.php.dist	(revision 5386)
+++ /branches/devel-composer/config/db.inc.php.dist	(revision 5386)
@@ -0,0 +1,63 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | Configuration file for database access                                |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2009, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+
+*/
+
+$rcmail_config = array();
+
+// PEAR database DSN for read/write operations
+// format is db_provider://user:password@host/database 
+// For examples see http://pear.php.net/manual/en/package.database.mdb2.intro-dsn.php
+// currently supported db_providers: mysql, mysqli, pgsql, sqlite, mssql or sqlsrv
+
+$rcmail_config['db_dsnw'] = 'mysql://roundcube:pass@localhost/roundcubemail';
+// postgres example: 'pgsql://roundcube:pass@localhost/roundcubemail';
+// Warning: for SQLite use absolute path in DSN:
+// sqlite example: 'sqlite:////full/path/to/sqlite.db?mode=0646';
+
+// PEAR database DSN for read only operations (if empty write database will be used)
+// useful for database replication
+$rcmail_config['db_dsnr'] = '';
+
+// maximum length of a query in bytes
+$rcmail_config['db_max_length'] = 512000;  // 500K
+
+// use persistent db-connections
+// beware this will not "always" work as expected
+// see: http://www.php.net/manual/en/features.persistent-connections.php
+$rcmail_config['db_persistent'] = FALSE;
+
+
+// you can define specific table names used to store webmail data
+$rcmail_config['db_table_users'] = 'users';
+$rcmail_config['db_table_identities'] = 'identities';
+$rcmail_config['db_table_contacts'] = 'contacts';
+$rcmail_config['db_table_contactgroups'] = 'contactgroups';
+$rcmail_config['db_table_contactgroupmembers'] = 'contactgroupmembers';
+$rcmail_config['db_table_session'] = 'session';
+$rcmail_config['db_table_cache'] = 'cache';
+$rcmail_config['db_table_cache_index'] = 'cache_index';
+$rcmail_config['db_table_cache_thread'] = 'cache_thread';
+$rcmail_config['db_table_cache_messages'] = 'cache_messages';
+
+
+// you can define specific sequence names used in PostgreSQL
+$rcmail_config['db_sequence_users'] = 'user_ids';
+$rcmail_config['db_sequence_identities'] = 'identity_ids';
+$rcmail_config['db_sequence_contacts'] = 'contact_ids';
+$rcmail_config['db_sequence_contactgroups'] = 'contactgroups_ids';
+$rcmail_config['db_sequence_cache'] = 'cache_ids';
+$rcmail_config['db_sequence_searches'] = 'search_ids';
+
+
+// end db config file
+
Index: /branches/devel-composer/config/main.inc.php.dist
===================================================================
--- /branches/devel-composer/config/main.inc.php.dist	(revision 5386)
+++ /branches/devel-composer/config/main.inc.php.dist	(revision 5386)
@@ -0,0 +1,773 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | Main configuration file                                               |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2010, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+
+*/
+
+$rcmail_config = array();
+
+// ----------------------------------
+// LOGGING/DEBUGGING
+// ----------------------------------
+
+// system error reporting: 1 = log; 2 = report (not implemented yet), 4 = show, 8 = trace
+$rcmail_config['debug_level'] = 1;
+
+// log driver:  'syslog' or 'file'.
+$rcmail_config['log_driver'] = 'file';
+
+// date format for log entries
+// (read http://php.net/manual/en/function.date.php for all format characters)  
+$rcmail_config['log_date_format'] = 'd-M-Y H:i:s O';
+
+// Syslog ident string to use, if using the 'syslog' log driver.
+$rcmail_config['syslog_id'] = 'roundcube';
+
+// Syslog facility to use, if using the 'syslog' log driver.
+// For possible values see installer or http://php.net/manual/en/function.openlog.php
+$rcmail_config['syslog_facility'] = LOG_USER;
+
+// Log sent messages to <log_dir>/sendmail or to syslog
+$rcmail_config['smtp_log'] = true;
+
+// Log successful logins to <log_dir>/userlogins or to syslog
+$rcmail_config['log_logins'] = false;
+
+// Log session authentication errors to <log_dir>/session or to syslog
+$rcmail_config['log_session'] = false;
+
+// Log SQL queries to <log_dir>/sql or to syslog
+$rcmail_config['sql_debug'] = false;
+
+// Log IMAP conversation to <log_dir>/imap or to syslog
+$rcmail_config['imap_debug'] = false;
+
+// Log LDAP conversation to <log_dir>/ldap or to syslog
+$rcmail_config['ldap_debug'] = false;
+
+// Log SMTP conversation to <log_dir>/smtp or to syslog
+$rcmail_config['smtp_debug'] = false;
+
+// ----------------------------------
+// IMAP
+// ----------------------------------
+
+// the mail host chosen to perform the log-in
+// leave blank to show a textbox at login, give a list of hosts
+// to display a pulldown menu or set one host as string.
+// To use SSL/TLS connection, enter hostname with prefix ssl:// or tls://
+// Supported replacement variables:
+// %n - http hostname ($_SERVER['SERVER_NAME'])
+// %d - domain (http hostname without the first part)
+// %s - domain name after the '@' from e-mail address provided at login screen
+// For example %n = mail.domain.tld, %d = domain.tld
+$rcmail_config['default_host'] = '';
+
+// TCP port used for IMAP connections
+$rcmail_config['default_port'] = 143;
+
+// IMAP AUTH type (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or empty to use
+// best server supported one)
+$rcmail_config['imap_auth_type'] = null;
+
+// If you know your imap's folder delimiter, you can specify it here.
+// Otherwise it will be determined automatically
+$rcmail_config['imap_delimiter'] = null;
+
+// If IMAP server doesn't support NAMESPACE extension, but you're
+// using shared folders or personal root folder is non-empty, you'll need to
+// set these options. All can be strings or arrays of strings.
+// Folders need to be ended with directory separator, e.g. "INBOX."
+// (special directory "~" is an exception to this rule)
+// These can be used also to overwrite server's namespaces
+$rcmail_config['imap_ns_personal'] = null;
+$rcmail_config['imap_ns_other']    = null;
+$rcmail_config['imap_ns_shared']   = null;
+
+// By default IMAP capabilities are readed after connection to IMAP server
+// In some cases, e.g. when using IMAP proxy, there's a need to refresh the list
+// after login. Set to True if you've got this case.
+$rcmail_config['imap_force_caps'] = false;
+
+// By default list of subscribed folders is determined using LIST-EXTENDED
+// extension if available. Some servers (dovecot 1.x) returns wrong results
+// for shared namespaces in this case. http://trac.roundcube.net/ticket/1486225
+// Enable this option to force LSUB command usage instead.
+$rcmail_config['imap_force_lsub'] = false;
+
+// IMAP connection timeout, in seconds. Default: 0 (no limit)
+$rcmail_config['imap_timeout'] = 0;
+
+// Optional IMAP authentication identifier to be used as authorization proxy
+$rcmail_config['imap_auth_cid'] = null;
+
+// Optional IMAP authentication password to be used for imap_auth_cid
+$rcmail_config['imap_auth_pw'] = null;
+
+// Type of IMAP indexes cache. Supported values: 'db', 'apc' and 'memcache'.
+$rcmail_config['imap_cache'] = null;
+
+// Enables messages cache. Only 'db' cache is supported.
+$rcmail_config['messages_cache'] = false;
+
+
+// ----------------------------------
+// SMTP
+// ----------------------------------
+
+// SMTP server host (for sending mails).
+// To use SSL/TLS connection, enter hostname with prefix ssl:// or tls://
+// If left blank, the PHP mail() function is used
+// Supported replacement variables:
+// %h - user's IMAP hostname
+// %n - http hostname ($_SERVER['SERVER_NAME'])
+// %d - domain (http hostname without the first part)
+// %z - IMAP domain (IMAP hostname without the first part)
+// For example %n = mail.domain.tld, %d = domain.tld
+$rcmail_config['smtp_server'] = '';
+
+// SMTP port (default is 25; 465 for SSL)
+$rcmail_config['smtp_port'] = 25;
+
+// SMTP username (if required) if you use %u as the username Roundcube
+// will use the current username for login
+$rcmail_config['smtp_user'] = '';
+
+// SMTP password (if required) if you use %p as the password Roundcube
+// will use the current user's password for login
+$rcmail_config['smtp_pass'] = '';
+
+// SMTP AUTH type (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or empty to use
+// best server supported one)
+$rcmail_config['smtp_auth_type'] = '';
+
+// Optional SMTP authentication identifier to be used as authorization proxy
+$rcmail_config['smtp_auth_cid'] = null;
+
+// Optional SMTP authentication password to be used for smtp_auth_cid
+$rcmail_config['smtp_auth_pw'] = null;
+
+// SMTP HELO host 
+// Hostname to give to the remote server for SMTP 'HELO' or 'EHLO' messages 
+// Leave this blank and you will get the server variable 'server_name' or 
+// localhost if that isn't defined. 
+$rcmail_config['smtp_helo_host'] = '';
+
+// SMTP connection timeout, in seconds. Default: 0 (no limit)
+$rcmail_config['smtp_timeout'] = 0;
+
+// ----------------------------------
+// SYSTEM
+// ----------------------------------
+
+// THIS OPTION WILL ALLOW THE INSTALLER TO RUN AND CAN EXPOSE SENSITIVE CONFIG DATA.
+// ONLY ENABLE IT IF YOU'RE REALLY SURE WHAT YOU'RE DOING!
+$rcmail_config['enable_installer'] = false;
+
+// use this folder to store log files (must be writeable for apache user)
+// This is used by the 'file' log driver.
+$rcmail_config['log_dir'] = 'logs/';
+
+// use this folder to store temp files (must be writeable for apache user)
+$rcmail_config['temp_dir'] = 'temp/';
+
+// lifetime of message cache
+// possible units: s, m, h, d, w
+$rcmail_config['message_cache_lifetime'] = '10d';
+
+// enforce connections over https
+// with this option enabled, all non-secure connections will be redirected.
+// set the port for the ssl connection as value of this option if it differs from the default 443
+$rcmail_config['force_https'] = false;
+
+// tell PHP that it should work as under secure connection
+// even if it doesn't recognize it as secure ($_SERVER['HTTPS'] is not set)
+// e.g. when you're running Roundcube behind a https proxy
+$rcmail_config['use_https'] = false;
+
+// Allow browser-autocompletion on login form.
+// 0 - disabled, 1 - username and host only, 2 - username, host, password
+$rcmail_config['login_autocomplete'] = 0;
+
+// If users authentication is not case sensitive this must be enabled.
+// You can also use it to force conversion of logins to lower case.
+// After enabling it all user records need to be updated, e.g. with query:
+// UPDATE users SET username = LOWER(username);
+$rcmail_config['login_lc'] = false;
+
+// automatically create a new Roundcube user when log-in the first time.
+// a new user will be created once the IMAP login succeeds.
+// set to false if only registered users can use this service
+$rcmail_config['auto_create_user'] = true;
+
+// replace Roundcube logo with this image
+// specify an URL relative to the document root of this Roundcube installation
+$rcmail_config['skin_logo'] = null;
+
+// Includes should be interpreted as PHP files
+$rcmail_config['skin_include_php'] = false;
+
+// Session lifetime in minutes
+// must be greater than 'keep_alive'/60
+$rcmail_config['session_lifetime'] = 10;
+
+// session domain: .example.org
+$rcmail_config['session_domain'] = '';
+
+// Backend to use for session storage. Can either be 'db' (default) or 'memcache'
+// If set to memcache, a list of servers need to be specified in 'memcache_hosts'
+// Make sure the Memcache extension (http://pecl.php.net/package/memcache) version >= 2.0.0 is installed
+$rcmail_config['session_storage'] = 'db';
+
+// Use these hosts for accessing memcached
+// Define any number of hosts in the form hostname:port
+$rcmail_config['memcache_hosts'] = null; // e.g. array( 'localhost:11211', '192.168.1.12:11211' );
+
+// check client IP in session athorization
+$rcmail_config['ip_check'] = false;
+
+// check referer of incoming requests
+$rcmail_config['referer_check'] = false;
+
+// X-Frame-Options HTTP header value sent to prevent from Clickjacking.
+// Possible values: sameorigin|deny. Set to false in order to disable sending them
+$rcmail_config['x_frame_options'] = 'sameorigin';
+
+// this key is used to encrypt the users imap password which is stored
+// in the session record (and the client cookie if remember password is enabled).
+// please provide a string of exactly 24 chars.
+$rcmail_config['des_key'] = 'rcmail-!24ByteDESkey*Str';
+
+// Automatically add this domain to user names for login
+// Only for IMAP servers that require full e-mail addresses for login
+// Specify an array with 'host' => 'domain' values to support multiple hosts
+// Supported replacement variables:
+// %h - user's IMAP hostname
+// %n - http hostname ($_SERVER['SERVER_NAME'])
+// %d - domain (http hostname without the first part)
+// %z - IMAP domain (IMAP hostname without the first part)
+// For example %n = mail.domain.tld, %d = domain.tld
+$rcmail_config['username_domain'] = '';
+
+// This domain will be used to form e-mail addresses of new users
+// Specify an array with 'host' => 'domain' values to support multiple hosts
+// Supported replacement variables:
+// %h - user's IMAP hostname
+// %n - http hostname ($_SERVER['SERVER_NAME'])
+// %d - domain (http hostname without the first part)
+// %z - IMAP domain (IMAP hostname without the first part)
+// For example %n = mail.domain.tld, %d = domain.tld
+$rcmail_config['mail_domain'] = '';
+
+// Password charset.
+// Use it if your authentication backend doesn't support UTF-8.
+// Defaults to ISO-8859-1 for backward compatibility
+$rcmail_config['password_charset'] = 'ISO-8859-1';
+
+// How many seconds must pass between emails sent by a user
+$rcmail_config['sendmail_delay'] = 0;
+
+// Maximum number of recipients per message. Default: 0 (no limit)
+$rcmail_config['max_recipients'] = 0; 
+
+// Maximum allowednumber of members of an address group. Default: 0 (no limit)
+// If 'max_recipients' is set this value should be less or equal
+$rcmail_config['max_group_members'] = 0; 
+
+// add this user-agent to message headers when sending
+$rcmail_config['useragent'] = 'Roundcube Webmail/'.RCMAIL_VERSION;
+
+// use this name to compose page titles
+$rcmail_config['product_name'] = 'Roundcube Webmail';
+
+// try to load host-specific configuration
+// see http://trac.roundcube.net/wiki/Howto_Config for more details
+$rcmail_config['include_host_config'] = false;
+
+// path to a text file which will be added to each sent message
+// paths are relative to the Roundcube root folder
+$rcmail_config['generic_message_footer'] = '';
+
+// path to a text file which will be added to each sent HTML message
+// paths are relative to the Roundcube root folder
+$rcmail_config['generic_message_footer_html'] = '';
+
+// add a received header to outgoing mails containing the creators IP and hostname
+$rcmail_config['http_received_header'] = false;
+
+// Whether or not to encrypt the IP address and the host name
+// these could, in some circles, be considered as sensitive information;
+// however, for the administrator, these could be invaluable help
+// when tracking down issues.
+$rcmail_config['http_received_header_encrypt'] = false;
+
+// This string is used as a delimiter for message headers when sending
+// a message via mail() function. Leave empty for auto-detection
+$rcmail_config['mail_header_delimiter'] = NULL;
+
+// number of chars allowed for line when wrapping text.
+// text wrapping is done when composing/sending messages
+$rcmail_config['line_length'] = 72;
+
+// send plaintext messages as format=flowed
+$rcmail_config['send_format_flowed'] = true;
+
+// don't allow these settings to be overriden by the user
+$rcmail_config['dont_override'] = array();
+
+// Set identities access level:
+// 0 - many identities with possibility to edit all params
+// 1 - many identities with possibility to edit all params but not email address
+// 2 - one identity with possibility to edit all params
+// 3 - one identity with possibility to edit all params but not email address
+$rcmail_config['identities_level'] = 0;
+
+// Mimetypes supported by the browser.
+// attachments of these types will open in a preview window
+// either a comma-separated list or an array: 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/pdf'
+$rcmail_config['client_mimetypes'] = null;  # null == default
+
+// 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;
+
+// ----------------------------------
+// PLUGINS
+// ----------------------------------
+
+// List of active plugins (in plugins/ directory)
+$rcmail_config['plugins'] = array();
+
+// ----------------------------------
+// USER INTERFACE
+// ----------------------------------
+
+// default messages sort column. Use empty value for default server's sorting, 
+// or 'arrival', 'date', 'subject', 'from', 'to', 'size', 'cc'
+$rcmail_config['message_sort_col'] = '';
+
+// default messages sort order
+$rcmail_config['message_sort_order'] = 'DESC';
+
+// These cols are shown in the message list. Available cols are:
+// subject, from, to, cc, replyto, date, size, status, flag, attachment, 'priority'
+$rcmail_config['list_cols'] = array('subject', 'status', 'from', 'date', 'size', 'flag', 'attachment');
+
+// the default locale setting (leave empty for auto-detection)
+// RFC1766 formatted language name like en_US, de_DE, de_CH, fr_FR, pt_BR
+$rcmail_config['language'] = null;
+
+// use this format for date display (date or strftime format)
+$rcmail_config['date_format'] = 'Y-m-d';
+
+// give this choice of date formats to the user to select from
+$rcmail_config['date_formats'] = array('Y-m-d', 'd-m-Y', 'Y/m/d', 'm/d/Y', 'd/m/Y', 'd.m.Y', 'j.n.Y');
+
+// use this format for time display (date or strftime format)
+$rcmail_config['time_format'] = 'H:i';
+
+// give this choice of time formats to the user to select from
+$rcmail_config['time_formats'] = array('G:i', 'H:i', 'g:i a', 'h:i A');
+
+// use this format for short date display (derived from date_format and time_format)
+$rcmail_config['date_short'] = 'D H:i';
+
+// use this format for detailed date/time formatting (derived from date_format and time_format)
+$rcmail_config['date_long'] = 'Y-m-d H:i';
+
+// store draft message is this mailbox
+// leave blank if draft messages should not be stored
+// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
+$rcmail_config['drafts_mbox'] = 'Drafts';
+
+// store spam messages in this mailbox
+// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
+$rcmail_config['junk_mbox'] = 'Junk';
+
+// store sent message is this mailbox
+// leave blank if sent messages should not be stored
+// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
+$rcmail_config['sent_mbox'] = 'Sent';
+
+// move messages to this folder when deleting them
+// leave blank if they should be deleted directly
+// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
+$rcmail_config['trash_mbox'] = 'Trash';
+
+// display these folders separately in the mailbox list.
+// these folders will also be displayed with localized names
+// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
+$rcmail_config['default_imap_folders'] = array('INBOX', 'Drafts', 'Sent', 'Junk', 'Trash');
+
+// automatically create the above listed default folders on login
+$rcmail_config['create_default_folders'] = false;
+
+// protect the default folders from renames, deletes, and subscription changes
+$rcmail_config['protect_default_folders'] = true;
+
+// if in your system 0 quota means no limit set this option to true 
+$rcmail_config['quota_zero_as_unlimited'] = false;
+
+// Make use of the built-in spell checker. It is based on GoogieSpell.
+// Since Google only accepts connections over https your PHP installatation
+// requires to be compiled with Open SSL support
+$rcmail_config['enable_spellcheck'] = true;
+
+// Enables spellchecker exceptions dictionary.
+// Setting it to 'shared' will make the dictionary shared by all users.
+$rcmail_config['spellcheck_dictionary'] = false;
+
+// Set the spell checking engine. 'googie' is the default. 'pspell' is also available,
+// but requires the Pspell extensions. When using Nox Spell Server, also set 'googie' here.
+$rcmail_config['spellcheck_engine'] = 'googie';
+
+// For a locally installed Nox Spell Server, please specify the URI to call it.
+// Get Nox Spell Server from http://orangoo.com/labs/?page_id=72
+// Leave empty to use the Google spell checking service, what means
+// that the message content will be sent to Google in order to check spelling
+$rcmail_config['spellcheck_uri'] = '';
+
+// These languages can be selected for spell checking.
+// Configure as a PHP style hash array: array('en'=>'English', 'de'=>'Deutsch');
+// Leave empty for default set of available language.
+$rcmail_config['spellcheck_languages'] = NULL;
+
+// Makes that words with all letters capitalized will be ignored (e.g. GOOGLE)
+$rcmail_config['spellcheck_ignore_caps'] = false;
+
+// Makes that words with numbers will be ignored (e.g. g00gle)
+$rcmail_config['spellcheck_ignore_nums'] = false;
+
+// Makes that words with symbols will be ignored (e.g. g@@gle)
+$rcmail_config['spellcheck_ignore_syms'] = false;
+
+// don't let users set pagesize to more than this value if set
+$rcmail_config['max_pagesize'] = 200;
+
+// Minimal value of user's 'keep_alive' setting (in seconds)
+// Must be less than 'session_lifetime'
+$rcmail_config['min_keep_alive'] = 60;
+
+// Enables files upload indicator. Requires APC installed and enabled apc.rfc1867 option.
+// By default refresh time is set to 1 second. You can set this value to true
+// or any integer value indicating number of seconds.
+$rcmail_config['upload_progress'] = false;
+
+// Specifies for how many seconds the Undo button will be available
+// after object delete action. Currently used with supporting address book sources.
+// Setting it to 0, disables the feature.
+$rcmail_config['undo_timeout'] = 0;
+
+// ----------------------------------
+// ADDRESSBOOK SETTINGS
+// ----------------------------------
+
+// This indicates which type of address book to use. Possible choises:
+// 'sql' (default) and 'ldap'.
+// If set to 'ldap' then it will look at using the first writable LDAP
+// address book as the primary address book and it will not display the
+// SQL address book in the 'Address Book' view.
+$rcmail_config['address_book_type'] = 'sql';
+
+// In order to enable public ldap search, configure an array like the Verisign
+// example further below. if you would like to test, simply uncomment the example.
+// Array key must contain only safe characters, ie. a-zA-Z0-9_
+$rcmail_config['ldap_public'] = array();
+
+// If you are going to use LDAP for individual address books, you will need to 
+// set 'user_specific' to true and use the variables to generate the appropriate DNs to access it.
+//
+// The recommended directory structure for LDAP is to store all the address book entries
+// under the users main entry, e.g.:
+//
+//  o=root
+//   ou=people
+//    uid=user@domain
+//  mail=contact@contactdomain
+//
+// So the base_dn would be uid=%fu,ou=people,o=root
+// The bind_dn would be the same as based_dn or some super user login.
+/* 
+ * example config for Verisign directory
+ *
+$rcmail_config['ldap_public']['Verisign'] = array(
+  'name'          => 'Verisign.com',
+  // Replacement variables supported in host names:
+  // %h - user's IMAP hostname
+  // %n - http hostname ($_SERVER['SERVER_NAME'])
+  // %d - domain (http hostname without the first part)
+  // %z - IMAP domain (IMAP hostname without the first part)
+  // For example %n = mail.domain.tld, %d = domain.tld
+  'hosts'         => array('directory.verisign.com'),
+  'port'          => 389,
+  'use_tls'	      => false,
+  'ldap_version'  => 3,       // using LDAPv3
+  'user_specific' => false,   // If true the base_dn, bind_dn and bind_pass default to the user's IMAP login.
+  // %fu - The full username provided, assumes the username is an email
+  //       address, uses the username_domain value if not an email address.
+  // %u  - The username prior to the '@'.
+  // %d  - The domain name after the '@'.
+  // %dc - The domain name hierarchal string e.g. "dc=test,dc=domain,dc=com"
+  // %dn - DN found by ldap search when search_filter/search_base_dn are used
+  'base_dn'       => '',
+  'bind_dn'       => '',
+  'bind_pass'     => '',
+  // It's possible to bind for an individual address book
+  // The login name is used to search for the DN to bind with
+  'search_base_dn' => '',
+  'search_filter'  => '',   // e.g. '(&(objectClass=posixAccount)(uid=%u))'
+  // Default for %dn variable if search doesn't return DN value
+  'search_dn_default' => '',
+  // Optional authentication identifier to be used as SASL authorization proxy
+  // bind_dn need to be empty
+  'auth_cid'       => '',
+  // SASL authentication method (for proxy auth), e.g. DIGEST-MD5
+  'auth_method'    => '',
+  // Indicates if the addressbook shall be hidden from the list.
+  // With this option enabled you can still search/view contacts.
+  'hidden'        => false,
+  // Indicates if the addressbook shall not list contacts but only allows searching.
+  'searchonly'    => 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
+  'writable'       => false,
+  // To create a new contact these are the object classes to specify
+  // (or any other classes you wish to use).
+  'LDAP_Object_Classes' => array('top', 'inetOrgPerson'),
+  // 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_rdn'       => 'mail',
+  // The required fields needed to build a new contact as required by
+  // the object classes (can include additional fields not required by the object classes).
+  'required_fields' => array('cn', 'sn', 'mail'),
+  'search_fields'   => array('mail', 'cn'),  // fields to search in
+  // mapping of contact fields to directory attributes
+  'fieldmap' => array(
+    // 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'        => '(objectClass=inetOrgPerson)',      // used for basic listing (if not empty) and will be &'d with search queries. example: status=act
+  'fuzzy_search'  => true,    // server allows wildcard search
+  'vlv'           => false,   // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
+  'numsub_filter' => '(objectClass=organizationalUnit)',   // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
+  'sizelimit'     => '0',     // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
+  'timelimit'     => '0',     // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
+
+  // definition for contact groups (uncomment if no groups are supported)
+  // for the groups base_dn, the user replacements %fu, %u, $d and %dc work as for base_dn (see above)
+  // if the groups base_dn is empty, the contact base_dn is used for the groups as well
+  // -> in this case, assure that groups and contacts are separated due to the concernig filters! 
+  'groups'        => array(
+    'base_dn'     => '',
+    'filter'      => '(objectClass=groupOfNames)',
+    'object_classes' => array("top", "groupOfNames"),
+    'member_attr'  => 'member',   // name of the member attribute, e.g. uniqueMember
+    'name_attr'    => 'cn',       // attribute to be used as group name
+  ),
+);
+*/
+
+// An ordered array of the ids of the addressbooks that should be searched
+// when populating address autocomplete fields server-side. ex: array('sql','Verisign');
+$rcmail_config['autocomplete_addressbooks'] = array('sql');
+
+// The minimum number of characters required to be typed in an autocomplete field
+// before address books will be searched. Most useful for LDAP directories that
+// may need to do lengthy results building given overly-broad searches
+$rcmail_config['autocomplete_min_length'] = 1;
+
+// Number of parallel autocomplete requests.
+// If there's more than one address book, n parallel (async) requests will be created,
+// where each request will search in one address book. By default (0), all address
+// books are searched in one request.
+$rcmail_config['autocomplete_threads'] = 0;
+
+// Max. numer of entries in autocomplete popup. Default: 15.
+$rcmail_config['autocomplete_max'] = 15;
+
+// 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
+// ----------------------------------
+
+// Use this charset as fallback for message decoding
+$rcmail_config['default_charset'] = 'ISO-8859-1';
+
+// skin name: folder from skins/
+$rcmail_config['skin'] = 'default';
+
+// show up to X items in list view
+$rcmail_config['pagesize'] = 40;
+
+// use this timezone to display date/time
+$rcmail_config['timezone'] = 'auto';
+
+// is daylight saving On?
+$rcmail_config['dst_active'] = (bool)date('I');
+
+// prefer displaying HTML messages
+$rcmail_config['prefer_html'] = true;
+
+// display remote inline images
+// 0 - Never, always ask
+// 1 - Ask if sender is not in address book
+// 2 - Always show inline images
+$rcmail_config['show_images'] = 0;
+
+// compose html formatted messages by default
+// 0 - never, 1 - always, 2 - on reply to HTML message only 
+$rcmail_config['htmleditor'] = 0;
+
+// show pretty dates as standard
+$rcmail_config['prettydate'] = true;
+
+// save compose message every 300 seconds (5min)
+$rcmail_config['draft_autosave'] = 300;
+
+// default setting if preview pane is enabled
+$rcmail_config['preview_pane'] = false;
+
+// Mark as read when viewed in preview pane (delay in seconds)
+// Set to -1 if messages in preview pane should not be marked as read
+$rcmail_config['preview_pane_mark_read'] = 0;
+
+// Clear Trash on logout
+$rcmail_config['logout_purge'] = false;
+
+// Compact INBOX on logout
+$rcmail_config['logout_expunge'] = false;
+
+// Display attached images below the message body 
+$rcmail_config['inline_images'] = true;
+
+// Encoding of long/non-ascii attachment names:
+// 0 - Full RFC 2231 compatible
+// 1 - RFC 2047 for 'name' and RFC 2231 for 'filename' parameter (Thunderbird's default)
+// 2 - Full 2047 compatible
+$rcmail_config['mime_param_folding'] = 1;
+
+// Set true if deleted messages should not be displayed
+// This will make the application run slower
+$rcmail_config['skip_deleted'] = false;
+
+// Set true to Mark deleted messages as read as well as deleted
+// False means that a message's read status is not affected by marking it as deleted
+$rcmail_config['read_when_deleted'] = true;
+
+// Set to true to never delete messages immediately
+// Use 'Purge' to remove messages marked as deleted
+$rcmail_config['flag_for_deletion'] = false;
+
+// Default interval for keep-alive/check-recent requests (in seconds)
+// Must be greater than or equal to 'min_keep_alive' and less than 'session_lifetime'
+$rcmail_config['keep_alive'] = 60;
+
+// If true all folders will be checked for recent messages
+$rcmail_config['check_all_folders'] = false;
+
+// If true, after message delete/move, the next message will be displayed
+$rcmail_config['display_next'] = false;
+
+// 0 - Do not expand threads 
+// 1 - Expand all threads automatically 
+// 2 - Expand only threads with unread messages 
+$rcmail_config['autoexpand_threads'] = 0;
+
+// When replying place cursor above original message (top posting)
+$rcmail_config['top_posting'] = false;
+
+// When replying strip original signature from message
+$rcmail_config['strip_existing_sig'] = true;
+
+// Show signature:
+// 0 - Never
+// 1 - Always
+// 2 - New messages only
+// 3 - Forwards and Replies only
+$rcmail_config['show_sig'] = 1;
+
+// When replying or forwarding place sender's signature above existing message
+$rcmail_config['sig_above'] = false;
+
+// Use MIME encoding (quoted-printable) for 8bit characters in message body
+$rcmail_config['force_7bit'] = false;
+
+// Defaults of the search field configuration.
+// The array can contain a per-folder list of header fields which should be considered when searching
+// The entry with key '*' stands for all folders which do not have a specific list set.
+// Please note that folder names should to be in sync with $rcmail_config['default_imap_folders']
+$rcmail_config['search_mods'] = null;  // Example: array('*' => array('subject'=>1, 'from'=>1), 'Sent' => array('subject'=>1, 'to'=>1));
+
+// Defaults of the addressbook search field configuration.
+$rcmail_config['addressbook_search_mods'] = null;  // Example: array('name'=>1, 'firstname'=>1, 'surname'=>1, 'email'=>1, '*'=>1);
+
+// 'Delete always'
+// This setting reflects if mail should be always deleted
+// when moving to Trash fails. This is necessary in some setups
+// when user is over quota and Trash is included in the quota.
+$rcmail_config['delete_always'] = false;
+
+// Behavior if a received message requests a message delivery notification (read receipt)
+// 0 = ask the user, 1 = send automatically, 2 = ignore (never send or ask)
+// 3 = send automatically if sender is in addressbook, otherwise ask the user
+// 4 = send automatically if sender is in addressbook, otherwise ignore
+$rcmail_config['mdn_requests'] = 0;
+
+// Return receipt checkbox default state
+$rcmail_config['mdn_default'] = 0;
+
+// Delivery Status Notification checkbox default state
+$rcmail_config['dsn_default'] = 0;
+
+// Place replies in the folder of the message being replied to
+$rcmail_config['reply_same_folder'] = false;
+
+// Sets default mode of Forward feature to "forward as attachment"
+$rcmail_config['forward_attachment'] = false;
+
+// Defines address book (internal index) to which new contacts will be added
+// By default it is the first writeable addressbook.
+// Note: Use '0' for built-in address book.
+$rcmail_config['default_addressbook'] = null;
+
+// Enables spell checking before sending a message.
+$rcmail_config['spellcheck_before_send'] = false;
+
+// end of config file
Index: /branches/devel-composer/config/mimetypes.php
===================================================================
--- /branches/devel-composer/config/mimetypes.php	(revision 5386)
+++ /branches/devel-composer/config/mimetypes.php	(revision 5386)
@@ -0,0 +1,49 @@
+<?php
+
+return array(
+  'xls' => 'application/vnd.ms-excel',
+  'xlm' => 'application/vnd.ms-excel',
+  'xla' => 'application/vnd.ms-excel',
+  'xlc' => 'application/vnd.ms-excel',
+  'xlt' => 'application/vnd.ms-excel',
+  'xlw' => 'application/vnd.ms-excel',
+  'pdf' => 'application/pdf',
+  'ppt' => 'application/vnd.ms-powerpoint',
+  'pps' => 'application/vnd.ms-powerpoint',
+  'pot' => 'application/vnd.ms-powerpoint',
+  'doc' => 'application/msword',
+  'dot' => 'application/msword',
+  'odc' => 'application/vnd.oasis.opendocument.chart',
+  'otc' => 'application/vnd.oasis.opendocument.chart-template',
+  'odf' => 'application/vnd.oasis.opendocument.formula',
+  'otf' => 'application/vnd.oasis.opendocument.formula-template',
+  'odg' => 'application/vnd.oasis.opendocument.graphics',
+  'otg' => 'application/vnd.oasis.opendocument.graphics-template',
+  'odi' => 'application/vnd.oasis.opendocument.image',
+  'oti' => 'application/vnd.oasis.opendocument.image-template',
+  'odp' => 'application/vnd.oasis.opendocument.presentation',
+  'otp' => 'application/vnd.oasis.opendocument.presentation-template',
+  'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
+  'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template',
+  'odt' => 'application/vnd.oasis.opendocument.text',
+  'otm' => 'application/vnd.oasis.opendocument.text-master',
+  'ott' => 'application/vnd.oasis.opendocument.text-template',
+  'oth' => 'application/vnd.oasis.opendocument.text-web',
+  'docm' => 'application/vnd.ms-word.document.macroEnabled.12',
+  'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+  'dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
+  'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+  'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
+  'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+  'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
+  'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+  'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+  'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
+  'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+  'xps' => 'application/vnd.ms-xpsdocument',
+  'rar' => 'application/x-rar-compressed',
+  'vcf' => 'text/vcard',
+  'ics' => 'text/calendar',
+);
+
+?>
Index: /branches/devel-composer/index.php
===================================================================
--- /branches/devel-composer/index.php	(revision 5386)
+++ /branches/devel-composer/index.php	(revision 5386)
@@ -0,0 +1,275 @@
+<?php
+/*
+ +-------------------------------------------------------------------------+
+ | Roundcube Webmail IMAP Client                                           |
+ | Version 0.7-svn                                                         |
+ |                                                                         |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team                         |
+ |                                                                         |
+ | This program is free software; you can redistribute it and/or modify    |
+ | it under the terms of the GNU General Public License version 2          |
+ | as published by the Free Software Foundation.                           |
+ |                                                                         |
+ | This program is distributed in the hope that it will be useful,         |
+ | but WITHOUT ANY WARRANTY; without even the implied warranty of          |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           |
+ | GNU General Public License for more details.                            |
+ |                                                                         |
+ | You should have received a copy of the GNU General Public License along |
+ | with this program; if not, write to the Free Software Foundation, Inc., |
+ | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.             |
+ |                                                                         |
+ +-------------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                          |
+ +-------------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+// include environment
+require_once 'program/include/iniset.php';
+
+// init application, start session, init output class, etc.
+$RCMAIL = rcmail::get_instance();
+
+// Make the whole PHP output non-cacheable (#1487797)
+send_nocacheing_headers();
+
+// turn on output buffering
+ob_start();
+
+// check if config files had errors
+if ($err_str = $RCMAIL->config->get_error()) {
+  raise_error(array(
+    'code' => 601,
+    'type' => 'php',
+    'message' => $err_str), false, true);
+}
+
+// check DB connections and exit on failure
+if ($err_str = $DB->is_error()) {
+  raise_error(array(
+    'code' => 603,
+    'type' => 'db',
+    'message' => $err_str), FALSE, TRUE);
+}
+
+// error steps
+if ($RCMAIL->action=='error' && !empty($_GET['_code'])) {
+  raise_error(array('code' => hexdec($_GET['_code'])), FALSE, TRUE);
+}
+
+// check if https is required (for login) and redirect if necessary
+if (empty($_SESSION['user_id']) && ($force_https = $RCMAIL->config->get('force_https', false))) {
+  $https_port = is_bool($force_https) ? 443 : $force_https;
+  if (!rcube_https_check($https_port)) {
+    $host  = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']);
+    $host .= ($https_port != 443 ? ':' . $https_port : '');
+    header('Location: https://' . $host . $_SERVER['REQUEST_URI']);
+    exit;
+  }
+}
+
+// trigger startup plugin hook
+$startup = $RCMAIL->plugins->exec_hook('startup', array('task' => $RCMAIL->task, 'action' => $RCMAIL->action));
+$RCMAIL->set_task($startup['task']);
+$RCMAIL->action = $startup['action'];
+
+// try to log in
+if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
+  $request_valid = $_SESSION['temp'] && $RCMAIL->check_request(RCUBE_INPUT_POST, 'login');
+
+  // purge the session in case of new login when a session already exists 
+  $RCMAIL->kill_session();
+
+  $auth = $RCMAIL->plugins->exec_hook('authenticate', array(
+    'host' => $RCMAIL->autoselect_host(),
+    'user' => trim(get_input_value('_user', RCUBE_INPUT_POST)),
+    'pass' => get_input_value('_pass', RCUBE_INPUT_POST, true,
+       $RCMAIL->config->get('password_charset', 'ISO-8859-1')),
+    'cookiecheck' => true,
+    'valid' => $request_valid,
+  ));
+
+  // check if client supports cookies
+  if ($auth['cookiecheck'] && empty($_COOKIE)) {
+    $OUTPUT->show_message("cookiesdisabled", 'warning');
+  }
+  else if ($auth['valid'] && !$auth['abort'] &&
+        !empty($auth['host']) && !empty($auth['user']) &&
+        $RCMAIL->login($auth['user'], $auth['pass'], $auth['host'])
+  ) {
+    // create new session ID, don't destroy the current session
+    // it was destroyed already by $RCMAIL->kill_session() above
+    $RCMAIL->session->remove('temp');
+    $RCMAIL->session->regenerate_id(false);
+
+    // send auth cookie if necessary
+    $RCMAIL->session->set_auth_cookie();
+
+    // log successful login
+    rcmail_log_login();
+
+    // restore original request parameters
+    $query = array();
+    if ($url = get_input_value('_url', RCUBE_INPUT_POST)) {
+      parse_str($url, $query);
+
+      // prevent endless looping on login page
+      if ($query['_task'] == 'login')
+        unset($query['_task']);
+    }
+
+    // allow plugins to control the redirect url after login success
+    $redir = $RCMAIL->plugins->exec_hook('login_after', $query + array('_task' => 'mail'));
+    unset($redir['abort'], $redir['_err']);
+
+    // send redirect
+    $OUTPUT->redirect($redir);
+  }
+  else {
+    $error_code = is_object($IMAP) ? $IMAP->get_error_code() : -1;
+
+    $OUTPUT->show_message($error_code < -1 ? 'imaperror' : (!$auth['valid'] ? 'invalidrequest' : 'loginfailed'), 'warning');
+    $RCMAIL->plugins->exec_hook('login_failed', array(
+      'code' => $error_code, 'host' => $auth['host'], 'user' => $auth['user']));
+    $RCMAIL->kill_session();
+  }
+}
+
+// end session (after optional referer check)
+else if ($RCMAIL->task == 'logout' && isset($_SESSION['user_id']) && (!$RCMAIL->config->get('referer_check') || rcube_check_referer())) {
+  $userdata = array('user' => $_SESSION['username'], 'host' => $_SESSION['imap_host'], 'lang' => $RCMAIL->user->language);
+  $OUTPUT->show_message('loggedout');
+  $RCMAIL->logout_actions();
+  $RCMAIL->kill_session();
+  $RCMAIL->plugins->exec_hook('logout_after', $userdata);
+}
+
+// check session and auth cookie
+else if ($RCMAIL->task != 'login' && $_SESSION['user_id'] && $RCMAIL->action != 'send') {
+  if (!$RCMAIL->session->check_auth()) {
+    $RCMAIL->kill_session();
+    $session_error = true;
+  }
+}
+
+// not logged in -> show login page
+if (empty($RCMAIL->user->ID)) {
+  // log session failures
+  if (($task = get_input_value('_task', RCUBE_INPUT_GPC)) && !in_array($task, array('login','logout')) && !$session_error && ($sess_id = $_COOKIE[ini_get('session.name')])) {
+    $RCMAIL->session->log("Aborted session " . $sess_id . "; no valid session data found");
+    $session_error = true;
+  }
+
+  if ($OUTPUT->ajax_call)
+    $OUTPUT->redirect(array('_err' => 'session'), 2000);
+
+  if (!empty($_REQUEST['_framed']))
+    $OUTPUT->command('redirect', $RCMAIL->url(array('_err' => 'session')));
+
+  // check if installer is still active
+  if ($RCMAIL->config->get('enable_installer') && is_readable('./installer/index.php')) {
+    $OUTPUT->add_footer(html::div(array('style' => "background:#ef9398; border:2px solid #dc5757; padding:0.5em; margin:2em auto; width:50em"),
+      html::tag('h2', array('style' => "margin-top:0.2em"), "Installer script is still accessible") .
+      html::p(null, "The install script of your Roundcube installation is still stored in its default location!") .
+      html::p(null, "Please <b>remove</b> the whole <tt>installer</tt> folder from the Roundcube directory because .
+        these files may expose sensitive configuration data like server passwords and encryption keys
+        to the public. Make sure you cannot access the <a href=\"./installer/\">installer script</a> from your browser.")
+      )
+    );
+  }
+
+  if ($session_error || $_REQUEST['_err'] == 'session')
+    $OUTPUT->show_message('sessionerror', 'error', null, true, -1);
+
+  $RCMAIL->set_task('login');
+  $OUTPUT->send('login');
+}
+// CSRF prevention
+else {
+  // don't check for valid request tokens in these actions
+  $request_check_whitelist = array('login'=>1, 'spell'=>1);
+
+  // check client X-header to verify request origin
+  if ($OUTPUT->ajax_call) {
+    if (rc_request_header('X-Roundcube-Request') != $RCMAIL->get_request_token() && !$RCMAIL->config->get('devel_mode')) {
+      header('HTTP/1.1 403 Forbidden');
+      die("Invalid Request");
+    }
+  }
+  // check request token in POST form submissions
+  else if (!empty($_POST) && !$request_check_whitelist[$RCMAIL->action] && !$RCMAIL->check_request()) {
+    $OUTPUT->show_message('invalidrequest', 'error');
+    $OUTPUT->send($RCMAIL->task);
+  }
+
+  // check referer if configured
+  if (!$request_check_whitelist[$RCMAIL->action] && $RCMAIL->config->get('referer_check') && !rcube_check_referer()) {
+    raise_error(array(
+      'code' => 403,
+      'type' => 'php',
+      'message' => "Referer check failed"), true, true);
+  }
+}
+
+// we're ready, user is authenticated and the request is safe
+$plugin = $RCMAIL->plugins->exec_hook('ready', array('task' => $RCMAIL->task, 'action' => $RCMAIL->action));
+$RCMAIL->set_task($plugin['task']);
+$RCMAIL->action = $plugin['action'];
+
+
+// handle special actions
+if ($RCMAIL->action == 'keep-alive') {
+  $OUTPUT->reset();
+  $RCMAIL->plugins->exec_hook('keep_alive', array());
+  $OUTPUT->send();
+}
+else if ($RCMAIL->action == 'save-pref') {
+  include INSTALL_PATH . 'program/steps/utils/save_pref.inc';
+}
+
+
+// include task specific functions
+if (is_file($incfile = INSTALL_PATH . 'program/steps/'.$RCMAIL->task.'/func.inc'))
+  include_once $incfile;
+
+// allow 5 "redirects" to another action
+$redirects = 0; $incstep = null;
+while ($redirects < 5) {
+  // execute a plugin action
+  if ($RCMAIL->plugins->is_plugin_task($RCMAIL->task)) {
+    if (!$RCMAIL->action) $RCMAIL->action = 'index';
+    $RCMAIL->plugins->exec_action($RCMAIL->task.'.'.$RCMAIL->action);
+    break;
+  }
+  else if (preg_match('/^plugin\./', $RCMAIL->action)) {
+    $RCMAIL->plugins->exec_action($RCMAIL->action);
+    break;
+  }
+  // try to include the step file
+  else if (($stepfile = $RCMAIL->get_action_file())
+    && is_file($incfile = INSTALL_PATH . 'program/steps/'.$RCMAIL->task.'/'.$stepfile)
+  ) {
+    include $incfile;
+    $redirects++;
+  }
+  else {
+    break;
+  }
+}
+
+
+// parse main template (default)
+$OUTPUT->send($RCMAIL->task);
+
+
+// if we arrive here, something went wrong
+raise_error(array(
+  'code' => 404,
+  'type' => 'php',
+  'line' => __LINE__,
+  'file' => __FILE__,
+  'message' => "Invalid request"), true, true);
+
Index: /branches/devel-composer/installer/check.php
===================================================================
--- /branches/devel-composer/installer/check.php	(revision 5386)
+++ /branches/devel-composer/installer/check.php	(revision 5386)
@@ -0,0 +1,219 @@
+<form action="index.php" method="get">
+<?php
+
+$required_php_exts = array(
+    'PCRE'      => 'pcre',
+    'DOM'       => 'dom',
+    'Session'   => 'session',
+    'XML'       => 'xml',
+    'JSON'      => 'json'
+);
+
+$optional_php_exts = array(
+    'FileInfo'  => 'fileinfo',
+    'Libiconv'  => 'iconv',
+    'Multibyte' => 'mbstring',
+    'OpenSSL'   => 'openssl',
+    'Mcrypt'    => 'mcrypt',
+    'Intl'      => 'intl',
+);
+
+$required_libs = array(
+    'PEAR'      => 'PEAR.php',
+    'MDB2'      => 'MDB2.php',
+    'Net_SMTP'  => 'Net/SMTP.php',
+    'Net_IDNA2' => 'Net/IDNA2.php',
+    'Mail_mime' => 'Mail/mime.php',
+);
+
+$supported_dbs = array(
+    'MySQL'         => 'mysql',
+    'MySQLi'        => 'mysqli',
+    'PostgreSQL'    => 'pgsql',
+    'SQLite (v2)'   => 'sqlite',
+);
+
+$ini_checks = array(
+    'file_uploads'                  => 1,
+    'session.auto_start'            => 0,
+    'zend.ze1_compatibility_mode'   => 0,
+    'mbstring.func_overload'        => 0,
+    'suhosin.session.encrypt'       => 0,
+);
+
+$optional_checks = array(
+    'date.timezone' => '-NOTEMPTY-',
+);
+
+$source_urls = array(
+    'Sockets'   => 'http://www.php.net/manual/en/book.sockets.php',
+    'Session'   => 'http://www.php.net/manual/en/book.session.php',
+    'PCRE'      => 'http://www.php.net/manual/en/book.pcre.php',
+    'FileInfo'  => 'http://www.php.net/manual/en/book.fileinfo.php',
+    'Libiconv'  => 'http://www.php.net/manual/en/book.iconv.php',
+    'Multibyte' => 'http://www.php.net/manual/en/book.mbstring.php',
+    'Mcrypt'    => 'http://www.php.net/manual/en/book.mcrypt.php',
+    'OpenSSL'   => 'http://www.php.net/manual/en/book.openssl.php',
+    'JSON'      => 'http://www.php.net/manual/en/book.json.php',
+    'DOM'       => 'http://www.php.net/manual/en/book.dom.php',
+    'Intl'      => 'http://www.php.net/manual/en/book.intl.php',
+    'PEAR'      => 'http://pear.php.net',
+    'MDB2'      => 'http://pear.php.net/package/MDB2',
+    'Net_SMTP'  => 'http://pear.php.net/package/Net_SMTP',
+    'Mail_mime' => 'http://pear.php.net/package/Mail_mime',
+);
+
+echo '<input type="hidden" name="_step" value="' . ($RCI->configured ? 3 : 2) . '" />';
+?>
+
+<h3>Checking PHP version</h3>
+<?php
+
+define('MIN_PHP_VERSION', '5.2.1');
+if (version_compare(PHP_VERSION, MIN_PHP_VERSION, '>=')) {
+    $RCI->pass('Version', 'PHP ' . PHP_VERSION . ' detected');
+} else {
+    $RCI->fail('Version', 'PHP Version ' . MIN_PHP_VERSION . ' or greater is required ' . PHP_VERSION . ' detected');
+}
+?>
+
+<h3>Checking PHP extensions</h3>
+<p class="hint">The following modules/extensions are <em>required</em> to run Roundcube:</p>
+<?php
+
+// get extensions location
+$ext_dir = ini_get('extension_dir');
+
+$prefix = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_' : '';
+foreach ($required_php_exts as $name => $ext) {
+    if (extension_loaded($ext)) {
+        $RCI->pass($name);
+    } else {
+        $_ext = $ext_dir . '/' . $prefix . $ext . '.' . PHP_SHLIB_SUFFIX;
+        $msg = @is_readable($_ext) ? 'Could be loaded. Please add in php.ini' : '';
+        $RCI->fail($name, $msg, $source_urls[$name]);
+    }
+    echo '<br />';
+}
+
+?>
+
+<p class="hint">The next couple of extensions are <em>optional</em> and recommended to get the best performance:</p>
+<?php
+
+foreach ($optional_php_exts as $name => $ext) {
+    if (extension_loaded($ext)) {
+        $RCI->pass($name);
+    }
+    else {
+        $_ext = $ext_dir . '/' . $prefix . $ext . '.' . PHP_SHLIB_SUFFIX;
+        $msg = @is_readable($_ext) ? 'Could be loaded. Please add in php.ini' : '';
+        $RCI->na($name, $msg, $source_urls[$name]);
+    }
+    echo '<br />';
+}
+
+?>
+
+
+<h3>Checking available databases</h3>
+<p class="hint">Check which of the supported extensions are installed. At least one of them is required.</p>
+
+<?php
+
+$prefix = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_' : '';
+foreach ($supported_dbs as $database => $ext) {
+    if (extension_loaded($ext)) {
+        $RCI->pass($database);
+    }
+    else {
+        $_ext = $ext_dir . '/' . $prefix . $ext . '.' . PHP_SHLIB_SUFFIX;
+        $msg = @is_readable($_ext) ? 'Could be loaded. Please add in php.ini' : 'Not installed';
+        $RCI->na($database, $msg, $source_urls[$database]);
+    }
+    echo '<br />';
+}
+
+?>
+
+
+<h3>Check for required 3rd party libs</h3>
+<p class="hint">This also checks if the include path is set correctly.</p>
+
+<?php
+
+foreach ($required_libs as $classname => $file) {
+    @include_once $file;
+    if (class_exists($classname)) {
+        $RCI->pass($classname);
+    }
+    else {
+        $RCI->fail($classname, "Failed to load $file", $source_urls[$classname]);
+    }
+    echo "<br />";
+}
+
+
+?>
+
+<h3>Checking php.ini/.htaccess settings</h3>
+<p class="hint">The following settings are <em>required</em> to run Roundcube:</p>
+
+<?php
+
+foreach ($ini_checks as $var => $val) {
+    $status = ini_get($var);
+    if ($val === '-NOTEMPTY-') {
+        if (empty($status)) {
+            $RCI->fail($var, "cannot be empty and needs to be set");
+        } else {
+            $RCI->pass($var);
+        }
+        echo '<br />';
+        continue;
+    }
+    if ($status == $val) {
+        $RCI->pass($var);
+    } else {
+      $RCI->fail($var, "is '$status', should be '$val'");
+    }
+    echo '<br />';
+}
+?>
+
+<p class="hint">The following settings are <em>optional</em> and recommended:</p>
+
+<?php
+
+foreach ($optional_checks as $var => $val) {
+    $status = ini_get($var);
+    if ($val === '-NOTEMPTY-') {
+        if (empty($status)) {
+            $RCI->optfail($var, "Could be set");
+        } else {
+            $RCI->pass($var);
+        }
+        echo '<br />';
+        continue;
+    }
+    if ($status == $val) {
+        $RCI->pass($var);
+    } else {
+      $RCI->optfail($var, "is '$status', could be '$val'");
+    }
+    echo '<br />';
+}
+?>
+
+<?php
+
+if ($RCI->failures) {
+  echo '<p class="warning">Sorry but your webserver does not meet the requirements for Roundcube!<br />
+            Please install the missing modules or fix the php.ini settings according to the above check results.<br />
+            Hint: only checks showing <span class="fail">NOT OK</span> need to be fixed.</p>';
+}
+echo '<p><br /><input type="submit" value="NEXT" ' . ($RCI->failures ? 'disabled' : '') . ' /></p>';
+
+?>
+
+</form>
Index: /branches/devel-composer/installer/client.js
===================================================================
--- /branches/devel-composer/installer/client.js	(revision 5386)
+++ /branches/devel-composer/installer/client.js	(revision 5386)
@@ -0,0 +1,36 @@
+
+function toggleblock(id, link)
+{
+  var block = document.getElementById(id);
+  
+  return false;
+}
+
+
+function addhostfield()
+{
+  var container = document.getElementById('defaulthostlist');
+  var row = document.createElement('div');
+  var input = document.createElement('input');
+  var link = document.createElement('a');
+  
+  input.name = '_default_host[]';
+  input.size = '30';
+  link.href = '#';
+  link.onclick = function() { removehostfield(this.parentNode); return false };
+  link.className = 'removelink';
+  link.innerHTML = 'remove';
+  
+  row.appendChild(input);
+  row.appendChild(link);
+  container.appendChild(row);
+}
+
+
+function removehostfield(row)
+{
+  var container = document.getElementById('defaulthostlist');
+  container.removeChild(row);
+}
+
+
Index: /branches/devel-composer/installer/config.php
===================================================================
--- /branches/devel-composer/installer/config.php	(revision 5386)
+++ /branches/devel-composer/installer/config.php	(revision 5386)
@@ -0,0 +1,634 @@
+<form action="index.php" method="post">
+<input type="hidden" name="_step" value="2" />
+<?php
+
+// also load the default config to fill in the fields
+$RCI->load_defaults();
+
+// register these boolean fields
+$RCI->bool_config_props = array(
+  'ip_check' => 1,
+  'enable_caching' => 1,
+  'enable_spellcheck' => 1,
+  'auto_create_user' => 1,
+  'smtp_log' => 1,
+  'prefer_html' => 1,
+  'preview_pane' => 1,
+  'debug_level' => 1,
+);
+
+// allow the current user to get to the next step
+$_SESSION['allowinstaller'] = true;
+
+if (!empty($_POST['submit'])) {
+  
+  echo '<p class="notice">Copy or download the following configurations and save them in two files';
+  echo ' (names above the text box) within the <tt>'.RCMAIL_CONFIG_DIR.'</tt> directory of your Roundcube installation.<br/>';
+  echo ' Make sure that there are no characters outside the <tt>&lt;?php ?&gt;</tt> brackets when saving the files.</p>';
+  
+  $textbox = new html_textarea(array('rows' => 16, 'cols' => 60, 'class' => "configfile"));
+  
+  echo '<div><em>main.inc.php (<a href="index.php?_getfile=main">download</a>)</em></div>';
+  echo $textbox->show(($_SESSION['main.inc.php'] = $RCI->create_config('main')));
+  
+  echo '<div style="margin-top:1em"><em>db.inc.php (<a href="index.php?_getfile=db">download</a>)</em></div>';
+  echo $textbox->show($_SESSION['db.inc.php'] = $RCI->create_config('db'));
+
+  echo '<p class="hint">Of course there are more options to configure.
+    Have a look at the config files or visit <a href="http://trac.roundcube.net/wiki/Howto_Config">Howto_Config</a> to find out.</p>';
+
+  echo '<p><input type="button" onclick="location.href=\'./index.php?_step=3\'" value="CONTINUE" /></p>';
+  
+  // echo '<style type="text/css"> .configblock { display:none } </style>';
+  echo "\n<hr style='margin-bottom:1.6em' />\n";
+}
+
+?>
+<fieldset>
+<legend>General configuration</legend>
+<dl class="configblock">
+
+<dt class="propname">product_name</dt>
+<dd>
+<?php
+
+$input_prodname = new html_inputfield(array('name' => '_product_name', 'size' => 30, 'id' => "cfgprodname"));
+echo $input_prodname->show($RCI->getprop('product_name'));
+
+?>
+<div>The name of your service (used to compose page titles)</div>
+</dd>
+
+<dt class="propname">temp_dir</dt>
+<dd>
+<?php
+
+$input_tempdir = new html_inputfield(array('name' => '_temp_dir', 'size' => 30, 'id' => "cfgtempdir"));
+echo $input_tempdir->show($RCI->getprop('temp_dir'));
+
+?>
+<div>Use this folder to store temp files (must be writeable for webserver)</div>
+</dd>
+
+
+<dt class="propname">ip_check</dt>
+<dd>
+<?php
+
+$check_ipcheck = new html_checkbox(array('name' => '_ip_check', 'id' => "cfgipcheck"));
+echo $check_ipcheck->show(intval($RCI->getprop('ip_check')), array('value' => 1));
+
+?>
+<label for="cfgipcheck">Check client IP in session authorization</label><br />
+
+<p class="hint">This increases security but can cause sudden logouts when someone uses a proxy with changing IPs.</p>
+</dd>
+
+<dt class="propname">des_key</dt>
+<dd>
+<?php
+
+$input_deskey = new html_inputfield(array('name' => '_des_key', 'size' => 30, 'id' => "cfgdeskey"));
+echo $input_deskey->show($RCI->getprop('des_key'));
+
+?>
+<div>This key is used to encrypt the users imap password before storing in the session record</div>
+<p class="hint">It's a random generated string to ensure that every installation has its own key.
+If you enter it manually please provide a string of exactly 24 chars.</p>
+</dd>
+
+<dt class="propname">enable_caching</dt>
+<dd>
+<?php
+
+$check_caching = new html_checkbox(array('name' => '_enable_caching', 'id' => "cfgcache"));
+echo $check_caching->show(intval($RCI->getprop('enable_caching')), array('value' => 1));
+
+?>
+<label for="cfgcache">Cache messages in local database</label><br />
+</dd>
+
+<dt class="propname">enable_spellcheck</dt>
+<dd>
+<?php
+$check_spell = new html_checkbox(array('name' => '_enable_spellcheck', 'id' => "cfgspellcheck"));
+echo $check_spell->show(intval($RCI->getprop('enable_spellcheck')), array('value' => 1));
+?>
+<label for="cfgspellcheck">Make use of the spell checker</label><br />
+</dd>
+
+<dt class="propname">spellcheck_engine</dt>
+<dd>
+<?php
+$select_spell = new html_select(array('name' => '_spellcheck_engine', 'id' => "cfgspellcheckengine"));
+if (extension_loaded('pspell'))
+  $select_spell->add('pspell', 'pspell');
+$select_spell->add('Googie', 'googie');
+
+echo $select_spell->show($RCI->is_post ? $_POST['_spellcheck_engine'] : 'pspell');
+
+?>
+<label for="cfgspellcheckengine">Which spell checker to use</label><br />
+
+<p class="hint">GoogieSpell implies that the message content will be sent to Google in order to check the spelling.</p>
+</dd>
+
+<dt class="propname">identities_level</dt>
+<dd>
+<?php
+
+$input_ilevel = new html_select(array('name' => '_identities_level', 'id' => "cfgidentitieslevel"));
+$input_ilevel->add('many identities with possibility to edit all params', 0);
+$input_ilevel->add('many identities with possibility to edit all params but not email address', 1);
+$input_ilevel->add('one identity with possibility to edit all params', 2);
+$input_ilevel->add('one identity with possibility to edit all params but not email address', 3);
+echo $input_ilevel->show($RCI->getprop('identities_level'), 0);
+
+?>
+<div>Level of identities access</div>
+<p class="hint">Defines what users can do with their identities.</p>
+</dd>
+
+</dl>
+</fieldset>
+
+<fieldset>
+<legend>Logging & Debugging</legend>
+<dl class="loggingblock">
+
+<dt class="propname">debug_level</dt>
+<dd>
+<?php
+
+$value = $RCI->getprop('debug_level');
+$check_debug = new html_checkbox(array('name' => '_debug_level[]'));
+echo $check_debug->show(($value & 1) ? 1 : 0 , array('value' => 1, 'id' => 'cfgdebug1'));
+echo '<label for="cfgdebug1">Log errors</label><br />';
+
+echo $check_debug->show(($value & 4) ? 4 : 0, array('value' => 4, 'id' => 'cfgdebug4'));
+echo '<label for="cfgdebug4">Print errors (to the browser)</label><br />';
+
+echo $check_debug->show(($value & 8) ? 8 : 0, array('value' => 8, 'id' => 'cfgdebug8'));
+echo '<label for="cfgdebug8">Verbose display (enables debug console)</label><br />';
+
+?>
+</dd>
+
+<dt class="propname">log_driver</dt>
+<dd>
+<?php
+
+$select_log_driver = new html_select(array('name' => '_log_driver', 'id' => "cfglogdriver"));
+$select_log_driver->add(array('file', 'syslog'), array('file', 'syslog'));
+echo $select_log_driver->show($RCI->getprop('log_driver', 'file'));
+
+?>
+<div>How to do logging? 'file' - write to files in the log directory, 'syslog' - use the syslog facility.</div>
+</dd>
+
+<dt class="propname">log_dir</dt>
+<dd>
+<?php
+
+$input_logdir = new html_inputfield(array('name' => '_log_dir', 'size' => 30, 'id' => "cfglogdir"));
+echo $input_logdir->show($RCI->getprop('log_dir'));
+
+?>
+<div>Use this folder to store log files (must be writeable for webserver). Note that this only applies if you are using the 'file' log_driver.</div>
+</dd>
+
+<dt class="propname">syslog_id</dt>
+<dd>
+<?php
+
+$input_syslogid = new html_inputfield(array('name' => '_syslog_id', 'size' => 30, 'id' => "cfgsyslogid"));
+echo $input_syslogid->show($RCI->getprop('syslog_id', 'roundcube'));
+
+?>
+<div>What ID to use when logging with syslog. Note that this only applies if you are using the 'syslog' log_driver.</div>
+</dd>
+
+<dt class="propname">syslog_facility</dt>
+<dd>
+<?php
+
+$input_syslogfacility = new html_select(array('name' => '_syslog_facility', 'id' => "cfgsyslogfacility"));
+$input_syslogfacility->add('user-level messages', LOG_USER);
+$input_syslogfacility->add('mail subsystem', LOG_MAIL);
+$input_syslogfacility->add('local level 0', LOG_LOCAL0);
+$input_syslogfacility->add('local level 1', LOG_LOCAL1);
+$input_syslogfacility->add('local level 2', LOG_LOCAL2);
+$input_syslogfacility->add('local level 3', LOG_LOCAL3);
+$input_syslogfacility->add('local level 4', LOG_LOCAL4);
+$input_syslogfacility->add('local level 5', LOG_LOCAL5);
+$input_syslogfacility->add('local level 6', LOG_LOCAL6);
+$input_syslogfacility->add('local level 7', LOG_LOCAL7);
+echo $input_syslogfacility->show($RCI->getprop('syslog_facility'), LOG_USER);
+
+?>
+<div>What ID to use when logging with syslog.  Note that this only applies if you are using the 'syslog' log_driver.</div>
+</dd>
+
+
+
+
+</dl>
+</fieldset>
+
+
+<fieldset>
+<legend>Database setup</legend>
+<dl class="configblock" id="cgfblockdb">
+<dt class="propname">db_dsnw</dt>
+<dd>
+<p>Database settings for read/write operations:</p>
+<?php
+
+require_once 'MDB2.php';
+
+$supported_dbs = array('MySQL' => 'mysql', 'MySQLi' => 'mysqli',
+    'PgSQL' => 'pgsql', 'SQLite' => 'sqlite');
+
+$select_dbtype = new html_select(array('name' => '_dbtype', 'id' => "cfgdbtype"));
+foreach ($supported_dbs AS $database => $ext) {
+    if (extension_loaded($ext)) {
+        $select_dbtype->add($database, $ext);
+    }
+}
+
+$input_dbhost = new html_inputfield(array('name' => '_dbhost', 'size' => 20, 'id' => "cfgdbhost"));
+$input_dbname = new html_inputfield(array('name' => '_dbname', 'size' => 20, 'id' => "cfgdbname"));
+$input_dbuser = new html_inputfield(array('name' => '_dbuser', 'size' => 20, 'id' => "cfgdbuser"));
+$input_dbpass = new html_passwordfield(array('name' => '_dbpass', 'size' => 20, 'id' => "cfgdbpass"));
+
+$dsnw = MDB2::parseDSN($RCI->getprop('db_dsnw'));
+
+echo $select_dbtype->show($RCI->is_post ? $_POST['_dbtype'] : $dsnw['phptype']);
+echo '<label for="cfgdbtype">Database type</label><br />';
+echo $input_dbhost->show($RCI->is_post ? $_POST['_dbhost'] : $dsnw['hostspec']);
+echo '<label for="cfgdbhost">Database server (omit for sqlite)</label><br />';
+echo $input_dbname->show($RCI->is_post ? $_POST['_dbname'] : $dsnw['database']);
+echo '<label for="cfgdbname">Database name (use absolute path and filename for sqlite)</label><br />';
+echo $input_dbuser->show($RCI->is_post ? $_POST['_dbuser'] : $dsnw['username']);
+echo '<label for="cfgdbuser">Database user name (needs write permissions)(omit for sqlite)</label><br />';
+echo $input_dbpass->show($RCI->is_post ? $_POST['_dbpass'] : $dsnw['password']);
+echo '<label for="cfgdbpass">Database password (omit for sqlite)</label><br />';
+
+?>
+</dd>
+</dl>
+</fieldset>
+
+
+<fieldset>
+<legend>IMAP Settings</legend>
+<dl class="configblock" id="cgfblockimap">
+<dt class="propname">default_host</dt>
+<dd>
+<div>The IMAP host(s) chosen to perform the log-in</div>
+<div id="defaulthostlist">
+<?php
+
+$text_imaphost = new html_inputfield(array('name' => '_default_host[]', 'size' => 30));
+$default_hosts = $RCI->get_hostlist();
+
+if (empty($default_hosts))
+  $default_hosts = array('');
+
+$i = 0;
+foreach ($default_hosts as $host) {
+  echo '<div id="defaulthostentry'.$i.'">' . $text_imaphost->show($host);
+  if ($i++ > 0)
+    echo '<a href="#" onclick="removehostfield(this.parentNode);return false" class="removelink" title="Remove this entry">remove</a>';
+  echo '</div>';
+}
+
+?>
+</div>
+<div><a href="javascript:addhostfield()" class="addlink" title="Add another field">add</a></div>
+
+<p class="hint">Leave blank to show a textbox at login. To use SSL/IMAPS connection, type ssl://hostname</p>
+</dd>
+
+<dt class="propname">default_port</dt>
+<dd>
+<?php
+
+$text_imapport = new html_inputfield(array('name' => '_default_port', 'size' => 6, 'id' => "cfgimapport"));
+echo $text_imapport->show($RCI->getprop('default_port'));
+
+?>
+<div>TCP port used for IMAP connections</div>
+</dd>
+
+<dt class="propname">username_domain</dt>
+<dd>
+<?php
+
+$text_userdomain = new html_inputfield(array('name' => '_username_domain', 'size' => 30, 'id' => "cfguserdomain"));
+echo $text_userdomain->show($RCI->getprop('username_domain'));
+
+?>
+<div>Automatically add this domain to user names for login</div>
+
+<p class="hint">Only for IMAP servers that require full e-mail addresses for login</p>
+</dd>
+
+<dt class="propname">auto_create_user</dt>
+<dd>
+<?php
+
+$check_autocreate = new html_checkbox(array('name' => '_auto_create_user', 'id' => "cfgautocreate"));
+echo $check_autocreate->show(intval($RCI->getprop('auto_create_user')), array('value' => 1));
+
+?>
+<label for="cfgautocreate">Automatically create a new Roundcube user when log-in the first time</label><br />
+
+<p class="hint">A user is authenticated by the IMAP server but it requires a local record to store settings
+and contacts. With this option enabled a new user record will automatically be created once the IMAP login succeeds.</p>
+
+<p class="hint">If this option is disabled, the login only succeeds if there's a matching user-record in the local Roundcube database
+what means that you have to create those records manually or disable this option after the first login.</p>
+</dd>
+
+<dt class="propname">sent_mbox</dt>
+<dd>
+<?php
+
+$text_sentmbox = new html_inputfield(array('name' => '_sent_mbox', 'size' => 20, 'id' => "cfgsentmbox"));
+echo $text_sentmbox->show($RCI->getprop('sent_mbox'));
+
+?>
+<div>Store sent messages in this folder</div>
+
+<p class="hint">Leave blank if sent messages should not be stored. Note: folder must include namespace prefix if any.</p>
+</dd>
+
+<dt class="propname">trash_mbox</dt>
+<dd>
+<?php
+
+$text_trashmbox = new html_inputfield(array('name' => '_trash_mbox', 'size' => 20, 'id' => "cfgtrashmbox"));
+echo $text_trashmbox->show($RCI->getprop('trash_mbox'));
+
+?>
+<div>Move messages to this folder when deleting them</div>
+
+<p class="hint">Leave blank if they should be deleted directly. Note: folder must include namespace prefix if any.</p>
+</dd>
+
+<dt class="propname">drafts_mbox</dt>
+<dd>
+<?php
+
+$text_draftsmbox = new html_inputfield(array('name' => '_drafts_mbox', 'size' => 20, 'id' => "cfgdraftsmbox"));
+echo $text_draftsmbox->show($RCI->getprop('drafts_mbox'));
+
+?>
+<div>Store draft messages in this folder</div>
+
+<p class="hint">Leave blank if they should not be stored. Note: folder must include namespace prefix if any.</p>
+</dd>
+
+<dt class="propname">junk_mbox</dt>
+<dd>
+<?php
+
+$text_junkmbox = new html_inputfield(array('name' => '_junk_mbox', 'size' => 20, 'id' => "cfgjunkmbox"));
+echo $text_junkmbox->show($RCI->getprop('junk_mbox'));
+
+?>
+<div>Store spam messages in this folder</div>
+
+<p class="hint">Note: folder must include namespace prefix if any.</p>
+</dd>
+
+</dd>
+</dl>
+</fieldset>
+
+
+<fieldset>
+<legend>SMTP Settings</legend>
+<dl class="configblock" id="cgfblocksmtp">
+<dt class="propname">smtp_server</dt>
+<dd>
+<?php
+
+$text_smtphost = new html_inputfield(array('name' => '_smtp_server', 'size' => 30, 'id' => "cfgsmtphost"));
+echo $text_smtphost->show($RCI->getprop('smtp_server'));
+
+?>
+<div>Use this host for sending mails</div>
+
+<p class="hint">To use SSL connection, set ssl://smtp.host.com. If left blank, the PHP mail() function is used</p>
+</dd>
+
+<dt class="propname">smtp_port</dt>
+<dd>
+<?php
+
+$text_smtpport = new html_inputfield(array('name' => '_smtp_port', 'size' => 6, 'id' => "cfgsmtpport"));
+echo $text_smtpport->show($RCI->getprop('smtp_port'));
+
+?>
+<div>SMTP port (default is 25; 465 for SSL; 587 for submission)</div>
+</dd>
+
+<dt class="propname">smtp_user/smtp_pass</dt>
+<dd>
+<?php
+
+$text_smtpuser = new html_inputfield(array('name' => '_smtp_user', 'size' => 20, 'id' => "cfgsmtpuser"));
+$text_smtppass = new html_passwordfield(array('name' => '_smtp_pass', 'size' => 20, 'id' => "cfgsmtppass"));
+echo $text_smtpuser->show($RCI->getprop('smtp_user'));
+echo $text_smtppass->show($RCI->getprop('smtp_pass'));
+
+?>
+<div>SMTP username and password (if required)</div>
+<p>
+<?php
+
+$check_smtpuser = new html_checkbox(array('name' => '_smtp_user_u', 'id' => "cfgsmtpuseru"));
+echo $check_smtpuser->show($RCI->getprop('smtp_user') == '%u' || $_POST['_smtp_user_u'] ? 1 : 0, array('value' => 1));
+
+?>
+<label for="cfgsmtpuseru">Use the current IMAP username and password for SMTP authentication</label>
+</p>
+</dd>
+<!--
+<dt class="propname">smtp_auth_type</dt>
+<dd>
+<?php
+/*
+$select_smtpauth = new html_select(array('name' => '_smtp_auth_type', 'id' => "cfgsmtpauth"));
+$select_smtpauth->add(array('(auto)', 'PLAIN', 'DIGEST-MD5', 'CRAM-MD5', 'LOGIN'), array('0', 'PLAIN', 'DIGEST-MD5', 'CRAM-MD5', 'LOGIN'));
+echo $select_smtpauth->show(intval($RCI->getprop('smtp_auth_type')));
+*/
+?>
+<div>Method to authenticate at the SMTP server. Choose (auto) if you don't know what this is</div>
+</dd>
+-->
+<dt class="propname">smtp_log</dt>
+<dd>
+<?php
+
+$check_smtplog = new html_checkbox(array('name' => '_smtp_log', 'id' => "cfgsmtplog"));
+echo $check_smtplog->show(intval($RCI->getprop('smtp_log')), array('value' => 1));
+
+?>
+<label for="cfgsmtplog">Log sent messages in <tt>{log_dir}/sendmail</tt> or to syslog.</label><br />
+</dd>
+
+</dl>
+</fieldset>
+
+
+<fieldset>
+<legend>Display settings &amp; user prefs</legend>
+<dl class="configblock" id="cgfblockdisplay">
+
+<dt class="propname">language <span class="userconf">*</span></dt>
+<dd>
+<?php
+
+$input_locale = new html_inputfield(array('name' => '_language', 'size' => 6, 'id' => "cfglocale"));
+echo $input_locale->show($RCI->getprop('language'));
+
+?>
+<div>The default locale setting. This also defines the language of the login screen.<br/>Leave it empty to auto-detect the user agent language.</div>
+<p class="hint">Enter a <a href="http://www.faqs.org/rfcs/rfc1766">RFC1766</a> formatted language name. Examples: en_US, de_DE, de_CH, fr_FR, pt_BR</p>
+</dd>
+
+<dt class="propname">skin <span class="userconf">*</span></dt>
+<dd>
+<?php
+
+$input_skin = new html_select(array('name' => '_skin', 'id' => "cfgskin"));
+$input_skin->add($RCI->list_skins());
+echo $input_skin->show($RCI->getprop('skin'));
+
+?>
+<div>Name of interface skin (folder in /skins)</div>
+</dd>
+
+<dt class="propname">skin_logo</dt>
+<dd>
+<?php
+
+$input_skin = new html_inputfield(array('name' => '_skin_logo', 'size' => 50, 'id' => "cfgskinlogo"));
+echo $input_skin->show($RCI->getprop('skin_logo'));
+
+?>
+<div>Custom image to display instead of the Roundcube logo.</div>
+<p class="hint">Enter a URL relative to the document root of this Roundcube installation.</p>
+</dd>
+
+<dt class="propname">pagesize <span class="userconf">*</span></dt>
+<dd>
+<?php
+
+$input_pagesize = new html_inputfield(array('name' => '_pagesize', 'size' => 6, 'id' => "cfgpagesize"));
+echo $input_pagesize->show($RCI->getprop('pagesize'));
+
+?>
+<div>Show up to X items in list view.</div>
+</dd>
+
+<dt class="propname">prefer_html <span class="userconf">*</span></dt>
+<dd>
+<?php
+
+$check_htmlview = new html_checkbox(array('name' => '_prefer_html', 'id' => "cfghtmlview", 'value' => 1));
+echo $check_htmlview->show(intval($RCI->getprop('prefer_html')));
+
+?>
+<label for="cfghtmlview">Prefer displaying HTML messages</label><br />
+</dd>
+
+<dt class="propname">preview_pane <span class="userconf">*</span></dt>
+<dd>
+<?php
+
+$check_prevpane = new html_checkbox(array('name' => '_preview_pane', 'id' => "cfgprevpane", 'value' => 1));
+echo $check_prevpane->show(intval($RCI->getprop('preview_pane')));
+
+?>
+<label for="cfgprevpane">If preview pane is enabled</label><br />
+</dd>
+
+<dt class="propname">htmleditor <span class="userconf">*</span></dt>
+<dd>
+<label for="cfghtmlcompose">Compose HTML formatted messages</label>
+<?php
+
+$select_htmlcomp = new html_select(array('name' => '_htmleditor', 'id' => "cfghtmlcompose"));
+$select_htmlcomp->add('never', 0);
+$select_htmlcomp->add('always', 1);
+$select_htmlcomp->add('on reply to HTML message only', 2);
+echo $select_htmlcomp->show(intval($RCI->getprop('htmleditor')));
+
+?>
+</dd>
+
+<dt class="propname">draft_autosave <span class="userconf">*</span></dt>
+<dd>
+<label for="cfgautosave">Save compose message every</label>
+<?php
+
+$select_autosave = new html_select(array('name' => '_draft_autosave', 'id' => 'cfgautosave'));
+$select_autosave->add('never', 0);
+foreach (array(1, 3, 5, 10) as $i => $min)
+  $select_autosave->add("$min min", $min*60);
+
+echo $select_autosave->show(intval($RCI->getprop('draft_autosave')));
+
+?>
+</dd>
+
+<dt class="propname">mdn_requests <span class="userconf">*</span></dt>
+<dd>
+<?php
+
+$mdn_opts = array(
+    0 => 'ask the user',
+    1 => 'send automatically',
+    3 => 'send receipt to user contacts, otherwise ask the user',
+    4 => 'send receipt to user contacts, otherwise ignore',
+    2 => 'ignore',
+);
+
+$select_mdnreq = new html_select(array('name' => '_mdn_requests', 'id' => "cfgmdnreq"));
+$select_mdnreq->add(array_values($mdn_opts), array_keys($mdn_opts));
+echo $select_mdnreq->show(intval($RCI->getprop('mdn_requests')));
+
+?>
+<div>Behavior if a received message requests a message delivery notification (read receipt)</div>
+</dd>
+
+<dt class="propname">mime_param_folding <span class="userconf">*</span></dt>
+<dd>
+<?php
+
+$select_param_folding = new html_select(array('name' => '_mime_param_folding', 'id' => "cfgmimeparamfolding"));
+$select_param_folding->add('Full RFC 2231 (Roundcube, Thunderbird)', '0'); 
+$select_param_folding->add('RFC 2047/2231 (MS Outlook, OE)', '1');
+$select_param_folding->add('Full RFC 2047 (deprecated)', '2');
+
+echo $select_param_folding->show(intval($RCI->getprop('mime_param_folding')));
+
+?>
+<div>How to encode attachment long/non-ascii names</div>
+</dd>
+
+</dl>
+
+<p class="hint"><span class="userconf">*</span>&nbsp; These settings are defaults for the user preferences</p>
+</fieldset>
+
+<?php
+
+echo '<p><input type="submit" name="submit" value="' . ($RCI->configured ? 'UPDATE' : 'CREATE') . ' CONFIG" ' . ($RCI->failures ? 'disabled' : '') . ' /></p>';
+
+?>
+</form>
Index: /branches/devel-composer/installer/index.php
===================================================================
--- /branches/devel-composer/installer/index.php	(revision 5386)
+++ /branches/devel-composer/installer/index.php	(revision 5386)
@@ -0,0 +1,154 @@
+<?php
+
+/*
+ +-------------------------------------------------------------------------+
+ | Roundcube Webmail setup tool                                            |
+ | Version 0.6                                                             |
+ |                                                                         |
+ | Copyright (C) 2009-2011, The Roundcube Dev Team                         |
+ |                                                                         |
+ | This program is free software; you can redistribute it and/or modify    |
+ | it under the terms of the GNU General Public License version 2          |
+ | as published by the Free Software Foundation.                           |
+ |                                                                         |
+ | This program is distributed in the hope that it will be useful,         |
+ | but WITHOUT ANY WARRANTY; without even the implied warranty of          |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           |
+ | GNU General Public License for more details.                            |
+ |                                                                         |
+ | You should have received a copy of the GNU General Public License along |
+ | with this program; if not, write to the Free Software Foundation, Inc., |
+ | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.             |
+ |                                                                         |
+ +-------------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                          |
+ +-------------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+ini_set('error_reporting', E_ALL&~E_NOTICE);
+ini_set('display_errors', 1);
+
+define('INSTALL_PATH', realpath(dirname(__FILE__) . '/../').'/');
+define('RCMAIL_CONFIG_DIR', INSTALL_PATH . 'config');
+
+$include_path  = INSTALL_PATH . 'program/lib' . PATH_SEPARATOR;
+$include_path .= INSTALL_PATH . 'program' . PATH_SEPARATOR;
+$include_path .= INSTALL_PATH . 'program/include' . PATH_SEPARATOR;
+$include_path .= ini_get('include_path');
+
+set_include_path($include_path);
+
+require_once 'utils.php';
+require_once 'main.inc';
+
+session_start();
+
+$RCI = rcube_install::get_instance();
+$RCI->load_config();
+
+if (isset($_GET['_getfile']) && in_array($_GET['_getfile'], array('main', 'db'))) {
+  $filename = $_GET['_getfile'] . '.inc.php';
+  if (!empty($_SESSION[$filename])) {
+    header('Content-type: text/plain');
+    header('Content-Disposition: attachment; filename="'.$filename.'"');
+    echo $_SESSION[$filename];
+    exit;
+  }
+  else {
+    header('HTTP/1.0 404 Not found');
+    die("The requested configuration was not found. Please run the installer from the beginning.");
+  }
+}
+
+if ($RCI->configured && ($RCI->getprop('enable_installer') || $_SESSION['allowinstaller']) &&
+    isset($_GET['_mergeconfig']) && in_array($_GET['_mergeconfig'], array('main', 'db'))) {
+  $filename = $_GET['_mergeconfig'] . '.inc.php';
+
+  header('Content-type: text/plain');
+  header('Content-Disposition: attachment; filename="'.$filename.'"');
+
+  $RCI->merge_config();
+  echo $RCI->create_config($_GET['_mergeconfig'], true);
+  exit;
+}
+
+// go to 'check env' step if we have a local configuration
+if ($RCI->configured && empty($_REQUEST['_step'])) {
+  header("Location: ./?_step=1");
+  exit;
+}
+
+?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<title>Roundcube Webmail Installer</title>
+<meta name="Robots" content="noindex,nofollow" />
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<link rel="stylesheet" type="text/css" href="styles.css" />
+<script type="text/javascript" src="client.js"></script>
+</head>
+
+<body>
+
+<div id="banner">
+  <div class="banner-bg"></div>
+  <div class="banner-logo"><a href="http://roundcube.net"><img src="images/rcube_logo.gif" width="210" height="55" border="0" alt="Roundcube - Open source webmail project" /></a></div>
+</div>
+
+<div id="topnav">
+  <a href="http://trac.roundcube.net/wiki/Howto_Install">How-to Wiki</a>
+</div>
+
+<div id="content">
+
+<?php
+
+  // exit if installation is complete
+  if ($RCI->configured && !$RCI->getprop('enable_installer') && !$_SESSION['allowinstaller']) {
+    // header("HTTP/1.0 404 Not Found");
+    echo '<h2 class="error">The installer is disabled!</h2>';
+    echo '<p>To enable it again, set <tt>$rcmail_config[\'enable_installer\'] = true;</tt> in RCMAIL_CONFIG_DIR/main.inc.php</p>';
+    echo '</div></body></html>';
+    exit;
+  }
+  
+?>
+
+<h1>Roundcube Webmail Installer</h1>
+
+<ol id="progress">
+<?php
+  
+  foreach (array('Check environment', 'Create config', 'Test config') as $i => $item) {
+    $j = $i + 1;
+    $link = ($RCI->step >= $j || $RCI->configured) ? '<a href="./index.php?_step='.$j.'">' . Q($item) . '</a>' : Q($item);
+    printf('<li class="step%d%s">%s</li>', $j+1, $RCI->step > $j ? ' passed' : ($RCI->step == $j ? ' current' : ''), $link);
+  }
+?>
+</ol>
+
+<?php
+$include_steps = array('./welcome.html', './check.php', './config.php', './test.php');
+
+if ($include_steps[$RCI->step]) {
+  include $include_steps[$RCI->step];
+}
+else {
+  header("HTTP/1.0 404 Not Found");
+  echo '<h2 class="error">Invalid step</h2>';
+}
+
+?>
+</div>
+
+<div id="footer">
+  Installer by the Roundcube Dev Team. Copyright &copy; 2008-2011 - Published under the GNU Public License;&nbsp;
+  Icons by <a href="http://famfamfam.com">famfamfam</a>
+</div>
+</body>
+</html>
Index: /branches/devel-composer/installer/rcube_install.php
===================================================================
--- /branches/devel-composer/installer/rcube_install.php	(revision 5386)
+++ /branches/devel-composer/installer/rcube_install.php	(revision 5386)
@@ -0,0 +1,768 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | rcube_install.php                                                     |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail package                    |
+ | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
+ | Licensed under the GNU Public License                                 |
+ +-----------------------------------------------------------------------+
+
+ $Id:  $
+
+*/
+
+
+/**
+ * Class to control the installation process of the Roundcube Webmail package
+ *
+ * @category Install
+ * @package  Roundcube
+ * @author Thomas Bruederli
+ */
+class rcube_install
+{
+  var $step;
+  var $is_post = false;
+  var $failures = 0;
+  var $config = array();
+  var $configured = false;
+  var $last_error = null;
+  var $db_map = array('pgsql' => 'postgres', 'mysqli' => 'mysql', 'sqlsrv' => 'mssql');
+  var $email_pattern = '([a-z0-9][a-z0-9\-\.\+\_]*@[a-z0-9]([a-z0-9\-][.]?)*[a-z0-9])';
+  var $bool_config_props = array();
+
+  var $obsolete_config = array('db_backend', 'double_auth');
+  var $replaced_config = array(
+    'skin_path' => 'skin',
+    'locale_string' => 'language',
+    'multiple_identities' => 'identities_level',
+    'addrbook_show_images' => 'show_images',
+    'imap_root' => 'imap_ns_personal',
+  );
+  
+  // these config options are required for a working system
+  var $required_config = array(
+    'db_dsnw', 'db_table_contactgroups', 'db_table_contactgroupmembers',
+    'des_key', 'session_lifetime',
+  );
+  
+  /**
+   * Constructor
+   */
+  function rcube_install()
+  {
+    $this->step = intval($_REQUEST['_step']);
+    $this->is_post = $_SERVER['REQUEST_METHOD'] == 'POST';
+  }
+  
+  /**
+   * Singleton getter
+   */
+  function get_instance()
+  {
+    static $inst;
+    
+    if (!$inst)
+      $inst = new rcube_install();
+    
+    return $inst;
+  }
+  
+  /**
+   * Read the default config files and store properties
+   */
+  function load_defaults()
+  {
+    $this->_load_config('.php.dist');
+  }
+
+
+  /**
+   * Read the local config files and store properties
+   */
+  function load_config()
+  {
+    $this->config = array();
+    $this->_load_config('.php');
+    $this->configured = !empty($this->config);
+  }
+
+  /**
+   * Read the default config file and store properties
+   * @access private
+   */
+  function _load_config($suffix)
+  {
+    if (is_readable($main_inc = RCMAIL_CONFIG_DIR . '/main.inc' . $suffix)) {
+      include($main_inc);
+      if (is_array($rcmail_config))
+        $this->config += $rcmail_config;
+    }
+    if (is_readable($db_inc = RCMAIL_CONFIG_DIR . '/db.inc'. $suffix)) {
+      include($db_inc);
+      if (is_array($rcmail_config))
+        $this->config += $rcmail_config;
+    }
+  }
+  
+  
+  /**
+   * Getter for a certain config property
+   *
+   * @param string Property name
+   * @param string Default value
+   * @return string The property value
+   */
+  function getprop($name, $default = '')
+  {
+    $value = $this->config[$name];
+    
+    if ($name == 'des_key' && !$this->configured && !isset($_REQUEST["_$name"]))
+      $value = rcube_install::random_key(24);
+    
+    return $value !== null && $value !== '' ? $value : $default;
+  }
+
+
+  /**
+   * Take the default config file and replace the parameters
+   * with the submitted form data
+   *
+   * @param string Which config file (either 'main' or 'db')
+   * @return string The complete config file content
+   */
+  function create_config($which, $force = false)
+  {
+    $out = @file_get_contents(RCMAIL_CONFIG_DIR . "/{$which}.inc.php.dist");
+
+    if (!$out)
+      return '[Warning: could not read the config template file]';
+
+    foreach ($this->config as $prop => $default) {
+
+      $value = (isset($_POST["_$prop"]) || $this->bool_config_props[$prop]) ? $_POST["_$prop"] : $default;
+
+      // convert some form data
+      if ($prop == 'debug_level') {
+        $val = 0;
+        if (is_array($value))
+          foreach ($value as $dbgval)
+            $val += intval($dbgval);
+        $value = $val;
+      }
+      else if ($which == 'db' && $prop == 'db_dsnw' && !empty($_POST['_dbtype'])) {
+        if ($_POST['_dbtype'] == 'sqlite')
+          $value = sprintf('%s://%s?mode=0646', $_POST['_dbtype'], $_POST['_dbname']{0} == '/' ? '/' . $_POST['_dbname'] : $_POST['_dbname']);
+        else
+          $value = sprintf('%s://%s:%s@%s/%s', $_POST['_dbtype'], 
+            rawurlencode($_POST['_dbuser']), rawurlencode($_POST['_dbpass']), $_POST['_dbhost'], $_POST['_dbname']);
+      }
+      else if ($prop == 'smtp_auth_type' && $value == '0') {
+        $value = '';
+      }
+      else if ($prop == 'default_host' && is_array($value)) {
+        $value = rcube_install::_clean_array($value);
+        if (count($value) <= 1)
+          $value = $value[0];
+      }
+      else if ($prop == 'pagesize') {
+        $value = max(2, intval($value));
+      }
+      else if ($prop == 'smtp_user' && !empty($_POST['_smtp_user_u'])) {
+        $value = '%u';
+      }
+      else if ($prop == 'smtp_pass' && !empty($_POST['_smtp_user_u'])) {
+        $value = '%p';
+      }
+      else if ($prop == 'default_imap_folders') {
+	    $value = Array();
+	    foreach ($this->config['default_imap_folders'] as $_folder) {
+	      switch($_folder) {
+	      case 'Drafts': $_folder = $this->config['drafts_mbox']; break;
+	      case 'Sent':   $_folder = $this->config['sent_mbox']; break;
+	      case 'Junk':   $_folder = $this->config['junk_mbox']; break;
+	      case 'Trash':  $_folder = $this->config['trash_mbox']; break;
+          }
+	    if (!in_array($_folder, $value))
+	      $value[] = $_folder;
+        }
+      }
+      else if (is_bool($default)) {
+        $value = (bool)$value;
+      }
+      else if (is_numeric($value)) {
+        $value = intval($value);
+      }
+
+      // skip this property
+      if (!$force && !$this->configured && ($value == $default))
+        continue;
+
+      // save change
+      $this->config[$prop] = $value;
+
+      // replace the matching line in config file
+      $out = preg_replace(
+        '/(\$rcmail_config\[\''.preg_quote($prop).'\'\])\s+=\s+(.+);/Uie',
+        "'\\1 = ' . rcube_install::_dump_var(\$value) . ';'",
+        $out);
+    }
+
+    return trim($out);
+  }
+
+
+  /**
+   * Check the current configuration for missing properties
+   * and deprecated or obsolete settings
+   *
+   * @return array List with problems detected
+   */
+  function check_config()
+  {
+    $this->config = array();
+    $this->load_defaults();
+    $defaults = $this->config;
+    
+    $this->load_config();
+    if (!$this->configured)
+      return null;
+    
+    $out = $seen = array();
+    $required = array_flip($this->required_config);
+    
+    // iterate over the current configuration
+    foreach ($this->config as $prop => $value) {
+      if ($replacement = $this->replaced_config[$prop]) {
+        $out['replaced'][] = array('prop' => $prop, 'replacement' => $replacement);
+        $seen[$replacement] = true;
+      }
+      else if (!$seen[$prop] && in_array($prop, $this->obsolete_config)) {
+        $out['obsolete'][] = array('prop' => $prop);
+        $seen[$prop] = true;
+      }
+    }
+    
+    // iterate over default config
+    foreach ($defaults as $prop => $value) {
+      if (!isset($seen[$prop]) && !isset($this->config[$prop]) && isset($required[$prop]))
+        $out['missing'][] = array('prop' => $prop);
+    }
+
+    // check config dependencies and contradictions
+    if ($this->config['enable_spellcheck'] && $this->config['spellcheck_engine'] == 'pspell') {
+      if (!extension_loaded('pspell')) {
+        $out['dependencies'][] = array('prop' => 'spellcheck_engine',
+          'explain' => 'This requires the <tt>pspell</tt> extension which could not be loaded.');
+      }
+      else if (!empty($this->config['spellcheck_languages'])) {
+        foreach ($this->config['spellcheck_languages'] as $lang => $descr)
+          if (!pspell_new($lang))
+            $out['dependencies'][] = array('prop' => 'spellcheck_languages',
+              'explain' => "You are missing pspell support for language $lang ($descr)");
+      }
+    }
+    
+    if ($this->config['log_driver'] == 'syslog') {
+      if (!function_exists('openlog')) {
+        $out['dependencies'][] = array('prop' => 'log_driver',
+          'explain' => 'This requires the <tt>sylog</tt> extension which could not be loaded.');
+      }
+      if (empty($this->config['syslog_id'])) {
+        $out['dependencies'][] = array('prop' => 'syslog_id',
+          'explain' => 'Using <tt>syslog</tt> for logging requires a syslog ID to be configured');
+      }
+    }
+    
+    // check ldap_public sources having global_search enabled
+    if (is_array($this->config['ldap_public']) && !is_array($this->config['autocomplete_addressbooks'])) {
+      foreach ($this->config['ldap_public'] as $ldap_public) {
+        if ($ldap_public['global_search']) {
+          $out['replaced'][] = array('prop' => 'ldap_public::global_search', 'replacement' => 'autocomplete_addressbooks');
+          break;
+        }
+      }
+    }
+    
+    return $out;
+  }
+  
+  
+  /**
+   * Merge the current configuration with the defaults
+   * and copy replaced values to the new options.
+   */
+  function merge_config()
+  {
+    $current = $this->config;
+    $this->config = array();
+    $this->load_defaults();
+    
+    foreach ($this->replaced_config as $prop => $replacement) {
+      if (isset($current[$prop])) {
+        if ($prop == 'skin_path')
+          $this->config[$replacement] = preg_replace('#skins/(\w+)/?$#', '\\1', $current[$prop]);
+        else if ($prop == 'multiple_identities')
+          $this->config[$replacement] = $current[$prop] ? 2 : 0;
+        else
+          $this->config[$replacement] = $current[$prop];
+      }
+      unset($current[$prop]);
+    }
+    
+    foreach ($this->obsolete_config as $prop) {
+      unset($current[$prop]);
+    }
+    
+    // add all ldap_public sources having global_search enabled to autocomplete_addressbooks
+    if (is_array($current['ldap_public'])) {
+      foreach ($current['ldap_public'] as $key => $ldap_public) {
+        if ($ldap_public['global_search']) {
+          $this->config['autocomplete_addressbooks'][] = $key;
+          unset($current['ldap_public'][$key]['global_search']);
+        }
+      }
+    }
+    
+    if ($current['keep_alive'] && $current['session_lifetime'] < $current['keep_alive'])
+      $current['session_lifetime'] = max(10, ceil($current['keep_alive'] / 60) * 2);
+    
+    $this->config  = array_merge($this->config, $current);
+    
+    foreach ((array)$current['ldap_public'] as $key => $values) {
+      $this->config['ldap_public'][$key] = $current['ldap_public'][$key];
+    }
+  }
+  
+  /**
+   * Compare the local database schema with the reference schema
+   * required for this version of Roundcube
+   *
+   * @param boolean True if the schema schould be updated
+   * @return boolean True if the schema is up-to-date, false if not or an error occured
+   */
+  function db_schema_check($DB, $update = false)
+  {
+    if (!$this->configured)
+      return false;
+    
+    // read reference schema from mysql.initial.sql
+    $db_schema = $this->db_read_schema(INSTALL_PATH . 'SQL/mysql.initial.sql');
+    $errors = array();
+    
+    // check list of tables
+    $existing_tables = $DB->list_tables();
+
+    foreach ($db_schema as $table => $cols) {
+      $table = !empty($this->config['db_table_'.$table]) ? $this->config['db_table_'.$table] : $table;
+      if (!in_array($table, $existing_tables)) {
+        $errors[] = "Missing table '".$table."'";
+      }
+      else {  // compare cols
+        $db_cols = $DB->list_cols($table);
+        $diff = array_diff(array_keys($cols), $db_cols);
+        if (!empty($diff))
+          $errors[] = "Missing columns in table '$table': " . join(',', $diff);
+      }
+    }
+    
+    return !empty($errors) ? $errors : false;
+  }
+
+  /**
+   * Utility function to read database schema from an .sql file
+   */
+  private function db_read_schema($schemafile)
+  {
+    $lines = file($schemafile);
+    $table_block = false;
+    $schema = array();
+    foreach ($lines as $line) {
+      if (preg_match('/^\s*create table `?([a-z0-9_]+)`?/i', $line, $m)) {
+        $table_block = $m[1];
+      }
+      else if ($table_block && preg_match('/^\s*`?([a-z0-9_-]+)`?\s+([a-z]+)/', $line, $m)) {
+        $col = $m[1];
+        if (!in_array(strtoupper($col), array('PRIMARY','KEY','INDEX','UNIQUE','CONSTRAINT','REFERENCES','FOREIGN'))) {
+          $schema[$table_block][$col] = $m[2];
+        }
+      }
+    }
+    
+    return $schema;
+  }
+  
+  
+  /**
+   * Compare the local database schema with the reference schema
+   * required for this version of Roundcube
+   *
+   * @param boolean True if the schema schould be updated
+   * @return boolean True if the schema is up-to-date, false if not or an error occured
+   */
+  function mdb2_schema_check($update = false)
+  {
+    if (!$this->configured)
+      return false;
+    
+    $options = array(
+      'use_transactions' => false,
+      'log_line_break' => "\n",
+      'idxname_format' => '%s',
+      'debug' => false,
+      'quote_identifier' => true,
+      'force_defaults' => false,
+      'portability' => true
+    );
+    
+    $dsnw = $this->config['db_dsnw'];
+    $schema = MDB2_Schema::factory($dsnw, $options);
+    $schema->db->supported['transactions'] = false;
+    
+    if (PEAR::isError($schema)) {
+      $this->raise_error(array('code' => $schema->getCode(), 'message' => $schema->getMessage() . ' ' . $schema->getUserInfo()));
+      return false;
+    }
+    else {
+      $definition = $schema->getDefinitionFromDatabase();
+      $definition['charset'] = 'utf8';
+      
+      if (PEAR::isError($definition)) {
+        $this->raise_error(array('code' => $definition->getCode(), 'message' => $definition->getMessage() . ' ' . $definition->getUserInfo()));
+        return false;
+      }
+      
+      // load reference schema
+      $dsn_arr = MDB2::parseDSN($this->config['db_dsnw']);
+
+      $ref_schema = INSTALL_PATH . 'SQL/' . $dsn_arr['phptype'] . '.schema.xml';
+      
+      if (is_readable($ref_schema)) {
+        $reference = $schema->parseDatabaseDefinition($ref_schema, false, array(), $schema->options['fail_on_invalid_names']);
+        
+        if (PEAR::isError($reference)) {
+          $this->raise_error(array('code' => $reference->getCode(), 'message' => $reference->getMessage() . ' ' . $reference->getUserInfo()));
+        }
+        else {
+          $diff = $schema->compareDefinitions($reference, $definition);
+          
+          if (empty($diff)) {
+            return true;
+          }
+          else if ($update) {
+            // update database schema with the diff from the above check
+            $success = $schema->alterDatabase($reference, $definition, $diff);
+            
+            if (PEAR::isError($success)) {
+              $this->raise_error(array('code' => $success->getCode(), 'message' => $success->getMessage() . ' ' . $success->getUserInfo()));
+            }
+            else
+              return true;
+          }
+          echo '<pre>'; var_dump($diff); echo '</pre>';
+          return false;
+        }
+      }
+      else
+        $this->raise_error(array('message' => "Could not find reference schema file ($ref_schema)"));
+        return false;
+    }
+    
+    return false;
+  }
+  
+  
+  /**
+   * Getter for the last error message
+   *
+   * @return string Error message or null if none exists
+   */
+  function get_error()
+  {
+      return $this->last_error['message'];
+  }
+  
+  
+  /**
+   * Return a list with all imap hosts configured
+   *
+   * @return array Clean list with imap hosts
+   */
+  function get_hostlist()
+  {
+    $default_hosts = (array)$this->getprop('default_host');
+    $out = array();
+    
+    foreach ($default_hosts as $key => $name) {
+      if (!empty($name))
+        $out[] = rcube_parse_host(is_numeric($key) ? $name : $key);
+    }
+    
+    return $out;
+  }
+  
+  /**
+   * Create a HTML dropdown to select a previous version of Roundcube
+   */
+  function versions_select($attrib = array())
+  {
+    $select = new html_select($attrib);
+    $select->add(array('0.1-stable', '0.1.1', '0.2-alpha', '0.2-beta', '0.2-stable', '0.3-stable', '0.3.1', '0.4-beta', '0.4.2', '0.5-beta', '0.5', '0.5.1'));
+    return $select;
+  }
+  
+  /**
+   * Return a list with available subfolders of the skin directory
+   */
+  function list_skins()
+  {
+    $skins = array();
+    $skindir = INSTALL_PATH . 'skins/';
+    foreach (glob($skindir . '*') as $path) {
+      if (is_dir($path) && is_readable($path)) {
+        $skins[] = substr($path, strlen($skindir));
+      }
+    }
+    return $skins;
+  }
+  
+  /**
+   * Display OK status
+   *
+   * @param string Test name
+   * @param string Confirm message
+   */
+  function pass($name, $message = '')
+  {
+    echo Q($name) . ':&nbsp; <span class="success">OK</span>';
+    $this->_showhint($message);
+  }
+  
+  
+  /**
+   * Display an error status and increase failure count
+   *
+   * @param string Test name
+   * @param string Error message
+   * @param string URL for details
+   */
+  function fail($name, $message = '', $url = '')
+  {
+    $this->failures++;
+    
+    echo Q($name) . ':&nbsp; <span class="fail">NOT OK</span>';
+    $this->_showhint($message, $url);
+  }
+
+
+  /**
+   * Display an error status for optional settings/features
+   *
+   * @param string Test name
+   * @param string Error message
+   * @param string URL for details
+   */
+  function optfail($name, $message = '', $url = '')
+  {
+    echo Q($name) . ':&nbsp; <span class="na">NOT OK</span>';
+    $this->_showhint($message, $url);
+  }
+  
+  
+  /**
+   * Display warning status
+   *
+   * @param string Test name
+   * @param string Warning message
+   * @param string URL for details
+   */
+  function na($name, $message = '', $url = '')
+  {
+    echo Q($name) . ':&nbsp; <span class="na">NOT AVAILABLE</span>';
+    $this->_showhint($message, $url);
+  }
+  
+  
+  function _showhint($message, $url = '')
+  {
+    $hint = Q($message);
+    
+    if ($url)
+      $hint .= ($hint ? '; ' : '') . 'See <a href="' . Q($url) . '" target="_blank">' . Q($url) . '</a>';
+      
+    if ($hint)
+      echo '<span class="indent">(' . $hint . ')</span>';
+  }
+  
+  
+  static function _clean_array($arr)
+  {
+    $out = array();
+    
+    foreach (array_unique($arr) as $k => $val) {
+      if (!empty($val)) {
+        if (is_numeric($k))
+          $out[] = $val;
+        else
+          $out[$k] = $val;
+      }
+    }
+    
+    return $out;
+  }
+  
+  
+  static function _dump_var($var) {
+    if (is_array($var)) {
+      if (empty($var)) {
+        return 'array()';
+      }
+      else {  // check if all keys are numeric
+        $isnum = true;
+        foreach ($var as $key => $value) {
+          if (!is_numeric($key)) {
+            $isnum = false;
+            break;
+          }
+        }
+        
+        if ($isnum)
+          return 'array(' . join(', ', array_map(array('rcube_install', '_dump_var'), $var)) . ')';
+      }
+    }
+    
+    return var_export($var, true);
+  }
+  
+  
+  /**
+   * Initialize the database with the according schema
+   *
+   * @param object rcube_db Database connection
+   * @return boolen True on success, False on error
+   */
+  function init_db($DB)
+  {
+    $engine = isset($this->db_map[$DB->db_provider]) ? $this->db_map[$DB->db_provider] : $DB->db_provider;
+    
+    // read schema file from /SQL/*
+    $fname = INSTALL_PATH . "SQL/$engine.initial.sql";
+    if ($sql = @file_get_contents($fname)) {
+      $this->exec_sql($sql, $DB);
+    }
+    else {
+      $this->fail('DB Schema', "Cannot read the schema file: $fname");
+      return false;
+    }
+    
+    if ($err = $this->get_error()) {
+      $this->fail('DB Schema', "Error creating database schema: $err");
+      return false;
+    }
+
+    return true;
+  }
+  
+  
+  /**
+   * Update database with SQL statements from SQL/*.update.sql
+   *
+   * @param object rcube_db Database connection
+   * @param string Version to update from
+   * @return boolen True on success, False on error
+   */
+  function update_db($DB, $version)
+  {
+    $version = strtolower($version);
+    $engine = isset($this->db_map[$DB->db_provider]) ? $this->db_map[$DB->db_provider] : $DB->db_provider;
+    
+    // read schema file from /SQL/*
+    $fname = INSTALL_PATH . "SQL/$engine.update.sql";
+    if ($lines = @file($fname, FILE_SKIP_EMPTY_LINES)) {
+      $from = false; $sql = '';
+      foreach ($lines as $line) {
+        $is_comment = preg_match('/^--/', $line);
+        if (!$from && $is_comment && preg_match('/from version\s([0-9.]+[a-z-]*)/', $line, $m)) {
+          $v = strtolower($m[1]);
+          if ($v == $version || version_compare($version, $v, '<='))
+            $from = true;
+        }
+        if ($from && !$is_comment)
+          $sql .= $line. "\n";
+      }
+      
+      if ($sql)
+        $this->exec_sql($sql, $DB);
+    }
+    else {
+      $this->fail('DB Schema', "Cannot read the update file: $fname");
+      return false;
+    }
+    
+    if ($err = $this->get_error()) {
+      $this->fail('DB Schema', "Error updating database: $err");
+      return false;
+    }
+
+    return true;
+  }
+  
+  
+  /**
+   * Execute the given SQL queries on the database connection
+   *
+   * @param string SQL queries to execute
+   * @param object rcube_db Database connection
+   * @return boolen True on success, False on error
+   */
+  function exec_sql($sql, $DB)
+  {
+    $buff = '';
+    foreach (explode("\n", $sql) as $line) {
+      if (preg_match('/^--/', $line) || trim($line) == '')
+        continue;
+        
+      $buff .= $line . "\n";
+      if (preg_match('/(;|^GO)$/', trim($line))) {
+        $DB->query($buff);
+        $buff = '';
+        if ($DB->is_error())
+          break;
+      }
+    }
+    
+    return !$DB->is_error();
+  }
+  
+  
+  /**
+   * Handler for Roundcube errors
+   */
+  function raise_error($p)
+  {
+      $this->last_error = $p;
+  }
+  
+  
+  /**
+   * Generarte a ramdom string to be used as encryption key
+   *
+   * @param int Key length
+   * @return string The generated random string
+   * @static
+   */
+  function random_key($length)
+  {
+    $alpha = 'ABCDEFGHIJKLMNOPQERSTUVXYZabcdefghijklmnopqrtsuvwxyz0123456789+*%&?!$-_=';
+    $out = '';
+    
+    for ($i=0; $i < $length; $i++)
+      $out .= $alpha{rand(0, strlen($alpha)-1)};
+    
+    return $out;
+  }
+  
+}
+
Index: /branches/devel-composer/installer/styles.css
===================================================================
--- /branches/devel-composer/installer/styles.css	(revision 5386)
+++ /branches/devel-composer/installer/styles.css	(revision 5386)
@@ -0,0 +1,235 @@
+body {
+	background: white;
+	font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
+	font-size: small;
+	color: black;
+	margin: 0;
+}
+
+#banner {
+	position: relative;
+	height: 58px;
+	margin: 0 0 1em 0;
+	padding: 10px 20px;
+	background: url('images/banner_gradient.gif') top left repeat-x #d8edfd;
+	overflow: hidden;
+}
+
+#banner .banner-bg {
+	position: absolute;
+	top: 0;
+	right: 0;
+	width: 630px;
+	height: 78px;
+	background: url('images/banner_schraffur.gif') top right no-repeat;
+	z-index: 0;
+}
+
+#banner .banner-logo {
+	position: absolute;
+	top: 10px;
+	left: 20px;
+	z-index: 4;
+}
+
+#banner .banner-logo a {
+	border: 0;
+}
+
+#topnav {
+	position: absolute;
+	top: 3.6em;
+	right: 20px;
+}
+
+#topnav a {
+	color: #666;
+}
+
+#content {
+	margin: 2em 20px;
+}
+
+#footer {
+  margin: 2em 20px 1em 20px;
+  padding-top: 0.6em;
+  font-size: smaller;
+  text-align: center;
+  border-top: 1px dotted #999;
+}
+
+#progress {
+  margin-bottom: 2em;
+  border: 1px solid #aaa;
+  background-color: #f9f9f9;
+}
+
+#progress:after {
+  content: ".";
+  display: block;
+  height: 0;
+  font-size: 0;
+  clear: both;
+  visibility: hidden;
+}
+
+#progress li {
+  float: left;
+  color: #999;
+  padding: 1em 5em 1em 0.2em;
+}
+
+#progress li a {
+  color: #999;
+  text-decoration: none;
+}
+
+#progress li a:hover {
+  text-decoration: underline;
+}
+
+#progress li.current {
+  color: #000;
+  font-weight: bold;
+}
+
+#progress li.passed,
+#progress li.passed a,
+#progress li.current a {
+  color: #333;
+}
+
+fieldset {
+  margin-bottom: 1.5em;
+  border: 1px solid #aaa;
+  background-color: #f9f9f9;
+}
+
+fieldset p.hint {
+  margin-top: 0.5em;
+}
+
+legend {
+  font-size: 1.1em;
+  font-weight: bold;
+}
+
+textarea.configfile {
+  background-color: #f9f9f9;
+  font-family: monospace;
+  font-size: 9pt;
+  width: 100%;
+  height: 30em;
+}
+
+.propname {
+  font-family: monospace;
+  font-size: 9pt;
+  margin-top: 1em;
+  margin-bottom: 0.6em;
+}
+
+dd div {
+  margin-top: 0.3em;
+}
+
+dd label {
+  padding-left: 0.5em;
+}
+
+th {
+  text-align: left;
+}
+
+ul li {
+  margin: 0.3em 0 0.4em -1em;
+}
+
+ul li ul li {
+  margin-bottom: 0.2em;
+}
+
+h3 {
+  font-size: 1.1em;
+  margin-top: 1.5em;
+  margin-bottom: 0.6em;
+}
+
+h4 {
+  margin-bottom: 0.2em;
+}
+
+a.blocktoggle {
+  color: #666;
+  text-decoration: none;
+}
+
+a.addlink {
+  color: #999;
+  font-size: 0.9em;
+  padding: 1px 0 1px 20px;
+  background: url('images/add.png') top left no-repeat;
+  text-decoration: none;
+}
+
+a.removelink {
+  color: #999;
+  font-size: 0.9em;
+  padding: 1px 0 1px 24px;
+  background: url('images/delete.png') 4px 0 no-repeat;
+  text-decoration: none;
+}
+
+.hint {
+  color: #666;
+  font-size: 0.95em;
+}
+
+.success {
+  color: #006400;
+  font-weight: bold !important;
+}
+
+.fail {
+  color: #ff0000 !important;
+  font-weight: bold !important;
+}
+
+.na {
+  color: #f60;
+  font-weight: bold;
+}
+
+.indent {
+  padding-left: 0.8em;
+}
+
+.notice {
+  padding: 1em;
+  background-color: #f7fdcb;
+  border: 2px solid #c2d071;
+}
+
+.suggestion {
+  padding: 0.6em;
+  background-color: #ebebeb;
+  border: 1px solid #999;
+}
+
+p.warning,
+div.warning {
+  padding: 1em;
+  background-color: #ef9398;
+  border: 2px solid #dc5757;
+}
+
+h3.warning {
+  color: #c00;
+  background: url('images/error.png') top left no-repeat;
+  padding-left: 24px;
+}
+
+.userconf {
+  color: #00c;
+  font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
+}
Index: /branches/devel-composer/installer/test.php
===================================================================
--- /branches/devel-composer/installer/test.php	(revision 5386)
+++ /branches/devel-composer/installer/test.php	(revision 5386)
@@ -0,0 +1,432 @@
+<form action="index.php?_step=3" method="post">
+
+<h3>Check config files</h3>
+<?php
+
+$read_main = is_readable(RCMAIL_CONFIG_DIR.'/main.inc.php');
+$read_db = is_readable(RCMAIL_CONFIG_DIR.'/db.inc.php');
+
+if ($read_main && !empty($RCI->config)) {
+  $RCI->pass('main.inc.php');
+}
+else if ($read_main) {
+  $RCI->fail('main.inc.php', 'Syntax error');
+}
+else if (!$read_main) {
+  $RCI->fail('main.inc.php', 'Unable to read file. Did you create the config files?');
+}
+echo '<br />';
+
+if ($read_db && !empty($RCI->config['db_table_users'])) {
+  $RCI->pass('db.inc.php');
+}
+else if ($read_db) {
+  $RCI->fail('db.inc.php', 'Syntax error');
+}
+else if (!$read_db) {
+  $RCI->fail('db.inc.php', 'Unable to read file. Did you create the config files?');
+}
+
+if ($RCI->configured && ($messages = $RCI->check_config())) {
+  
+  if (is_array($messages['missing'])) {
+    echo '<h3 class="warning">Missing config options</h3>';
+    echo '<p class="hint">The following config options are not present in the current configuration.<br/>';
+    echo 'Please check the default config files and add the missing properties to your local config files.</p>';
+    
+    echo '<ul class="configwarings">';
+    foreach ($messages['missing'] as $msg) {
+      echo html::tag('li', null, html::span('propname', $msg['prop']) . ($msg['name'] ? ':&nbsp;' . $msg['name'] : ''));
+    }    
+    echo '</ul>';
+  }
+
+  if (is_array($messages['replaced'])) {
+    echo '<h3 class="warning">Replaced config options</h3>';
+    echo '<p class="hint">The following config options have been replaced or renamed. ';
+    echo 'Please update them accordingly in your config files.</p>';
+    
+    echo '<ul class="configwarings">';
+    foreach ($messages['replaced'] as $msg) {
+      echo html::tag('li', null, html::span('propname', $msg['prop']) .
+        ' was replaced by ' . html::span('propname', $msg['replacement']));
+    }
+    echo '</ul>';
+  }
+
+  if (is_array($messages['obsolete'])) {
+    echo '<h3>Obsolete config options</h3>';
+    echo '<p class="hint">You still have some obsolete or inexistent properties set. This isn\'t a problem but should be noticed.</p>';
+    
+    echo '<ul class="configwarings">';
+    foreach ($messages['obsolete'] as $msg) {
+      echo html::tag('li', null, html::span('propname', $msg['prop']) . ($msg['name'] ? ':&nbsp;' . $msg['name'] : ''));
+    }
+    echo '</ul>';
+  }
+  
+  echo '<p class="suggestion">OK, lazy people can download the updated config files here: ';
+  echo html::a(array('href' => './?_mergeconfig=main'), 'main.inc.php') . ' &nbsp;';
+  echo html::a(array('href' => './?_mergeconfig=db'), 'db.inc.php');
+  echo "</p>";
+  
+  
+  if (is_array($messages['dependencies'])) {
+    echo '<h3 class="warning">Dependency check failed</h3>';
+    echo '<p class="hint">Some of your configuration settings require other options to be configured or additional PHP modules to be installed</p>';
+    
+    echo '<ul class="configwarings">';
+    foreach ($messages['dependencies'] as $msg) {
+      echo html::tag('li', null, html::span('propname', $msg['prop']) . ': ' . $msg['explain']);
+    }
+    echo '</ul>';
+  }
+
+  
+}
+
+?>
+
+<h3>Check if directories are writable</h3>
+<p>Roundcube may need to write/save files into these directories</p>
+<?php
+
+if ($RCI->configured) {
+    $pass = false;
+
+    $dirs[] = $RCI->config['temp_dir'] ? $RCI->config['temp_dir'] : 'temp';
+    if($RCI->config['log_driver'] != 'syslog')
+      $dirs[] = $RCI->config['log_dir'] ? $RCI->config['log_dir'] : 'logs';
+
+    foreach ($dirs as $dir) {
+        $dirpath = $dir[0] == '/' ? $dir : INSTALL_PATH . $dir;
+        if (is_writable(realpath($dirpath))) {
+            $RCI->pass($dir);
+            $pass = true;
+        }
+        else {
+            $RCI->fail($dir, 'not writeable for the webserver');
+        }
+        echo '<br />';
+    }
+    
+    if (!$pass)
+        echo '<p class="hint">Use <tt>chmod</tt> or <tt>chown</tt> to grant write privileges to the webserver</p>';
+}
+else {
+    $RCI->fail('Config', 'Could not read config files');
+}
+
+?>
+
+<h3>Check DB config</h3>
+<?php
+
+$db_working = false;
+if ($RCI->configured) {
+    if (!empty($RCI->config['db_dsnw'])) {
+
+        $DB = new rcube_mdb2($RCI->config['db_dsnw'], '', false);
+        $DB->db_connect('w');
+        if (!($db_error_msg = $DB->is_error())) {
+            $RCI->pass('DSN (write)');
+            echo '<br />';
+            $db_working = true;
+        }
+        else {
+            $RCI->fail('DSN (write)', $db_error_msg);
+            echo '<p class="hint">Make sure that the configured database exists and that the user has write privileges<br />';
+            echo 'DSN: ' . $RCI->config['db_dsnw'] . '</p>';
+        }
+    }
+    else {
+        $RCI->fail('DSN (write)', 'not set');
+    }
+}
+else {
+    $RCI->fail('Config', 'Could not read config files');
+}
+
+// initialize db with schema found in /SQL/*
+if ($db_working && $_POST['initdb']) {
+    if (!($success = $RCI->init_db($DB))) {
+        $db_working = false;
+        echo '<p class="warning">Please try to inizialize the database manually as described in the INSTALL guide.
+          Make sure that the configured database extists and that the user as write privileges</p>';
+    }
+}
+
+else if ($db_working && $_POST['updatedb']) {
+  if (!($success = $RCI->update_db($DB, $_POST['version']))) {
+      $updatefile = INSTALL_PATH . 'SQL/' . (isset($RCI->db_map[$DB->db_provider]) ? $RCI->db_map[$DB->db_provider] : $DB->db_provider) . '.update.sql';
+      echo '<p class="warning">Please manually execute the SQL statements from '.$updatefile.' on your database.<br/>';
+      echo 'See comments in the file and execute queries below the comment with the currently installed version number.</p>';
+  }
+}
+
+// test database
+if ($db_working) {
+    $db_read = $DB->query("SELECT count(*) FROM {$RCI->config['db_table_users']}");
+    if ($DB->db_error) {
+        $RCI->fail('DB Schema', "Database not initialized");
+        echo '<p><input type="submit" name="initdb" value="Initialize database" /></p>';
+        $db_working = false;
+    }
+    else if ($err = $RCI->db_schema_check($DB, $update = !empty($_POST['updatedb']))) {
+        $RCI->fail('DB Schema', "Database schema differs");
+        echo '<ul style="margin:0"><li>' . join("</li>\n<li>", $err) . "</li></ul>";
+        $select = $RCI->versions_select(array('name' => 'version'));
+        echo '<p class="suggestion">You should run the update queries to get the schmea fixed.<br/><br/>Version to update from: ' . $select->show() . '&nbsp;<input type="submit" name="updatedb" value="Update" /></p>';
+//        echo '<p class="warning">Please manually execute the SQL statements from '.$updatefile.' on your database.<br/>';
+//        echo 'See comments in the file and execute queries that are superscribed with the currently installed version number.</p>';
+        $db_working = false;
+    }
+    else {
+        $RCI->pass('DB Schema');
+        echo '<br />';
+    }
+}
+
+// more database tests
+if ($db_working) {
+    // write test
+    $insert_id = md5(uniqid());
+    $db_write = $DB->query("INSERT INTO {$RCI->config['db_table_session']} (sess_id, created, ip, vars) VALUES (?, ".$DB->now().", '127.0.0.1', 'foo')", $insert_id);
+
+    if ($db_write) {
+      $RCI->pass('DB Write');
+      $DB->query("DELETE FROM {$RCI->config['db_table_session']} WHERE sess_id=?", $insert_id);
+    }
+    else {
+      $RCI->fail('DB Write', $RCI->get_error());
+    }
+    echo '<br />';
+    
+    // check timezone settings
+    $tz_db = 'SELECT ' . $DB->unixtimestamp($DB->now()) . ' AS tz_db';
+    $tz_db = $DB->query($tz_db);
+    $tz_db = $DB->fetch_assoc($tz_db);
+    $tz_db = (int) $tz_db['tz_db'];
+    $tz_local = (int) time();
+    $tz_diff  = $tz_local - $tz_db;
+
+    // sometimes db and web servers are on separate hosts, so allow a 30 minutes delta
+    if (abs($tz_diff) > 1800) {
+        $RCI->fail('DB Time', "Database time differs {$td_ziff}s from PHP time");
+    }
+    else {
+        $RCI->pass('DB Time');
+    }
+}
+
+?>
+
+<h3>Test SMTP config</h3>
+
+<p>
+Server: <?php echo rcube_parse_host($RCI->getprop('smtp_server', 'PHP mail()')); ?><br />
+Port: <?php echo $RCI->getprop('smtp_port'); ?><br />
+
+<?php
+
+if ($RCI->getprop('smtp_server')) {
+  $user = $RCI->getprop('smtp_user', '(none)');
+  $pass = $RCI->getprop('smtp_pass', '(none)');
+  
+  if ($user == '%u') {
+    $user_field = new html_inputfield(array('name' => '_smtp_user'));
+    $user = $user_field->show($_POST['_smtp_user']);
+  }
+  if ($pass == '%p') {
+    $pass_field = new html_passwordfield(array('name' => '_smtp_pass'));
+    $pass = $pass_field->show();
+  }
+  
+  echo "User: $user<br />";
+  echo "Password: $pass<br />";
+}
+
+$from_field = new html_inputfield(array('name' => '_from', 'id' => 'sendmailfrom'));
+$to_field = new html_inputfield(array('name' => '_to', 'id' => 'sendmailto'));
+
+?>
+</p>
+
+<?php
+
+if (isset($_POST['sendmail'])) {
+
+  echo '<p>Trying to send email...<br />';
+
+  $from = idn_to_ascii(trim($_POST['_from']));
+  $to   = idn_to_ascii(trim($_POST['_to']));
+
+  if (preg_match('/^' . $RCI->email_pattern . '$/i', $from) &&
+      preg_match('/^' . $RCI->email_pattern . '$/i', $to)
+  ) {
+    $headers = array(
+      'From'    => $from,
+      'To'      => $to,
+      'Subject' => 'Test message from Roundcube',
+    );
+
+    $body = 'This is a test to confirm that Roundcube can send email.';
+    $smtp_response = array();
+
+    // send mail using configured SMTP server
+    if ($RCI->getprop('smtp_server')) {
+      $CONFIG = $RCI->config;
+
+      if (!empty($_POST['_smtp_user'])) {
+        $CONFIG['smtp_user'] = $_POST['_smtp_user'];
+      }
+      if (!empty($_POST['_smtp_pass'])) {
+        $CONFIG['smtp_pass'] = $_POST['_smtp_pass'];
+      }
+
+      $mail_object  = new Mail_mime();
+      $send_headers = $mail_object->headers($headers);
+
+      $SMTP = new rcube_smtp();
+      $SMTP->connect(rcube_parse_host($RCI->getprop('smtp_server')),
+        $RCI->getprop('smtp_port'), $CONFIG['smtp_user'], $CONFIG['smtp_pass']);
+
+      $status = $SMTP->send_mail($headers['From'], $headers['To'],
+          ($foo = $mail_object->txtHeaders($send_headers)), $body);
+
+      $smtp_response = $SMTP->get_response();
+    }
+    else {    // use mail()
+      $header_str = 'From: ' . $headers['From'];
+      
+      if (ini_get('safe_mode'))
+        $status = mail($headers['To'], $headers['Subject'], $body, $header_str);
+      else
+        $status = mail($headers['To'], $headers['Subject'], $body, $header_str, '-f'.$headers['From']);
+      
+      if (!$status)
+        $smtp_response[] = 'Mail delivery with mail() failed. Check your error logs for details';
+    }
+
+    if ($status) {
+        $RCI->pass('SMTP send');
+    }
+    else {
+        $RCI->fail('SMTP send', join('; ', $smtp_response));
+    }
+  }
+  else {
+    $RCI->fail('SMTP send', 'Invalid sender or recipient');
+  }
+  
+  echo '</p>';
+}
+
+?>
+
+<table>
+<tbody>
+  <tr>
+    <td><label for="sendmailfrom">Sender</label></td>
+    <td><?php echo $from_field->show($_POST['_from']); ?></td>
+  </tr>
+  <tr>
+    <td><label for="sendmailto">Recipient</label></td>
+    <td><?php echo $to_field->show($_POST['_to']); ?></td>
+  </tr>
+</tbody>
+</table>
+
+<p><input type="submit" name="sendmail" value="Send test mail" /></p>
+
+
+<h3>Test IMAP config</h3>
+
+<?php
+
+$default_hosts = $RCI->get_hostlist();
+if (!empty($default_hosts)) {
+  $host_field = new html_select(array('name' => '_host', 'id' => 'imaphost'));
+  $host_field->add($default_hosts);
+}
+else {
+  $host_field = new html_inputfield(array('name' => '_host', 'id' => 'imaphost'));
+}
+
+$user_field = new html_inputfield(array('name' => '_user', 'id' => 'imapuser'));
+$pass_field = new html_passwordfield(array('name' => '_pass', 'id' => 'imappass'));
+
+?>
+
+<table>
+<tbody>
+  <tr>
+    <td><label for="imaphost">Server</label></td>
+    <td><?php echo $host_field->show($_POST['_host']); ?></td>
+  </tr>
+  <tr>
+    <td>Port</td>
+    <td><?php echo $RCI->getprop('default_port'); ?></td>
+  </tr>
+    <tr>
+      <td><label for="imapuser">Username</label></td>
+      <td><?php echo $user_field->show($_POST['_user']); ?></td>
+    </tr>
+    <tr>
+      <td><label for="imappass">Password</label></td>
+      <td><?php echo $pass_field->show(); ?></td>
+    </tr>
+</tbody>
+</table>
+
+<?php
+
+if (isset($_POST['imaptest']) && !empty($_POST['_host']) && !empty($_POST['_user'])) {
+
+  echo '<p>Connecting to ' . Q($_POST['_host']) . '...<br />';
+
+  $imap_host = trim($_POST['_host']);
+  $imap_port = $RCI->getprop('default_port');
+  $a_host    = parse_url($imap_host);
+
+  if ($a_host['host']) {
+    $imap_host = $a_host['host'];
+    $imap_ssl  = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
+    if (isset($a_host['port']))
+      $imap_port = $a_host['port'];
+    else if ($imap_ssl && $imap_ssl != 'tls' && (!$imap_port || $imap_port == 143))
+      $imap_port = 993;
+  }
+
+  $imap_host = idn_to_ascii($imap_host);
+  $imap_user = idn_to_ascii($_POST['_user']);
+
+  $imap = new rcube_imap(null);
+  if ($imap->connect($imap_host, $imap_user, $_POST['_pass'], $imap_port, $imap_ssl)) {
+    $RCI->pass('IMAP connect', 'SORT capability: ' . ($imap->get_capability('SORT') ? 'yes' : 'no'));
+    $imap->close();
+  }
+  else {
+    $RCI->fail('IMAP connect', $RCI->get_error());
+  }
+}
+
+?>
+
+<p><input type="submit" name="imaptest" value="Check login" /></p>
+
+</form>
+
+<hr />
+
+<p class="warning">
+
+After completing the installation and the final tests please <b>remove</b> the whole
+installer folder from the document root of the webserver or make sure that
+<tt>enable_installer</tt> option in config/main.inc.php is disabled.<br />
+<br />
+
+These files may expose sensitive configuration data like server passwords and encryption keys
+to the public. Make sure you cannot access this installer from your browser.
+
+</p>
Index: /branches/devel-composer/installer/utils.php
===================================================================
--- /branches/devel-composer/installer/utils.php	(revision 5386)
+++ /branches/devel-composer/installer/utils.php	(revision 5386)
@@ -0,0 +1,80 @@
+<?php
+/*
+ +-------------------------------------------------------------------------+
+ | Roundcube Webmail IMAP Client                                           |
+ | Version 0.6                                                             |
+ |                                                                         |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team                         |
+ |                                                                         |
+ | This program is free software; you can redistribute it and/or modify    |
+ | it under the terms of the GNU General Public License version 2          |
+ | as published by the Free Software Foundation.                           |
+ |                                                                         |
+ | This program is distributed in the hope that it will be useful,         |
+ | but WITHOUT ANY WARRANTY; without even the implied warranty of          |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           |
+ | GNU General Public License for more details.                            |
+ |                                                                         |
+ | You should have received a copy of the GNU General Public License along |
+ | with this program; if not, write to the Free Software Foundation, Inc., |
+ | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.             |
+ |                                                                         |
+ +-------------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                          |
+ +-------------------------------------------------------------------------+
+
+ $Id: index.php 2696 2009-07-02 06:38:26Z thomasb $
+
+*/
+
+/**
+ * Use PHP5 autoload for dynamic class loading
+ * (copy from program/include/iniset.php)
+ */
+function __autoload($classname)
+{
+    $filename = preg_replace(
+        array(
+            '/MDB2_(.+)/',
+            '/Mail_(.+)/',
+            '/Net_(.+)/',
+            '/Auth_(.+)/',
+            '/^html_.+/',
+            '/^utf8$/'
+        ),
+        array(
+            'MDB2/\\1',
+            'Mail/\\1',
+            'Net/\\1',
+            'Auth/\\1',
+            'html',
+            'utf8.class'
+        ),
+        $classname
+    );
+    include_once $filename. '.php';
+}
+
+
+/**
+ * Fake internal error handler to catch errors
+ */
+function raise_error($p)
+{
+    $rci = rcube_install::get_instance();
+    $rci->raise_error($p);
+}
+
+/**
+ * Local callback function for PEAR errors
+ */
+function rcube_pear_error($err)
+{
+    raise_error(array(
+        'code' => $err->getCode(),
+        'message' => $err->getMessage(),
+    ));
+}
+
+// set PEAR error handling (will also load the PEAR main class)
+PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'rcube_pear_error');
Index: /branches/devel-composer/installer/welcome.html
===================================================================
--- /branches/devel-composer/installer/welcome.html	(revision 5386)
+++ /branches/devel-composer/installer/welcome.html	(revision 5386)
@@ -0,0 +1,33 @@
+<form action="index.php" methond="get">
+<input type="hidden" name="_step" value="1" />
+
+<p>Welcome to the interactive install script for the Roundcube Webmail package</p>
+<p>First let's check your local environment and find out if everything Roundcube needs is available.</p>
+
+<p>The basic requirements are:</p>
+<ul>
+  <li>PHP Version 5.2.0 or greater including
+    <ul>
+      <li>PCRE (perl compatible regular expression)</li>
+      <li>Session support</li>
+      <li>Libiconv (recommended)</li>
+      <li>OpenSSL (recommended)</li>
+      <li>FileInfo (optional)</li>
+      <li>Multibyte/mbstring (optional)</li>
+      <li>Mcrypt (optional)</li>
+      </ul>
+    </li>
+    <li>php.ini options:
+      <ul>
+        <li>error_reporting E_ALL &amp; ~E_NOTICE (or lower)</li>
+        <li>file_uploads on (for attachment upload features)</li>
+        <li>session.auto_start needs to be off</li>
+      </ul>
+    </li>
+    <li>A MySQL or PostgreSQL database engine or the SQLite extension for PHP</li>
+    <li>An SMTP server (recommended) or PHP configured for mail delivery</li>
+</ul>
+
+<input type="submit" value="START INSTALLATION" />
+
+</form>
Index: /branches/devel-composer/logs/.htaccess
===================================================================
--- /branches/devel-composer/logs/.htaccess	(revision 5386)
+++ /branches/devel-composer/logs/.htaccess	(revision 5386)
@@ -0,0 +1,2 @@
+Order allow,deny
+Deny from all 
Index: /branches/devel-composer/program/.htaccess
===================================================================
--- /branches/devel-composer/program/.htaccess	(revision 5386)
+++ /branches/devel-composer/program/.htaccess	(revision 5386)
@@ -0,0 +1,4 @@
+<IfModule mod_rewrite.c>
+RewriteEngine On
+RewriteRule !^js|.*\.gif$ - [F]
+</IfModule>
Index: /branches/devel-composer/program/include/clisetup.php
===================================================================
--- /branches/devel-composer/program/include/clisetup.php	(revision 5386)
+++ /branches/devel-composer/program/include/clisetup.php	(revision 5386)
@@ -0,0 +1,92 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/clisetup.php                                          |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2010, The Roundcube Dev Team                            |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Setup the command line environment and provide some utitlity        |
+ |   functions.                                                          |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+if (php_sapi_name() != 'cli') {
+  die('Not on the "shell" (php-cli).');
+}
+
+require_once INSTALL_PATH . 'program/include/iniset.php';
+
+// Unset max. execution time limit, set to 120 seconds in iniset.php
+@set_time_limit(0);
+
+/**
+ * Parse commandline arguments into a hash array
+ */
+function get_opt($aliases=array())
+{
+	$args = array();
+	for ($i=1; $i<count($_SERVER['argv']); $i++)
+	{
+		$arg = $_SERVER['argv'][$i];
+		if (substr($arg, 0, 2) == '--')
+		{
+			$sp = strpos($arg, '=');
+			$key = substr($arg, 2, $sp - 2);
+			$value = substr($arg, $sp+1);
+		}
+		else if ($arg{0} == '-')
+		{
+			$key = substr($arg, 1);
+			$value = $_SERVER['argv'][++$i];
+		}
+		else
+			continue;
+
+		$args[$key] = preg_replace(array('/^["\']/', '/["\']$/'), '', $value);
+		
+		if ($alias = $aliases[$key])
+			$args[$alias] = $args[$key];
+	}
+
+	return $args;
+}
+
+
+/**
+ * from http://blogs.sitepoint.com/2009/05/01/interactive-cli-password-prompt-in-php/
+ */
+function prompt_silent($prompt = "Password:")
+{
+  if (preg_match('/^win/i', PHP_OS)) {
+    $vbscript = sys_get_temp_dir() . 'prompt_password.vbs';
+    file_put_contents($vbscript, 'wscript.echo(InputBox("' . addslashes($prompt) . '", "", "password here"))');
+    $command = "cscript //nologo " . escapeshellarg($vbscript);
+    $password = rtrim(shell_exec($command));
+    unlink($vbscript);
+    return $password;
+  }
+  else {
+    $command = "/usr/bin/env bash -c 'echo OK'";
+    if (rtrim(shell_exec($command)) !== 'OK') {
+      echo $prompt;
+      $pass = trim(fgets(STDIN));
+      echo chr(8)."\r" . $prompt . str_repeat("*", strlen($pass))."\n";
+      return $pass;
+    }
+    $command = "/usr/bin/env bash -c 'read -s -p \"" . addslashes($prompt) . "\" mypassword && echo \$mypassword'";
+    $password = rtrim(shell_exec($command));
+    echo "\n";
+    return $password;
+  }
+}
+
+?>
Index: /branches/devel-composer/program/include/html.php
===================================================================
--- /branches/devel-composer/program/include/html.php	(revision 5386)
+++ /branches/devel-composer/program/include/html.php	(revision 5386)
@@ -0,0 +1,760 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/html.php                                              |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2010, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Helper class to create valid XHTML code                             |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+ */
+
+
+/**
+ * Class for HTML code creation
+ *
+ * @package HTML
+ */
+class html
+{
+    protected $tagname;
+    protected $attrib = array();
+    protected $allowed = array();
+    protected $content;
+
+    public static $lc_tags = true;
+    public static $common_attrib = array('id','class','style','title','align');
+    public static $containers = array('iframe','div','span','p','h1','h2','h3','form','textarea','table','thead','tbody','tr','th','td','style','script');
+
+    /**
+     * Constructor
+     *
+     * @param array $attrib Hash array with tag attributes
+     */
+    public function __construct($attrib = array())
+    {
+        if (is_array($attrib)) {
+            $this->attrib = $attrib;
+        }
+    }
+
+    /**
+     * Return the tag code
+     *
+     * @return string The finally composed HTML tag
+     */
+    public function show()
+    {
+        return self::tag($this->tagname, $this->attrib, $this->content, array_merge(self::$common_attrib, $this->allowed));
+    }
+
+    /****** STATIC METHODS *******/
+
+    /**
+     * Generic method to create a HTML tag
+     *
+     * @param string $tagname Tag name
+     * @param array  $attrib  Tag attributes as key/value pairs
+     * @param string $content Optinal Tag content (creates a container tag)
+     * @param array  $allowed_attrib List with allowed attributes, omit to allow all
+     * @return string The XHTML tag
+     */
+    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" : '';
+
+        $tagname = self::$lc_tags ? strtolower($tagname) : $tagname;
+        if (isset($content) || in_array($tagname, self::$containers)) {
+            $templ = $attrib['noclose'] ? "<%s%s>%s" : "<%s%s>%s</%s>%s";
+            unset($attrib['noclose']);
+            return sprintf($templ, $tagname, self::attrib_string($attrib, $allowed_attrib), $content, $tagname, $suffix);
+        }
+        else {
+            return sprintf("<%s%s />%s", $tagname, self::attrib_string($attrib, $allowed_attrib), $suffix);
+        }
+    }
+
+    /**
+     * Derrived method for <div> containers
+     *
+     * @param mixed  $attr Hash array with tag attributes or string with class name
+     * @param string $cont Div content
+     * @return string HTML code
+     * @see html::tag()
+     */
+    public static function div($attr = null, $cont = null)
+    {
+        if (is_string($attr)) {
+            $attr = array('class' => $attr);
+        }
+        return self::tag('div', $attr, $cont, array_merge(self::$common_attrib, array('onclick')));
+    }
+
+    /**
+     * Derrived method for <p> blocks
+     *
+     * @param mixed  $attr Hash array with tag attributes or string with class name
+     * @param string $cont Paragraph content
+     * @return string HTML code
+     * @see html::tag()
+     */
+    public static function p($attr = null, $cont = null)
+    {
+        if (is_string($attr)) {
+            $attr = array('class' => $attr);
+        }
+        return self::tag('p', $attr, $cont, self::$common_attrib);
+    }
+
+    /**
+     * Derrived method to create <img />
+     *
+     * @param mixed $attr Hash array with tag attributes or string with image source (src)
+     * @return string HTML code
+     * @see html::tag()
+     */
+    public static function img($attr = null)
+    {
+        if (is_string($attr)) {
+            $attr = array('src' => $attr);
+        }
+        return self::tag('img', $attr + array('alt' => ''), null, array_merge(self::$common_attrib,
+	    array('src','alt','width','height','border','usemap')));
+    }
+
+    /**
+     * Derrived method for link tags
+     *
+     * @param mixed  $attr Hash array with tag attributes or string with link location (href)
+     * @param string $cont Link content
+     * @return string HTML code
+     * @see html::tag()
+     */
+    public static function a($attr, $cont)
+    {
+        if (is_string($attr)) {
+            $attr = array('href' => $attr);
+        }
+        return self::tag('a', $attr, $cont, array_merge(self::$common_attrib,
+	    array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup')));
+    }
+
+    /**
+     * Derrived method for inline span tags
+     *
+     * @param mixed  $attr Hash array with tag attributes or string with class name
+     * @param string $cont Tag content
+     * @return string HTML code
+     * @see html::tag()
+     */
+    public static function span($attr, $cont)
+    {
+        if (is_string($attr)) {
+            $attr = array('class' => $attr);
+        }
+        return self::tag('span', $attr, $cont, self::$common_attrib);
+    }
+
+    /**
+     * Derrived method for form element labels
+     *
+     * @param mixed  $attr Hash array with tag attributes or string with 'for' attrib
+     * @param string $cont Tag content
+     * @return string HTML code
+     * @see html::tag()
+     */
+    public static function label($attr, $cont)
+    {
+        if (is_string($attr)) {
+            $attr = array('for' => $attr);
+        }
+        return self::tag('label', $attr, $cont, array_merge(self::$common_attrib, array('for')));
+    }
+
+    /**
+     * Derrived method to create <iframe></iframe>
+     *
+     * @param mixed $attr Hash array with tag attributes or string with frame source (src)
+     * @return string HTML code
+     * @see html::tag()
+     */
+    public static function iframe($attr = null, $cont = null)
+    {
+        if (is_string($attr)) {
+            $attr = array('src' => $attr);
+        }
+        return self::tag('iframe', $attr, $cont, array_merge(self::$common_attrib,
+	    array('src','name','width','height','border','frameborder')));
+    }
+
+    /**
+     * Derrived method for line breaks
+     *
+     * @return string HTML code
+     * @see html::tag()
+     */
+    public static function br()
+    {
+        return self::tag('br');
+    }
+
+    /**
+     * Create string with attributes
+     *
+     * @param array $attrib Associative arry with tag attributes
+     * @param array $allowed List of allowed attributes
+     * @return string Valid attribute string
+     */
+    public static function attrib_string($attrib = array(), $allowed = null)
+    {
+        if (empty($attrib)) {
+            return '';
+        }
+
+        $allowed_f = array_flip((array)$allowed);
+        $attrib_arr = array();
+        foreach ($attrib as $key => $value) {
+            // skip size if not numeric
+            if (($key=='size' && !is_numeric($value))) {
+                continue;
+            }
+
+            // ignore "internal" or not allowed attributes
+            if ($key == 'nl' || ($allowed && !isset($allowed_f[$key])) || $value === null) {
+                continue;
+            }
+
+            // skip empty eventhandlers
+            if (preg_match('/^on[a-z]+/', $key) && !$value) {
+                continue;
+            }
+
+            // attributes with no value
+            if (in_array($key, array('checked', 'multiple', 'disabled', 'selected'))) {
+                if ($value) {
+                    $attrib_arr[] = sprintf('%s="%s"', $key, $key);
+                }
+            }
+            else if ($key=='value') {
+                $attrib_arr[] = sprintf('%s="%s"', $key, Q($value, 'strict', false));
+            }
+            else {
+                $attrib_arr[] = sprintf('%s="%s"', $key, Q($value));
+            }
+        }
+        return count($attrib_arr) ? ' '.implode(' ', $attrib_arr) : '';
+    }
+}
+
+/**
+ * Class to create an HTML input field
+ *
+ * @package HTML
+ */
+class html_inputfield extends html
+{
+    protected $tagname = 'input';
+    protected $type = 'text';
+    protected $allowed = array('type','name','value','size','tabindex',
+	'autocomplete','checked','onchange','onclick','disabled','readonly',
+	'spellcheck','results','maxlength','src','multiple');
+
+    /**
+     * Object constructor
+     *
+     * @param array $attrib Associative array with tag attributes
+     */
+    public function __construct($attrib = array())
+    {
+        if (is_array($attrib)) {
+            $this->attrib = $attrib;
+        }
+
+        if ($attrib['type']) {
+            $this->type = $attrib['type'];
+        }
+
+        if ($attrib['newline']) {
+            $this->newline = true;
+        }
+    }
+
+    /**
+     * Compose input tag
+     *
+     * @param string $value Field value
+     * @param array  $attrib Additional attributes to override
+     * @return string HTML output
+     */
+    public function show($value = null, $attrib = null)
+    {
+        // overwrite object attributes
+        if (is_array($attrib)) {
+            $this->attrib = array_merge($this->attrib, $attrib);
+        }
+
+        // set value attribute
+        if ($value !== null) {
+            $this->attrib['value'] = $value;
+        }
+        // set type
+        $this->attrib['type'] = $this->type;
+        return parent::show();
+    }
+}
+
+/**
+ * Class to create an HTML password field
+ *
+ * @package HTML
+ */
+class html_passwordfield extends html_inputfield
+{
+    protected $type = 'password';
+}
+
+/**
+ * Class to create an hidden HTML input field
+ *
+ * @package HTML
+ */
+
+class html_hiddenfield extends html_inputfield
+{
+    protected $type = 'hidden';
+    protected $fields_arr = array();
+    protected $newline = true;
+
+    /**
+     * Constructor
+     *
+     * @param array $attrib Named tag attributes
+     */
+    public function __construct($attrib = null)
+    {
+        if (is_array($attrib)) {
+            $this->add($attrib);
+        }
+    }
+
+    /**
+     * Add a hidden field to this instance
+     *
+     * @param array $attrib Named tag attributes
+     */
+    public function add($attrib)
+    {
+        $this->fields_arr[] = $attrib;
+    }
+
+    /**
+     * Create HTML code for the hidden fields
+     *
+     * @return string Final HTML code
+     */
+    public function show()
+    {
+        $out = '';
+        foreach ($this->fields_arr as $attrib) {
+            $out .= self::tag($this->tagname, array('type' => $this->type) + $attrib);
+        }
+        return $out;
+    }
+}
+
+/**
+ * Class to create HTML radio buttons
+ *
+ * @package HTML
+ */
+class html_radiobutton extends html_inputfield
+{
+    protected $type = 'radio';
+
+    /**
+     * Get HTML code for this object
+     *
+     * @param string $value  Value of the checked field
+     * @param array  $attrib Additional attributes to override
+     * @return string HTML output
+     */
+    public function show($value = '', $attrib = null)
+    {
+        // overwrite object attributes
+        if (is_array($attrib)) {
+            $this->attrib = array_merge($this->attrib, $attrib);
+        }
+
+        // set value attribute
+        $this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']);
+
+        return parent::show();
+    }
+}
+
+/**
+ * Class to create HTML checkboxes
+ *
+ * @package HTML
+ */
+class html_checkbox extends html_inputfield
+{
+    protected $type = 'checkbox';
+
+    /**
+     * Get HTML code for this object
+     *
+     * @param string $value  Value of the checked field
+     * @param array  $attrib Additional attributes to override
+     * @return string HTML output
+     */
+    public function show($value = '', $attrib = null)
+    {
+        // overwrite object attributes
+        if (is_array($attrib)) {
+            $this->attrib = array_merge($this->attrib, $attrib);
+        }
+
+        // set value attribute
+        $this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']);
+
+        return parent::show();
+    }
+}
+
+/**
+ * Class to create an HTML textarea
+ *
+ * @package HTML
+ */
+class html_textarea extends html
+{
+    protected $tagname = 'textarea';
+    protected $allowed = array('name','rows','cols','wrap','tabindex',
+	'onchange','disabled','readonly','spellcheck');
+
+    /**
+     * Get HTML code for this object
+     *
+     * @param string $value  Textbox value
+     * @param array  $attrib Additional attributes to override
+     * @return string HTML output
+     */
+    public function show($value = '', $attrib = null)
+    {
+        // overwrite object attributes
+        if (is_array($attrib)) {
+            $this->attrib = array_merge($this->attrib, $attrib);
+        }
+
+        // take value attribute as content
+        if (empty($value) && !empty($this->attrib['value'])) {
+            $value = $this->attrib['value'];
+        }
+
+        // make shure we don't print the value attribute
+        if (isset($this->attrib['value'])) {
+            unset($this->attrib['value']);
+        }
+
+        if (!empty($value) && !preg_match('/mce_editor/', $this->attrib['class'])) {
+            $value = Q($value, 'strict', false);
+        }
+
+        return self::tag($this->tagname, $this->attrib, $value,
+	    array_merge(self::$common_attrib, $this->allowed));
+    }
+}
+
+/**
+ * Builder for HTML drop-down menus
+ * Syntax:<pre>
+ * // create instance. arguments are used to set attributes of select-tag
+ * $select = new html_select(array('name' => 'fieldname'));
+ *
+ * // add one option
+ * $select->add('Switzerland', 'CH');
+ *
+ * // add multiple options
+ * $select->add(array('Switzerland','Germany'), array('CH','DE'));
+ *
+ * // generate pulldown with selection 'Switzerland'  and return html-code
+ * // as second argument the same attributes available to instanciate can be used
+ * print $select->show('CH');
+ * </pre>
+ *
+ * @package HTML
+ */
+class html_select extends html
+{
+    protected $tagname = 'select';
+    protected $options = array();
+    protected $allowed = array('name','size','tabindex','autocomplete',
+	'multiple','onchange','disabled','rel');
+    
+    /**
+     * Add a new option to this drop-down
+     *
+     * @param mixed $names  Option name or array with option names
+     * @param mixed $values Option value or array with option values
+     */
+    public function add($names, $values = null)
+    {
+        if (is_array($names)) {
+            foreach ($names as $i => $text) {
+                $this->options[] = array('text' => $text, 'value' => $values[$i]);
+            }
+        }
+        else {
+            $this->options[] = array('text' => $names, 'value' => $values);
+        }
+    }
+
+    /**
+     * Get HTML code for this object
+     *
+     * @param string $select Value of the selection option
+     * @param array  $attrib Additional attributes to override
+     * @return string HTML output
+     */
+    public function show($select = array(), $attrib = null)
+    {
+        // overwrite object attributes
+        if (is_array($attrib)) {
+            $this->attrib = array_merge($this->attrib, $attrib);
+        }
+
+        $this->content = "\n";
+        $select = (array)$select;
+        foreach ($this->options as $option) {
+            $attr = array(
+                'value' => $option['value'],
+                'selected' => (in_array($option['value'], $select, true) ||
+                  in_array($option['text'], $select, true)) ? 1 : null);
+
+            $this->content .= self::tag('option', $attr, Q($option['text']));
+        }
+        return parent::show();
+    }
+}
+
+
+/**
+ * Class to build an HTML table
+ *
+ * @package HTML
+ */
+class html_table extends html
+{
+    protected $tagname = 'table';
+    protected $allowed = array('id','class','style','width','summary',
+	    'cellpadding','cellspacing','border');
+
+    private $header = array();
+    private $rows = array();
+    private $rowindex = 0;
+    private $colindex = 0;
+
+    /**
+     * Constructor
+     *
+     * @param array $attrib Named tag attributes
+     */
+    public function __construct($attrib = array())
+    {
+        $this->attrib = array_merge($attrib, array('summary' => '', 'border' => 0));
+    }
+
+    /**
+     * Add a table cell
+     *
+     * @param array  $attr Cell attributes
+     * @param string $cont Cell content
+     */
+    public function add($attr, $cont)
+    {
+        if (is_string($attr)) {
+            $attr = array('class' => $attr);
+        }
+
+        $cell = new stdClass;
+        $cell->attrib = $attr;
+        $cell->content = $cont;
+
+        $this->rows[$this->rowindex]->cells[$this->colindex] = $cell;
+        $this->colindex++;
+
+        if ($this->attrib['cols'] && $this->colindex == $this->attrib['cols']) {
+            $this->add_row();
+        }
+    }
+
+    /**
+     * Add a table header cell
+     *
+     * @param array  $attr Cell attributes
+     * @param string $cont Cell content
+     */
+    public function add_header($attr, $cont)
+    {
+        if (is_string($attr))
+    	    $attr = array('class' => $attr);
+
+        $cell = new stdClass;
+        $cell->attrib = $attr;
+        $cell->content = $cont;
+        $this->header[] = $cell;
+    }
+
+     /**
+     * Remove a column from a table
+     * Useful for plugins making alterations
+     * 
+     * @param string $class 
+     */
+    public function remove_column($class)
+    {
+        // Remove the header
+        foreach ($this->header as $index=>$header){
+            if ($header->attrib['class'] == $class){
+                unset($this->header[$index]);
+                break;
+            }
+        }
+
+        // Remove cells from rows
+        foreach ($this->rows as $i=>$row){
+            foreach ($row->cells as $j=>$cell){
+                if ($cell->attrib['class'] == $class){
+                    unset($this->rows[$i]->cells[$j]);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Jump to next row
+     *
+     * @param array $attr Row attributes
+     */
+    public function add_row($attr = array())
+    {
+        $this->rowindex++;
+        $this->colindex = 0;
+        $this->rows[$this->rowindex] = new stdClass;
+        $this->rows[$this->rowindex]->attrib = $attr;
+        $this->rows[$this->rowindex]->cells = array();
+    }
+
+    /**
+     * Set row attributes
+     *
+     * @param array $attr  Row attributes
+     * @param int   $index Optional row index (default current row index)
+     */
+    public function set_row_attribs($attr = array(), $index = null)
+    {
+        if (is_string($attr))
+    	    $attr = array('class' => $attr);
+
+        if ($index === null)
+            $index = $this->rowindex;
+
+        $this->rows[$index]->attrib = $attr;
+    }
+
+    /**
+     * Get row attributes
+     *
+     * @param int $index Row index
+     *
+     * @return array Row attributes
+     */
+    public function get_row_attribs($index = null)
+    {
+        if ($index === null)
+            $index = $this->rowindex;
+
+        return $this->rows[$index] ? $this->rows[$index]->attrib : null;
+    }
+
+    /**
+     * Build HTML output of the table data
+     *
+     * @param array $attrib Table attributes
+     * @return string The final table HTML code
+     */
+    public function show($attrib = null)
+    {
+        if (is_array($attrib))
+            $this->attrib = array_merge($this->attrib, $attrib);
+
+        $thead = $tbody = "";
+
+        // include <thead>
+        if (!empty($this->header)) {
+            $rowcontent = '';
+            foreach ($this->header as $c => $col) {
+                $rowcontent .= self::tag('td', $col->attrib, $col->content);
+            }
+            $thead = self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib));
+        }
+
+        foreach ($this->rows as $r => $row) {
+            $rowcontent = '';
+            foreach ($row->cells as $c => $col) {
+                $rowcontent .= self::tag('td', $col->attrib, $col->content);
+            }
+
+            if ($r < $this->rowindex || count($row->cells)) {
+                $tbody .= self::tag('tr', $row->attrib, $rowcontent, parent::$common_attrib);
+            }
+        }
+
+        if ($this->attrib['rowsonly']) {
+            return $tbody;
+        }
+
+        // add <tbody>
+        $this->content = $thead . self::tag('tbody', null, $tbody);
+
+        unset($this->attrib['cols'], $this->attrib['rowsonly']);
+        return parent::show();
+    }
+
+    /**
+     * Count number of rows
+     *
+     * @return The number of rows
+     */
+    public function size()
+    {
+      return count($this->rows);
+    }
+
+    /**
+     * Remove table body (all rows)
+     */
+    public function remove_body()
+    {
+        $this->rows     = array();
+        $this->rowindex = 0;
+    }
+
+}
+
Index: /branches/devel-composer/program/include/iniset.php
===================================================================
--- /branches/devel-composer/program/include/iniset.php	(revision 5386)
+++ /branches/devel-composer/program/include/iniset.php	(revision 5386)
@@ -0,0 +1,135 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/iniset.php                                            |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Setup the application envoronment required to process               |
+ |   any request.                                                        |
+ +-----------------------------------------------------------------------+
+ | Author: Till Klampaeckel <till@php.net>                               |
+ |         Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+// Some users are not using Installer, so we'll check some
+// critical PHP settings here. Only these, which doesn't provide
+// an error/warning in the logs later. See (#1486307).
+$crit_opts = array(
+    'mbstring.func_overload' => 0,
+    'suhosin.session.encrypt' => 0,
+    'session.auto_start' => 0,
+    'file_uploads' => 1,
+    'magic_quotes_runtime' => 0,
+);
+foreach ($crit_opts as $optname => $optval) {
+    if ($optval != ini_get($optname)) {
+        die("ERROR: Wrong '$optname' option value. Read REQUIREMENTS section in INSTALL file or use Roundcube Installer, please!");
+    }
+}
+
+// application constants
+define('RCMAIL_VERSION', '0.7-svn');
+define('RCMAIL_CHARSET', 'UTF-8');
+define('JS_OBJECT_NAME', 'rcmail');
+define('RCMAIL_START', microtime(true));
+
+if (!defined('INSTALL_PATH')) {
+    define('INSTALL_PATH', dirname($_SERVER['SCRIPT_FILENAME']).'/');
+}
+
+if (!defined('RCMAIL_CONFIG_DIR')) {
+    define('RCMAIL_CONFIG_DIR', INSTALL_PATH . 'config');
+}
+
+// make sure path_separator is defined
+if (!defined('PATH_SEPARATOR')) {
+    define('PATH_SEPARATOR', (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') ? ';' : ':');
+}
+
+// RC include folders MUST be included FIRST to avoid other
+// possible not compatible libraries (i.e PEAR) to be included
+// instead the ones provided by RC
+$include_path = INSTALL_PATH . 'program/lib' . PATH_SEPARATOR;
+$include_path.= ini_get('include_path');
+
+if (set_include_path($include_path) === false) {
+    die("Fatal error: ini_set/set_include_path does not work.");
+}
+
+ini_set('error_reporting', E_ALL&~E_NOTICE);
+
+// increase maximum execution time for php scripts
+// (does not work in safe mode)
+@set_time_limit(120);
+
+// set internal encoding for mbstring extension
+if (extension_loaded('mbstring')) {
+    mb_internal_encoding(RCMAIL_CHARSET);
+    @mb_regex_encoding(RCMAIL_CHARSET);
+}
+
+/**
+ * Use PHP5 autoload for dynamic class loading
+ * 
+ * @todo Make Zend, PEAR etc play with this
+ * @todo Make our classes conform to a more straight forward CS.
+ */
+function rcube_autoload($classname)
+{
+    $filename = preg_replace(
+        array(
+            '/MDB2_(.+)/',
+            '/Mail_(.+)/',
+            '/Net_(.+)/',
+            '/Auth_(.+)/',
+            '/^html_.+/',
+            '/^utf8$/',
+        ),
+        array(
+            'MDB2/\\1',
+            'Mail/\\1',
+            'Net/\\1',
+            'Auth/\\1',
+            'html',
+            'utf8.class',
+        ),
+        $classname
+    );
+
+    if ($fp = @fopen("$filename.php", 'r', true)) {
+        fclose($fp);
+        include_once("$filename.php");
+        return true;
+    }
+
+    return false;
+}
+
+spl_autoload_register('rcube_autoload');
+
+/**
+ * Local callback function for PEAR errors
+ */
+function rcube_pear_error($err)
+{
+    error_log(sprintf("%s (%s): %s",
+        $err->getMessage(),
+        $err->getCode(),
+        $err->getUserinfo()), 0);
+}
+
+// set PEAR error handling (will also load the PEAR main class)
+PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'rcube_pear_error');
+
+// include global functions
+require_once INSTALL_PATH . 'program/include/main.inc';
+require_once INSTALL_PATH . 'program/include/rcube_shared.inc';
Index: /branches/devel-composer/program/include/main.inc
===================================================================
--- /branches/devel-composer/program/include/main.inc	(revision 5386)
+++ /branches/devel-composer/program/include/main.inc	(revision 5386)
@@ -0,0 +1,2382 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/main.inc                                              |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Provide basic functions for the webmail package                     |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+/**
+ * Roundcube Webmail common functions
+ *
+ * @package Core
+ * @author Thomas Bruederli <roundcube@gmail.com>
+ */
+
+require_once 'utf7.inc';
+require_once INSTALL_PATH . 'program/include/rcube_shared.inc';
+
+// define constannts for input reading
+define('RCUBE_INPUT_GET', 0x0101);
+define('RCUBE_INPUT_POST', 0x0102);
+define('RCUBE_INPUT_GPC', 0x0103);
+
+
+
+/**
+ * Return correct name for a specific database table
+ *
+ * @param string Table name
+ * @return string Translated table name
+ */
+function get_table_name($table)
+  {
+  global $CONFIG;
+
+  // return table name if configured
+  $config_key = 'db_table_'.$table;
+
+  if (strlen($CONFIG[$config_key]))
+    return $CONFIG[$config_key];
+
+  return $table;
+  }
+
+
+/**
+ * Return correct name for a specific database sequence
+ * (used for Postgres only)
+ *
+ * @param string Secuence name
+ * @return string Translated sequence name
+ */
+function get_sequence_name($sequence)
+  {
+  // return sequence name if configured
+  $config_key = 'db_sequence_'.$sequence;
+  $opt = rcmail::get_instance()->config->get($config_key);
+
+  if (!empty($opt))
+    return $opt;
+    
+  return $sequence;
+  }
+
+
+/**
+ * Get localized text in the desired language
+ * It's a global wrapper for rcmail::gettext()
+ *
+ * @param mixed Named parameters array or label name
+ * @param string Domain to search in (e.g. plugin name)
+ * @return string Localized text
+ * @see rcmail::gettext()
+ */
+function rcube_label($p, $domain=null)
+{
+  return rcmail::get_instance()->gettext($p, $domain);
+}
+
+
+/**
+ * Global wrapper of rcmail::text_exists()
+ * to check whether a text label is defined
+ *
+ * @see rcmail::text_exists()
+ */
+function rcube_label_exists($name, $domain=null, &$ref_domain = null)
+{
+  return rcmail::get_instance()->text_exists($name, $domain, $ref_domain);
+}
+
+
+/**
+ * Overwrite action variable
+ *
+ * @param string New action value
+ */
+function rcmail_overwrite_action($action)
+  {
+  $app = rcmail::get_instance();
+  $app->action = $action;
+  $app->output->set_env('action', $action);
+  }
+
+
+/**
+ * Compose an URL for a specific action
+ *
+ * @param string  Request action
+ * @param array   More URL parameters
+ * @param string  Request task (omit if the same)
+ * @return The application URL
+ */
+function rcmail_url($action, $p=array(), $task=null)
+{
+  $app = rcmail::get_instance();
+  return $app->url((array)$p + array('_action' => $action, 'task' => $task));
+}
+
+
+/**
+ * Garbage collector function for temp files.
+ * Remove temp files older than two days
+ */
+function rcmail_temp_gc()
+{
+  $rcmail = rcmail::get_instance();
+
+  $tmp = unslashify($rcmail->config->get('temp_dir'));
+  $expire = mktime() - 172800;  // expire in 48 hours
+
+  if ($dir = opendir($tmp)) {
+    while (($fname = readdir($dir)) !== false) {
+      if ($fname{0} == '.')
+        continue;
+
+      if (filemtime($tmp.'/'.$fname) < $expire)
+        @unlink($tmp.'/'.$fname);
+    }
+
+    closedir($dir);
+  }
+}
+
+
+/**
+ * Garbage collector for cache entries.
+ * Remove all expired message cache records
+ * @return void
+ */
+function rcmail_cache_gc()
+{
+  $rcmail = rcmail::get_instance();
+  $db = $rcmail->get_dbh();
+
+  // get target timestamp
+  $ts = get_offset_time($rcmail->config->get('message_cache_lifetime', '30d'), -1);
+
+  $db->query("DELETE FROM ".get_table_name('cache_messages')
+        ." WHERE changed < " . $db->fromunixtime($ts));
+
+  $db->query("DELETE FROM ".get_table_name('cache_index')
+        ." WHERE changed < " . $db->fromunixtime($ts));
+
+  $db->query("DELETE FROM ".get_table_name('cache_thread')
+        ." WHERE changed < " . $db->fromunixtime($ts));
+
+  $db->query("DELETE FROM ".get_table_name('cache')
+        ." WHERE created < " . $db->fromunixtime($ts));
+}
+
+
+/**
+ * Catch an error and throw an exception.
+ *
+ * @param  int    Level of the error
+ * @param  string Error message
+ */ 
+function rcube_error_handler($errno, $errstr)
+{
+  throw new ErrorException($errstr, 0, $errno);
+}
+
+
+/**
+ * Convert a string from one charset to another.
+ * Uses mbstring and iconv functions if possible
+ *
+ * @param  string Input string
+ * @param  string Suspected charset of the input string
+ * @param  string Target charset to convert to; defaults to RCMAIL_CHARSET
+ * @return string Converted string
+ */
+function rcube_charset_convert($str, $from, $to=NULL)
+{
+  static $iconv_options = null;
+  static $mbstring_loaded = null;
+  static $mbstring_list = null;
+  static $conv = null;
+
+  $error = false;
+
+  $to = empty($to) ? strtoupper(RCMAIL_CHARSET) : rcube_parse_charset($to);
+  $from = rcube_parse_charset($from);
+
+  if ($from == $to || empty($str) || empty($from))
+    return $str;
+
+  // convert charset using iconv module
+  if (function_exists('iconv') && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
+    if ($iconv_options === null) {
+      // ignore characters not available in output charset
+      $iconv_options = '//IGNORE';
+      if (iconv('', $iconv_options, '') === false) {
+        // iconv implementation does not support options
+        $iconv_options = '';
+      }
+    }
+
+    // throw an exception if iconv reports an illegal character in input
+    // it means that input string has been truncated
+    set_error_handler('rcube_error_handler', E_NOTICE);
+    try {
+      $_iconv = iconv($from, $to . $iconv_options, $str);
+    } catch (ErrorException $e) {
+      $_iconv = false;
+    }
+    restore_error_handler();
+    if ($_iconv !== false) {
+      return $_iconv;
+    }
+  }
+
+  if ($mbstring_loaded === null)
+    $mbstring_loaded = extension_loaded('mbstring');
+
+  // convert charset using mbstring module
+  if ($mbstring_loaded) {
+    $aliases['WINDOWS-1257'] = 'ISO-8859-13';
+
+    if ($mbstring_list === null) {
+      $mbstring_list = mb_list_encodings();
+      $mbstring_list = array_map('strtoupper', $mbstring_list);
+    }
+
+    $mb_from = $aliases[$from] ? $aliases[$from] : $from;
+    $mb_to = $aliases[$to] ? $aliases[$to] : $to;
+
+    // return if encoding found, string matches encoding and convert succeeded
+    if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) {
+      if (mb_check_encoding($str, $mb_from) && ($out = mb_convert_encoding($str, $mb_to, $mb_from)))
+        return $out;
+    }
+  }
+
+  // convert charset using bundled classes/functions
+  if ($to == 'UTF-8') {
+    if ($from == 'UTF7-IMAP') {
+      if ($_str = utf7_to_utf8($str))
+        return $_str;
+    }
+    else if ($from == 'UTF-7') {
+      if ($_str = rcube_utf7_to_utf8($str))
+        return $_str;
+    }
+    else if (($from == 'ISO-8859-1') && function_exists('utf8_encode')) {
+      return utf8_encode($str);
+    }
+    else if (class_exists('utf8')) {
+      if (!$conv)
+        $conv = new utf8($from);
+      else
+        $conv->loadCharset($from);
+
+      if($_str = $conv->strToUtf8($str))
+        return $_str;
+    }
+    $error = true;
+  }
+
+  // encode string for output
+  if ($from == 'UTF-8') {
+    // @TODO: we need a function for UTF-7 (RFC2152) conversion
+    if ($to == 'UTF7-IMAP' || $to == 'UTF-7') {
+      if ($_str = utf8_to_utf7($str))
+        return $_str;
+    }
+    else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) {
+      return utf8_decode($str);
+    }
+    else if (class_exists('utf8')) {
+      if (!$conv)
+        $conv = new utf8($to);
+      else
+        $conv->loadCharset($from);
+
+      if ($_str = $conv->strToUtf8($str))
+        return $_str;
+    }
+    $error = true;
+  }
+
+  // return UTF-8 or original string
+  return $str;
+}
+
+
+/**
+ * Parse and validate charset name string (see #1485758).
+ * Sometimes charset string is malformed, there are also charset aliases 
+ * but we need strict names for charset conversion (specially utf8 class)
+ *
+ * @param  string Input charset name
+ * @return string The validated charset name
+ */
+function rcube_parse_charset($input)
+{
+  static $charsets = array();
+  $charset = strtoupper($input);
+
+  if (isset($charsets[$input]))
+    return $charsets[$input];
+
+  $charset = preg_replace(array(
+    '/^[^0-9A-Z]+/',    // e.g. _ISO-8859-JP$SIO
+    '/\$.*$/',          // e.g. _ISO-8859-JP$SIO
+    '/UNICODE-1-1-*/',  // RFC1641/1642
+    '/^X-/',            // X- prefix (e.g. X-ROMAN8 => ROMAN8)
+    ), '', $charset);
+
+  if ($charset == 'BINARY')
+    return $charsets[$input] = null;
+
+  # Aliases: some of them from HTML5 spec.
+  $aliases = array(
+    'USASCII'       => 'WINDOWS-1252',
+    'ANSIX31101983' => 'WINDOWS-1252',
+    'ANSIX341968'   => 'WINDOWS-1252',
+    'UNKNOWN8BIT'   => 'ISO-8859-15',
+    'UNKNOWN'       => 'ISO-8859-15',
+    'USERDEFINED'   => 'ISO-8859-15',
+    'KSC56011987'   => 'EUC-KR',
+    'GB2312' 	    => 'GBK',
+    'GB231280'	    => 'GBK',
+    'UNICODE'	    => 'UTF-8',
+    'UTF7IMAP'	    => 'UTF7-IMAP',
+    'TIS620'	    => 'WINDOWS-874',
+    'ISO88599'	    => 'WINDOWS-1254',
+    'ISO885911'	    => 'WINDOWS-874',
+    'MACROMAN'	    => 'MACINTOSH',
+    '77'            => 'MAC',
+    '128'           => 'SHIFT-JIS',
+    '129'           => 'CP949',
+    '130'           => 'CP1361',
+    '134'           => 'GBK',
+    '136'           => 'BIG5',
+    '161'           => 'WINDOWS-1253',
+    '162'           => 'WINDOWS-1254',
+    '163'           => 'WINDOWS-1258',
+    '177'           => 'WINDOWS-1255',
+    '178'           => 'WINDOWS-1256',
+    '186'           => 'WINDOWS-1257',
+    '204'           => 'WINDOWS-1251',
+    '222'           => 'WINDOWS-874',
+    '238'           => 'WINDOWS-1250',
+    'MS950'         => 'CP950',
+    'WINDOWS949'    => 'UHC',
+  );
+
+  // allow A-Z and 0-9 only
+  $str = preg_replace('/[^A-Z0-9]/', '', $charset);
+
+  if (isset($aliases[$str]))
+    $result = $aliases[$str];
+  // UTF
+  else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m))
+    $result = 'UTF-' . $m[1] . $m[2];
+  // ISO-8859
+  else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
+    $iso = 'ISO-8859-' . ($m[1] ? $m[1] : 1);
+    // some clients sends windows-1252 text as latin1,
+    // it is safe to use windows-1252 for all latin1
+    $result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso;
+  }
+  // handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE
+  else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) {
+    $result = 'WINDOWS-' . $m[2];
+  }
+  // LATIN
+  else if (preg_match('/LATIN(.*)/', $str, $m)) {
+    $aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10,
+        '7' => 13, '8' => 14, '9' => 15, '10' => 16,
+        'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8);
+
+    // some clients sends windows-1252 text as latin1,
+    // it is safe to use windows-1252 for all latin1
+    if ($m[1] == 1) {
+      $result = 'WINDOWS-1252';
+    }
+    // if iconv is not supported we need ISO labels, it's also safe for iconv
+    else if (!empty($aliases[$m[1]])) {
+      $result = 'ISO-8859-'.$aliases[$m[1]];
+    }
+    // iconv requires convertion of e.g. LATIN-1 to LATIN1
+    else {
+      $result = $str;
+    }
+  }
+  else {
+    $result = $charset;
+  }
+
+  $charsets[$input] = $result;
+
+  return $result;
+}
+
+
+/**
+ * Converts string from standard UTF-7 (RFC 2152) to UTF-8.
+ *
+ * @param  string  Input string
+ * @return string  The converted string
+ */
+function rcube_utf7_to_utf8($str)
+{
+  $Index_64 = array(
+    0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
+    0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
+    0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0,
+    1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0,
+    0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
+    1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
+    0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
+    1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
+  );
+
+  $u7len = strlen($str);
+  $str = strval($str);
+  $res = '';
+
+  for ($i=0; $u7len > 0; $i++, $u7len--)
+  {
+    $u7 = $str[$i];
+    if ($u7 == '+')
+    {
+      $i++;
+      $u7len--;
+      $ch = '';
+
+      for (; $u7len > 0; $i++, $u7len--)
+      {
+        $u7 = $str[$i];
+
+        if (!$Index_64[ord($u7)])
+          break;
+
+	$ch .= $u7;
+      }
+
+      if ($ch == '') {
+        if ($u7 == '-')
+          $res .= '+';
+        continue;
+      }
+
+      $res .= rcube_utf16_to_utf8(base64_decode($ch));
+    }
+    else
+    {
+      $res .= $u7;
+    }
+  }
+
+  return $res;
+}
+
+/**
+ * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
+ *
+ * @param  string  Input string
+ * @return string  The converted string
+ */
+function rcube_utf16_to_utf8($str)
+{
+  $len = strlen($str);
+  $dec = '';
+
+  for ($i = 0; $i < $len; $i += 2) {
+    $c = ord($str[$i]) << 8 | ord($str[$i + 1]);
+    if ($c >= 0x0001 && $c <= 0x007F) {
+      $dec .= chr($c);
+    } else if ($c > 0x07FF) {
+      $dec .= chr(0xE0 | (($c >> 12) & 0x0F));
+      $dec .= chr(0x80 | (($c >>  6) & 0x3F));
+      $dec .= chr(0x80 | (($c >>  0) & 0x3F));
+    } else {
+      $dec .= chr(0xC0 | (($c >>  6) & 0x1F));
+      $dec .= chr(0x80 | (($c >>  0) & 0x3F));
+    }
+  }
+  return $dec;
+}
+
+
+/**
+ * Replacing specials characters to a specific encoding type
+ *
+ * @param  string  Input string
+ * @param  string  Encoding type: text|html|xml|js|url
+ * @param  string  Replace mode for tags: show|replace|remove
+ * @param  boolean Convert newlines
+ * @return string  The quoted string
+ */
+function rep_specialchars_output($str, $enctype='', $mode='', $newlines=TRUE)
+  {
+  static $html_encode_arr = false;
+  static $js_rep_table = false;
+  static $xml_rep_table = false;
+
+  if (!$enctype)
+    $enctype = $OUTPUT->type;
+
+  // encode for HTML output
+  if ($enctype=='html')
+    {
+    if (!$html_encode_arr)
+      {
+      $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
+      unset($html_encode_arr['?']);
+      }
+
+    $ltpos = strpos($str, '<');
+    $encode_arr = $html_encode_arr;
+
+    // don't replace quotes and html tags
+    if (($mode=='show' || $mode=='') && $ltpos!==false && strpos($str, '>', $ltpos)!==false)
+      {
+      unset($encode_arr['"']);
+      unset($encode_arr['<']);
+      unset($encode_arr['>']);
+      unset($encode_arr['&']);
+      }
+    else if ($mode=='remove')
+      $str = strip_tags($str);
+
+    $out = strtr($str, $encode_arr);
+
+    // avoid douple quotation of &
+    $out = preg_replace('/&amp;([A-Za-z]{2,6}|#[0-9]{2,4});/', '&\\1;', $out);
+
+    return $newlines ? nl2br($out) : $out;
+    }
+
+  // if the replace tables for XML and JS are not yet defined
+  if ($js_rep_table===false)
+    {
+    $js_rep_table = $xml_rep_table = array();
+    $xml_rep_table['&'] = '&amp;';
+
+    for ($c=160; $c<256; $c++)  // can be increased to support more charsets
+      $xml_rep_table[chr($c)] = "&#$c;";
+
+    $xml_rep_table['"'] = '&quot;';
+    $js_rep_table['"'] = '\\"';
+    $js_rep_table["'"] = "\\'";
+    $js_rep_table["\\"] = "\\\\";
+    // Unicode line and paragraph separators (#1486310)
+    $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A8))] = '&#8232;';
+    $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A9))] = '&#8233;';
+    }
+
+  // encode for javascript use
+  if ($enctype=='js')
+    return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
+
+  // encode for plaintext
+  if ($enctype=='text')
+    return str_replace("\r\n", "\n", $mode=='remove' ? strip_tags($str) : $str);
+
+  if ($enctype=='url')
+    return rawurlencode($str);
+
+  // encode for XML
+  if ($enctype=='xml')
+    return strtr($str, $xml_rep_table);
+
+  // no encoding given -> return original string
+  return $str;
+  }
+  
+/**
+ * Quote a given string.
+ * Shortcut function for rep_specialchars_output
+ *
+ * @return string HTML-quoted string
+ * @see rep_specialchars_output()
+ */
+function Q($str, $mode='strict', $newlines=TRUE)
+  {
+  return rep_specialchars_output($str, 'html', $mode, $newlines);
+  }
+
+/**
+ * Quote a given string for javascript output.
+ * Shortcut function for rep_specialchars_output
+ * 
+ * @return string JS-quoted string
+ * @see rep_specialchars_output()
+ */
+function JQ($str)
+  {
+  return rep_specialchars_output($str, 'js');
+  }
+
+
+/**
+ * Read input value and convert it for internal use
+ * Performs stripslashes() and charset conversion if necessary
+ * 
+ * @param  string   Field name to read
+ * @param  int      Source to get value from (GPC)
+ * @param  boolean  Allow HTML tags in field value
+ * @param  string   Charset to convert into
+ * @return string   Field value or NULL if not available
+ */
+function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL)
+{
+  $value = NULL;
+  
+  if ($source==RCUBE_INPUT_GET && isset($_GET[$fname]))
+    $value = $_GET[$fname];
+  else if ($source==RCUBE_INPUT_POST && isset($_POST[$fname]))
+    $value = $_POST[$fname];
+  else if ($source==RCUBE_INPUT_GPC)
+    {
+    if (isset($_POST[$fname]))
+      $value = $_POST[$fname];
+    else if (isset($_GET[$fname]))
+      $value = $_GET[$fname];
+    else if (isset($_COOKIE[$fname]))
+      $value = $_COOKIE[$fname];
+    }
+
+  return parse_input_value($value, $allow_html, $charset);
+}
+
+/**
+ * Parse/validate input value. See get_input_value()
+ * Performs stripslashes() and charset conversion if necessary
+ * 
+ * @param  string   Input value
+ * @param  boolean  Allow HTML tags in field value
+ * @param  string   Charset to convert into
+ * @return string   Parsed value
+ */
+function parse_input_value($value, $allow_html=FALSE, $charset=NULL)
+{
+  global $OUTPUT;
+
+  if (empty($value))
+    return $value;
+
+  if (is_array($value)) {
+    foreach ($value as $idx => $val)
+      $value[$idx] = parse_input_value($val, $allow_html, $charset);
+    return $value;
+  }
+
+  // strip single quotes if magic_quotes_sybase is enabled
+  if (ini_get('magic_quotes_sybase'))
+    $value = str_replace("''", "'", $value);
+  // strip slashes if magic_quotes enabled
+  else if (get_magic_quotes_gpc() || get_magic_quotes_runtime())
+    $value = stripslashes($value);
+
+  // remove HTML tags if not allowed    
+  if (!$allow_html)
+    $value = strip_tags($value);
+  
+  // convert to internal charset
+  if (is_object($OUTPUT) && $charset)
+    return rcube_charset_convert($value, $OUTPUT->get_charset(), $charset);
+  else
+    return $value;
+}
+
+/**
+ * Convert array of request parameters (prefixed with _)
+ * to a regular array with non-prefixed keys.
+ *
+ * @param  int   Source to get value from (GPC)
+ * @return array Hash array with all request parameters
+ */
+function request2param($mode = RCUBE_INPUT_GPC, $ignore = 'task|action')
+{
+  $out = array();
+  $src = $mode == RCUBE_INPUT_GET ? $_GET : ($mode == RCUBE_INPUT_POST ? $_POST : $_REQUEST);
+  foreach ($src as $key => $value) {
+    $fname = $key[0] == '_' ? substr($key, 1) : $key;
+    if ($ignore && !preg_match("/($ignore)/", $fname))
+      $out[$fname] = get_input_value($key, $mode);
+  }
+  
+  return $out;
+}
+
+/**
+ * Remove all non-ascii and non-word chars
+ * except ., -, _
+ */
+function asciiwords($str, $css_id = false, $replace_with = '')
+{
+  $allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : '');
+  return preg_replace("/[^$allowed]/i", $replace_with, $str);
+}
+
+/**
+ * Convert the given string into a valid HTML identifier
+ * Same functionality as done in app.js with this.identifier_expr
+ *
+ */
+function html_identifier($str)
+{
+  return asciiwords($str, true, '_');
+}
+
+/**
+ * Remove single and double quotes from given string
+ *
+ * @param string Input value
+ * @return string Dequoted string
+ */
+function strip_quotes($str)
+{
+  return str_replace(array("'", '"'), '', $str);
+}
+
+
+/**
+ * Remove new lines characters from given string
+ *
+ * @param string Input value
+ * @return string Stripped string
+ */
+function strip_newlines($str)
+{
+  return preg_replace('/[\r\n]/', '', $str);
+}
+
+
+/**
+ * Create a HTML table based on the given data
+ *
+ * @param  array  Named table attributes
+ * @param  mixed  Table row data. Either a two-dimensional array or a valid SQL result set
+ * @param  array  List of cols to show
+ * @param  string Name of the identifier col
+ * @return string HTML table code
+ */
+function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col)
+{
+  global $RCMAIL;
+
+  $table = new html_table(/*array('cols' => count($a_show_cols))*/);
+
+  // add table header
+  if (!$attrib['noheader'])
+    foreach ($a_show_cols as $col)
+      $table->add_header($col, Q(rcube_label($col)));
+
+  $c = 0;
+  if (!is_array($table_data))
+  {
+    $db = $RCMAIL->get_dbh();
+    while ($table_data && ($sql_arr = $db->fetch_assoc($table_data)))
+    {
+      $table->add_row(array('id' => 'rcmrow' . html_identifier($sql_arr[$id_col])));
+
+      // format each col
+      foreach ($a_show_cols as $col)
+        $table->add($col, Q($sql_arr[$col]));
+
+      $c++;
+    }
+  }
+  else {
+    foreach ($table_data as $row_data)
+    {
+      $class = !empty($row_data['class']) ? $row_data['class'] : '';
+
+      $table->add_row(array('id' => 'rcmrow' . html_identifier($row_data[$id_col]), 'class' => $class));
+
+      // format each col
+      foreach ($a_show_cols as $col)
+        $table->add($col, Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
+
+      $c++;
+    }
+  }
+
+  return $table->show($attrib);
+}
+
+
+/**
+ * Create an edit field for inclusion on a form
+ * 
+ * @param string col field name
+ * @param string value field value
+ * @param array attrib HTML element attributes for field
+ * @param string type HTML element type (default 'text')
+ * @return string HTML field definition
+ */
+function rcmail_get_edit_field($col, $value, $attrib, $type='text')
+{
+  static $colcounts = array();
+  
+  $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') {
+    $attrib['cols'] = $attrib['size'];
+    $input = new html_textarea($attrib);
+  }
+  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 (isset($_POST[$fname])) {
+    $postvalue = get_input_value($fname, RCUBE_INPUT_POST, true);
+    $value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
+  }
+
+  $out = $input->show($value);
+
+  return $out;
+}
+
+
+/**
+ * Replace all css definitions with #container [def]
+ * and remove css-inlined scripting
+ *
+ * @param string CSS source code
+ * @param string Container ID to use as prefix
+ * @return string Modified CSS source
+ */
+function rcmail_mod_css_styles($source, $container_id)
+  {
+  $last_pos = 0;
+  $replacements = new rcube_string_replacer;
+
+  // ignore the whole block if evil styles are detected
+  $stripped = preg_replace('/[^a-z\(:;]/', '', rcmail_xss_entity_decode($source));
+  if (preg_match('/expression|behavior|url\(|import[^a]/', $stripped))
+    return '/* evil! */';
+
+  // remove css comments (sometimes used for some ugly hacks)
+  $source = preg_replace('!/\*(.+)\*/!Ums', '', $source);
+
+  // cut out all contents between { and }
+  while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos)))
+  {
+    $key = $replacements->add(substr($source, $pos+1, $pos2-($pos+1)));
+    $source = substr($source, 0, $pos+1) . $replacements->get_replacement($key) . substr($source, $pos2, strlen($source)-$pos2);
+    $last_pos = $pos+2;
+  }
+
+  // remove html comments and add #container to each tag selector.
+  // also replace body definition because we also stripped off the <body> tag
+  $styles = preg_replace(
+    array(
+      '/(^\s*<!--)|(-->\s*$)/',
+      '/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
+      '/'.preg_quote($container_id, '/').'\s+body/i',
+    ),
+    array(
+      '',
+      "\\1#$container_id \\2",
+      $container_id,
+    ),
+    $source);
+
+  // put block contents back in
+  $styles = $replacements->resolve($styles);
+
+  return $styles;
+  }
+
+
+/**
+ * Decode escaped entities used by known XSS exploits.
+ * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
+ *
+ * @param string CSS content to decode
+ * @return string Decoded string
+ */
+function rcmail_xss_entity_decode($content)
+{
+  $out = html_entity_decode(html_entity_decode($content));
+  $out = preg_replace_callback('/\\\([0-9a-f]{4})/i', 'rcmail_xss_entity_decode_callback', $out);
+  $out = preg_replace('#/\*.*\*/#Um', '', $out);
+  return $out;
+}
+
+
+/**
+ * preg_replace_callback callback for rcmail_xss_entity_decode_callback
+ *
+ * @param array matches result from preg_replace_callback
+ * @return string decoded entity
+ */ 
+function rcmail_xss_entity_decode_callback($matches)
+{ 
+  return chr(hexdec($matches[1]));
+}
+
+/**
+ * Compose a valid attribute string for HTML tags
+ *
+ * @param array Named tag attributes
+ * @param array List of allowed attributes
+ * @return string HTML formatted attribute string
+ */
+function create_attrib_string($attrib, $allowed_attribs=array('id', 'class', 'style'))
+  {
+  // allow the following attributes to be added to the <iframe> tag
+  $attrib_str = '';
+  foreach ($allowed_attribs as $a)
+    if (isset($attrib[$a]))
+      $attrib_str .= sprintf(' %s="%s"', $a, str_replace('"', '&quot;', $attrib[$a]));
+
+  return $attrib_str;
+  }
+
+
+/**
+ * Convert a HTML attribute string attributes to an associative array (name => value)
+ *
+ * @param string Input string
+ * @return array Key-value pairs of parsed attributes
+ */
+function parse_attrib_string($str)
+  {
+  $attrib = array();
+  preg_match_all('/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui', stripslashes($str), $regs, PREG_SET_ORDER);
+
+  // convert attributes to an associative array (name => value)
+  if ($regs) {
+    foreach ($regs as $attr) {
+      $attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]);
+    }
+  }
+
+  return $attrib;
+  }
+
+
+/**
+ * Improved equivalent to strtotime()
+ *
+ * @param string Date string
+ * @return int 
+ */
+function rcube_strtotime($date)
+{
+  // check for MS Outlook vCard date format YYYYMMDD
+  if (preg_match('/^([12][90]\d\d)([01]\d)(\d\d)$/', trim($date), $matches)) {
+    return mktime(0,0,0, intval($matches[2]), intval($matches[3]), intval($matches[1]));
+  }
+  else if (is_numeric($date))
+    return $date;
+
+  // support non-standard "GMTXXXX" literal
+  $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
+
+  // if date parsing fails, we have a date in non-rfc format.
+  // remove token from the end and try again
+  while ((($ts = @strtotime($date)) === false) || ($ts < 0)) {
+    $d = explode(' ', $date);
+    array_pop($d);
+    if (!$d) break;
+    $date = implode(' ', $d);
+  }
+
+  return $ts;
+}
+
+
+/**
+ * Convert the given date to a human readable form
+ * This uses the date formatting properties from config
+ *
+ * @param mixed Date representation (string or timestamp)
+ * @param string Date format to use
+ * @return string Formatted date string
+ */
+function format_date($date, $format=NULL)
+{
+  global $RCMAIL, $CONFIG;
+  
+  $ts = NULL;
+
+  if (!empty($date))
+    $ts = rcube_strtotime($date);
+
+  if (empty($ts))
+    return '';
+
+  // get user's timezone offset
+  $tz = $RCMAIL->config->get_timezone();
+
+  // convert time to user's timezone
+  $timestamp = $ts - date('Z', $ts) + ($tz * 3600);
+
+  // get current timestamp in user's timezone
+  $now = time();  // local time
+  $now -= (int)date('Z'); // make GMT time
+  $now += ($tz * 3600); // user's time
+  $now_date = getdate($now);
+
+  $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
+  $week_limit  = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
+
+  // define date format depending on current time
+  if (!$format) {
+    if ($CONFIG['prettydate'] && $timestamp > $today_limit && $timestamp < $now) {
+      $format = $RCMAIL->config->get('date_today', $RCMAIL->config->get('time_format', 'H:i'));
+      $today  = true;
+    }
+    else if ($CONFIG['prettydate'] && $timestamp > $week_limit && $timestamp < $now)
+      $format = $RCMAIL->config->get('date_short', 'D H:i');
+    else
+      $format = $RCMAIL->config->get('date_long', 'Y-m-d H:i');
+  }
+
+  // strftime() format
+  if (preg_match('/%[a-z]+/i', $format)) {
+    $format = strftime($format, $timestamp);
+    return $today ? (rcube_label('today') . ' ' . $format) : $format;
+  }
+
+  // parse format string manually in order to provide localized weekday and month names
+  // an alternative would be to convert the date() format string to fit with strftime()
+  $out = '';
+  for($i=0; $i<strlen($format); $i++) {
+    if ($format[$i]=='\\')  // skip escape chars
+      continue;
+
+    // write char "as-is"
+    if ($format[$i]==' ' || $format{$i-1}=='\\')
+      $out .= $format[$i];
+    // weekday (short)
+    else if ($format[$i]=='D')
+      $out .= rcube_label(strtolower(date('D', $timestamp)));
+    // weekday long
+    else if ($format[$i]=='l')
+      $out .= rcube_label(strtolower(date('l', $timestamp)));
+    // month name (short)
+    else if ($format[$i]=='M')
+      $out .= rcube_label(strtolower(date('M', $timestamp)));
+    // month name (long)
+    else if ($format[$i]=='F')
+      $out .= rcube_label('long'.strtolower(date('M', $timestamp)));
+    else if ($format[$i]=='x')
+      $out .= strftime('%x %X', $timestamp);
+    else
+      $out .= date($format[$i], $timestamp);
+  }
+
+  if ($today) {
+    $label = rcube_label('today');
+    // replcae $ character with "Today" label (#1486120)
+    if (strpos($out, '$') !== false) {
+      $out = preg_replace('/\$/', $label, $out, 1);
+    }
+    else {
+      $out = $label . ' ' . $out;
+    }
+  }
+
+  return $out;
+}
+
+
+/**
+ * Compose a valid representation of name and e-mail address
+ *
+ * @param string E-mail address
+ * @param string Person name
+ * @return string Formatted string
+ */
+function format_email_recipient($email, $name='')
+{
+  if ($name && $name != $email) {
+    // Special chars as defined by RFC 822 need to in quoted string (or escaped).
+    return sprintf('%s <%s>', preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name) ? '"'.addcslashes($name, '"').'"' : $name, trim($email));
+  }
+
+  return trim($email);
+}
+
+
+/**
+ * Return the mailboxlist in HTML
+ *
+ * @param array Named parameters
+ * @return string HTML code for the gui object
+ */
+function rcmail_mailbox_list($attrib)
+{
+  global $RCMAIL;
+  static $a_mailboxes;
+
+  $attrib += array('maxlength' => 100, 'realnames' => false);
+
+  // add some labels to client
+  $RCMAIL->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
+
+  $type = $attrib['type'] ? $attrib['type'] : 'ul';
+  unset($attrib['type']);
+
+  if ($type=='ul' && !$attrib['id'])
+    $attrib['id'] = 'rcmboxlist';
+
+  if (empty($attrib['folder_name']))
+    $attrib['folder_name'] = '*';
+
+  // get mailbox list
+  $mbox_name = $RCMAIL->imap->get_mailbox_name();
+
+  // build the folders tree
+  if (empty($a_mailboxes)) {
+    // get mailbox list
+    $a_folders = $RCMAIL->imap->list_mailboxes('', $attrib['folder_name'], $attrib['folder_filter']);
+    $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
+    $a_mailboxes = array();
+
+    foreach ($a_folders as $folder)
+      rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
+  }
+
+  // allow plugins to alter the folder tree or to localize folder names
+  $hook = $RCMAIL->plugins->exec_hook('render_mailboxlist', array('list' => $a_mailboxes, 'delimiter' => $delimiter));
+
+  if ($type == 'select') {
+    $select = new html_select($attrib);
+
+    // add no-selection option
+    if ($attrib['noselection'])
+      $select->add(rcube_label($attrib['noselection']), '');
+
+    rcmail_render_folder_tree_select($hook['list'], $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
+    $out = $select->show();
+  }
+  else {
+    $js_mailboxlist = array();
+    $out = html::tag('ul', $attrib, rcmail_render_folder_tree_html($hook['list'], $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
+
+    $RCMAIL->output->add_gui_object('mailboxlist', $attrib['id']);
+    $RCMAIL->output->set_env('mailboxes', $js_mailboxlist);
+    $RCMAIL->output->set_env('collapsed_folders', (string)$RCMAIL->config->get('collapsed_folders'));
+  }
+
+  return $out;
+}
+
+
+/**
+ * Return the mailboxlist as html_select object
+ *
+ * @param array Named parameters
+ * @return html_select HTML drop-down object
+ */
+function rcmail_mailbox_select($p = array())
+{
+  global $RCMAIL;
+
+  $p += array('maxlength' => 100, 'realnames' => false);
+  $a_mailboxes = array();
+
+  if (empty($p['folder_name']))
+    $p['folder_name'] = '*';
+
+  if ($p['unsubscribed'])
+    $list = $RCMAIL->imap->list_unsubscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
+  else
+    $list = $RCMAIL->imap->list_mailboxes('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
+
+  $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
+
+  foreach ($list as $folder) {
+    if (empty($p['exceptions']) || !in_array($folder, $p['exceptions']))
+      rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
+  }
+
+  $select = new html_select($p);
+
+  if ($p['noselection'])
+    $select->add($p['noselection'], '');
+
+  rcmail_render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p['exceptions']);
+
+  return $select;
+}
+
+
+/**
+ * Create a hierarchical array of the mailbox list
+ * @access private
+ * @return void
+ */
+function rcmail_build_folder_tree(&$arrFolders, $folder, $delm='/', $path='')
+{
+  global $RCMAIL;
+
+  // Handle namespace prefix
+  $prefix = '';
+  if (!$path) {
+    $n_folder = $folder;
+    $folder = $RCMAIL->imap->mod_mailbox($folder);
+
+    if ($n_folder != $folder) {
+      $prefix = substr($n_folder, 0, -strlen($folder));
+    }
+  }
+
+  $pos = strpos($folder, $delm);
+
+  if ($pos !== false) {
+    $subFolders = substr($folder, $pos+1);
+    $currentFolder = substr($folder, 0, $pos);
+
+    // sometimes folder has a delimiter as the last character
+    if (!strlen($subFolders))
+      $virtual = false;
+    else if (!isset($arrFolders[$currentFolder]))
+      $virtual = true;
+    else
+      $virtual = $arrFolders[$currentFolder]['virtual'];
+  }
+  else {
+    $subFolders = false;
+    $currentFolder = $folder;
+    $virtual = false;
+  }
+
+  $path .= $prefix.$currentFolder;
+
+  if (!isset($arrFolders[$currentFolder])) {
+    // Check \Noselect option (if options are in cache)
+    if (!$virtual && ($opts = $RCMAIL->imap->mailbox_options($path))) {
+      $virtual = in_array('\\Noselect', $opts);
+    }
+
+    $arrFolders[$currentFolder] = array(
+      'id' => $path,
+      'name' => rcube_charset_convert($currentFolder, 'UTF7-IMAP'),
+      'virtual' => $virtual,
+      'folders' => array());
+  }
+  else
+    $arrFolders[$currentFolder]['virtual'] = $virtual;
+
+  if (strlen($subFolders))
+    rcmail_build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
+}
+
+
+/**
+ * Return html for a structured list &lt;ul&gt; for the mailbox tree
+ * @access private
+ * @return string
+ */
+function rcmail_render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel=0)
+{
+  global $RCMAIL, $CONFIG;
+
+  $maxlength = intval($attrib['maxlength']);
+  $realnames = (bool)$attrib['realnames'];
+  $msgcounts = $RCMAIL->imap->get_cache('messagecount');
+
+  $idx = 0;
+  $out = '';
+  foreach ($arrFolders as $key => $folder) {
+    $title = null;
+
+    if (($folder_class = rcmail_folder_classname($folder['id'])) && !$realnames) {
+      $foldername = rcube_label($folder_class);
+    }
+    else {
+      $foldername = $folder['name'];
+
+      // shorten the folder name to a given length
+      if ($maxlength && $maxlength > 1) {
+        $fname = abbreviate_string($foldername, $maxlength);
+        if ($fname != $foldername)
+          $title = $foldername;
+        $foldername = $fname;
+      }
+    }
+
+    // make folder name safe for ids and class names
+    $folder_id = html_identifier($folder['id']);
+    $classes = array('mailbox');
+
+    // set special class for Sent, Drafts, Trash and Junk
+    if ($folder['id'] == $CONFIG['sent_mbox'])
+      $classes[] = 'sent';
+    else if ($folder['id'] == $CONFIG['drafts_mbox'])
+      $classes[] = 'drafts';
+    else if ($folder['id'] == $CONFIG['trash_mbox'])
+      $classes[] = 'trash';
+    else if ($folder['id'] == $CONFIG['junk_mbox'])
+      $classes[] = 'junk';
+    else if ($folder['id'] == 'INBOX')
+      $classes[] = 'inbox';
+    else
+      $classes[] = '_'.asciiwords($folder_class ? $folder_class : strtolower($folder['id']), true);
+
+    if ($folder['id'] == $mbox_name)
+      $classes[] = 'selected';
+
+    $collapsed = strpos($CONFIG['collapsed_folders'], '&'.rawurlencode($folder['id']).'&') !== false;
+    $unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
+
+    if ($folder['virtual'])
+      $classes[] = 'virtual';
+    else if ($unread)
+      $classes[] = 'unread';
+
+    $js_name = JQ($folder['id']);
+    $html_name = Q($foldername) . ($unread ? html::span('unreadcount', " ($unread)") : '');
+    $link_attrib = $folder['virtual'] ? array() : array(
+      'href' => rcmail_url('', array('_mbox' => $folder['id'])),
+      'onclick' => sprintf("return %s.command('list','%s',this)", JS_OBJECT_NAME, $js_name),
+      'rel' => $folder['id'],
+      'title' => $title,
+    );
+
+    $out .= html::tag('li', array(
+        'id' => "rcmli".$folder_id,
+        'class' => join(' ', $classes),
+        'noclose' => true),
+      html::a($link_attrib, $html_name) .
+      (!empty($folder['folders']) ? html::div(array(
+        'class' => ($collapsed ? 'collapsed' : 'expanded'),
+        'style' => "position:absolute",
+        'onclick' => sprintf("%s.command('collapse-folder', '%s')", JS_OBJECT_NAME, $js_name)
+      ), '&nbsp;') : ''));
+
+    $jslist[$folder_id] = array('id' => $folder['id'], 'name' => $foldername, 'virtual' => $folder['virtual']);
+
+    if (!empty($folder['folders'])) {
+      $out .= html::tag('ul', array('style' => ($collapsed ? "display:none;" : null)),
+        rcmail_render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
+    }
+
+    $out .= "</li>\n";
+    $idx++;
+  }
+
+  return $out;
+}
+
+
+/**
+ * Return html for a flat list <select> for the mailbox tree
+ * @access private
+ * @return string
+ */
+function rcmail_render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames=false, $nestLevel=0, $exceptions=array())
+{
+  $out = '';
+
+  foreach ($arrFolders as $key => $folder) {
+    if (empty($exceptions) || !in_array($folder['id'], $exceptions)) {
+      if (!$realnames && ($folder_class = rcmail_folder_classname($folder['id'])))
+        $foldername = rcube_label($folder_class);
+      else {
+        $foldername = $folder['name'];
+
+        // shorten the folder name to a given length
+        if ($maxlength && $maxlength>1)
+          $foldername = abbreviate_string($foldername, $maxlength);
+      }
+
+      $select->add(str_repeat('&nbsp;', $nestLevel*4) . $foldername, $folder['id']);
+    }
+    else if ($nestLevel)
+      continue;
+
+    if (!empty($folder['folders']))
+      $out .= rcmail_render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
+        $select, $realnames, $nestLevel+1, $exceptions);
+  }
+
+  return $out;
+}
+
+
+/**
+ * Return internal name for the given folder if it matches the configured special folders
+ * @access private
+ * @return string
+ */
+function rcmail_folder_classname($folder_id)
+{
+  global $CONFIG;
+
+  if ($folder_id == 'INBOX')
+    return 'inbox';
+
+  // for these mailboxes we have localized labels and css classes
+  foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
+  {
+    if ($folder_id == $CONFIG[$smbx.'_mbox'])
+      return $smbx;
+  }
+}
+
+
+/**
+ * Try to localize the given IMAP folder name.
+ * UTF-7 decode it in case no localized text was found
+ *
+ * @param string Folder name
+ * @return string Localized folder name in UTF-8 encoding
+ */
+function rcmail_localize_foldername($name)
+{
+  if ($folder_class = rcmail_folder_classname($name))
+    return rcube_label($folder_class);
+  else
+    return rcube_charset_convert($name, 'UTF7-IMAP');
+}
+
+
+function rcmail_localize_folderpath($path)
+{
+    global $RCMAIL;
+
+    $protect_folders = $RCMAIL->config->get('protect_default_folders');
+    $default_folders = (array) $RCMAIL->config->get('default_imap_folders');
+    $delimiter       = $RCMAIL->imap->get_hierarchy_delimiter();
+    $path            = explode($delimiter, $path);
+    $result          = array();
+
+    foreach ($path as $idx => $dir) {
+        $directory = implode($delimiter, array_slice($path, 0, $idx+1));
+        if ($protect_folders && in_array($directory, $default_folders)) {
+            unset($result);
+            $result[] = rcmail_localize_foldername($directory);
+        }
+        else {
+            $result[] = rcube_charset_convert($dir, 'UTF7-IMAP');
+        }
+    }
+
+    return implode($delimiter, $result);
+}
+
+
+function rcmail_quota_display($attrib)
+{
+  global $OUTPUT;
+
+  if (!$attrib['id'])
+    $attrib['id'] = 'rcmquotadisplay';
+
+  if(isset($attrib['display']))
+    $_SESSION['quota_display'] = $attrib['display'];
+
+  $OUTPUT->add_gui_object('quotadisplay', $attrib['id']);
+
+  $quota = rcmail_quota_content($attrib);
+
+  $OUTPUT->add_script('rcmail.set_quota('.json_serialize($quota).');', 'docready');
+
+  return html::span($attrib, '');
+}
+
+
+function rcmail_quota_content($attrib=NULL)
+{
+  global $RCMAIL;
+
+  $quota = $RCMAIL->imap->get_quota();
+  $quota = $RCMAIL->plugins->exec_hook('quota', $quota);
+
+  $quota_result = (array) $quota;
+  $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
+
+  if (!$quota['total'] && $RCMAIL->config->get('quota_zero_as_unlimited')) {
+    $quota_result['title'] = rcube_label('unlimited');
+    $quota_result['percent'] = 0;
+  }
+  else if ($quota['total']) {
+    if (!isset($quota['percent']))
+      $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
+
+    $title = sprintf('%s / %s (%.0f%%)',
+        show_bytes($quota['used'] * 1024), show_bytes($quota['total'] * 1024),
+        $quota_result['percent']);
+
+    $quota_result['title'] = $title;
+
+    if ($attrib['width'])
+      $quota_result['width'] = $attrib['width'];
+    if ($attrib['height'])
+      $quota_result['height']	= $attrib['height'];
+  }
+  else {
+    $quota_result['title'] = rcube_label('unknown');
+    $quota_result['percent'] = 0;
+  }
+
+  return $quota_result;
+}
+
+
+/**
+ * Outputs error message according to server error/response codes
+ *
+ * @param string Fallback message label
+ * @param string Fallback message label arguments
+ *
+ * @return void
+ */
+function rcmail_display_server_error($fallback=null, $fallback_args=null)
+{
+    global $RCMAIL;
+
+    $err_code = $RCMAIL->imap->get_error_code();
+    $res_code = $RCMAIL->imap->get_response_code();
+
+    if ($res_code == rcube_imap::NOPERM) {
+        $RCMAIL->output->show_message('errornoperm', 'error');
+    }
+    else if ($res_code == rcube_imap::READONLY) {
+        $RCMAIL->output->show_message('errorreadonly', 'error');
+    }
+    else if ($err_code && ($err_str = $RCMAIL->imap->get_error_str())) {
+        // try to detect access rights problem and display appropriate message
+        if (stripos($err_str, 'Permission denied') !== false)
+            $RCMAIL->output->show_message('errornoperm', 'error');
+        else
+            $RCMAIL->output->show_message('servererrormsg', 'error', array('msg' => $err_str));
+    }
+    else if ($fallback) {
+        $RCMAIL->output->show_message($fallback, 'error', $fallback_args);
+    }
+
+    return true;
+}
+
+
+/**
+ * Output HTML editor scripts
+ *
+ * @param string Editor mode
+ * @return void
+ */
+function rcube_html_editor($mode='')
+{
+  global $RCMAIL, $CONFIG;
+
+  $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode));
+
+  if ($hook['abort'])
+    return;
+
+  $lang = strtolower($_SESSION['language']);
+
+  // TinyMCE uses 'tw' for zh_TW (which is wrong, because tw is a code of Twi language)
+  $lang = ($lang == 'zh_tw') ? 'tw' : substr($lang, 0, 2);
+
+  if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js'))
+    $lang = 'en';
+
+  $RCMAIL->output->include_script('tiny_mce/tiny_mce.js');
+  $RCMAIL->output->include_script('editor.js');
+  $RCMAIL->output->add_script(sprintf("rcmail_editor_init(%s)",
+    json_encode(array(
+        'mode'       => $mode,
+        'skin_path'  => '$__skin_path',
+        'lang'       => $lang,
+        'spellcheck' => intval($CONFIG['enable_spellcheck']),
+        'spelldict'  => intval($CONFIG['spellcheck_dictionary']),
+    ))), 'foot');
+}
+
+
+/**
+ * Replaces TinyMCE's emoticon images with plain-text representation
+ *
+ * @param string HTML content
+ * @return string HTML content
+ */
+function rcmail_replace_emoticons($html)
+{
+  $emoticons = array(
+    '8-)' => 'smiley-cool',
+    ':-#' => 'smiley-foot-in-mouth',
+    ':-*' => 'smiley-kiss',
+    ':-X' => 'smiley-sealed',
+    ':-P' => 'smiley-tongue-out',
+    ':-@' => 'smiley-yell',
+    ":'(" => 'smiley-cry',
+    ':-(' => 'smiley-frown',
+    ':-D' => 'smiley-laughing',
+    ':-)' => 'smiley-smile',
+    ':-S' => 'smiley-undecided',
+    ':-$' => 'smiley-embarassed',
+    'O:-)' => 'smiley-innocent',
+    ':-|' => 'smiley-money-mouth',
+    ':-O' => 'smiley-surprised',
+    ';-)' => 'smiley-wink',
+  );
+
+  foreach ($emoticons as $idx => $file) {
+    // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
+    $search[]  = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
+    $replace[] = $idx;
+  }
+
+  return preg_replace($search, $replace, $html);
+}
+
+
+/**
+ * Send the given message using the configured method
+ *
+ * @param object $message    Reference to Mail_MIME object
+ * @param string $from       Sender address string
+ * @param array  $mailto     Array of recipient address strings
+ * @param array  $smtp_error SMTP error array (reference)
+ * @param string $body_file  Location of file with saved message body (reference),
+ *                           used when delay_file_io is enabled
+ * @param array  $smtp_opts  SMTP options (e.g. DSN request)
+ *
+ * @return boolean Send status.
+ */
+function rcmail_deliver_message(&$message, $from, $mailto, &$smtp_error, &$body_file=null, $smtp_opts=null)
+{
+  global $CONFIG, $RCMAIL;
+
+  $headers = $message->headers();
+
+  // send thru SMTP server using custom SMTP library
+  if ($CONFIG['smtp_server']) {
+    // generate list of recipients
+    $a_recipients = array($mailto);
+
+    if (strlen($headers['Cc']))
+      $a_recipients[] = $headers['Cc'];
+    if (strlen($headers['Bcc']))
+      $a_recipients[] = $headers['Bcc'];
+
+    // clean Bcc from header for recipients
+    $send_headers = $headers;
+    unset($send_headers['Bcc']);
+    // here too, it because txtHeaders() below use $message->_headers not only $send_headers
+    unset($message->_headers['Bcc']);
+
+    $smtp_headers = $message->txtHeaders($send_headers, true);
+
+    if ($message->getParam('delay_file_io')) {
+      // use common temp dir
+      $temp_dir = $RCMAIL->config->get('temp_dir');
+      $body_file = tempnam($temp_dir, 'rcmMsg');
+      if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
+        raise_error(array('code' => 650, 'type' => 'php',
+            'file' => __FILE__, 'line' => __LINE__,
+            'message' => "Could not create message: ".$mime_result->getMessage()),
+            TRUE, FALSE);
+        return false;
+      }
+      $msg_body = fopen($body_file, 'r');
+    } else {
+      $msg_body = $message->get();
+    }
+
+    // send message
+    if (!is_object($RCMAIL->smtp))
+      $RCMAIL->smtp_init(true);
+
+    $sent = $RCMAIL->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $smtp_opts);
+    $smtp_response = $RCMAIL->smtp->get_response();
+    $smtp_error = $RCMAIL->smtp->get_error();
+
+    // log error
+    if (!$sent)
+      raise_error(array('code' => 800, 'type' => 'smtp', 'line' => __LINE__, 'file' => __FILE__,
+                        'message' => "SMTP error: ".join("\n", $smtp_response)), TRUE, FALSE);
+  }
+  // send mail using PHP's mail() function
+  else {
+    // unset some headers because they will be added by the mail() function
+    $headers_enc = $message->headers($headers);
+    $headers_php = $message->_headers;
+    unset($headers_php['To'], $headers_php['Subject']);
+
+    // reset stored headers and overwrite
+    $message->_headers = array();
+    $header_str = $message->txtHeaders($headers_php);
+
+    // #1485779
+    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+      if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
+        $headers_enc['To'] = implode(', ', $m[1]);
+      }
+    }
+
+    $msg_body = $message->get();
+
+    if (PEAR::isError($msg_body))
+      raise_error(array('code' => 650, 'type' => 'php',
+            'file' => __FILE__, 'line' => __LINE__,
+            'message' => "Could not create message: ".$msg_body->getMessage()),
+            TRUE, FALSE);
+    else {
+      $delim   = $RCMAIL->config->header_delimiter();
+      $to      = $headers_enc['To'];
+      $subject = $headers_enc['Subject'];
+      $header_str = rtrim($header_str);
+
+      if ($delim != "\r\n") {
+        $header_str = str_replace("\r\n", $delim, $header_str);
+        $msg_body   = str_replace("\r\n", $delim, $msg_body);
+        $to         = str_replace("\r\n", $delim, $to);
+        $subject    = str_replace("\r\n", $delim, $subject);
+      }
+
+      if (ini_get('safe_mode'))
+        $sent = mail($to, $subject, $msg_body, $header_str);
+      else
+        $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
+    }
+  }
+
+  if ($sent) {
+    $RCMAIL->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
+
+    // remove MDN headers after sending
+    unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
+
+    // get all recipients
+    if ($headers['Cc'])
+      $mailto .= $headers['Cc'];
+    if ($headers['Bcc'])
+      $mailto .= $headers['Bcc'];
+    if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
+      $mailto = implode(', ', array_unique($m[1]));
+
+    if ($CONFIG['smtp_log']) {
+      write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
+        $RCMAIL->user->get_username(),
+        $_SERVER['REMOTE_ADDR'],
+        $mailto,
+        !empty($smtp_response) ? join('; ', $smtp_response) : ''));
+    }
+  }
+
+  if (is_resource($msg_body)) {
+    fclose($msg_body);
+  }
+
+  $message->_headers = array();
+  $message->headers($headers);
+
+  return $sent;
+}
+
+
+// Returns unique Message-ID
+function rcmail_gen_message_id()
+{
+  global $RCMAIL;
+
+  $local_part  = md5(uniqid('rcmail'.mt_rand(),true));
+  $domain_part = $RCMAIL->user->get_username('domain');
+
+  // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
+  if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
+    if (($host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']))
+      && preg_match('/\.[a-z]+$/i', $host)) {
+        $domain_part = $host;
+    }
+    else if (($host = preg_replace('/:[0-9]+$/', '', $_SERVER['SERVER_NAME']))
+      && preg_match('/\.[a-z]+$/i', $host)) {
+        $domain_part = $host;
+    }
+  }
+
+  return sprintf('<%s@%s>', $local_part, $domain_part);
+}
+
+
+// Returns RFC2822 formatted current date in user's timezone
+function rcmail_user_date()
+{
+  global $RCMAIL, $CONFIG;
+
+  // get user's timezone
+  $tz = $RCMAIL->config->get_timezone();
+
+  $date = time() + $tz * 60 * 60;
+  $date = gmdate('r', $date);
+  $tz   = sprintf('%+05d', intval($tz) * 100 + ($tz - intval($tz)) * 60);
+  $date = preg_replace('/[+-][0-9]{4}$/', $tz, $date);
+
+  return $date;
+}
+
+
+/**
+ * Check if working in SSL mode
+ *
+ * @param integer HTTPS port number
+ * @param boolean Enables 'use_https' option checking
+ * @return boolean
+ */
+function rcube_https_check($port=null, $use_https=true)
+{
+  global $RCMAIL;
+
+  if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off')
+    return true;
+  if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
+    return true;
+  if ($port && $_SERVER['SERVER_PORT'] == $port)
+    return true;
+  if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https'))
+    return true;
+
+  return false;
+}
+
+
+/**
+ * For backward compatibility.
+ *
+ * @global rcmail $RCMAIL
+ * @param string $var_name Variable name.
+ * @return void
+ */
+function rcube_sess_unset($var_name=null)
+{
+  global $RCMAIL;
+
+  $RCMAIL->session->remove($var_name);
+}
+
+
+/**
+ * Replaces hostname variables
+ *
+ * @param string $name Hostname
+ * @param string $host Optional IMAP hostname
+ * @return string
+ */
+function rcube_parse_host($name, $host='')
+{
+  // %n - host
+  $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
+  // %d - domain name without first part, e.g. %n=mail.domain.tld, %d=domain.tld
+  $d = preg_replace('/^[^\.]+\./', '', $n);
+  // %h - IMAP host
+  $h = $_SESSION['imap_host'] ? $_SESSION['imap_host'] : $host;
+  // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
+  $z = preg_replace('/^[^\.]+\./', '', $h);
+  // %s - domain name after the '@' from e-mail address provided at login screen. Returns FALSE if an invalid email is provided
+  if ( strpos($name, '%s') !== false ){
+    $user_email = rcube_idn_convert(get_input_value('_user', RCUBE_INPUT_POST), true);
+    if ( preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s) < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false )
+      return false;
+  }
+
+  $name = str_replace(array('%n', '%d', '%h', '%z', '%s'), array($n, $d, $h, $z, $s[2]), $name);
+  return $name;
+}
+
+
+/**
+ * E-mail address validation
+ *
+ * @param string $email Email address
+ * @param boolean $dns_check True to check dns
+ * @return boolean
+ */
+function check_email($email, $dns_check=true)
+{
+  // Check for invalid characters
+  if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email))
+    return false;
+
+  // Check for length limit specified by RFC 5321 (#1486453)
+  if (strlen($email) > 254) 
+    return false;
+
+  $email_array = explode('@', $email);
+
+  // Check that there's one @ symbol
+  if (count($email_array) < 2)
+    return false;
+
+  $domain_part = array_pop($email_array);
+  $local_part = implode('@', $email_array);
+
+  // from PEAR::Validate
+  $regexp = '&^(?:
+	("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| 			 	#1 quoted name
+	([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*)) 	#2 OR dot-atom (RFC5322)
+	$&xi';
+
+  if (!preg_match($regexp, $local_part))
+    return false;
+
+  // Check domain part
+  if (preg_match('/^\[*(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\]*$/', $domain_part))
+    return true; // IP address
+  else {
+    // If not an IP address
+    $domain_array = explode('.', $domain_part);
+    if (sizeof($domain_array) < 2)
+      return false; // Not enough parts to be a valid domain
+
+    foreach ($domain_array as $part)
+      if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part))
+        return false;
+
+    if (!$dns_check || !rcmail::get_instance()->config->get('email_dns_check'))
+      return true;
+
+    if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
+      $lookup = array();
+      @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
+      foreach ($lookup as $line) {
+        if (strpos($line, 'MX preference'))
+          return true;
+      }
+      return false;
+    }
+
+    // find MX record(s)
+    if (getmxrr($domain_part, $mx_records))
+      return true;
+
+    // find any DNS record
+    if (checkdnsrr($domain_part, 'ANY'))
+      return true;
+  }
+
+  return false;
+}
+
+/*
+ * Idn_to_ascii wrapper.
+ * Intl/Idn modules version of this function doesn't work with e-mail address
+ */
+function rcube_idn_to_ascii($str)
+{
+  return rcube_idn_convert($str, true);
+}
+
+/*
+ * Idn_to_ascii wrapper.
+ * Intl/Idn modules version of this function doesn't work with e-mail address
+ */
+function rcube_idn_to_utf8($str)
+{
+  return rcube_idn_convert($str, false);
+}
+
+function rcube_idn_convert($input, $is_utf=false)
+{
+  if ($at = strpos($input, '@')) {
+    $user   = substr($input, 0, $at);
+    $domain = substr($input, $at+1);
+  }
+  else {
+    $domain = $input;
+  }
+
+  $domain = $is_utf ? idn_to_ascii($domain) : idn_to_utf8($domain);
+
+  if ($domain === false) {
+    return '';
+  }
+
+  return $at ? $user . '@' . $domain : $domain;
+}
+
+
+/**
+ * Helper class to turn relative urls into absolute ones
+ * using a predefined base
+ */
+class rcube_base_replacer
+{
+  private $base_url;
+
+  public function __construct($base)
+  {
+    $this->base_url = $base;
+  }
+
+  public function callback($matches)
+  {
+    return $matches[1] . '="' . make_absolute_url($matches[3], $this->base_url) . '"';
+  }
+}
+
+
+/****** debugging and logging functions ********/
+
+/**
+ * Print or write debug messages
+ *
+ * @param mixed Debug message or data
+ * @return void
+ */
+function console()
+{
+    $args = func_get_args();
+
+    if (class_exists('rcmail', false)) {
+        $rcmail = rcmail::get_instance();
+        if (is_object($rcmail->plugins)) {
+            $plugin = $rcmail->plugins->exec_hook('console', array('args' => $args));
+            if ($plugin['abort'])
+                return;
+            $args = $plugin['args'];
+        }
+    }
+
+    $msg = array();
+    foreach ($args as $arg)
+        $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
+
+    write_log('console', join(";\n", $msg));
+}
+
+
+/**
+ * Append a line to a logfile in the logs directory.
+ * Date will be added automatically to the line.
+ *
+ * @param $name name of log file
+ * @param line Line to append
+ * @return void
+ */
+function write_log($name, $line)
+{
+  global $CONFIG, $RCMAIL;
+
+  if (!is_string($line))
+    $line = var_export($line, true);
+ 
+  if (empty($CONFIG['log_date_format']))
+    $CONFIG['log_date_format'] = 'd-M-Y H:i:s O';
+  
+  $date = date($CONFIG['log_date_format']);
+  
+  // trigger logging hook
+  if (is_object($RCMAIL) && is_object($RCMAIL->plugins)) {
+    $log = $RCMAIL->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
+    $name = $log['name'];
+    $line = $log['line'];
+    $date = $log['date'];
+    if ($log['abort'])
+      return true;
+  }
+ 
+  if ($CONFIG['log_driver'] == 'syslog') {
+    $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
+    syslog($prio, $line);
+    return true;
+  }
+  else {
+    $line = sprintf("[%s]: %s\n", $date, $line);
+
+    // log_driver == 'file' is assumed here
+    if (empty($CONFIG['log_dir']))
+      $CONFIG['log_dir'] = INSTALL_PATH.'logs';
+
+    // try to open specific log file for writing
+    $logfile = $CONFIG['log_dir'].'/'.$name;
+    if ($fp = @fopen($logfile, 'a')) {
+      fwrite($fp, $line);
+      fflush($fp);
+      fclose($fp);
+      return true;
+    }
+    else
+      trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
+  }
+
+  return false;
+}
+
+
+/**
+ * Write login data (name, ID, IP address) to the 'userlogins' log file.
+ *
+ * @return void
+ */
+function rcmail_log_login()
+{
+  global $RCMAIL;
+
+  if (!$RCMAIL->config->get('log_logins') || !$RCMAIL->user)
+    return;
+
+  write_log('userlogins', sprintf('Successful login for %s (ID: %d) from %s in session %s',
+    $RCMAIL->user->get_username(), $RCMAIL->user->ID, rcmail_remote_ip(), session_id()));
+}
+
+
+/**
+ * Returns remote IP address and forwarded addresses if found
+ *
+ * @return string Remote IP address(es)
+ */
+function rcmail_remote_ip()
+{
+    $address = $_SERVER['REMOTE_ADDR'];
+
+    // append the NGINX X-Real-IP header, if set
+    if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
+        $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
+    }
+    // append the X-Forwarded-For header, if set
+    if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+        $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
+    }
+
+    if (!empty($remote_ip))
+        $address .= '(' . implode(',', $remote_ip) . ')';
+
+    return $address;
+}
+
+
+/**
+ * Check whether the HTTP referer matches the current request
+ *
+ * @return boolean True if referer is the same host+path, false if not
+ */
+function rcube_check_referer()
+{
+  $uri = parse_url($_SERVER['REQUEST_URI']);
+  $referer = parse_url(rc_request_header('Referer'));
+  return $referer['host'] == rc_request_header('Host') && $referer['path'] == $uri['path'];
+}
+
+
+/**
+ * @access private
+ * @return mixed
+ */
+function rcube_timer()
+{
+  return microtime(true);
+}
+
+
+/**
+ * @access private
+ * @return void
+ */
+function rcube_print_time($timer, $label='Timer', $dest='console')
+{
+  static $print_count = 0;
+
+  $print_count++;
+  $now = rcube_timer();
+  $diff = $now-$timer;
+
+  if (empty($label))
+    $label = 'Timer '.$print_count;
+
+  write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
+}
+
+
+/**
+ * Throw system error and show error page
+ *
+ * @param array Named parameters
+ *  - code: Error code (required)
+ *  - type: Error type [php|db|imap|javascript] (required)
+ *  - message: Error message
+ *  - file: File where error occured
+ *  - line: Line where error occured
+ * @param boolean True to log the error
+ * @param boolean Terminate script execution
+ */
+// may be defined in Installer
+if (!function_exists('raise_error')) {
+function raise_error($arg=array(), $log=false, $terminate=false)
+{
+    global $__page_content, $CONFIG, $OUTPUT, $ERROR_CODE, $ERROR_MESSAGE;
+
+    // report bug (if not incompatible browser)
+    if ($log && $arg['type'] && $arg['message'])
+        rcube_log_bug($arg);
+
+    // display error page and terminate script
+    if ($terminate) {
+        $ERROR_CODE = $arg['code'];
+        $ERROR_MESSAGE = $arg['message'];
+        include INSTALL_PATH . 'program/steps/utils/error.inc';
+        exit;
+    }
+}
+}
+
+
+/**
+ * Report error according to configured debug_level
+ *
+ * @param array Named parameters
+ * @return void
+ * @see raise_error()
+ */
+function rcube_log_bug($arg_arr)
+{
+    global $CONFIG;
+
+    $program = strtoupper($arg_arr['type']);
+    $level   = $CONFIG['debug_level'];
+
+    // disable errors for ajax requests, write to log instead (#1487831)
+    if (($level & 4) && !empty($_REQUEST['_remote'])) {
+        $level = ($level ^ 4) | 1;
+    }
+
+    // write error to local log file
+    if ($level & 1) {
+        $post_query = ($_SERVER['REQUEST_METHOD'] == 'POST' ? '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']) : '');
+        $log_entry = sprintf("%s Error: %s%s (%s %s)",
+            $program,
+            $arg_arr['message'],
+            $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
+            $_SERVER['REQUEST_METHOD'],
+            $_SERVER['REQUEST_URI'] . $post_query);
+
+        if (!write_log('errors', $log_entry)) {
+            // send error to PHPs error handler if write_log didn't succeed
+            trigger_error($arg_arr['message']);
+        }
+    }
+
+    // report the bug to the global bug reporting system
+    if ($level & 2) {
+        // TODO: Send error via HTTP
+    }
+
+    // show error if debug_mode is on
+    if ($level & 4) {
+        print "<b>$program Error";
+
+        if (!empty($arg_arr['file']) && !empty($arg_arr['line']))
+            print " in $arg_arr[file] ($arg_arr[line])";
+
+        print ':</b>&nbsp;';
+        print nl2br($arg_arr['message']);
+        print '<br />';
+        flush();
+    }
+}
+
+function rcube_upload_progress()
+{
+    global $RCMAIL;
+
+    $prefix = ini_get('apc.rfc1867_prefix');
+    $params = array(
+        'action' => $RCMAIL->action,
+        'name' => get_input_value('_progress', RCUBE_INPUT_GET),
+    );
+
+    if (function_exists('apc_fetch')) {
+        $status = apc_fetch($prefix . $params['name']);
+
+        if (!empty($status)) {
+            $status['percent'] = round($status['current']/$status['total']*100);
+            $params = array_merge($status, $params);
+        }
+    }
+
+    if (isset($params['percent']))
+        $params['text'] = rcube_label(array('name' => 'uploadprogress', 'vars' => array(
+            'percent' => $params['percent'] . '%',
+            'current' => show_bytes($params['current']),
+            'total'   => show_bytes($params['total'])
+        )));
+
+    $RCMAIL->output->command('upload_progress_update', $params);
+    $RCMAIL->output->send();
+}
+
+function rcube_upload_init()
+{
+    global $RCMAIL;
+
+    // Enable upload progress bar
+    if (($seconds = $RCMAIL->config->get('upload_progress')) && ini_get('apc.rfc1867')) {
+        if ($field_name = ini_get('apc.rfc1867_name')) {
+            $RCMAIL->output->set_env('upload_progress_name', $field_name);
+            $RCMAIL->output->set_env('upload_progress_time', (int) $seconds);
+        }
+    }
+
+    // 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;
+
+    $RCMAIL->output->set_env('max_filesize', $max_filesize);
+    $max_filesize = show_bytes($max_filesize);
+    $RCMAIL->output->set_env('filesizeerror', rcube_label(array(
+        'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
+
+    return $max_filesize;
+}
+
+/**
+ * Initializes client-side autocompletion
+ */
+function rcube_autocomplete_init()
+{
+    global $RCMAIL;
+    static $init;
+
+    if ($init)
+        return;
+
+    $init = 1;
+
+    if (($threads = (int)$RCMAIL->config->get('autocomplete_threads')) > 0) {
+      $book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql');
+      if (count($book_types) > 1) {
+        $RCMAIL->output->set_env('autocomplete_threads', $threads);
+        $RCMAIL->output->set_env('autocomplete_sources', $book_types);
+      }
+    }
+
+    $RCMAIL->output->set_env('autocomplete_max', (int)$RCMAIL->config->get('autocomplete_max', 15));
+    $RCMAIL->output->set_env('autocomplete_min_length', $RCMAIL->config->get('autocomplete_min_length'));
+    $RCMAIL->output->add_label('autocompletechars', 'autocompletemore');
+}
Index: /branches/devel-composer/program/include/rcmail.php
===================================================================
--- /branches/devel-composer/program/include/rcmail.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcmail.php	(revision 5386)
@@ -0,0 +1,1716 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcmail.php                                            |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
+ | Copyright (C) 2011, Kolab Systems AG                                  |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Application class providing core functions and holding              |
+ |   instances of all 'global' objects like db- and imap-connections     |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Application class of Roundcube Webmail
+ * implemented as singleton
+ *
+ * @package Core
+ */
+class rcmail
+{
+  /**
+   * Main tasks.
+   *
+   * @var array
+   */
+  static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
+
+  /**
+   * Singleton instace of rcmail
+   *
+   * @var rcmail
+   */
+  static private $instance;
+
+  /**
+   * Stores instance of rcube_config.
+   *
+   * @var rcube_config
+   */
+  public $config;
+
+  /**
+   * Stores rcube_user instance.
+   *
+   * @var rcube_user
+   */
+  public $user;
+
+  /**
+   * Instace of database class.
+   *
+   * @var rcube_mdb2
+   */
+  public $db;
+
+  /**
+   * Instace of Memcache class.
+   *
+   * @var rcube_mdb2
+   */
+  public $memcache;
+
+  /**
+   * Instace of rcube_session class.
+   *
+   * @var rcube_session
+   */
+  public $session;
+
+  /**
+   * Instance of rcube_smtp class.
+   *
+   * @var rcube_smtp
+   */
+  public $smtp;
+
+  /**
+   * Instance of rcube_imap class.
+   *
+   * @var rcube_imap
+   */
+  public $imap;
+
+  /**
+   * Instance of rcube_template class.
+   *
+   * @var rcube_template
+   */
+  public $output;
+
+  /**
+   * Instance of rcube_plugin_api.
+   *
+   * @var rcube_plugin_api
+   */
+  public $plugins;
+
+  /**
+   * Current task.
+   *
+   * @var string
+   */
+  public $task;
+
+  /**
+   * Current action.
+   *
+   * @var string
+   */
+  public $action = '';
+  public $comm_path = './';
+
+  private $texts;
+  private $address_books = array();
+  private $caches = array();
+  private $action_map = array();
+  private $shutdown_functions = array();
+
+
+  /**
+   * This implements the 'singleton' design pattern
+   *
+   * @return rcmail The one and only instance
+   */
+  static function get_instance()
+  {
+    if (!self::$instance) {
+      self::$instance = new rcmail();
+      self::$instance->startup();  // init AFTER object was linked with self::$instance
+    }
+
+    return self::$instance;
+  }
+
+
+  /**
+   * Private constructor
+   */
+  private function __construct()
+  {
+    // load configuration
+    $this->config = new rcube_config();
+
+    register_shutdown_function(array($this, 'shutdown'));
+  }
+
+
+  /**
+   * Initial startup function
+   * to register session, create database and imap connections
+   *
+   * @todo Remove global vars $DB, $USER
+   */
+  private function startup()
+  {
+    // initialize syslog
+    if ($this->config->get('log_driver') == 'syslog') {
+      $syslog_id = $this->config->get('syslog_id', 'roundcube');
+      $syslog_facility = $this->config->get('syslog_facility', LOG_USER);
+      openlog($syslog_id, LOG_ODELAY, $syslog_facility);
+    }
+
+    // connect to database
+    $GLOBALS['DB'] = $this->get_dbh();
+
+    // start session
+    $this->session_init();
+
+    // create user object
+    $this->set_user(new rcube_user($_SESSION['user_id']));
+
+    // configure session (after user config merge!)
+    $this->session_configure();
+
+    // set task and action properties
+    $this->set_task(get_input_value('_task', RCUBE_INPUT_GPC));
+    $this->action = asciiwords(get_input_value('_action', RCUBE_INPUT_GPC));
+
+    // reset some session parameters when changing task
+    if ($this->task != 'utils') {
+      if ($this->session && $_SESSION['task'] != $this->task)
+        $this->session->remove('page');
+      // set current task to session
+      $_SESSION['task'] = $this->task;
+    }
+
+    // init output class
+    if (!empty($_REQUEST['_remote']))
+      $GLOBALS['OUTPUT'] = $this->json_init();
+    else
+      $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
+
+    // create plugin API and load plugins
+    $this->plugins = rcube_plugin_api::get_instance();
+
+    // init plugins
+    $this->plugins->init();
+  }
+
+
+  /**
+   * Setter for application task
+   *
+   * @param string Task to set
+   */
+  public function set_task($task)
+  {
+    $task = asciiwords($task);
+
+    if ($this->user && $this->user->ID)
+      $task = !$task ? 'mail' : $task;
+    else
+      $task = 'login';
+
+    $this->task = $task;
+    $this->comm_path = $this->url(array('task' => $this->task));
+
+    if ($this->output)
+      $this->output->set_env('task', $this->task);
+  }
+
+
+  /**
+   * Setter for system user object
+   *
+   * @param rcube_user Current user instance
+   */
+  public function set_user($user)
+  {
+    if (is_object($user)) {
+      $this->user = $user;
+      $GLOBALS['USER'] = $this->user;
+
+      // overwrite config with user preferences
+      $this->config->set_user_prefs((array)$this->user->get_prefs());
+    }
+
+    $_SESSION['language'] = $this->user->language = $this->language_prop($this->config->get('language', $_SESSION['language']));
+
+    // set localization
+    setlocale(LC_ALL, $_SESSION['language'] . '.utf8', 'en_US.utf8');
+
+    // workaround for http://bugs.php.net/bug.php?id=18556
+    if (in_array($_SESSION['language'], array('tr_TR', 'ku', 'az_AZ')))
+      setlocale(LC_CTYPE, 'en_US' . '.utf8');
+  }
+
+
+  /**
+   * Check the given string and return a valid language code
+   *
+   * @param string Language code
+   * @return string Valid language code
+   */
+  private function language_prop($lang)
+  {
+    static $rcube_languages, $rcube_language_aliases;
+
+    // user HTTP_ACCEPT_LANGUAGE if no language is specified
+    if (empty($lang) || $lang == 'auto') {
+       $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
+       $lang = str_replace('-', '_', $accept_langs[0]);
+     }
+
+    if (empty($rcube_languages)) {
+      @include(INSTALL_PATH . 'program/localization/index.inc');
+    }
+
+    // check if we have an alias for that language
+    if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) {
+      $lang = $rcube_language_aliases[$lang];
+    }
+    // try the first two chars
+    else if (!isset($rcube_languages[$lang])) {
+      $short = substr($lang, 0, 2);
+
+      // check if we have an alias for the short language code
+      if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) {
+        $lang = $rcube_language_aliases[$short];
+      }
+      // expand 'nn' to 'nn_NN'
+      else if (!isset($rcube_languages[$short])) {
+        $lang = $short.'_'.strtoupper($short);
+      }
+    }
+
+    if (!isset($rcube_languages[$lang]) || !is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
+      $lang = 'en_US';
+    }
+
+    return $lang;
+  }
+
+
+  /**
+   * Get the current database connection
+   *
+   * @return rcube_mdb2  Database connection object
+   */
+  public function get_dbh()
+  {
+    if (!$this->db) {
+      $config_all = $this->config->all();
+
+      $this->db = new rcube_mdb2($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']);
+      $this->db->sqlite_initials = INSTALL_PATH . 'SQL/sqlite.initial.sql';
+      $this->db->set_debug((bool)$config_all['sql_debug']);
+    }
+
+    return $this->db;
+  }
+  
+  
+  /**
+   * Get global handle for memcache access
+   *
+   * @return object Memcache
+   */
+  public function get_memcache()
+  {
+    if (!isset($this->memcache)) {
+      // no memcache support in PHP
+      if (!class_exists('Memcache')) {
+        $this->memcache = false;
+        return false;
+      }
+
+      $this->memcache = new Memcache;
+      $this->mc_available = 0;
+      
+      // add alll configured hosts to pool
+      $pconnect = $this->config->get('memcache_pconnect', true);
+      foreach ($this->config->get('memcache_hosts', array()) as $host) {
+        list($host, $port) = explode(':', $host);
+        if (!$port) $port = 11211;
+        $this->mc_available += intval($this->memcache->addServer($host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure')));
+      }
+      
+      // test connection and failover (will result in $this->mc_available == 0 on complete failure)
+      $this->memcache->increment('__CONNECTIONTEST__', 1);  // NOP if key doesn't exist
+
+      if (!$this->mc_available)
+        $this->memcache = false;
+    }
+
+    return $this->memcache;
+  }
+  
+  /**
+   * Callback for memcache failure
+   */
+  public function memcache_failure($host, $port)
+  {
+    static $seen = array();
+    
+    // only report once
+    if (!$seen["$host:$port"]++) {
+      $this->mc_available--;
+      raise_error(array('code' => 604, 'type' => 'db',
+        'line' => __LINE__, 'file' => __FILE__,
+        'message' => "Memcache failure on host $host:$port"),
+        true, false);
+    }
+  }
+
+
+  /**
+   * Initialize and get cache object
+   *
+   * @param string $name   Cache identifier
+   * @param string $type   Cache type ('db', 'apc' or 'memcache')
+   * @param int    $ttl    Expiration time for cache items in seconds
+   * @param bool   $packed Enables/disables data serialization
+   *
+   * @return rcube_cache Cache object
+   */
+  public function get_cache($name, $type='db', $ttl=0, $packed=true)
+  {
+    if (!isset($this->caches[$name])) {
+      $this->caches[$name] = new rcube_cache($type, $_SESSION['user_id'], $name, $ttl, $packed);
+    }
+
+    return $this->caches[$name];
+  }
+
+
+  /**
+   * Return instance of the internal address book class
+   *
+   * @param string  Address book identifier
+   * @param boolean True if the address book needs to be writeable
+   *
+   * @return rcube_contacts Address book object
+   */
+  public function get_address_book($id, $writeable = false)
+  {
+    $contacts    = null;
+    $ldap_config = (array)$this->config->get('ldap_public');
+    $abook_type  = strtolower($this->config->get('address_book_type'));
+
+    // 'sql' is the alias for '0' used by autocomplete
+    if ($id == 'sql')
+        $id = '0';
+
+    // use existing instance
+    if (isset($this->address_books[$id]) && is_object($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']));
+    }
+    else if ($id === '0') {
+      $contacts = new rcube_contacts($this->db, $this->user->ID);
+    }
+    else {
+      $plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable));
+
+      // plugin returned instance of a rcube_addressbook
+      if ($plugin['instance'] instanceof rcube_addressbook) {
+        $contacts = $plugin['instance'];
+      }
+      // get first source from the list
+      else if (!$id) {
+        $source = reset($this->get_address_sources($writeable));
+        if (!empty($source)) {
+          $contacts = $this->get_address_book($source['id']);
+          if ($contacts)
+            $id = $source['id'];
+        }
+      }
+    }
+
+    if (!$contacts) {
+      raise_error(array(
+        'code' => 700, 'type' => 'php',
+        'file' => __FILE__, 'line' => __LINE__,
+        'message' => "Addressbook source ($id) not found!"),
+        true, true);
+    }
+
+    // add to the 'books' array for shutdown function
+    if (!isset($this->address_books[$id]))
+      $this->address_books[$id] = $contacts;
+
+    return $contacts;
+  }
+
+
+  /**
+   * Return address books list
+   *
+   * @param boolean True if the address book needs to be writeable
+   *
+   * @return array  Address books array
+   */
+  public function get_address_sources($writeable = false)
+  {
+    $abook_type = strtolower($this->config->get('address_book_type'));
+    $ldap_config = $this->config->get('ldap_public');
+    $autocomplete = (array) $this->config->get('autocomplete_addressbooks');
+    $list = array();
+
+    // We are using the DB address book
+    if ($abook_type != 'ldap') {
+      if (!isset($this->address_books['0']))
+        $this->address_books['0'] = new rcube_contacts($this->db, $this->user->ID);
+      $list['0'] = array(
+        'id'       => '0',
+        'name'     => rcube_label('personaladrbook'),
+        'groups'   => $this->address_books['0']->groups,
+        'readonly' => $this->address_books['0']->readonly,
+        'autocomplete' => in_array('sql', $autocomplete),
+        'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
+      );
+    }
+
+    if ($ldap_config) {
+      $ldap_config = (array) $ldap_config;
+      foreach ($ldap_config as $id => $prop)
+        $list[$id] = array(
+          'id'       => $id,
+          'name'     => $prop['name'],
+          'groups'   => is_array($prop['groups']),
+          'readonly' => !$prop['writable'],
+          'hidden'   => $prop['hidden'],
+          'autocomplete' => in_array($id, $autocomplete)
+        );
+    }
+
+    $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
+    $list = $plugin['sources'];
+
+    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;
+  }
+
+
+  /**
+   * Init output object for GUI and add common scripts.
+   * This will instantiate a rcmail_template object and set
+   * environment vars according to the current session and configuration
+   *
+   * @param boolean True if this request is loaded in a (i)frame
+   * @return rcube_template Reference to HTML output object
+   */
+  public function load_gui($framed = false)
+  {
+    // init output page
+    if (!($this->output instanceof rcube_template))
+      $this->output = new rcube_template($this->task, $framed);
+
+    // set keep-alive/check-recent interval
+    if ($this->session && ($keep_alive = $this->session->get_keep_alive())) {
+      $this->output->set_env('keep_alive', $keep_alive);
+    }
+
+    if ($framed) {
+      $this->comm_path .= '&_framed=1';
+      $this->output->set_env('framed', true);
+    }
+
+    $this->output->set_env('task', $this->task);
+    $this->output->set_env('action', $this->action);
+    $this->output->set_env('comm_path', $this->comm_path);
+    $this->output->set_charset(RCMAIL_CHARSET);
+
+    // add some basic labels to client
+    $this->output->add_label('loading', 'servererror');
+
+    return $this->output;
+  }
+
+
+  /**
+   * Create an output object for JSON responses
+   *
+   * @return rcube_json_output Reference to JSON output object
+   */
+  public function json_init()
+  {
+    if (!($this->output instanceof rcube_json_output))
+      $this->output = new rcube_json_output($this->task);
+
+    return $this->output;
+  }
+
+
+  /**
+   * Create SMTP object and connect to server
+   *
+   * @param boolean True if connection should be established
+   */
+  public function smtp_init($connect = false)
+  {
+    $this->smtp = new rcube_smtp();
+
+    if ($connect)
+      $this->smtp->connect();
+  }
+
+
+  /**
+   * Create global IMAP object and connect to server
+   *
+   * @param boolean True if connection should be established
+   * @todo Remove global $IMAP
+   */
+  public function imap_init($connect = false)
+  {
+    // already initialized
+    if (is_object($this->imap))
+      return;
+
+    $this->imap = new rcube_imap();
+    $this->imap->debug_level = $this->config->get('debug_level');
+    $this->imap->skip_deleted = $this->config->get('skip_deleted');
+
+    // enable caching of imap data
+    $imap_cache = $this->config->get('imap_cache');
+    $messages_cache = $this->config->get('messages_cache');
+    // for backward compatybility
+    if ($imap_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
+        $imap_cache     = 'db';
+        $messages_cache = true;
+    }
+    if ($imap_cache)
+        $this->imap->set_caching($imap_cache);
+    if ($messages_cache)
+        $this->imap->set_messages_caching(true);
+
+    // set pagesize from config
+    $this->imap->set_pagesize($this->config->get('pagesize', 50));
+
+    // Setting root and delimiter before establishing the connection
+    // can save time detecting them using NAMESPACE and LIST
+    $options = array(
+      'auth_type'   => $this->config->get('imap_auth_type', 'check'),
+      'auth_cid'    => $this->config->get('imap_auth_cid'),
+      'auth_pw'     => $this->config->get('imap_auth_pw'),
+      'debug'       => (bool) $this->config->get('imap_debug', 0),
+      'force_caps'  => (bool) $this->config->get('imap_force_caps'),
+      'timeout'     => (int) $this->config->get('imap_timeout', 0),
+    );
+
+    $this->imap->set_options($options);
+
+    // set global object for backward compatibility
+    $GLOBALS['IMAP'] = $this->imap;
+
+    $hook = $this->plugins->exec_hook('imap_init', array('fetch_headers' => $this->imap->fetch_add_headers));
+    if ($hook['fetch_headers'])
+      $this->imap->fetch_add_headers = $hook['fetch_headers'];
+
+    // support this parameter for backward compatibility but log warning
+    if ($connect) {
+      $this->imap_connect();
+      raise_error(array(
+        'code' => 800, 'type' => 'imap',
+        'file' => __FILE__, 'line' => __LINE__,
+        'message' => "rcube::imap_init(true) is deprecated, use rcube::imap_connect() instead"),
+        true, false);
+    }
+  }
+
+
+  /**
+   * Connect to IMAP server with stored session data
+   *
+   * @return bool True on success, false on error
+   */
+  public function imap_connect()
+  {
+    if (!$this->imap)
+      $this->imap_init();
+
+    if ($_SESSION['imap_host'] && !$this->imap->conn->connected()) {
+      if (!$this->imap->connect($_SESSION['imap_host'], $_SESSION['username'], $this->decrypt($_SESSION['password']), $_SESSION['imap_port'], $_SESSION['imap_ssl'])) {
+        if ($this->output)
+          $this->output->show_message($this->imap->get_error_code() == -1 ? 'imaperror' : 'sessionerror', 'error');
+      }
+      else {
+        $this->set_imap_prop();
+        return $this->imap->conn;
+      }
+    }
+
+    return false;
+  }
+
+
+  /**
+   * Create session object and start the session.
+   */
+  public function session_init()
+  {
+    // session started (Installer?)
+    if (session_id())
+      return;
+
+    // set session domain
+    if ($domain = $this->config->get('session_domain')) {
+      ini_set('session.cookie_domain', $domain);
+    }
+    // set session garbage collecting time according to session_lifetime
+    $lifetime = $this->config->get('session_lifetime', 0) * 60;
+    if ($lifetime) {
+      ini_set('session.gc_maxlifetime', $lifetime * 2);
+    }
+
+    ini_set('session.cookie_secure', rcube_https_check());
+    ini_set('session.name', 'roundcube_sessid');
+    ini_set('session.use_cookies', 1);
+    ini_set('session.use_only_cookies', 1);
+    ini_set('session.serialize_handler', 'php');
+
+    // use database for storing session data
+    $this->session = new rcube_session($this->get_dbh(), $this->config);
+
+    $this->session->register_gc_handler('rcmail_temp_gc');
+    if ($this->config->get('enable_caching'))
+      $this->session->register_gc_handler('rcmail_cache_gc');
+
+    // start PHP session (if not in CLI mode)
+    if ($_SERVER['REMOTE_ADDR'])
+      session_start();
+
+    // set initial session vars
+    if (!$_SESSION['user_id'])
+      $_SESSION['temp'] = true;
+  }
+
+
+  /**
+   * Configure session object internals
+   */
+  public function session_configure()
+  {
+    if (!$this->session)
+      return;
+
+    $lifetime = $this->config->get('session_lifetime', 0) * 60;
+
+    // set keep-alive/check-recent interval
+    if ($keep_alive = $this->config->get('keep_alive')) {
+      // be sure that it's less than session lifetime
+      if ($lifetime)
+        $keep_alive = min($keep_alive, $lifetime - 30);
+      $keep_alive = max(60, $keep_alive);
+      $this->session->set_keep_alive($keep_alive);
+    }
+
+    $this->session->set_secret($this->config->get('des_key') . $_SERVER['HTTP_USER_AGENT']);
+    $this->session->set_ip_check($this->config->get('ip_check'));
+  }
+
+
+  /**
+   * Perfom login to the IMAP server and to the webmail service.
+   * This will also create a new user entry if auto_create_user is configured.
+   *
+   * @param string IMAP user name
+   * @param string IMAP password
+   * @param string IMAP host
+   * @return boolean True on success, False on failure
+   */
+  function login($username, $pass, $host=NULL)
+  {
+    $user = NULL;
+    $config = $this->config->all();
+
+    if (!$host)
+      $host = $config['default_host'];
+
+    // Validate that selected host is in the list of configured hosts
+    if (is_array($config['default_host'])) {
+      $allowed = false;
+      foreach ($config['default_host'] as $key => $host_allowed) {
+        if (!is_numeric($key))
+          $host_allowed = $key;
+        if ($host == $host_allowed) {
+          $allowed = true;
+          break;
+        }
+      }
+      if (!$allowed)
+        return false;
+      }
+    else if (!empty($config['default_host']) && $host != rcube_parse_host($config['default_host']))
+      return false;
+
+    // parse $host URL
+    $a_host = parse_url($host);
+    if ($a_host['host']) {
+      $host = $a_host['host'];
+      $imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
+      if (!empty($a_host['port']))
+        $imap_port = $a_host['port'];
+      else if ($imap_ssl && $imap_ssl != 'tls' && (!$config['default_port'] || $config['default_port'] == 143))
+        $imap_port = 993;
+    }
+
+    $imap_port = $imap_port ? $imap_port : $config['default_port'];
+
+    /* Modify username with domain if required
+       Inspired by Marco <P0L0_notspam_binware.org>
+    */
+    // Check if we need to add domain
+    if (!empty($config['username_domain']) && strpos($username, '@') === false) {
+      if (is_array($config['username_domain']) && isset($config['username_domain'][$host]))
+        $username .= '@'.rcube_parse_host($config['username_domain'][$host], $host);
+      else if (is_string($config['username_domain']))
+        $username .= '@'.rcube_parse_host($config['username_domain'], $host);
+    }
+
+    // Convert username to lowercase. If IMAP backend
+    // is case-insensitive we need to store always the same username (#1487113)
+    if ($config['login_lc']) {
+      $username = mb_strtolower($username);
+    }
+
+    // try to resolve email address from virtuser table
+    if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) {
+      $username = $virtuser;
+    }
+
+    // Here we need IDNA ASCII
+    // Only rcube_contacts class is using domain names in Unicode
+    $host = rcube_idn_to_ascii($host);
+    if (strpos($username, '@')) {
+      // lowercase domain name
+      list($local, $domain) = explode('@', $username);
+      $username = $local . '@' . mb_strtolower($domain);
+      $username = rcube_idn_to_ascii($username);
+    }
+
+    // user already registered -> overwrite username
+    if ($user = rcube_user::query($username, $host))
+      $username = $user->data['username'];
+
+    if (!$this->imap)
+      $this->imap_init();
+
+    // try IMAP login
+    if (!($imap_login = $this->imap->connect($host, $username, $pass, $imap_port, $imap_ssl))) {
+      // try with lowercase
+      $username_lc = mb_strtolower($username);
+      if ($username_lc != $username) {
+        // try to find user record again -> overwrite username
+        if (!$user && ($user = rcube_user::query($username_lc, $host)))
+          $username_lc = $user->data['username'];
+
+        if ($imap_login = $this->imap->connect($host, $username_lc, $pass, $imap_port, $imap_ssl))
+          $username = $username_lc;
+      }
+    }
+
+    // exit if IMAP login failed
+    if (!$imap_login)
+      return false;
+
+    // user already registered -> update user's record
+    if (is_object($user)) {
+      // update last login timestamp
+      $user->touch();
+    }
+    // create new system user
+    else if ($config['auto_create_user']) {
+      if ($created = rcube_user::create($username, $host)) {
+        $user = $created;
+      }
+      else {
+        raise_error(array(
+          'code' => 620, 'type' => 'php',
+          'file' => __FILE__, 'line' => __LINE__,
+          'message' => "Failed to create a user record. Maybe aborted by a plugin?"
+          ), true, false);
+      }
+    }
+    else {
+      raise_error(array(
+        'code' => 621, 'type' => 'php',
+        'file' => __FILE__, 'line' => __LINE__,
+        'message' => "Access denied for new user $username. 'auto_create_user' is disabled"
+        ), true, false);
+    }
+
+    // login succeeded
+    if (is_object($user) && $user->ID) {
+      // Configure environment
+      $this->set_user($user);
+      $this->set_imap_prop();
+      $this->session_configure();
+
+      // fix some old settings according to namespace prefix
+      $this->fix_namespace_settings($user);
+
+      // create default folders on first login
+      if ($config['create_default_folders'] && (!empty($created) || empty($user->data['last_login']))) {
+        $this->imap->create_default_folders();
+      }
+
+      // set session vars
+      $_SESSION['user_id']   = $user->ID;
+      $_SESSION['username']  = $user->data['username'];
+      $_SESSION['imap_host'] = $host;
+      $_SESSION['imap_port'] = $imap_port;
+      $_SESSION['imap_ssl']  = $imap_ssl;
+      $_SESSION['password']  = $this->encrypt($pass);
+      $_SESSION['login_time'] = mktime();
+
+      if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_')
+        $_SESSION['timezone'] = floatval($_REQUEST['_timezone']);
+      if (isset($_REQUEST['_dstactive']) && $_REQUEST['_dstactive'] != '_default_')
+        $_SESSION['dst_active'] = intval($_REQUEST['_dstactive']);
+
+      // force reloading complete list of subscribed mailboxes
+      $this->imap->clear_cache('mailboxes', true);
+
+      return true;
+    }
+
+    return false;
+  }
+
+
+  /**
+   * Set root dir and last stored mailbox
+   * This must be done AFTER connecting to the server!
+   */
+  public function set_imap_prop()
+  {
+    $this->imap->set_charset($this->config->get('default_charset', RCMAIL_CHARSET));
+
+    if ($default_folders = $this->config->get('default_imap_folders')) {
+      $this->imap->set_default_mailboxes($default_folders);
+    }
+    if (isset($_SESSION['mbox'])) {
+      $this->imap->set_mailbox($_SESSION['mbox']);
+    }
+    if (isset($_SESSION['page'])) {
+      $this->imap->set_page($_SESSION['page']);
+    }
+  }
+
+
+  /**
+   * Auto-select IMAP host based on the posted login information
+   *
+   * @return string Selected IMAP host
+   */
+  public function autoselect_host()
+  {
+    $default_host = $this->config->get('default_host');
+    $host = null;
+
+    if (is_array($default_host)) {
+      $post_host = get_input_value('_host', RCUBE_INPUT_POST);
+
+      // direct match in default_host array
+      if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) {
+        $host = $post_host;
+      }
+
+      // try to select host by mail domain
+      list($user, $domain) = explode('@', get_input_value('_user', RCUBE_INPUT_POST));
+      if (!empty($domain)) {
+        foreach ($default_host as $imap_host => $mail_domains) {
+          if (is_array($mail_domains) && in_array($domain, $mail_domains)) {
+            $host = $imap_host;
+            break;
+          }
+        }
+      }
+
+      // take the first entry if $host is still an array
+      if (empty($host)) {
+        $host = array_shift($default_host);
+      }
+    }
+    else if (empty($default_host)) {
+      $host = get_input_value('_host', RCUBE_INPUT_POST);
+    }
+    else
+      $host = rcube_parse_host($default_host);
+
+    return $host;
+  }
+
+
+  /**
+   * Get localized text in the desired language
+   *
+   * @param mixed   $attrib  Named parameters array or label name
+   * @param string  $domain  Label domain (plugin) name
+   *
+   * @return string Localized text
+   */
+  public function gettext($attrib, $domain=null)
+  {
+    // load localization files if not done yet
+    if (empty($this->texts))
+      $this->load_language();
+
+    // extract attributes
+    if (is_string($attrib))
+      $attrib = array('name' => $attrib);
+
+    $nr = is_numeric($attrib['nr']) ? $attrib['nr'] : 1;
+    $name = $attrib['name'] ? $attrib['name'] : '';
+
+    // attrib contain text values: use them from now
+    if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us']))
+        $this->texts[$name] = $setval;
+
+    // check for text with domain
+    if ($domain && ($text_item = $this->texts[$domain.'.'.$name]))
+      ;
+    // text does not exist
+    else if (!($text_item = $this->texts[$name])) {
+      return "[$name]";
+    }
+
+    // make text item array
+    $a_text_item = is_array($text_item) ? $text_item : array('single' => $text_item);
+
+    // decide which text to use
+    if ($nr == 1) {
+      $text = $a_text_item['single'];
+    }
+    else if ($nr > 0) {
+      $text = $a_text_item['multiple'];
+    }
+    else if ($nr == 0) {
+      if ($a_text_item['none'])
+        $text = $a_text_item['none'];
+      else if ($a_text_item['single'])
+        $text = $a_text_item['single'];
+      else if ($a_text_item['multiple'])
+        $text = $a_text_item['multiple'];
+    }
+
+    // default text is single
+    if ($text == '') {
+      $text = $a_text_item['single'];
+    }
+
+    // replace vars in text
+    if (is_array($attrib['vars'])) {
+      foreach ($attrib['vars'] as $var_key => $var_value)
+        $text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text);
+    }
+
+    // format output
+    if (($attrib['uppercase'] && strtolower($attrib['uppercase']=='first')) || $attrib['ucfirst'])
+      return ucfirst($text);
+    else if ($attrib['uppercase'])
+      return mb_strtoupper($text);
+    else if ($attrib['lowercase'])
+      return mb_strtolower($text);
+
+    return $text;
+  }
+
+
+  /**
+   * Check if the given text label exists
+   *
+   * @param string  $name       Label name
+   * @param string  $domain     Label domain (plugin) name or '*' for all domains
+   * @param string  $ref_domain Sets domain name if label is found
+   *
+   * @return boolean True if text exists (either in the current language or in en_US)
+   */
+  public function text_exists($name, $domain = null, &$ref_domain = null)
+  {
+    // load localization files if not done yet
+    if (empty($this->texts))
+      $this->load_language();
+
+    if (isset($this->texts[$name])) {
+        $ref_domain = '';
+        return true;
+    }
+
+    // any of loaded domains (plugins)
+    if ($domain == '*') {
+      foreach ($this->plugins->loaded_plugins() as $domain)
+        if (isset($this->texts[$domain.'.'.$name])) {
+          $ref_domain = $domain;
+          return true;
+        }
+    }
+    // specified domain
+    else if ($domain) {
+      $ref_domain = $domain;
+      return isset($this->texts[$domain.'.'.$name]);
+    }
+
+    return false;
+  }
+
+  /**
+   * Load a localization package
+   *
+   * @param string Language ID
+   */
+  public function load_language($lang = null, $add = array())
+  {
+    $lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
+
+    // load localized texts
+    if (empty($this->texts) || $lang != $_SESSION['language']) {
+      $this->texts = array();
+
+      // handle empty lines after closing PHP tag in localization files
+      ob_start();
+
+      // get english labels (these should be complete)
+      @include(INSTALL_PATH . 'program/localization/en_US/labels.inc');
+      @include(INSTALL_PATH . 'program/localization/en_US/messages.inc');
+
+      if (is_array($labels))
+        $this->texts = $labels;
+      if (is_array($messages))
+        $this->texts = array_merge($this->texts, $messages);
+
+      // include user language files
+      if ($lang != 'en' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
+        include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc');
+        include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc');
+
+        if (is_array($labels))
+          $this->texts = array_merge($this->texts, $labels);
+        if (is_array($messages))
+          $this->texts = array_merge($this->texts, $messages);
+      }
+
+      ob_end_clean();
+
+      $_SESSION['language'] = $lang;
+    }
+
+    // append additional texts (from plugin)
+    if (is_array($add) && !empty($add))
+      $this->texts += $add;
+  }
+
+
+  /**
+   * Read directory program/localization and return a list of available languages
+   *
+   * @return array List of available localizations
+   */
+  public function list_languages()
+  {
+    static $sa_languages = array();
+
+    if (!sizeof($sa_languages)) {
+      @include(INSTALL_PATH . 'program/localization/index.inc');
+
+      if ($dh = @opendir(INSTALL_PATH . 'program/localization')) {
+        while (($name = readdir($dh)) !== false) {
+          if ($name[0] == '.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name))
+            continue;
+
+          if ($label = $rcube_languages[$name])
+            $sa_languages[$name] = $label;
+        }
+        closedir($dh);
+      }
+    }
+
+    return $sa_languages;
+  }
+
+
+  /**
+   * Destroy session data and remove cookie
+   */
+  public function kill_session()
+  {
+    $this->plugins->exec_hook('session_destroy');
+
+    $this->session->kill();
+    $_SESSION = array('language' => $this->user->language, 'temp' => true);
+    $this->user->reset();
+  }
+
+
+  /**
+   * Do server side actions on logout
+   */
+  public function logout_actions()
+  {
+    $config = $this->config->all();
+
+    // on logout action we're not connected to imap server
+    if (($config['logout_purge'] && !empty($config['trash_mbox'])) || $config['logout_expunge']) {
+      if (!$this->session->check_auth())
+        return;
+
+      $this->imap_connect();
+    }
+
+    if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
+      $this->imap->clear_mailbox($config['trash_mbox']);
+    }
+
+    if ($config['logout_expunge']) {
+      $this->imap->expunge('INBOX');
+    }
+
+    // Try to save unsaved user preferences
+    if (!empty($_SESSION['preferences'])) {
+      $this->user->save_prefs(unserialize($_SESSION['preferences']));
+    }
+  }
+
+
+  /**
+   * Function to be executed in script shutdown
+   * Registered with register_shutdown_function()
+   */
+  public function shutdown()
+  {
+    foreach ($this->shutdown_functions as $function)
+      call_user_func($function);
+
+    if (is_object($this->smtp))
+      $this->smtp->disconnect();
+
+    foreach ($this->address_books as $book) {
+      if (is_object($book) && is_a($book, 'rcube_addressbook'))
+        $book->close();
+    }
+
+    foreach ($this->caches as $cache) {
+        if (is_object($cache))
+            $cache->close();
+    }
+
+    if (is_object($this->imap))
+      $this->imap->close();
+
+    // before closing the database connection, write session data
+    if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
+      $this->session->cleanup();
+      session_write_close();
+    }
+
+    // write performance stats to logs/console
+    if ($this->config->get('devel_mode')) {
+      if (function_exists('memory_get_usage'))
+        $mem = show_bytes(memory_get_usage());
+      if (function_exists('memory_get_peak_usage'))
+        $mem .= '/'.show_bytes(memory_get_peak_usage());
+
+      $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
+      if (defined('RCMAIL_START'))
+        rcube_print_time(RCMAIL_START, $log);
+      else
+        console($log);
+    }
+  }
+
+
+  /**
+   * Registers shutdown function to be executed on shutdown.
+   * The functions will be executed before destroying any
+   * objects like smtp, imap, session, etc.
+   *
+   * @param callback Function callback
+   */
+  public function add_shutdown_function($function)
+  {
+    $this->shutdown_functions[] = $function;
+  }
+
+
+  /**
+   * Generate a unique token to be used in a form request
+   *
+   * @return string The request token
+   */
+  public function get_request_token()
+  {
+    $sess_id = $_COOKIE[ini_get('session.name')];
+    if (!$sess_id) $sess_id = session_id();
+    $plugin = $this->plugins->exec_hook('request_token', array('value' => md5('RT' . $this->user->ID . $this->config->get('des_key') . $sess_id)));
+    return $plugin['value'];
+  }
+
+
+  /**
+   * Check if the current request contains a valid token
+   *
+   * @param int Request method
+   * @return boolean True if request token is valid false if not
+   */
+  public function check_request($mode = RCUBE_INPUT_POST)
+  {
+    $token = get_input_value('_token', $mode);
+    $sess_id = $_COOKIE[ini_get('session.name')];
+    return !empty($sess_id) && $token == $this->get_request_token();
+  }
+
+
+  /**
+   * Create unique authorization hash
+   *
+   * @param string Session ID
+   * @param int Timestamp
+   * @return string The generated auth hash
+   */
+  private function get_auth_hash($sess_id, $ts)
+  {
+    $auth_string = sprintf('rcmail*sess%sR%s*Chk:%s;%s',
+      $sess_id,
+      $ts,
+      $this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] : '***.***.***.***',
+      $_SERVER['HTTP_USER_AGENT']);
+
+    if (function_exists('sha1'))
+      return sha1($auth_string);
+    else
+      return md5($auth_string);
+  }
+
+
+  /**
+   * Encrypt using 3DES
+   *
+   * @param string $clear clear text input
+   * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
+   * @param boolean $base64 whether or not to base64_encode() the result before returning
+   *
+   * @return string encrypted text
+   */
+  public function encrypt($clear, $key = 'des_key', $base64 = true)
+  {
+    if (!$clear)
+      return '';
+    /*-
+     * Add a single canary byte to the end of the clear text, which
+     * will help find out how much of padding will need to be removed
+     * upon decryption; see http://php.net/mcrypt_generic#68082
+     */
+    $clear = pack("a*H2", $clear, "80");
+
+    if (function_exists('mcrypt_module_open') &&
+        ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
+    {
+      $iv = $this->create_iv(mcrypt_enc_get_iv_size($td));
+      mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
+      $cipher = $iv . mcrypt_generic($td, $clear);
+      mcrypt_generic_deinit($td);
+      mcrypt_module_close($td);
+    }
+    else {
+      @include_once 'des.inc';
+
+      if (function_exists('des')) {
+        $des_iv_size = 8;
+        $iv = $this->create_iv($des_iv_size);
+        $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv);
+      }
+      else {
+        raise_error(array(
+          'code' => 500, 'type' => 'php',
+          'file' => __FILE__, 'line' => __LINE__,
+          'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available"
+        ), true, true);
+      }
+    }
+
+    return $base64 ? base64_encode($cipher) : $cipher;
+  }
+
+  /**
+   * Decrypt 3DES-encrypted string
+   *
+   * @param string $cipher encrypted text
+   * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
+   * @param boolean $base64 whether or not input is base64-encoded
+   *
+   * @return string decrypted text
+   */
+  public function decrypt($cipher, $key = 'des_key', $base64 = true)
+  {
+    if (!$cipher)
+      return '';
+
+    $cipher = $base64 ? base64_decode($cipher) : $cipher;
+
+    if (function_exists('mcrypt_module_open') &&
+        ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
+    {
+      $iv_size = mcrypt_enc_get_iv_size($td);
+      $iv = substr($cipher, 0, $iv_size);
+
+      // session corruption? (#1485970)
+      if (strlen($iv) < $iv_size)
+        return '';
+
+      $cipher = substr($cipher, $iv_size);
+      mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
+      $clear = mdecrypt_generic($td, $cipher);
+      mcrypt_generic_deinit($td);
+      mcrypt_module_close($td);
+    }
+    else {
+      @include_once 'des.inc';
+
+      if (function_exists('des')) {
+        $des_iv_size = 8;
+        $iv = substr($cipher, 0, $des_iv_size);
+        $cipher = substr($cipher, $des_iv_size);
+        $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv);
+      }
+      else {
+        raise_error(array(
+          'code' => 500, 'type' => 'php',
+          'file' => __FILE__, 'line' => __LINE__,
+          'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available"
+        ), true, true);
+      }
+    }
+
+    /*-
+     * Trim PHP's padding and the canary byte; see note in
+     * rcmail::encrypt() and http://php.net/mcrypt_generic#68082
+     */
+    $clear = substr(rtrim($clear, "\0"), 0, -1);
+
+    return $clear;
+  }
+
+  /**
+   * Generates encryption initialization vector (IV)
+   *
+   * @param int Vector size
+   * @return string Vector string
+   */
+  private function create_iv($size)
+  {
+    // mcrypt_create_iv() can be slow when system lacks entrophy
+    // we'll generate IV vector manually
+    $iv = '';
+    for ($i = 0; $i < $size; $i++)
+        $iv .= chr(mt_rand(0, 255));
+    return $iv;
+  }
+
+  /**
+   * Build a valid URL to this instance of Roundcube
+   *
+   * @param mixed Either a string with the action or url parameters as key-value pairs
+   * @return string Valid application URL
+   */
+  public function url($p)
+  {
+    if (!is_array($p))
+      $p = array('_action' => @func_get_arg(0));
+
+    $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
+    $p['_task'] = $task;
+    unset($p['task']);
+
+    $url = './';
+    $delm = '?';
+    foreach (array_reverse($p) as $key => $val) {
+      if ($val !== '') {
+        $par = $key[0] == '_' ? $key : '_'.$key;
+        $url .= $delm.urlencode($par).'='.urlencode($val);
+        $delm = '&';
+      }
+    }
+    return $url;
+  }
+
+
+  /**
+   * 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
+   * @param string Cookie value
+   * @param string Expiration time
+   */
+  public static function setcookie($name, $value, $exp = 0)
+  {
+    if (headers_sent())
+      return;
+
+    $cookie = session_get_cookie_params();
+
+    setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'],
+      rcube_https_check(), true);
+  }
+
+  /**
+   * Registers action aliases for current task
+   *
+   * @param array $map Alias-to-filename hash array
+   */
+  public function register_action_map($map)
+  {
+    if (is_array($map)) {
+      foreach ($map as $idx => $val) {
+        $this->action_map[$idx] = $val;
+      }
+    }
+  }
+
+  /**
+   * Returns current action filename
+   *
+   * @param array $map Alias-to-filename hash array
+   */
+  public function get_action_file()
+  {
+    if (!empty($this->action_map[$this->action])) {
+      return $this->action_map[$this->action];
+    }
+
+    return strtr($this->action, '-', '_') . '.inc';
+  }
+
+  /**
+   * Fixes some user preferences according to namespace handling change.
+   * Old Roundcube versions were using folder names with removed namespace prefix.
+   * Now we need to add the prefix on servers where personal namespace has prefix.
+   *
+   * @param rcube_user $user User object
+   */
+  private function fix_namespace_settings($user)
+  {
+    $prefix     = $this->imap->get_namespace('prefix');
+    $prefix_len = strlen($prefix);
+
+    if (!$prefix_len)
+      return;
+
+    $prefs = $this->config->all();
+    if (!empty($prefs['namespace_fixed']))
+      return;
+
+    // Build namespace prefix regexp
+    $ns     = $this->imap->get_namespace();
+    $regexp = array();
+
+    foreach ($ns as $entry) {
+      if (!empty($entry)) {
+        foreach ($entry as $item) {
+          if (strlen($item[0])) {
+            $regexp[] = preg_quote($item[0], '/');
+          }
+        }
+      }
+    }
+    $regexp = '/^('. implode('|', $regexp).')/';
+
+    // Fix preferences
+    $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
+    foreach ($opts as $opt) {
+      if ($value = $prefs[$opt]) {
+        if ($value != 'INBOX' && !preg_match($regexp, $value)) {
+          $prefs[$opt] = $prefix.$value;
+        }
+      }
+    }
+
+    if (!empty($prefs['default_imap_folders'])) {
+      foreach ($prefs['default_imap_folders'] as $idx => $name) {
+        if ($name != 'INBOX' && !preg_match($regexp, $name)) {
+          $prefs['default_imap_folders'][$idx] = $prefix.$name;
+        }
+      }
+    }
+
+    if (!empty($prefs['search_mods'])) {
+      $folders = array();
+      foreach ($prefs['search_mods'] as $idx => $value) {
+        if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
+          $idx = $prefix.$idx;
+        }
+        $folders[$idx] = $value;
+      }
+      $prefs['search_mods'] = $folders;
+    }
+
+    if (!empty($prefs['message_threading'])) {
+      $folders = array();
+      foreach ($prefs['message_threading'] as $idx => $value) {
+        if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
+          $idx = $prefix.$idx;
+        }
+        $folders[$prefix.$idx] = $value;
+      }
+      $prefs['message_threading'] = $folders;
+    }
+
+    if (!empty($prefs['collapsed_folders'])) {
+      $folders     = explode('&&', $prefs['collapsed_folders']);
+      $count       = count($folders);
+      $folders_str = '';
+
+      if ($count) {
+          $folders[0]        = substr($folders[0], 1);
+          $folders[$count-1] = substr($folders[$count-1], 0, -1);
+      }
+
+      foreach ($folders as $value) {
+        if ($value != 'INBOX' && !preg_match($regexp, $value)) {
+          $value = $prefix.$value;
+        }
+        $folders_str .= '&'.$value.'&';
+      }
+      $prefs['collapsed_folders'] = $folders_str;
+    }
+
+    $prefs['namespace_fixed'] = true;
+
+    // save updated preferences and reset imap settings (default folders)
+    $user->save_prefs($prefs);
+    $this->set_imap_prop();
+  }
+
+}
Index: /branches/devel-composer/program/include/rcube_addressbook.php
===================================================================
--- /branches/devel-composer/program/include/rcube_addressbook.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_addressbook.php	(revision 5386)
@@ -0,0 +1,482 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_addressbook.php                                 |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2006-2011, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Interface to the local address book database                        |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Abstract skeleton of an address book/repository
+ *
+ * @package Addressbook
+ */
+abstract class rcube_addressbook
+{
+    /** constants for error reporting **/
+    const ERROR_READ_ONLY = 1;
+    const ERROR_NO_CONNECTION = 2;
+    const ERROR_VALIDATE = 3;
+    const ERROR_SAVING = 4;
+    const ERROR_SEARCH = 5;
+
+    /** public properties (mandatory) */
+    public $primary_key;
+    public $groups = false;
+    public $readonly = true;
+    public $searchonly = false;
+    public $undelete = false;
+    public $ready = false;
+    public $group_id = null;
+    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;
+
+    /**
+     * Returns addressbook name (e.g. for addressbooks listing)
+     */
+    abstract function get_name();
+
+    /**
+     * Save a search string for future listings
+     *
+     * @param mixed Search params to use in listing method, obtained by get_search_set()
+     */
+    abstract function set_search_set($filter);
+
+    /**
+     * Getter for saved search properties
+     *
+     * @return mixed Search properties used by this class
+     */
+    abstract function get_search_set();
+
+    /**
+     * Reset saved results and search parameters
+     */
+    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
+     * @param  int    Only return this number of records, use negative values for tail
+     * @return array  Indexed list of contact records, each a hash array
+     */
+    abstract function list_records($cols=null, $subset=0);
+
+    /**
+     * Search records
+     *
+     * @param array   List of fields to search in
+     * @param string  Search value
+     * @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 object rcube_result_set List of contact records and 'count' value
+     */
+    abstract function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array());
+
+    /**
+     * Count number of available contacts in database
+     *
+     * @return rcube_result_set Result set with values for 'count' and 'first'
+     */
+    abstract function count();
+
+    /**
+     * Return the last result set
+     *
+     * @return rcube_result_set Current result set or NULL if nothing selected yet
+     */
+    abstract function get_result();
+
+    /**
+     * Get a specific contact record
+     *
+     * @param mixed record identifier(s)
+     * @param boolean True to return record as associative array, otherwise a result set is returned
+     *
+     * @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
+     * Called on script shutdown
+     */
+    function close() { }
+
+    /**
+     * Set internal list page
+     *
+     * @param  number  Page number to list
+     * @access public
+     */
+    function set_page($page)
+    {
+        $this->list_page = (int)$page;
+    }
+
+    /**
+     * Set internal page size
+     *
+     * @param  number  Number of messages to display on one page
+     * @access public
+     */
+    function set_pagesize($size)
+    {
+        $this->page_size = (int)$size;
+    }
+
+
+    /**
+     * Check the given data before saving.
+     * If input isn't valid, the message to display can be fetched using get_error()
+     *
+     * @param array Assoziative array with data to save
+     * @param boolean Attempt to fix/complete record automatically
+     * @return boolean True if input is valid, False if not.
+     */
+    public function validate(&$save_data, $autofix = false)
+    {
+        // check validity of email addresses
+        foreach ($this->get_col_values('email', $save_data, true) as $email) {
+            if (strlen($email)) {
+                if (!check_email(rcube_idn_to_ascii($email))) {
+                    $this->set_error(self::ERROR_VALIDATE, rcube_label(array('name' => 'emailformaterror', 'vars' => array('email' => $email))));
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+
+    /**
+     * 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
+     */
+    function insert($save_data, $check=false)
+    {
+        /* empty for read-only address books */
+    }
+
+    /**
+     * 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)
+    {
+        /* empty for read-only address books */
+    }
+
+    /**
+     * Mark one or more contact records as deleted
+     *
+     * @param array  Record identifiers
+     * @param bool   Remove records irreversible (see self::undelete)
+     */
+    function delete($ids, $force=true)
+    {
+        /* empty for read-only address books */
+    }
+
+    /**
+     * Unmark delete flag on contact record(s)
+     *
+     * @param array  Record identifiers
+     */
+    function undelete($ids)
+    {
+        /* empty for read-only address books */
+    }
+
+    /**
+     * Mark all records in database as deleted
+     */
+    function delete_all()
+    {
+        /* empty for read-only address books */
+    }
+
+    /**
+     * Setter for the current group
+     * (empty, has to be re-implemented by extending class)
+     */
+    function set_group($gid) { }
+
+    /**
+     * 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($search = null)
+    {
+        /* empty for address books don't supporting groups */
+        return array();
+    }
+
+    /**
+     * Get group properties such as name and email address(es)
+     *
+     * @param string Group identifier
+     * @return array Group properties as hash array
+     */
+    function get_group($group_id)
+    {
+        /* empty for address books don't supporting groups */
+        return null;
+    }
+
+    /**
+     * Create a contact group with the given name
+     *
+     * @param string The group name
+     * @return mixed False on error, array with record props in success
+     */
+    function create_group($name)
+    {
+        /* empty for address books don't supporting groups */
+        return false;
+    }
+
+    /**
+     * Delete the given group and all linked group members
+     *
+     * @param string Group identifier
+     * @return boolean True on success, false if no data was changed
+     */
+    function delete_group($gid)
+    {
+        /* empty for address books don't supporting groups */
+        return false;
+    }
+
+    /**
+     * Rename a specific contact group
+     *
+     * @param string Group identifier
+     * @param string New name to set for this group
+     * @param string New group identifier (if changed, otherwise don't set)
+     * @return boolean New name on success, false if no data was changed
+     */
+    function rename_group($gid, $newname, &$newid)
+    {
+        /* empty for address books don't supporting groups */
+        return false;
+    }
+
+    /**
+     * Add the given contact records the a certain group
+     *
+     * @param string  Group identifier
+     * @param array   List of contact identifiers to be added
+     * @return int    Number of contacts added
+     */
+    function add_to_group($group_id, $ids)
+    {
+        /* empty for address books don't supporting groups */
+        return 0;
+    }
+
+    /**
+     * Remove the given contact records from a certain group
+     *
+     * @param string  Group identifier
+     * @param array   List of contact identifiers to be removed
+     * @return int    Number of deleted group members
+     */
+    function remove_from_group($group_id, $ids)
+    {
+        /* empty for address books don't supporting groups */
+        return 0;
+    }
+
+    /**
+     * Get group assignments of a specific contact record
+     *
+     * @param mixed Record identifier
+     *
+     * @return array List of assigned groups as ID=>Name pairs
+     * @since 0.5-beta
+     */
+    function get_record_groups($id)
+    {
+        /* 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;
+    }
+
+
+    /**
+     * Normalize the given string for fulltext search.
+     * Currently only optimized for Latin-1 characters; to be extended
+     *
+     * @param string Input string (UTF-8)
+     * @return string Normalized string
+     */
+    protected static function normalize_string($str)
+    {
+        // split by words
+        $arr = explode(" ", preg_replace(
+            array('/[\s;\+\-\/]+/i', '/(\d)[-.\s]+(\d)/', '/\s\w{1,3}\s/'),
+            array(' ', '\\1\\2', ' '),
+            $str));
+
+        foreach ($arr as $i => $part) {
+            if (utf8_encode(utf8_decode($part)) == $part) {  // is latin-1 ?
+                $arr[$i] = utf8_encode(strtr(strtolower(strtr(utf8_decode($part),
+                    'ÇçäâàåéêëèïîìÅÉöôòüûùÿøØáíóúñÑÁÂÀãÃÊËÈÍÎÏÓÔõÕÚÛÙýÝ',
+                    'ccaaaaeeeeiiiaeooouuuyooaiounnaaaaaeeeiiioooouuuyy')),
+                    array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u')));
+            }
+            else
+                $arr[$i] = mb_strtolower($part);
+        }
+
+        return join(" ", $arr);
+    }
+
+
+    /**
+     * Compose a valid display name from the given structured contact data
+     *
+     * @param array  Hash array with contact data as key-value pairs
+     * @param bool   The name will be used on the list
+     *
+     * @return string Display name
+     */
+    public static function compose_display_name($contact, $list_mode = false)
+    {
+        $contact = rcmail::get_instance()->plugins->exec_hook('contact_displayname', $contact);
+        $fn = $contact['name'];
+
+        if (!$fn)
+            $fn = join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])));
+
+        // use email address part for name
+        $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email'];
+
+        if ($email && (empty($fn) || $fn == $email)) {
+            // Use full email address on contacts list
+            if ($list_mode)
+                return $email;
+
+            list($emailname) = explode('@', $email);
+            if (preg_match('/(.*)[\.\-\_](.*)/', $emailname, $match))
+                $fn = trim(ucfirst($match[1]).' '.ucfirst($match[2]));
+            else
+                $fn = ucfirst($emailname);
+        }
+
+        return $fn;
+    }
+
+}
+
Index: /branches/devel-composer/program/include/rcube_browser.php
===================================================================
--- /branches/devel-composer/program/include/rcube_browser.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_browser.php	(revision 5386)
@@ -0,0 +1,74 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_browser.php                                     |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2007-2009, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Class representing the client browser's properties                  |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+/** 
+ * rcube_browser
+ * 
+ * Provide details about the client's browser based on the User-Agent header
+ *
+ * @package Core
+ */
+class rcube_browser
+{
+    function __construct()
+    {
+        $HTTP_USER_AGENT = strtolower($_SERVER['HTTP_USER_AGENT']);
+
+        $this->ver = 0;
+        $this->win = strstr($HTTP_USER_AGENT, 'win');
+        $this->mac = strstr($HTTP_USER_AGENT, 'mac');
+        $this->linux = strstr($HTTP_USER_AGENT, 'linux');
+        $this->unix  = strstr($HTTP_USER_AGENT, 'unix');
+
+        $this->opera = strstr($HTTP_USER_AGENT, 'opera');
+        $this->ns4 = strstr($HTTP_USER_AGENT, 'mozilla/4') && !stristr($HTTP_USER_AGENT, 'msie');
+        $this->ns  = ($this->ns4 || strstr($HTTP_USER_AGENT, 'netscape'));
+        $this->ie  = !$this->opera && stristr($HTTP_USER_AGENT, 'compatible; msie');
+        $this->mz  = !$this->ie && strstr($HTTP_USER_AGENT, 'mozilla/5');
+        $this->chrome = strstr($HTTP_USER_AGENT, 'chrome');
+        $this->khtml = strstr($HTTP_USER_AGENT, 'khtml');
+        $this->safari = !$this->chrome && ($this->khtml || strstr($HTTP_USER_AGENT, 'safari'));
+
+        if ($this->ns || $this->chrome) {
+            $test = preg_match('/(mozilla|chrome)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs);
+            $this->ver = $test ? (float)$regs[2] : 0;
+        }
+        else if ($this->mz) {
+            $test = preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs);
+            $this->ver = $test ? (float)$regs[1] : 0;
+        }
+        else if ($this->ie || $this->opera) {
+            $test = preg_match('/(msie|opera) ([0-9.]+)/', $HTTP_USER_AGENT, $regs);
+            $this->ver = $test ? (float)$regs[2] : 0;
+        }
+
+        if (preg_match('/ ([a-z]{2})-([a-z]{2})/', $HTTP_USER_AGENT, $regs))
+            $this->lang =  $regs[1];
+        else
+            $this->lang =  'en';
+
+        $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;
+    }
+}
+
Index: /branches/devel-composer/program/include/rcube_cache.php
===================================================================
--- /branches/devel-composer/program/include/rcube_cache.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_cache.php	(revision 5386)
@@ -0,0 +1,554 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_cache.php                                       |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2011, The Roundcube Dev Team                            |
+ | Copyright (C) 2011, Kolab Systems AG                                  |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Caching engine                                                      |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ | Author: Aleksander Machniak <alec@alec.pl>                            |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Interface class for accessing Roundcube cache
+ *
+ * @package    Cache
+ * @author     Thomas Bruederli <roundcube@gmail.com>
+ * @author     Aleksander Machniak <alec@alec.pl>
+ * @version    1.1
+ */
+class rcube_cache
+{
+    /**
+     * Instance of rcube_mdb2 or Memcache class
+     *
+     * @var rcube_mdb2/Memcache
+     */
+    private $db;
+    private $type;
+    private $userid;
+    private $prefix;
+    private $ttl;
+    private $packed;
+    private $index;
+    private $cache         = array();
+    private $cache_keys    = array();
+    private $cache_changes = array();
+    private $cache_sums    = array();
+
+
+    /**
+     * Object constructor.
+     *
+     * @param string $type   Engine type ('db' or 'memcache' or 'apc')
+     * @param int    $userid User identifier
+     * @param string $prefix Key name prefix
+     * @param int    $ttl    Expiration time of memcache/apc items in seconds (max.2592000)
+     * @param bool   $packed Enables/disabled data serialization.
+     *                       It's possible to disable data serialization if you're sure
+     *                       stored data will be always a safe string
+     */
+    function __construct($type, $userid, $prefix='', $ttl=0, $packed=true)
+    {
+        $rcmail = rcmail::get_instance();
+        $type   = strtolower($type);
+
+        if ($type == 'memcache') {
+            $this->type = 'memcache';
+            $this->db   = $rcmail->get_memcache();
+        }
+        else if ($type == 'apc') {
+            $this->type = 'apc';
+            $this->db   = function_exists('apc_exists'); // APC 3.1.4 required
+        }
+        else {
+            $this->type = 'db';
+            $this->db   = $rcmail->get_dbh();
+        }
+
+        $this->userid    = (int) $userid;
+        $this->ttl       = (int) $ttl;
+        $this->packed    = $packed;
+        $this->prefix    = $prefix;
+    }
+
+
+    /**
+     * Returns cached value.
+     *
+     * @param string $key Cache key name
+     *
+     * @return mixed Cached value
+     */
+    function get($key)
+    {
+        if (!array_key_exists($key, $this->cache)) {
+            return $this->read_record($key);
+        }
+
+        return $this->cache[$key];
+    }
+
+
+    /**
+     * Sets (add/update) value in cache.
+     *
+     * @param string $key  Cache key name
+     * @param mixed  $data Cache data
+     */
+    function set($key, $data)
+    {
+        $this->cache[$key]         = $data;
+        $this->cache_changed       = true;
+        $this->cache_changes[$key] = true;
+    }
+
+
+    /**
+     * Returns cached value without storing it in internal memory.
+     *
+     * @param string $key Cache key name
+     *
+     * @return mixed Cached value
+     */
+    function read($key)
+    {
+        if (array_key_exists($key, $this->cache)) {
+            return $this->cache[$key];
+        }
+
+        return $this->read_record($key, true);
+    }
+
+
+    /**
+     * Sets (add/update) value in cache and immediately saves
+     * it in the backend, no internal memory will be used.
+     *
+     * @param string $key  Cache key name
+     * @param mixed  $data Cache data
+     *
+     * @param boolean True on success, False on failure
+     */
+    function write($key, $data)
+    {
+        return $this->write_record($key, $this->packed ? serialize($data) : $data);
+    }
+
+
+    /**
+     * Clears the cache.
+     *
+     * @param string  $key         Cache key name or pattern
+     * @param boolean $prefix_mode Enable it to clear all keys starting
+     *                             with prefix specified in $key
+     */
+    function remove($key=null, $prefix_mode=false)
+    {
+        // Remove all keys
+        if ($key === null) {
+            $this->cache         = array();
+            $this->cache_changed = false;
+            $this->cache_changes = array();
+            $this->cache_keys    = array();
+        }
+        // Remove keys by name prefix
+        else if ($prefix_mode) {
+            foreach (array_keys($this->cache) as $k) {
+                if (strpos($k, $key) === 0) {
+                    $this->cache[$k] = null;
+                    $this->cache_changes[$k] = false;
+                    unset($this->cache_keys[$k]);
+                }
+            }
+        }
+        // Remove one key by name
+        else {
+            $this->cache[$key] = null;
+            $this->cache_changes[$key] = false;
+            unset($this->cache_keys[$key]);
+        }
+
+        // Remove record(s) from the backend
+        $this->remove_record($key, $prefix_mode);
+    }
+
+
+    /**
+     * Remove cache records older than ttl
+     */
+    function expunge()
+    {
+        if ($this->type == 'db' && $this->db) {
+            $this->db->query(
+                "DELETE FROM ".get_table_name('cache').
+                " WHERE user_id = ?".
+                " AND cache_key LIKE ?".
+                " AND " . $this->db->unixtimestamp('created')." < ?",
+                $this->userid,
+                $this->prefix.'.%',
+                time() - $this->ttl);
+        }
+    }
+
+
+    /**
+     * Writes the cache back to the DB.
+     */
+    function close()
+    {
+        if (!$this->cache_changed) {
+            return;
+        }
+
+        foreach ($this->cache as $key => $data) {
+            // The key has been used
+            if ($this->cache_changes[$key]) {
+                // Make sure we're not going to write unchanged data
+                // by comparing current md5 sum with the sum calculated on DB read
+                $data = $this->packed ? serialize($data) : $data;
+
+                if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
+                    $this->write_record($key, $data);
+                }
+            }
+        }
+
+        $this->write_index();
+    }
+
+
+    /**
+     * Reads cache entry.
+     *
+     * @param string  $key     Cache key name
+     * @param boolean $nostore Enable to skip in-memory store
+     *
+     * @return mixed Cached value
+     */
+    private function read_record($key, $nostore=false)
+    {
+        if (!$this->db) {
+            return null;
+        }
+
+        if ($this->type != 'db') {
+            if ($this->type == 'memcache') {
+                $data = $this->db->get($this->ckey($key));
+            }
+            else if ($this->type == 'apc') {
+                $data = apc_fetch($this->ckey($key));
+	        }
+
+            if ($data) {
+                $md5sum = md5($data);
+                $data   = $this->packed ? unserialize($data) : $data;
+
+                if ($nostore) {
+                    return $data;
+                }
+
+                $this->cache_sums[$key] = $md5sum;
+                $this->cache[$key]      = $data;
+            }
+            else {
+                $this->cache[$key] = null;
+            }
+        }
+        else {
+            $sql_result = $this->db->limitquery(
+                "SELECT cache_id, data, cache_key".
+                " FROM ".get_table_name('cache').
+                " WHERE user_id = ?".
+                " AND cache_key = ?".
+                // for better performance we allow more records for one key
+                // get the newer one
+                " ORDER BY created DESC",
+                0, 1, $this->userid, $this->prefix.'.'.$key);
+
+            if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
+                $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
+                if ($sql_arr['data']) {
+                    $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data'];
+                }
+
+                if ($nostore) {
+                    return $data;
+                }
+
+                $this->cache[$key]      = $data;
+	            $this->cache_sums[$key] = $md5sum;
+                $this->cache_keys[$key] = $sql_arr['cache_id'];
+            }
+            else {
+                $this->cache[$key] = null;
+            }
+        }
+
+        return $this->cache[$key];
+    }
+
+
+    /**
+     * Writes single cache record into DB.
+     *
+     * @param string $key  Cache key name
+     * @param mxied  $data Serialized cache data 
+     *
+     * @param boolean True on success, False on failure
+     */
+    private function write_record($key, $data)
+    {
+        if (!$this->db) {
+            return false;
+        }
+
+        if ($this->type == 'memcache' || $this->type == 'apc') {
+            return $this->add_record($this->ckey($key), $data);
+        }
+
+        $key_exists = $this->cache_keys[$key];
+        $key        = $this->prefix . '.' . $key;
+
+        // Remove NULL rows (here we don't need to check if the record exist)
+        if ($data == 'N;') {
+            $this->db->query(
+                "DELETE FROM ".get_table_name('cache').
+                " WHERE user_id = ?".
+                " AND cache_key = ?",
+                $this->userid, $key);
+
+            return true;
+        }
+
+        // update existing cache record
+        if ($key_exists) {
+            $result = $this->db->query(
+                "UPDATE ".get_table_name('cache').
+                " SET created = ". $this->db->now().", data = ?".
+                " WHERE user_id = ?".
+                " AND cache_key = ?",
+                $data, $this->userid, $key);
+        }
+        // add new cache record
+        else {
+            // for better performance we allow more records for one key
+            // so, no need to check if record exist (see rcube_cache::read_record())
+            $result = $this->db->query(
+                "INSERT INTO ".get_table_name('cache').
+                " (created, user_id, cache_key, data)".
+                " VALUES (".$this->db->now().", ?, ?, ?)",
+                $this->userid, $key, $data);
+        }
+
+        return $this->db->affected_rows($result);
+    }
+
+
+    /**
+     * Deletes the cache record(s).
+     *
+     * @param string  $key         Cache key name or pattern
+     * @param boolean $prefix_mode Enable it to clear all keys starting
+     *                             with prefix specified in $key
+     *
+     */
+    private function remove_record($key=null, $prefix_mode=false)
+    {
+        if (!$this->db) {
+            return;
+        }
+
+        if ($this->type != 'db') {
+            $this->load_index();
+
+            // Remove all keys
+            if ($key === null) {
+                foreach ($this->index as $key) {
+                    $this->delete_record($key, false);
+                }
+                $this->index = array();
+            }
+            // Remove keys by name prefix
+            else if ($prefix_mode) {
+                foreach ($this->index as $k) {
+                    if (strpos($k, $key) === 0) {
+                        $this->delete_record($k);
+                    }
+                }
+            }
+            // Remove one key by name
+            else {
+                $this->delete_record($key);
+            }
+
+            return;
+        }
+
+        // Remove all keys (in specified cache)
+        if ($key === null) {
+            $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
+        }
+        // Remove keys by name prefix
+        else if ($prefix_mode) {
+            $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
+        }
+        // Remove one key by name
+        else {
+            $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
+        }
+
+        $this->db->query(
+            "DELETE FROM ".get_table_name('cache').
+            " WHERE user_id = ?" . $where,
+            $this->userid);
+    }
+
+
+    /**
+     * Adds entry into memcache/apc DB.
+     *
+     * @param string  $key   Cache key name
+     * @param mxied   $data  Serialized cache data
+     * @param bollean $index Enables immediate index update
+     *
+     * @param boolean True on success, False on failure
+     */
+    private function add_record($key, $data, $index=false)
+    {
+        if ($this->type == 'memcache') {
+            $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
+            if (!$result)
+                $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
+        }
+        else if ($this->type == 'apc') {
+            if (apc_exists($key))
+                apc_delete($key);
+            $result = apc_store($key, $data, $this->ttl);
+        }
+
+        // Update index
+        if ($index && $result) {
+            $this->load_index();
+
+            if (array_search($key, $this->index) === false) {
+                $this->index[] = $key;
+                $data = serialize($this->index);
+                $this->add_record($this->ikey(), $data);
+            }
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Deletes entry from memcache/apc DB.
+     */
+    private function delete_record($key, $index=true)
+    {
+        if ($this->type == 'memcache')
+            $this->db->delete($this->ckey($key));
+        else
+            apc_delete($this->ckey($key));
+
+        if ($index) {
+            if (($idx = array_search($key, $this->index)) !== false) {
+                unset($this->index[$idx]);
+            }
+        }
+    }
+
+
+    /**
+     * Writes the index entry into memcache/apc DB.
+     */
+    private function write_index()
+    {
+        if (!$this->db) {
+            return;
+        }
+
+        if ($this->type == 'db') {
+            return;
+        }
+
+        $this->load_index();
+
+        // Make sure index contains new keys
+        foreach ($this->cache as $key => $value) {
+            if ($value !== null) {
+                if (array_search($key, $this->index) === false) {
+                    $this->index[] = $key;
+                }
+            }
+        }
+
+        $data = serialize($this->index);
+        $this->add_record($this->ikey(), $data);
+    }
+
+
+    /**
+     * Gets the index entry from memcache/apc DB.
+     */
+    private function load_index()
+    {
+        if (!$this->db) {
+            return;
+        }
+
+        if ($this->index !== null) {
+            return;
+        }
+
+        $index_key = $this->ikey();
+        if ($this->type == 'memcache') {
+            $data = $this->db->get($index_key);
+        }
+        else if ($this->type == 'apc') {
+            $data = apc_fetch($index_key);
+        }
+
+        $this->index = $data ? unserialize($data) : array();
+    }
+
+
+    /**
+     * Creates per-user cache key name (for memcache and apc)
+     *
+     * @param string $key Cache key name
+     *
+     * @return string Cache key
+     */
+    private function ckey($key)
+    {
+        return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
+    }
+
+
+    /**
+     * Creates per-user index cache key name (for memcache and apc)
+     *
+     * @return string Cache key
+     */
+    private function ikey()
+    {
+        // This way each cache will have its own index
+        return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');
+    }
+}
Index: /branches/devel-composer/program/include/rcube_config.php
===================================================================
--- /branches/devel-composer/program/include/rcube_config.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_config.php	(revision 5386)
@@ -0,0 +1,349 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_config.php                                      |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2008-2010, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Class to read configuration settings                                |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+/**
+ * Configuration class for Roundcube
+ *
+ * @package Core
+ */
+class rcube_config
+{
+    private $prop = array();
+    private $errors = array();
+    private $userprefs = array();
+
+
+    /**
+     * Object constructor
+     */
+    public function __construct()
+    {
+        $this->load();
+    }
+
+
+    /**
+     * Load config from local config file
+     *
+     * @todo Remove global $CONFIG
+     */
+    private function load()
+    {
+        // load main config file
+        if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/main.inc.php'))
+            $this->errors[] = 'main.inc.php was not found.';
+
+        // load database config
+        if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/db.inc.php'))
+            $this->errors[] = 'db.inc.php was not found.';
+
+        // load host-specific configuration
+        $this->load_host_config();
+
+        // set skin (with fallback to old 'skin_path' property)
+        if (empty($this->prop['skin']) && !empty($this->prop['skin_path']))
+            $this->prop['skin'] = str_replace('skins/', '', unslashify($this->prop['skin_path']));
+        else if (empty($this->prop['skin']))
+            $this->prop['skin'] = 'default';
+
+        // fix paths
+        $this->prop['log_dir'] = $this->prop['log_dir'] ? realpath(unslashify($this->prop['log_dir'])) : INSTALL_PATH . 'logs';
+        $this->prop['temp_dir'] = $this->prop['temp_dir'] ? realpath(unslashify($this->prop['temp_dir'])) : INSTALL_PATH . 'temp';
+
+        // fix default imap folders encoding
+        foreach (array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox') as $folder)
+            $this->prop[$folder] = rcube_charset_convert($this->prop[$folder], RCMAIL_CHARSET, 'UTF7-IMAP');
+
+        if (!empty($this->prop['default_imap_folders']))
+            foreach ($this->prop['default_imap_folders'] as $n => $folder)
+                $this->prop['default_imap_folders'][$n] = rcube_charset_convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP');
+
+        // set PHP error logging according to config
+        if ($this->prop['debug_level'] & 1) {
+            ini_set('log_errors', 1);
+
+            if ($this->prop['log_driver'] == 'syslog') {
+                ini_set('error_log', 'syslog');
+            }
+            else {
+                ini_set('error_log', $this->prop['log_dir'].'/errors');
+            }
+        }
+
+        // enable display_errors in 'show' level, but not for ajax requests
+        ini_set('display_errors', intval(empty($_REQUEST['_remote']) && ($this->prop['debug_level'] & 4)));
+        
+        // set timezone auto settings values
+        if ($this->prop['timezone'] == 'auto') {
+          $this->prop['dst_active'] = intval(date('I'));
+          $this->prop['_timezone_value']   = date('Z') / 3600 - $this->prop['dst_active'];
+        }
+
+        // export config data
+        $GLOBALS['CONFIG'] = &$this->prop;
+    }
+
+    /**
+     * Load a host-specific config file if configured
+     * This will merge the host specific configuration with the given one
+     */
+    private function load_host_config()
+    {
+        $fname = null;
+
+        if (is_array($this->prop['include_host_config'])) {
+            $fname = $this->prop['include_host_config'][$_SERVER['HTTP_HOST']];
+        }
+        else if (!empty($this->prop['include_host_config'])) {
+            $fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $_SERVER['HTTP_HOST']) . '.inc.php';
+        }
+
+        if ($fname) {
+            $this->load_from_file(RCMAIL_CONFIG_DIR . '/' . $fname);
+        }
+    }
+
+
+    /**
+     * Read configuration from a file
+     * and merge with the already stored config values
+     *
+     * @param string $fpath Full path to the config file to be loaded
+     * @return booelan True on success, false on failure
+     */
+    public function load_from_file($fpath)
+    {
+        if (is_file($fpath) && is_readable($fpath)) {
+            // use output buffering, we don't need any output here 
+            ob_start();
+            include($fpath);
+            ob_end_clean();
+
+            if (is_array($rcmail_config)) {
+                $this->prop = array_merge($this->prop, $rcmail_config, $this->userprefs);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Getter for a specific config parameter
+     *
+     * @param  string $name Parameter name
+     * @param  mixed  $def  Default value if not set
+     * @return mixed  The requested config value
+     */
+    public function get($name, $def = null)
+    {
+        $result = isset($this->prop[$name]) ? $this->prop[$name] : $def;
+        $rcmail = rcmail::get_instance();
+        
+        if ($name == 'timezone' && isset($this->prop['_timezone_value']))
+            $result = $this->prop['_timezone_value'];
+
+        if (is_object($rcmail->plugins)) {
+            $plugin = $rcmail->plugins->exec_hook('config_get', array(
+                'name' => $name, 'default' => $def, 'result' => $result));
+
+            return $plugin['result'];
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Setter for a config parameter
+     *
+     * @param string $name  Parameter name
+     * @param mixed  $value Parameter value
+     */
+    public function set($name, $value)
+    {
+        $this->prop[$name] = $value;
+    }
+
+
+    /**
+     * Override config options with the given values (eg. user prefs)
+     *
+     * @param array $prefs Hash array with config props to merge over
+     */
+    public function merge($prefs)
+    {
+        $this->prop = array_merge($this->prop, $prefs, $this->userprefs);
+    }
+
+
+    /**
+     * Merge the given prefs over the current config
+     * and make sure that they survive further merging.
+     *
+     * @param array $prefs Hash array with user prefs
+     */
+    public function set_user_prefs($prefs)
+    {
+        // Honor the dont_override setting for any existing user preferences
+        $dont_override = $this->get('dont_override');
+        if (is_array($dont_override) && !empty($dont_override)) {
+            foreach ($prefs as $key => $pref) {
+                if (in_array($key, $dont_override)) {
+                    unset($prefs[$key]);
+                }
+            }
+        }
+
+        $this->userprefs = $prefs;
+        $this->prop      = array_merge($this->prop, $prefs);
+
+        // override timezone settings with client values
+        if ($this->prop['timezone'] == 'auto') {
+            $this->prop['_timezone_value'] = isset($_SESSION['timezone']) ? $_SESSION['timezone'] : $this->prop['_timezone_value'];
+            $this->prop['dst_active'] = $this->userprefs['dst_active'] = isset($_SESSION['dst_active']) ? $_SESSION['dst_active'] : $this->prop['dst_active'];
+        }
+        else if (isset($this->prop['_timezone_value']))
+           unset($this->prop['_timezone_value']);
+    }
+
+
+    /**
+     * Getter for all config options
+     *
+     * @return array  Hash array containg all config properties
+     */
+    public function all()
+    {
+        return $this->prop;
+    }
+
+    /**
+     * Special getter for user's timezone offset including DST
+     *
+     * @return float  Timezone offset (in hours)
+     */
+    public function get_timezone()
+    {
+      return floatval($this->get('timezone')) + intval($this->get('dst_active'));
+    }
+
+    /**
+     * Return requested DES crypto key.
+     *
+     * @param string $key Crypto key name
+     * @return string Crypto key
+     */
+    public function get_crypto_key($key)
+    {
+        // Bomb out if the requested key does not exist
+        if (!array_key_exists($key, $this->prop)) {
+            raise_error(array(
+                'code' => 500, 'type' => 'php',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Request for unconfigured crypto key \"$key\""
+            ), true, true);
+        }
+
+        $key = $this->prop[$key];
+
+        // Bomb out if the configured key is not exactly 24 bytes long
+        if (strlen($key) != 24) {
+            raise_error(array(
+                'code' => 500, 'type' => 'php',
+	            'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Configured crypto key '$key' is not exactly 24 bytes long"
+            ), true, true);
+        }
+
+        return $key;
+    }
+
+
+    /**
+     * Try to autodetect operating system and find the correct line endings
+     *
+     * @return string The appropriate mail header delimiter
+     */
+    public function header_delimiter()
+    {
+        // use the configured delimiter for headers
+        if (!empty($this->prop['mail_header_delimiter'])) {
+            $delim = $this->prop['mail_header_delimiter'];
+            if ($delim == "\n" || $delim == "\r\n")
+                return $delim;
+            else
+                raise_error(array(
+                    'code' => 500, 'type' => 'php',
+	                'file' => __FILE__, 'line' => __LINE__,
+                    'message' => "Invalid mail_header_delimiter setting"
+                ), true, false);
+        }
+
+        $php_os = strtolower(substr(PHP_OS, 0, 3));
+
+        if ($php_os == 'win')
+            return "\r\n";
+
+        if ($php_os == 'mac')
+            return "\r\n";
+
+        return "\n";
+    }
+
+
+    /**
+     * Return the mail domain configured for the given host
+     *
+     * @param string  $host   IMAP host
+     * @param boolean $encode If true, domain name will be converted to IDN ASCII
+     * @return string Resolved SMTP host
+     */
+    public function mail_domain($host, $encode=true)
+    {
+        $domain = $host;
+
+        if (is_array($this->prop['mail_domain'])) {
+            if (isset($this->prop['mail_domain'][$host]))
+                $domain = $this->prop['mail_domain'][$host];
+        }
+        else if (!empty($this->prop['mail_domain']))
+            $domain = rcube_parse_host($this->prop['mail_domain']);
+
+        if ($encode)
+            $domain = rcube_idn_to_ascii($domain);
+
+        return $domain;
+    }
+
+
+    /**
+     * Getter for error state
+     *
+     * @return mixed Error message on error, False if no errors
+     */
+    public function get_error()
+    {
+        return empty($this->errors) ? false : join("\n", $this->errors);
+    }
+
+}
Index: /branches/devel-composer/program/include/rcube_contacts.php
===================================================================
--- /branches/devel-composer/program/include/rcube_contacts.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_contacts.php	(revision 5386)
@@ -0,0 +1,933 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_contacts.php                                    |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2006-2011, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Interface to the local address book database                        |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Model class for the local address book database
+ *
+ * @package Addressbook
+ */
+class rcube_contacts extends rcube_addressbook
+{
+    // protected for backward compat. with some plugins
+    protected $db_name = 'contacts';
+    protected $db_groups = 'contactgroups';
+    protected $db_groupmembers = 'contactgroupmembers';
+
+    /**
+     * Store database connection.
+     *
+     * @var rcube_mdb2
+     */
+    private $db = null;
+    private $user_id = 0;
+    private $filter = null;
+    private $result = null;
+    private $name;
+    private $cache;
+    private $table_cols = array('name', 'email', 'firstname', 'surname');
+    private $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'nickname',
+      'jobtitle', 'organization', 'department', 'maidenname', 'email', 'phone',
+      'address', 'street', 'locality', 'zipcode', 'region', 'country', 'website', 'im', 'notes');
+
+    // public properties
+    public $primary_key = 'contact_id';
+    public $readonly = false;
+    public $groups = true;
+    public $undelete = 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');
+
+
+    /**
+     * Object constructor
+     *
+     * @param object  Instance of the rcube_db class
+     * @param integer User-ID
+     */
+    function __construct($dbconn, $user)
+    {
+        $this->db = $dbconn;
+        $this->user_id = $user;
+        $this->ready = $this->db && !$this->db->is_error();
+    }
+
+
+    /**
+     * Returns addressbook name
+     */
+     function get_name()
+     {
+        return $this->name;
+     }
+
+
+    /**
+     * Save a search string for future listings
+     *
+     * @param string SQL params to use in listing method
+     */
+    function set_search_set($filter)
+    {
+        $this->filter = $filter;
+        $this->cache = null;
+    }
+
+
+    /**
+     * Getter for saved search properties
+     *
+     * @return mixed Search properties used by this class
+     */
+    function get_search_set()
+    {
+        return $this->filter;
+    }
+
+
+    /**
+     * Setter for the current group
+     * (empty, has to be re-implemented by extending class)
+     */
+    function set_group($gid)
+    {
+        $this->group_id = $gid;
+        $this->cache = null;
+    }
+
+
+    /**
+     * Reset all saved results and search parameters
+     */
+    function reset()
+    {
+        $this->result = null;
+        $this->filter = null;
+        $this->cache = null;
+    }
+
+
+    /**
+     * List all active contact groups of this source
+     *
+     * @param string  Search string to match group name
+     * @return array  Indexed list of contact groups, each a hash array
+     */
+    function list_groups($search = null)
+    {
+        $results = array();
+
+        if (!$this->groups)
+            return $results;
+
+        $sql_filter = $search ? " AND " . $this->db->ilike('name', '%'.$search.'%') : '';
+
+        $sql_result = $this->db->query(
+            "SELECT * FROM ".get_table_name($this->db_groups).
+            " WHERE del<>1".
+            " AND user_id=?".
+            $sql_filter.
+            " ORDER BY name",
+            $this->user_id);
+
+        while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
+            $sql_arr['ID'] = $sql_arr['contactgroup_id'];
+            $results[]     = $sql_arr;
+        }
+
+        return $results;
+    }
+
+
+    /**
+     * Get group properties such as name and email address(es)
+     *
+     * @param string Group identifier
+     * @return array Group properties as hash array
+     */
+    function get_group($group_id)
+    {
+        $sql_result = $this->db->query(
+            "SELECT * FROM ".get_table_name($this->db_groups).
+            " WHERE del<>1".
+            " AND contactgroup_id=?".
+            " AND user_id=?",
+            $group_id, $this->user_id);
+            
+        if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
+            $sql_arr['ID'] = $sql_arr['contactgroup_id'];
+            return $sql_arr;
+        }
+        
+        return null;
+    }
+
+    /**
+     * List the current set of contact records
+     *
+     * @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
+     */
+    function list_records($cols=null, $subset=0, $nocount=false)
+    {
+        if ($nocount || $this->list_page <= 1) {
+            // create dummy result, we don't need a count now
+            $this->result = new rcube_result_set();
+        } else {
+            // count all records
+            $this->result = $this->count();
+        }
+
+        $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
+        $length = $subset != 0 ? abs($subset) : $this->page_size;
+
+        if ($this->group_id)
+            $join = " LEFT JOIN ".get_table_name($this->db_groupmembers)." AS m".
+                " ON (m.contact_id = c.".$this->primary_key.")";
+
+        $sql_result = $this->db->limitquery(
+            "SELECT * FROM ".get_table_name($this->db_name)." AS c" .
+            $join .
+            " WHERE c.del<>1" .
+                " AND c.user_id=?" .
+                ($this->group_id ? " AND m.contactgroup_id=?" : "").
+                ($this->filter ? " AND (".$this->filter.")" : "") .
+            " ORDER BY ". $this->db->concat('c.name', 'c.email'),
+            $start_row,
+            $length,
+            $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'])) {
+                if (empty($sql_arr['email']))
+                  $sql_arr['email'] = $this->get_col_values('email', $sql_arr, true);
+                $sql_arr['name'] = $sql_arr['email'][0];
+            }
+
+            $this->result->add($sql_arr);
+        }
+
+        $cnt = count($this->result->records);
+
+        // update counter
+        if ($nocount)
+            $this->result->count = $cnt;
+        else if ($this->list_page <= 1) {
+            if ($cnt < $this->page_size && $subset == 0)
+                $this->result->count = $cnt;
+            else if (isset($this->cache['count']))
+                $this->result->count = $this->cache['count'];
+            else
+                $this->result->count = $this->_count();
+        }
+
+        return $this->result;
+    }
+
+
+    /**
+     * Search contacts
+     *
+     * @param mixed   $fields   The field name of array of field names to search in
+     * @param mixed   $value    Search value (or array of values when $fields is array)
+     * @param boolean $strict   True for strict (=), False for partial (LIKE) matching
+     * @param boolean $select   True if results are requested, False if count only
+     * @param boolean $nocount  True to skip the count query (select only)
+     * @param array   $required List of fields that cannot be empty
+     *
+     * @return object rcube_result_set Contact records and 'count' value
+     */
+    function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
+    {
+        if (!is_array($fields))
+            $fields = array($fields);
+        if (!is_array($required) && !empty($required))
+            $required = array($required);
+
+        $where = $and_where = array();
+
+        foreach ($fields as $idx => $col) {
+            // direct ID search
+            if ($col == 'ID' || $col == $this->primary_key) {
+                $ids     = !is_array($value) ? explode(',', $value) : $value;
+                $ids     = $this->db->array2list($ids, 'integer');
+                $where[] = 'c.' . $this->primary_key.' IN ('.$ids.')';
+                continue;
+            }
+            // fulltext search in all fields
+            else if ($col == '*') {
+                $words = array();
+                foreach (explode(" ", self::normalize_string($value)) as $word)
+                    $words[] = $this->db->ilike('words', '%'.$word.'%');
+                $where[] = '(' . join(' AND ', $words) . ')';
+            }
+            else {
+                $val = is_array($value) ? $value[$idx] : $value;
+                // table column
+                if (in_array($col, $this->table_cols)) {
+                    if ($strict) {
+                        $where[] = $this->db->quoteIdentifier($col).' = '.$this->db->quote($val);
+                    }
+                    else {
+                        $where[] = $this->db->ilike($col, '%'.$val.'%');
+                    }
+                }
+                // vCard field
+                else {
+                    if (in_array($col, $this->fulltext_cols)) {
+                        foreach (explode(" ", self::normalize_string($val)) as $word)
+                            $words[] = $this->db->ilike('words', '%'.$word.'%');
+                        $where[] = '(' . join(' AND ', $words) . ')';
+                    }
+                    if (is_array($value))
+                        $post_search[$col] = mb_strtolower($val);
+                }
+            }
+        }
+
+        foreach (array_intersect($required, $this->table_cols) as $col) {
+            $and_where[] = $this->db->quoteIdentifier($col).' <> '.$this->db->quote('');
+        }
+
+        if (!empty($where)) {
+            // use AND operator for advanced searches
+            $where = join(is_array($value) ? ' AND ' : ' OR ', $where);
+        }
+
+        if (!empty($and_where))
+            $where = ($where ? "($where) AND " : '') . join(' AND ', $and_where);
+
+        // Post-searching in vCard data fields
+        // we will search in all records and then build a where clause for their IDs
+        if (!empty($post_search)) {
+            $ids = array(0);
+            // build key name regexp
+            $regexp = '/^(' . implode(array_keys($post_search), '|') . ')(?:.*)$/';
+            // use initial WHERE clause, to limit records number if possible
+            if (!empty($where))
+                $this->set_search_set($where);
+
+            // count result pages
+            $cnt   = $this->count();
+            $pages = ceil($cnt / $this->page_size);
+            $scnt  = count($post_search);
+
+            // get (paged) result
+            for ($i=0; $i<$pages; $i++) {
+                $this->list_records(null, $i, true);
+                while ($row = $this->result->next()) {
+                    $id = $row[$this->primary_key];
+                    $found = array();
+                    foreach (preg_grep($regexp, array_keys($row)) as $col) {
+                        $pos     = strpos($col, ':');
+                        $colname = $pos ? substr($col, 0, $pos) : $col;
+                        $search  = $post_search[$colname];
+                        foreach ((array)$row[$col] as $value) {
+                            // composite field, e.g. address
+                            if (is_array($value)) {
+                                $value = implode($value);
+                            }
+                            $value = mb_strtolower($value);
+                            if (($strict && $value == $search) || (!$strict && strpos($value, $search) !== false)) {
+                                $found[$colname] = true;
+                                break;
+                            }
+                        }
+                    }
+                    // all fields match
+                    if (count($found) >= $scnt) {
+                        $ids[] = $id;
+                    }
+                }
+            }
+
+            // build WHERE clause
+            $ids = $this->db->array2list($ids, 'integer');
+            $where = 'c.' . $this->primary_key.' IN ('.$ids.')';
+            // reset counter
+            unset($this->cache['count']);
+
+            // when we know we have an empty result
+            if ($ids == '0') {
+                $this->set_search_set($where);
+                return ($this->result = new rcube_result_set(0, 0));
+            }
+        }
+
+        if (!empty($where)) {
+            $this->set_search_set($where);
+            if ($select)
+                $this->list_records(null, 0, $nocount);
+            else
+                $this->result = $this->count();
+        }
+
+        return $this->result;
+    }
+
+
+    /**
+     * Count number of available contacts in database
+     *
+     * @return rcube_result_set Result object
+     */
+    function count()
+    {
+        $count = isset($this->cache['count']) ? $this->cache['count'] : $this->_count();
+
+        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
+    }
+
+
+    /**
+     * Count number of available contacts in database
+     *
+     * @return int Contacts count
+     */
+    private function _count()
+    {
+        if ($this->group_id)
+            $join = " LEFT JOIN ".get_table_name($this->db_groupmembers)." AS m".
+                " ON (m.contact_id=c.".$this->primary_key.")";
+
+        // count contacts for this user
+        $sql_result = $this->db->query(
+            "SELECT COUNT(c.contact_id) AS rows".
+            " FROM ".get_table_name($this->db_name)." AS c".
+                $join.
+            " WHERE c.del<>1".
+            " AND c.user_id=?".
+            ($this->group_id ? " AND m.contactgroup_id=?" : "").
+            ($this->filter ? " AND (".$this->filter.")" : ""),
+            $this->user_id,
+            $this->group_id
+        );
+
+        $sql_arr = $this->db->fetch_assoc($sql_result);
+
+        $this->cache['count'] = (int) $sql_arr['rows'];
+
+        return $this->cache['count'];
+    }
+
+
+    /**
+     * Return the last result set
+     *
+     * @return mixed Result array or NULL if nothing selected yet
+     */
+    function get_result()
+    {
+        return $this->result;
+    }
+
+
+    /**
+     * Get a specific contact record
+     *
+     * @param mixed record identifier(s)
+     * @return mixed Result object with all record fields or False if not found
+     */
+    function get_record($id, $assoc=false)
+    {
+        // return cached result
+        if ($this->result && ($first = $this->result->first()) && $first[$this->primary_key] == $id)
+            return $assoc ? $first : $this->result;
+
+        $this->db->query(
+            "SELECT * FROM ".get_table_name($this->db_name).
+            " WHERE contact_id=?".
+                " AND user_id=?".
+                " AND del<>1",
+            $id,
+            $this->user_id
+        );
+
+        if ($sql_arr = $this->db->fetch_assoc()) {
+            $record = $this->convert_db_data($sql_arr);
+            $this->result = new rcube_result_set(1);
+            $this->result->add($record);
+        }
+
+        return $assoc && $record ? $record : $this->result;
+    }
+
+
+    /**
+     * Get group assignments of a specific contact record
+     *
+     * @param mixed Record identifier
+     * @return array List of assigned groups as ID=>Name pairs
+     */
+    function get_record_groups($id)
+    {
+      $results = array();
+
+      if (!$this->groups)
+          return $results;
+
+      $sql_result = $this->db->query(
+        "SELECT cgm.contactgroup_id, cg.name FROM " . get_table_name($this->db_groupmembers) . " AS cgm" .
+        " LEFT JOIN " . get_table_name($this->db_groups) . " AS cg ON (cgm.contactgroup_id = cg.contactgroup_id AND cg.del<>1)" .
+        " WHERE cgm.contact_id=?",
+        $id
+      );
+      while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
+        $results[$sql_arr['contactgroup_id']] = $sql_arr['name'];
+      }
+
+      return $results;
+    }
+
+
+    /**
+     * Check the given data before saving.
+     * If input not valid, the message to display can be fetched using get_error()
+     *
+     * @param array Assoziative array with data to save
+     * @param boolean Try to fix/complete record automatically
+     * @return boolean True if input is valid, False if not.
+     */
+    public function validate(&$save_data, $autofix = false)
+    {
+        // validate e-mail addresses
+        $valid = parent::validate($save_data, $autofix);
+
+        // require at least one e-mail address (syntax check is already done)
+        if ($valid && !array_filter($this->get_col_values('email', $save_data, true))) {
+            $this->set_error(self::ERROR_VALIDATE, 'noemailwarning');
+            $valid = false;
+        }
+
+        return $valid;
+    }
+
+
+    /**
+     * Create a new contact record
+     *
+     * @param array Associative array with save data
+     * @return integer|boolean The created record ID on success, False on error
+     */
+    function insert($save_data, $check=false)
+    {
+        if (!is_array($save_data))
+            return false;
+
+        $insert_id = $existing = 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, false, false))
+                            break 2;
+                    }
+                }
+            }
+        }
+
+        $save_data = $this->convert_save_data($save_data);
+        $a_insert_cols = $a_insert_values = array();
+
+        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(
+                "INSERT INTO ".get_table_name($this->db_name).
+                " (user_id, changed, del, ".join(', ', $a_insert_cols).")".
+                " VALUES (".intval($this->user_id).", ".$this->db->now().", 0, ".join(', ', $a_insert_values).")"
+            );
+
+            $insert_id = $this->db->insert_id($this->db_name);
+        }
+
+        // also add the newly created contact to the active group
+        if ($insert_id && $this->group_id)
+            $this->add_to_group($this->group_id, $insert_id);
+
+        $this->cache = null;
+
+        return $insert_id;
+    }
+
+
+    /**
+     * Update a specific contact record
+     *
+     * @param mixed Record identifier
+     * @param array Assoziative array with save data
+     * @return boolean True on success, False on error
+     */
+    function update($id, $save_cols)
+    {
+        $updated = false;
+        $write_sql = array();
+        $record = $this->get_record($id, true);
+        $save_cols = $this->convert_save_data($save_cols, $record);
+
+        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(
+                "UPDATE ".get_table_name($this->db_name).
+                " SET changed=".$this->db->now().", ".join(', ', $write_sql).
+                " WHERE contact_id=?".
+                    " AND user_id=?".
+                    " AND del<>1",
+                $id,
+                $this->user_id
+            );
+
+            $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();
+        $words = '';
+
+        // 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);
+            $fulltext = in_array($field, $this->fulltext_cols);
+            foreach ((array)$values as $value) {
+                if (isset($value))
+                    $vcard->set($field, $value, $section);
+                if ($fulltext && is_array($value))
+                    $words .= ' ' . self::normalize_string(join(" ", $value));
+                else if ($fulltext && strlen($value) >= 3)
+                    $words .= ' ' . self::normalize_string($value);
+            }
+        }
+        $out['vcard'] = $vcard->export(false);
+
+        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);
+
+        // join words for fulltext search
+        $out['words'] = join(" ", array_unique(explode(" ", $words)));
+
+        return $out;
+    }
+
+
+    /**
+     * Mark one or more contact records as deleted
+     *
+     * @param array   Record identifiers
+     * @param boolean Remove record(s) irreversible (unsupported)
+     */
+    function delete($ids, $force=true)
+    {
+        if (!is_array($ids))
+            $ids = explode(',', $ids);
+
+        $ids = $this->db->array2list($ids, 'integer');
+
+        // flag record as deleted (always)
+        $this->db->query(
+            "UPDATE ".get_table_name($this->db_name).
+            " SET del=1, changed=".$this->db->now().
+            " WHERE user_id=?".
+                " AND contact_id IN ($ids)",
+            $this->user_id
+        );
+
+        $this->cache = null;
+
+        return $this->db->affected_rows();
+    }
+
+
+    /**
+     * Undelete one or more contact records
+     *
+     * @param array  Record identifiers
+     */
+    function undelete($ids)
+    {
+        if (!is_array($ids))
+            $ids = explode(',', $ids);
+
+        $ids = $this->db->array2list($ids, 'integer');
+
+        // clear deleted flag
+        $this->db->query(
+            "UPDATE ".get_table_name($this->db_name).
+            " SET del=0, changed=".$this->db->now().
+            " WHERE user_id=?".
+                " AND contact_id IN ($ids)",
+            $this->user_id
+        );
+
+        $this->cache = null;
+
+        return $this->db->affected_rows();
+    }
+
+
+    /**
+     * Remove all records from the database
+     */
+    function delete_all()
+    {
+        $this->cache = null;
+
+        $this->db->query("UPDATE ".get_table_name($this->db_name).
+            " SET del=1, changed=".$this->db->now().
+            " WHERE user_id = ?", $this->user_id);
+
+        return $this->db->affected_rows();
+    }
+
+
+    /**
+     * Create a contact group with the given name
+     *
+     * @param string The group name
+     * @return mixed False on error, array with record props in success
+     */
+    function create_group($name)
+    {
+        $result = false;
+
+        // make sure we have a unique name
+        $name = $this->unique_groupname($name);
+
+        $this->db->query(
+            "INSERT INTO ".get_table_name($this->db_groups).
+            " (user_id, changed, name)".
+            " VALUES (".intval($this->user_id).", ".$this->db->now().", ".$this->db->quote($name).")"
+        );
+
+        if ($insert_id = $this->db->insert_id($this->db_groups))
+            $result = array('id' => $insert_id, 'name' => $name);
+
+        return $result;
+    }
+
+
+    /**
+     * Delete the given group (and all linked group members)
+     *
+     * @param string Group identifier
+     * @return boolean True on success, false if no data was changed
+     */
+    function delete_group($gid)
+    {
+        // flag group record as deleted
+        $sql_result = $this->db->query(
+            "UPDATE ".get_table_name($this->db_groups).
+            " SET del=1, changed=".$this->db->now().
+            " WHERE contactgroup_id=?".
+            " AND user_id=?",
+            $gid, $this->user_id
+        );
+
+        $this->cache = null;
+
+        return $this->db->affected_rows();
+    }
+
+
+    /**
+     * Rename a specific contact group
+     *
+     * @param string Group identifier
+     * @param string New name to set for this group
+     * @return boolean New name on success, false if no data was changed
+     */
+    function rename_group($gid, $newname)
+    {
+        // make sure we have a unique name
+        $name = $this->unique_groupname($newname);
+
+        $sql_result = $this->db->query(
+            "UPDATE ".get_table_name($this->db_groups).
+            " SET name=?, changed=".$this->db->now().
+            " WHERE contactgroup_id=?".
+            " AND user_id=?",
+            $name, $gid, $this->user_id
+        );
+
+        return $this->db->affected_rows() ? $name : false;
+    }
+
+
+    /**
+     * Add the given contact records the a certain group
+     *
+     * @param string  Group identifier
+     * @param array   List of contact identifiers to be added
+     * @return int    Number of contacts added 
+     */
+    function add_to_group($group_id, $ids)
+    {
+        if (!is_array($ids))
+            $ids = explode(',', $ids);
+
+        $added = 0;
+        $exists = array();
+
+        // get existing assignments ...
+        $sql_result = $this->db->query(
+            "SELECT contact_id FROM ".get_table_name($this->db_groupmembers).
+            " WHERE contactgroup_id=?".
+                " AND contact_id IN (".$this->db->array2list($ids, 'integer').")",
+            $group_id
+        );
+        while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
+            $exists[] = $sql_arr['contact_id'];
+        }
+        // ... and remove them from the list
+        $ids = array_diff($ids, $exists);
+
+        foreach ($ids as $contact_id) {
+            $this->db->query(
+                "INSERT INTO ".get_table_name($this->db_groupmembers).
+                " (contactgroup_id, contact_id, created)".
+                " VALUES (?, ?, ".$this->db->now().")",
+                $group_id,
+                $contact_id
+            );
+
+            if (!$this->db->db_error)
+                $added++;
+        }
+
+        return $added;
+    }
+
+
+    /**
+     * Remove the given contact records from a certain group
+     *
+     * @param string  Group identifier
+     * @param array   List of contact identifiers to be removed
+     * @return int    Number of deleted group members
+     */
+    function remove_from_group($group_id, $ids)
+    {
+        if (!is_array($ids))
+            $ids = explode(',', $ids);
+
+        $ids = $this->db->array2list($ids, 'integer');
+
+        $sql_result = $this->db->query(
+            "DELETE FROM ".get_table_name($this->db_groupmembers).
+            " WHERE contactgroup_id=?".
+                " AND contact_id IN ($ids)",
+            $group_id
+        );
+
+        return $this->db->affected_rows();
+    }
+
+
+    /**
+     * Check for existing groups with the same name
+     *
+     * @param string Name to check
+     * @return string A group name which is unique for the current use
+     */
+    private function unique_groupname($name)
+    {
+        $checkname = $name;
+        $num = 2; $hit = false;
+
+        do {
+            $sql_result = $this->db->query(
+                "SELECT 1 FROM ".get_table_name($this->db_groups).
+                " WHERE del<>1".
+                    " AND user_id=?".
+                    " AND name=?",
+                $this->user_id,
+                $checkname);
+
+            // append number to make name unique
+            if ($hit = $this->db->num_rows($sql_result))
+                $checkname = $name . ' ' . $num++;
+        } while ($hit > 0);
+
+        return $checkname;
+    }
+
+}
Index: /branches/devel-composer/program/include/rcube_html_page.php
===================================================================
--- /branches/devel-composer/program/include/rcube_html_page.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_html_page.php	(revision 5386)
@@ -0,0 +1,313 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_html_page.php                                   |
+ |                                                                       |
+ | This file is part of the Roundcube PHP suite                          |
+ | Copyright (C) 2005-2011 The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | CONTENTS:                                                             |
+ |   Class to build XHTML page output                                    |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+/**
+ * Class for HTML page creation
+ *
+ * @package HTML
+ */
+class rcube_html_page
+{
+    protected $scripts_path = '';
+    protected $script_files = array();
+    protected $css_files = array();
+    protected $scripts = array();
+    protected $charset = RCMAIL_CHARSET;
+
+    protected $script_tag_file = "<script type=\"text/javascript\" src=\"%s\"></script>\n";
+    protected $script_tag  =  "<script type=\"text/javascript\">\n/* <![CDATA[ */\n%s\n/* ]]> */\n</script>\n";
+    protected $link_css_file = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n";
+    protected $default_template = "<html>\n<head><title></title></head>\n<body></body>\n</html>";
+
+    protected $title = '';
+    protected $header = '';
+    protected $footer = '';
+    protected $body = '';
+    protected $base_path = '';
+
+
+    /** Constructor */
+    public function __construct() {}
+
+    /**
+     * Link an external script file
+     *
+     * @param string File URL
+     * @param string Target position [head|foot]
+     */
+    public function include_script($file, $position='head')
+    {
+        static $sa_files = array();
+        
+        if (!preg_match('|^https?://|i', $file) && $file[0] != '/')
+            $file = $this->scripts_path . $file . (($fs = @filemtime($this->scripts_path . $file)) ? '?s='.$fs : '');
+
+        if (in_array($file, $sa_files)) {
+            return;
+        }
+
+        $sa_files[] = $file;
+
+        if (!is_array($this->script_files[$position])) {
+            $this->script_files[$position] = array();
+        }
+        $this->script_files[$position][] = $file;
+    }
+
+    /**
+     * Add inline javascript code
+     *
+     * @param string JS code snippet
+     * @param string Target position [head|head_top|foot]
+     */
+    public function add_script($script, $position='head')
+    {
+        if (!isset($this->scripts[$position])) {
+            $this->scripts[$position] = "\n".rtrim($script);
+        } else {
+            $this->scripts[$position] .= "\n".rtrim($script);
+        }
+    }
+
+    /**
+     * Link an external css file
+     *
+     * @param string File URL
+     */
+    public function include_css($file)
+    {
+        $this->css_files[] = $file;
+    }
+
+    /**
+     * Add HTML code to the page header
+     *
+     * @param string $str HTML code
+     */
+    public function add_header($str)
+    {
+        $this->header .= "\n".$str;
+    }
+
+    /**
+     * Add HTML code to the page footer
+     * To be added right befor </body>
+     *
+     * @param string $str HTML code
+     */
+    public function add_footer($str)
+    {
+        $this->footer .= "\n".$str;
+    }
+
+    /**
+     * Setter for page title
+     *
+     * @param string $t Page title
+     */
+    public function set_title($t)
+    {
+        $this->title = $t;
+    }
+
+    /**
+     * Setter for output charset.
+     * To be specified in a meta tag and sent as http-header
+     *
+     * @param string $charset Charset
+     */
+    public function set_charset($charset)
+    {
+        $this->charset = $charset;
+    }
+
+    /**
+     * Getter for output charset
+     *
+     * @return string Output charset
+     */
+    public function get_charset()
+    {
+        return $this->charset;
+    }
+
+    /**
+     * Reset all saved properties
+     */
+    public function reset()
+    {
+        $this->script_files = array();
+        $this->scripts      = array();
+        $this->title        = '';
+        $this->header       = '';
+        $this->footer       = '';
+        $this->body         = '';
+    }
+
+    /**
+     * Process template and write to stdOut
+     *
+     * @param string HTML template
+     * @param string Base for absolute paths
+     */
+    public function write($templ='', $base_path='')
+    {
+        $output = empty($templ) ? $this->default_template : trim($templ);
+
+        // set default page title
+        if (empty($this->title)) {
+            $this->title = 'Roundcube Mail';
+        }
+
+        // replace specialchars in content
+        $page_title  = Q($this->title, 'show', FALSE);
+        $page_header = '';
+        $page_footer = '';
+
+        // include meta tag with charset
+        if (!empty($this->charset)) {
+            if (!headers_sent()) {
+                header('Content-Type: text/html; charset=' . $this->charset);
+            }
+            $page_header = '<meta http-equiv="content-type"';
+            $page_header.= ' content="text/html; charset=';
+            $page_header.= $this->charset . '" />'."\n";
+        }
+
+        // definition of the code to be placed in the document header and footer
+        if (is_array($this->script_files['head'])) {
+            foreach ($this->script_files['head'] as $file) {
+                $page_header .= sprintf($this->script_tag_file, $file);
+            }
+        }
+
+        $head_script = $this->scripts['head_top'] . $this->scripts['head'];
+        if (!empty($head_script)) {
+            $page_header .= sprintf($this->script_tag, $head_script);
+        }
+
+        if (!empty($this->header)) {
+            $page_header .= $this->header;
+        }
+
+        // put docready commands into page footer
+        if (!empty($this->scripts['docready'])) {
+            $this->add_script('$(document).ready(function(){ ' . $this->scripts['docready'] . "\n});", 'foot');
+        }
+
+        if (is_array($this->script_files['foot'])) {
+            foreach ($this->script_files['foot'] as $file) {
+                $page_footer .= sprintf($this->script_tag_file, $file);
+            }
+        }
+
+        if (!empty($this->footer)) {
+            $page_footer .= $this->footer . "\n";
+        }
+
+        if (!empty($this->scripts['foot'])) {
+            $page_footer .= sprintf($this->script_tag, $this->scripts['foot']);
+        }
+
+        // find page header
+        if ($hpos = stripos($output, '</head>')) {
+            $page_header .= "\n";
+        }
+        else {
+            if (!is_numeric($hpos)) {
+                $hpos = stripos($output, '<body');
+            }
+            if (!is_numeric($hpos) && ($hpos = stripos($output, '<html'))) {
+                while ($output[$hpos] != '>') {
+                    $hpos++;
+                }
+                $hpos++;
+            }
+            $page_header = "<head>\n<title>$page_title</title>\n$page_header\n</head>\n";
+        }
+
+        // add page hader
+        if ($hpos) {
+            $output = substr_replace($output, $page_header, $hpos, 0);
+        }
+        else {
+            $output = $page_header . $output;
+        }
+
+        // add page footer
+        if (($fpos = strripos($output, '</body>')) || ($fpos = strripos($output, '</html>'))) {
+            $output = substr_replace($output, $page_footer."\n", $fpos, 0);
+        }
+        else {
+            $output .= "\n".$page_footer;
+        }
+
+        // add css files in head, before scripts, for speed up with parallel downloads
+        if (!empty($this->css_files) && 
+            (($pos = stripos($output, '<script ')) || ($pos = stripos($output, '</head>')))
+        ) {
+            $css = '';
+            foreach ($this->css_files as $file) {
+                $css .= sprintf($this->link_css_file, $file);
+            }
+            $output = substr_replace($output, $css, $pos, 0);
+        }
+
+        $this->base_path = $base_path;
+
+        // correct absolute paths in images and other tags
+        // add timestamp to .js and .css filename
+        $output = preg_replace_callback(
+            '!(src|href|background)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i',
+            array($this, 'file_callback'), $output);
+        $output = str_replace('$__skin_path', $base_path, $output);
+
+        // trigger hook with final HTML content to be sent
+        $hook = rcmail::get_instance()->plugins->exec_hook("send_page", array('content' => $output));
+        if (!$hook['abort']) {
+            if ($this->charset != RCMAIL_CHARSET)
+                echo rcube_charset_convert($hook['content'], RCMAIL_CHARSET, $this->charset);
+            else
+                echo $hook['content'];
+        }
+    }
+
+    /**
+     * Callback function for preg_replace_callback in write()
+     *
+     * @return string Parsed string
+     */
+    private function file_callback($matches)
+    {
+	    $file = $matches[3];
+
+        // correct absolute paths
+	    if ($file[0] == '/')
+	        $file = $this->base_path . $file;
+
+        // add file modification timestamp
+	    if (preg_match('/\.(js|css)$/', $file))
+    	    $file .= '?s=' . @filemtime($file);
+
+	    return sprintf("%s=%s%s%s", $matches[1], $matches[2], $file, $matches[4]);
+    }
+}
+
Index: /branches/devel-composer/program/include/rcube_imap.php
===================================================================
--- /branches/devel-composer/program/include/rcube_imap.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_imap.php	(revision 5386)
@@ -0,0 +1,4674 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_imap.php                                        |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
+ | Copyright (C) 2011, Kolab Systems AG                                  |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   IMAP Engine                                                         |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ | Author: Aleksander Machniak <alec@alec.pl>                            |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Interface class for accessing an IMAP server
+ *
+ * @package    Mail
+ * @author     Thomas Bruederli <roundcube@gmail.com>
+ * @author     Aleksander Machniak <alec@alec.pl>
+ * @version    2.0
+ */
+class rcube_imap
+{
+    public $debug_level = 1;
+    public $skip_deleted = false;
+    public $page_size = 10;
+    public $list_page = 1;
+    public $threading = false;
+    public $fetch_add_headers = '';
+    public $get_all_headers = false;
+
+    /**
+     * Instance of rcube_imap_generic
+     *
+     * @var rcube_imap_generic
+     */
+    public $conn;
+
+    /**
+     * Instance of rcube_imap_cache
+     *
+     * @var rcube_imap_cache
+     */
+    private $mcache;
+
+    /**
+     * Instance of rcube_cache
+     *
+     * @var rcube_cache
+     */
+    private $cache;
+
+    /**
+     * Internal (in-memory) cache
+     *
+     * @var array
+     */
+    private $icache = array();
+
+    private $mailbox = 'INBOX';
+    private $delimiter = NULL;
+    private $namespace = NULL;
+    private $sort_field = '';
+    private $sort_order = 'DESC';
+    private $default_charset = 'ISO-8859-1';
+    private $struct_charset = NULL;
+    private $default_folders = array('INBOX');
+    private $uid_id_map = array();
+    private $msg_headers = array();
+    public  $search_set = NULL;
+    public  $search_string = '';
+    private $search_charset = '';
+    private $search_sort_field = '';
+    private $search_threads = false;
+    private $search_sorted = false;
+    private $options = array('auth_method' => 'check');
+    private $host, $user, $pass, $port, $ssl;
+    private $caching = false;
+    private $messages_caching = false;
+
+    /**
+     * All (additional) headers used (in any way) by Roundcube
+     * Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
+     * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
+     *
+     * @var array
+     * @see rcube_imap::fetch_add_headers
+     */
+    private $all_headers = array(
+        'IN-REPLY-TO',
+        'BCC',
+        'MESSAGE-ID',
+        'CONTENT-TRANSFER-ENCODING',
+        'REFERENCES',
+        'X-DRAFT-INFO',
+        'MAIL-FOLLOWUP-TO',
+        'MAIL-REPLY-TO',
+        'RETURN-PATH',
+    );
+
+    const UNKNOWN       = 0;
+    const NOPERM        = 1;
+    const READONLY      = 2;
+    const TRYCREATE     = 3;
+    const INUSE         = 4;
+    const OVERQUOTA     = 5;
+    const ALREADYEXISTS = 6;
+    const NONEXISTENT   = 7;
+    const CONTACTADMIN  = 8;
+
+
+    /**
+     * Object constructor.
+     */
+    function __construct()
+    {
+        $this->conn = new rcube_imap_generic();
+
+        // Set namespace and delimiter from session,
+        // so some methods would work before connection
+        if (isset($_SESSION['imap_namespace']))
+            $this->namespace = $_SESSION['imap_namespace'];
+        if (isset($_SESSION['imap_delimiter']))
+            $this->delimiter = $_SESSION['imap_delimiter'];
+    }
+
+
+    /**
+     * Connect to an IMAP server
+     *
+     * @param  string   $host    Host to connect
+     * @param  string   $user    Username for IMAP account
+     * @param  string   $pass    Password for IMAP account
+     * @param  integer  $port    Port to connect to
+     * @param  string   $use_ssl SSL schema (either ssl or tls) or null if plain connection
+     * @return boolean  TRUE on success, FALSE on failure
+     * @access public
+     */
+    function connect($host, $user, $pass, $port=143, $use_ssl=null)
+    {
+        // check for OpenSSL support in PHP build
+        if ($use_ssl && extension_loaded('openssl'))
+            $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
+        else if ($use_ssl) {
+            raise_error(array('code' => 403, 'type' => 'imap',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "OpenSSL not available"), true, false);
+            $port = 143;
+        }
+
+        $this->options['port'] = $port;
+
+        if ($this->options['debug']) {
+            $this->conn->setDebug(true, array($this, 'debug_handler'));
+
+            $this->options['ident'] = array(
+                'name' => 'Roundcube Webmail',
+                'version' => RCMAIL_VERSION,
+                'php' => PHP_VERSION,
+                'os' => PHP_OS,
+                'command' => $_SERVER['REQUEST_URI'],
+            );
+        }
+
+        $attempt = 0;
+        do {
+            $data = rcmail::get_instance()->plugins->exec_hook('imap_connect',
+                array_merge($this->options, array('host' => $host, 'user' => $user,
+                    'attempt' => ++$attempt)));
+
+            if (!empty($data['pass']))
+                $pass = $data['pass'];
+
+            $this->conn->connect($data['host'], $data['user'], $pass, $data);
+        } while(!$this->conn->connected() && $data['retry']);
+
+        $this->host = $data['host'];
+        $this->user = $data['user'];
+        $this->pass = $pass;
+        $this->port = $port;
+        $this->ssl  = $use_ssl;
+
+        if ($this->conn->connected()) {
+            // get namespace and delimiter
+            $this->set_env();
+            return true;
+        }
+        // write error log
+        else if ($this->conn->error) {
+            if ($pass && $user) {
+                $message = sprintf("Login failed for %s from %s. %s",
+                    $user, rcmail_remote_ip(), $this->conn->error);
+
+                raise_error(array('code' => 403, 'type' => 'imap',
+                    'file' => __FILE__, 'line' => __LINE__,
+                    'message' => $message), true, false);
+            }
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Close IMAP connection
+     * Usually done on script shutdown
+     *
+     * @access public
+     */
+    function close()
+    {
+        $this->conn->closeConnection();
+        if ($this->mcache)
+            $this->mcache->close();
+    }
+
+
+    /**
+     * Close IMAP connection and re-connect
+     * This is used to avoid some strange socket errors when talking to Courier IMAP
+     *
+     * @access public
+     */
+    function reconnect()
+    {
+        $this->conn->closeConnection();
+        $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
+
+        // issue SELECT command to restore connection status
+        if ($connected && strlen($this->mailbox))
+            $this->conn->select($this->mailbox);
+    }
+
+
+    /**
+     * Returns code of last error
+     *
+     * @return int Error code
+     */
+    function get_error_code()
+    {
+        return $this->conn->errornum;
+    }
+
+
+    /**
+     * Returns message of last error
+     *
+     * @return string Error message
+     */
+    function get_error_str()
+    {
+        return $this->conn->error;
+    }
+
+
+    /**
+     * Returns code of last command response
+     *
+     * @return int Response code
+     */
+    function get_response_code()
+    {
+        switch ($this->conn->resultcode) {
+            case 'NOPERM':
+                return self::NOPERM;
+            case 'READ-ONLY':
+                return self::READONLY;
+            case 'TRYCREATE':
+                return self::TRYCREATE;
+            case 'INUSE':
+                return self::INUSE;
+            case 'OVERQUOTA':
+                return self::OVERQUOTA;
+            case 'ALREADYEXISTS':
+                return self::ALREADYEXISTS;
+            case 'NONEXISTENT':
+                return self::NONEXISTENT;
+            case 'CONTACTADMIN':
+                return self::CONTACTADMIN;
+            default:
+                return self::UNKNOWN;
+        }
+    }
+
+
+    /**
+     * Returns last command response
+     *
+     * @return string Response
+     */
+    function get_response_str()
+    {
+        return $this->conn->result;
+    }
+
+
+    /**
+     * Set options to be used in rcube_imap_generic::connect()
+     *
+     * @param array $opt Options array
+     */
+    function set_options($opt)
+    {
+        $this->options = array_merge($this->options, (array)$opt);
+    }
+
+
+    /**
+     * Set default message charset
+     *
+     * This will be used for message decoding if a charset specification is not available
+     *
+     * @param  string $cs Charset string
+     * @access public
+     */
+    function set_charset($cs)
+    {
+        $this->default_charset = $cs;
+    }
+
+
+    /**
+     * This list of folders will be listed above all other folders
+     *
+     * @param  array $arr Indexed list of folder names
+     * @access public
+     */
+    function set_default_mailboxes($arr)
+    {
+        if (is_array($arr)) {
+            $this->default_folders = $arr;
+
+            // add inbox if not included
+            if (!in_array('INBOX', $this->default_folders))
+                array_unshift($this->default_folders, 'INBOX');
+        }
+    }
+
+
+    /**
+     * Set internal mailbox reference.
+     *
+     * All operations will be perfomed on this mailbox/folder
+     *
+     * @param  string $mailbox Mailbox/Folder name
+     * @access public
+     */
+    function set_mailbox($mailbox)
+    {
+        if ($this->mailbox == $mailbox)
+            return;
+
+        $this->mailbox = $mailbox;
+
+        // clear messagecount cache for this mailbox
+        $this->_clear_messagecount($mailbox);
+    }
+
+
+    /**
+     * Forces selection of a mailbox
+     *
+     * @param  string $mailbox Mailbox/Folder name
+     * @access public
+     */
+    function select_mailbox($mailbox=null)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        $selected = $this->conn->select($mailbox);
+
+        if ($selected && $this->mailbox != $mailbox) {
+            // clear messagecount cache for this mailbox
+            $this->_clear_messagecount($mailbox);
+            $this->mailbox = $mailbox;
+        }
+    }
+
+
+    /**
+     * Set internal list page
+     *
+     * @param  number $page Page number to list
+     * @access public
+     */
+    function set_page($page)
+    {
+        $this->list_page = (int)$page;
+    }
+
+
+    /**
+     * Set internal page size
+     *
+     * @param  number $size Number of messages to display on one page
+     * @access public
+     */
+    function set_pagesize($size)
+    {
+        $this->page_size = (int)$size;
+    }
+
+
+    /**
+     * Save a set of message ids for future message listing methods
+     *
+     * @param  string  IMAP Search query
+     * @param  array   List of message ids or NULL if empty
+     * @param  string  Charset of search string
+     * @param  string  Sorting field
+     * @param  string  True if set is sorted (SORT was used for searching)
+     */
+    function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false)
+    {
+        if (is_array($str) && $msgs == null)
+            list($str, $msgs, $charset, $sort_field, $threads, $sorted) = $str;
+        if ($msgs === false)
+            $msgs = array();
+        else if ($msgs != null && !is_array($msgs))
+            $msgs = explode(',', $msgs);
+
+        $this->search_string     = $str;
+        $this->search_set        = $msgs;
+        $this->search_charset    = $charset;
+        $this->search_sort_field = $sort_field;
+        $this->search_threads    = $threads;
+        $this->search_sorted     = $sorted;
+    }
+
+
+    /**
+     * Return the saved search set as hash array
+     * @return array Search set
+     */
+    function get_search_set()
+    {
+        return array($this->search_string,
+	        $this->search_set,
+        	$this->search_charset,
+        	$this->search_sort_field,
+        	$this->search_threads,
+        	$this->search_sorted,
+	    );
+    }
+
+
+    /**
+     * Returns the currently used mailbox name
+     *
+     * @return  string Name of the mailbox/folder
+     * @access  public
+     */
+    function get_mailbox_name()
+    {
+        return $this->conn->connected() ? $this->mailbox : '';
+    }
+
+
+    /**
+     * Returns the IMAP server's capability
+     *
+     * @param   string  $cap Capability name
+     * @return  mixed   Capability value or TRUE if supported, FALSE if not
+     * @access  public
+     */
+    function get_capability($cap)
+    {
+        return $this->conn->getCapability(strtoupper($cap));
+    }
+
+
+    /**
+     * Sets threading flag to the best supported THREAD algorithm
+     *
+     * @param  boolean  $enable TRUE to enable and FALSE
+     * @return string   Algorithm or false if THREAD is not supported
+     * @access public
+     */
+    function set_threading($enable=false)
+    {
+        $this->threading = false;
+
+        if ($enable && ($caps = $this->get_capability('THREAD'))) {
+            if (in_array('REFS', $caps))
+                $this->threading = 'REFS';
+            else if (in_array('REFERENCES', $caps))
+                $this->threading = 'REFERENCES';
+            else if (in_array('ORDEREDSUBJECT', $caps))
+                $this->threading = 'ORDEREDSUBJECT';
+        }
+
+        return $this->threading;
+    }
+
+
+    /**
+     * Checks the PERMANENTFLAGS capability of the current mailbox
+     * and returns true if the given flag is supported by the IMAP server
+     *
+     * @param   string  $flag Permanentflag name
+     * @return  boolean True if this flag is supported
+     * @access  public
+     */
+    function check_permflag($flag)
+    {
+        $flag = strtoupper($flag);
+        $imap_flag = $this->conn->flags[$flag];
+        return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
+    }
+
+
+    /**
+     * Returns the delimiter that is used by the IMAP server for folder separation
+     *
+     * @return  string  Delimiter string
+     * @access  public
+     */
+    function get_hierarchy_delimiter()
+    {
+        return $this->delimiter;
+    }
+
+
+    /**
+     * Get namespace
+     *
+     * @param string $name Namespace array index: personal, other, shared, prefix
+     *
+     * @return  array  Namespace data
+     * @access  public
+     */
+    function get_namespace($name=null)
+    {
+        $ns = $this->namespace;
+
+        if ($name) {
+            return isset($ns[$name]) ? $ns[$name] : null;
+        }
+
+        unset($ns['prefix']);
+        return $ns;
+    }
+
+
+    /**
+     * Sets delimiter and namespaces
+     *
+     * @access private
+     */
+    private function set_env()
+    {
+        if ($this->delimiter !== null && $this->namespace !== null) {
+            return;
+        }
+
+        $config = rcmail::get_instance()->config;
+        $imap_personal  = $config->get('imap_ns_personal');
+        $imap_other     = $config->get('imap_ns_other');
+        $imap_shared    = $config->get('imap_ns_shared');
+        $imap_delimiter = $config->get('imap_delimiter');
+
+        if (!$this->conn->connected())
+            return;
+
+        $ns = $this->conn->getNamespace();
+
+        // Set namespaces (NAMESPACE supported)
+        if (is_array($ns)) {
+            $this->namespace = $ns;
+        }
+        else {
+            $this->namespace = array(
+                'personal' => NULL,
+                'other'    => NULL,
+                'shared'   => NULL,
+            );
+        }
+
+        if ($imap_delimiter) {
+            $this->delimiter = $imap_delimiter;
+        }
+        if (empty($this->delimiter)) {
+            $this->delimiter = $this->namespace['personal'][0][1];
+        }
+        if (empty($this->delimiter)) {
+            $this->delimiter = $this->conn->getHierarchyDelimiter();
+        }
+        if (empty($this->delimiter)) {
+            $this->delimiter = '/';
+        }
+
+        // Overwrite namespaces
+        if ($imap_personal !== null) {
+            $this->namespace['personal'] = NULL;
+            foreach ((array)$imap_personal as $dir) {
+                $this->namespace['personal'][] = array($dir, $this->delimiter);
+            }
+        }
+        if ($imap_other !== null) {
+            $this->namespace['other'] = NULL;
+            foreach ((array)$imap_other as $dir) {
+                if ($dir) {
+                    $this->namespace['other'][] = array($dir, $this->delimiter);
+                }
+            }
+        }
+        if ($imap_shared !== null) {
+            $this->namespace['shared'] = NULL;
+            foreach ((array)$imap_shared as $dir) {
+                if ($dir) {
+                    $this->namespace['shared'][] = array($dir, $this->delimiter);
+                }
+            }
+        }
+
+        // Find personal namespace prefix for mod_mailbox()
+        // Prefix can be removed when there is only one personal namespace
+        if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) {
+            $this->namespace['prefix'] = $this->namespace['personal'][0][0];
+        }
+
+        $_SESSION['imap_namespace'] = $this->namespace;
+        $_SESSION['imap_delimiter'] = $this->delimiter;
+    }
+
+
+    /**
+     * Get message count for a specific mailbox
+     *
+     * @param  string  $mailbox Mailbox/folder name
+     * @param  string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
+     * @param  boolean $force   Force reading from server and update cache
+     * @param  boolean $status  Enables storing folder status info (max UID/count),
+     *                          required for mailbox_status()
+     * @return int     Number of messages
+     * @access public
+     */
+    function messagecount($mailbox='', $mode='ALL', $force=false, $status=true)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        return $this->_messagecount($mailbox, $mode, $force, $status);
+    }
+
+
+    /**
+     * Private method for getting nr of messages
+     *
+     * @param string  $mailbox Mailbox name
+     * @param string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
+     * @param boolean $force   Force reading from server and update cache
+     * @param boolean $status  Enables storing folder status info (max UID/count),
+     *                         required for mailbox_status()
+     * @return int Number of messages
+     * @access  private
+     * @see     rcube_imap::messagecount()
+     */
+    private function _messagecount($mailbox, $mode='ALL', $force=false, $status=true)
+    {
+        $mode = strtoupper($mode);
+
+        // count search set
+        if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
+            if ($this->search_threads)
+                return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
+            else
+                return count((array)$this->search_set);
+        }
+
+        $a_mailbox_cache = $this->get_cache('messagecount');
+
+        // return cached value
+        if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
+            return $a_mailbox_cache[$mailbox][$mode];
+
+        if (!is_array($a_mailbox_cache[$mailbox]))
+            $a_mailbox_cache[$mailbox] = array();
+
+        if ($mode == 'THREADS') {
+            $res   = $this->_threadcount($mailbox, $msg_count);
+            $count = $res['count'];
+
+            if ($status) {
+                $this->set_folder_stats($mailbox, 'cnt', $res['msgcount']);
+                $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->id2uid($res['maxuid'], $mailbox) : 0);
+            }
+        }
+        // RECENT count is fetched a bit different
+        else if ($mode == 'RECENT') {
+            $count = $this->conn->countRecent($mailbox);
+        }
+        // use SEARCH for message counting
+        else if ($this->skip_deleted) {
+            $search_str = "ALL UNDELETED";
+            $keys       = array('COUNT');
+            $need_uid   = false;
+
+            if ($mode == 'UNSEEN') {
+                $search_str .= " UNSEEN";
+            }
+            else {
+                if ($this->messages_caching) {
+                    $keys[] = 'ALL';
+                }
+                if ($status) {
+                    $keys[]   = 'MAX';
+                    $need_uid = true;
+                }
+            }
+
+            // get message count using (E)SEARCH
+            // not very performant but more precise (using UNDELETED)
+            $index = $this->conn->search($mailbox, $search_str, $need_uid, $keys);
+
+            $count = is_array($index) ? $index['COUNT'] : 0;
+
+            if ($mode == 'ALL') {
+                if ($this->messages_caching) {
+                    // Save additional info required by cache status check
+                    $this->icache['undeleted_idx'] = array($mailbox, $index['ALL'], $index['COUNT']);
+                }
+                if ($status) {
+                    $this->set_folder_stats($mailbox, 'cnt', $count);
+                    $this->set_folder_stats($mailbox, 'maxuid', is_array($index) ? $index['MAX'] : 0);
+                }
+            }
+        }
+        else {
+            if ($mode == 'UNSEEN')
+                $count = $this->conn->countUnseen($mailbox);
+            else {
+                $count = $this->conn->countMessages($mailbox);
+                if ($status) {
+                    $this->set_folder_stats($mailbox,'cnt', $count);
+                    $this->set_folder_stats($mailbox, 'maxuid', $count ? $this->id2uid($count, $mailbox) : 0);
+                }
+            }
+        }
+
+        $a_mailbox_cache[$mailbox][$mode] = (int)$count;
+
+        // write back to cache
+        $this->update_cache('messagecount', $a_mailbox_cache);
+
+        return (int)$count;
+    }
+
+
+    /**
+     * Private method for getting nr of threads
+     *
+     * @param string $mailbox   Folder name
+     *
+     * @returns array Array containing items: 'count' - threads count,
+     *                'msgcount' = messages count, 'maxuid' = max. UID in the set
+     * @access  private
+     */
+    private function _threadcount($mailbox)
+    {
+        $result = array();
+
+        if (!empty($this->icache['threads'])) {
+            $dcount = count($this->icache['threads']['depth']);
+            $result = array(
+                'count'    => count($this->icache['threads']['tree']),
+                'msgcount' => $dcount,
+                'maxuid'   => $dcount ? max(array_keys($this->icache['threads']['depth'])) : 0,
+            );
+        }
+        else if (is_array($result = $this->fetch_threads($mailbox))) {
+            $dcount = count($result[1]);
+            $result = array(
+                'count'    => count($result[0]),
+                'msgcount' => $dcount,
+                'maxuid'   => $dcount ? max(array_keys($result[1])) : 0,
+            );
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Public method for listing headers
+     * convert mailbox name with root dir first
+     *
+     * @param   string   $mailbox    Mailbox/folder name
+     * @param   int      $page       Current page to list
+     * @param   string   $sort_field Header field to sort by
+     * @param   string   $sort_order Sort order [ASC|DESC]
+     * @param   int      $slice      Number of slice items to extract from result array
+     * @return  array    Indexed array with message header objects
+     * @access  public
+     */
+    function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, $slice);
+    }
+
+
+    /**
+     * Private method for listing message headers
+     *
+     * @param   string   $mailbox    Mailbox name
+     * @param   int      $page       Current page to list
+     * @param   string   $sort_field Header field to sort by
+     * @param   string   $sort_order Sort order [ASC|DESC]
+     * @param   int      $slice      Number of slice items to extract from result array
+     *
+     * @return  array    Indexed array with message header objects
+     * @see     rcube_imap::list_headers
+     */
+    private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
+    {
+        if (!strlen($mailbox))
+            return array();
+
+        // use saved message set
+        if ($this->search_string && $mailbox == $this->mailbox)
+            return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
+
+        if ($this->threading)
+            return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $slice);
+
+        $this->_set_sort_order($sort_field, $sort_order);
+
+        $page = $page ? $page : $this->list_page;
+
+        // Use messages cache
+        if ($mcache = $this->get_mcache_engine()) {
+            $msg_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
+
+            if (empty($msg_index))
+                return array();
+
+            $from      = ($page-1) * $this->page_size;
+            $to        = $from + $this->page_size;
+            $msg_index = array_values($msg_index); // UIDs
+            $is_uid    = true;
+            $sorted    = true;
+
+            if ($from || $to)
+                $msg_index = array_slice($msg_index, $from, $to - $from);
+
+            if ($slice)
+                $msg_index = array_slice($msg_index, -$slice, $slice);
+
+            $a_msg_headers = $mcache->get_messages($mailbox, $msg_index);
+        }
+        // retrieve headers from IMAP
+        // use message index sort as default sorting (for better performance)
+        else if (!$this->sort_field) {
+            if ($this->skip_deleted) {
+                // @TODO: this could be cached
+                if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
+                    list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
+                    $msg_index = array_slice($msg_index, $begin, $end-$begin);
+                }
+            }
+            else if ($max = $this->conn->countMessages($mailbox)) {
+                list($begin, $end) = $this->_get_message_range($max, $page);
+                $msg_index = range($begin+1, $end);
+            }
+            else
+                $msg_index = array();
+
+            if ($slice && $msg_index)
+                $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
+
+            // fetch reqested headers from server
+            if ($msg_index)
+                $a_msg_headers = $this->fetch_headers($mailbox, $msg_index);
+        }
+        // use SORT command
+        else if ($this->get_capability('SORT') &&
+            // Courier-IMAP provides SORT capability but allows to disable it by admin (#1486959)
+            ($msg_index = $this->conn->sort($mailbox, $this->sort_field,
+                $this->skip_deleted ? 'UNDELETED' : '', true)) !== false
+        ) {
+            if (!empty($msg_index)) {
+                list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
+                $msg_index = array_slice($msg_index, $begin, $end-$begin);
+                $is_uid    = true;
+
+                if ($slice)
+                    $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
+
+                // fetch reqested headers from server
+                $a_msg_headers = $this->fetch_headers($mailbox, $msg_index, true);
+            }
+        }
+        // fetch specified header for all messages and sort
+        else if ($msg_index = $this->conn->fetchHeaderIndex($mailbox, "1:*",
+            $this->sort_field, $this->skip_deleted)
+        ) {
+            asort($msg_index); // ASC
+            $msg_index = array_keys($msg_index);
+            list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
+            $msg_index = array_slice($msg_index, $begin, $end-$begin);
+
+            if ($slice)
+                $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
+
+            // fetch reqested headers from server
+            $a_msg_headers = $this->fetch_headers($mailbox, $msg_index);
+        }
+
+        // return empty array if no messages found
+        if (!is_array($a_msg_headers) || empty($a_msg_headers))
+            return array();
+
+        // use this class for message sorting
+        $sorter = new rcube_header_sorter();
+        $sorter->set_index($msg_index, $is_uid);
+        $sorter->sort_headers($a_msg_headers);
+
+        if ($this->sort_order == 'DESC' && !$sorted)
+            $a_msg_headers = array_reverse($a_msg_headers);
+
+        return array_values($a_msg_headers);
+    }
+
+
+    /**
+     * Private method for listing message headers using threads
+     *
+     * @param   string   $mailbox    Mailbox/folder name
+     * @param   int      $page       Current page to list
+     * @param   string   $sort_field Header field to sort by
+     * @param   string   $sort_order Sort order [ASC|DESC]
+     * @param   int      $slice      Number of slice items to extract from result array
+     *
+     * @return  array    Indexed array with message header objects
+     * @see     rcube_imap::list_headers
+     */
+    private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
+    {
+        $this->_set_sort_order($sort_field, $sort_order);
+
+        $page   = $page ? $page : $this->list_page;
+        $mcache = $this->get_mcache_engine();
+
+        // get all threads (not sorted)
+        if ($mcache)
+            list ($thread_tree, $msg_depth, $has_children) = $mcache->get_thread($mailbox);
+        else
+            list ($thread_tree, $msg_depth, $has_children) = $this->fetch_threads($mailbox);
+
+        if (empty($thread_tree))
+            return array();
+
+        $msg_index = $this->sort_threads($mailbox, $thread_tree);
+
+        return $this->_fetch_thread_headers($mailbox,
+            $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
+    }
+
+
+    /**
+     * Method for fetching threads data
+     *
+     * @param  string $mailbox  Folder name
+     * @param  bool   $force    Use IMAP server, no cache
+     *
+     * @return  array    Array with thread data
+     */
+    function fetch_threads($mailbox, $force = false)
+    {
+        if (!$force && ($mcache = $this->get_mcache_engine())) {
+            // don't store in self's internal cache, cache has it's own internal cache
+            return $mcache->get_thread($mailbox);
+        }
+
+        if (empty($this->icache['threads'])) {
+            // get all threads
+            $result = $this->conn->thread($mailbox, $this->threading,
+                $this->skip_deleted ? 'UNDELETED' : '');
+
+            // add to internal (fast) cache
+            $this->icache['threads'] = array();
+            $this->icache['threads']['tree'] = is_array($result) ? $result[0] : array();
+            $this->icache['threads']['depth'] = is_array($result) ? $result[1] : array();
+            $this->icache['threads']['has_children'] = is_array($result) ? $result[2] : array();
+        }
+
+        return array(
+            $this->icache['threads']['tree'],
+            $this->icache['threads']['depth'],
+            $this->icache['threads']['has_children'],
+        );
+    }
+
+
+    /**
+     * Private method for fetching threaded messages headers
+     *
+     * @param string  $mailbox      Mailbox name
+     * @param array   $thread_tree  Thread tree data
+     * @param array   $msg_depth    Thread depth data
+     * @param array   $has_children Thread children data
+     * @param array   $msg_index    Messages index
+     * @param int     $page         List page number
+     * @param int     $slice        Number of threads to slice
+     *
+     * @return array  Messages headers
+     * @access  private
+     */
+    private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
+    {
+        // now get IDs for current page
+        list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
+        $msg_index = array_slice($msg_index, $begin, $end-$begin);
+
+        if ($slice)
+            $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
+
+        if ($this->sort_order == 'DESC')
+            $msg_index = array_reverse($msg_index);
+
+        // flatten threads array
+        // @TODO: fetch children only in expanded mode (?)
+        $all_ids = array();
+        foreach ($msg_index as $root) {
+            $all_ids[] = $root;
+            if (!empty($thread_tree[$root]))
+                $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
+        }
+
+        // fetch reqested headers from server
+        $a_msg_headers = $this->fetch_headers($mailbox, $all_ids);
+
+        // return empty array if no messages found
+        if (!is_array($a_msg_headers) || empty($a_msg_headers))
+            return array();
+
+        // use this class for message sorting
+        $sorter = new rcube_header_sorter();
+        $sorter->set_index($all_ids);
+        $sorter->sort_headers($a_msg_headers);
+
+        // Set depth, has_children and unread_children fields in headers
+        $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
+
+        return array_values($a_msg_headers);
+    }
+
+
+    /**
+     * Private method for setting threaded messages flags:
+     * depth, has_children and unread_children
+     *
+     * @param  array  $headers      Reference to headers array indexed by message ID
+     * @param  array  $msg_depth    Array of messages depth indexed by message ID
+     * @param  array  $msg_children Array of messages children flags indexed by message ID
+     * @return array   Message headers array indexed by message ID
+     * @access private
+     */
+    private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
+    {
+        $parents = array();
+
+        foreach ($headers as $idx => $header) {
+            $id = $header->id;
+            $depth = $msg_depth[$id];
+            $parents = array_slice($parents, 0, $depth);
+
+            if (!empty($parents)) {
+                $headers[$idx]->parent_uid = end($parents);
+                if (empty($header->flags['SEEN']))
+                    $headers[$parents[0]]->unread_children++;
+            }
+            array_push($parents, $header->uid);
+
+            $headers[$idx]->depth = $depth;
+            $headers[$idx]->has_children = $msg_children[$id];
+        }
+    }
+
+
+    /**
+     * Private method for listing a set of message headers (search results)
+     *
+     * @param   string   $mailbox    Mailbox/folder name
+     * @param   int      $page       Current page to list
+     * @param   string   $sort_field Header field to sort by
+     * @param   string   $sort_order Sort order [ASC|DESC]
+     * @param   int  $slice      Number of slice items to extract from result array
+     * @return  array    Indexed array with message header objects
+     * @access  private
+     * @see     rcube_imap::list_header_set()
+     */
+    private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
+    {
+        if (!strlen($mailbox) || empty($this->search_set))
+            return array();
+
+        // use saved messages from searching
+        if ($this->threading)
+            return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
+
+        // search set is threaded, we need a new one
+        if ($this->search_threads) {
+            if (empty($this->search_set['tree']))
+                return array();
+            $this->search('', $this->search_string, $this->search_charset, $sort_field);
+        }
+
+        $msgs = $this->search_set;
+        $a_msg_headers = array();
+        $page = $page ? $page : $this->list_page;
+        $start_msg = ($page-1) * $this->page_size;
+
+        $this->_set_sort_order($sort_field, $sort_order);
+
+        // quickest method (default sorting)
+        if (!$this->search_sort_field && !$this->sort_field) {
+            if ($sort_order == 'DESC')
+                $msgs = array_reverse($msgs);
+
+            // get messages uids for one page
+            $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
+
+            if ($slice)
+                $msgs = array_slice($msgs, -$slice, $slice);
+
+            // fetch headers
+            $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
+
+            // I didn't found in RFC that FETCH always returns messages sorted by index
+            $sorter = new rcube_header_sorter();
+            $sorter->set_index($msgs);
+            $sorter->sort_headers($a_msg_headers);
+
+            return array_values($a_msg_headers);
+        }
+
+        // sorted messages, so we can first slice array and then fetch only wanted headers
+        if ($this->search_sorted) { // SORT searching result
+            // reset search set if sorting field has been changed
+            if ($this->sort_field && $this->search_sort_field != $this->sort_field)
+                $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
+
+            // return empty array if no messages found
+            if (empty($msgs))
+                return array();
+
+            if ($sort_order == 'DESC')
+                $msgs = array_reverse($msgs);
+
+            // get messages uids for one page
+            $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
+
+            if ($slice)
+                $msgs = array_slice($msgs, -$slice, $slice);
+
+            // fetch headers
+            $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
+
+            $sorter = new rcube_header_sorter();
+            $sorter->set_index($msgs);
+            $sorter->sort_headers($a_msg_headers);
+
+            return array_values($a_msg_headers);
+        }
+        else { // SEARCH result, need sorting
+            $cnt = count($msgs);
+            // 300: experimantal value for best result
+            if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
+                // use memory less expensive (and quick) method for big result set
+                $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
+                // get messages uids for one page...
+                $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
+                if ($slice)
+                    $msgs = array_slice($msgs, -$slice, $slice);
+                // ...and fetch headers
+                $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
+
+
+                // return empty array if no messages found
+                if (!is_array($a_msg_headers) || empty($a_msg_headers))
+                    return array();
+
+                $sorter = new rcube_header_sorter();
+                $sorter->set_index($msgs);
+                $sorter->sort_headers($a_msg_headers);
+
+                return array_values($a_msg_headers);
+            }
+            else {
+                // for small result set we can fetch all messages headers
+                $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
+
+                // return empty array if no messages found
+                if (!is_array($a_msg_headers) || empty($a_msg_headers))
+                    return array();
+
+                // if not already sorted
+                $a_msg_headers = $this->conn->sortHeaders(
+                    $a_msg_headers, $this->sort_field, $this->sort_order);
+
+                // only return the requested part of the set
+                $a_msg_headers = array_slice(array_values($a_msg_headers),
+                    $start_msg, min($cnt-$start_msg, $this->page_size));
+
+                if ($slice)
+                    $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
+
+                return $a_msg_headers;
+            }
+        }
+    }
+
+
+    /**
+     * Private method for listing a set of threaded message headers (search results)
+     *
+     * @param   string   $mailbox    Mailbox/folder name
+     * @param   int      $page       Current page to list
+     * @param   string   $sort_field Header field to sort by
+     * @param   string   $sort_order Sort order [ASC|DESC]
+     * @param   int      $slice      Number of slice items to extract from result array
+     * @return  array    Indexed array with message header objects
+     * @access  private
+     * @see     rcube_imap::list_header_set()
+     */
+    private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
+    {
+        // update search_set if previous data was fetched with disabled threading
+        if (!$this->search_threads) {
+            if (empty($this->search_set))
+                return array();
+            $this->search('', $this->search_string, $this->search_charset, $sort_field);
+        }
+
+        // empty result
+        if (empty($this->search_set['tree']))
+            return array();
+
+        $thread_tree = $this->search_set['tree'];
+        $msg_depth = $this->search_set['depth'];
+        $has_children = $this->search_set['children'];
+        $a_msg_headers = array();
+
+        $page = $page ? $page : $this->list_page;
+        $start_msg = ($page-1) * $this->page_size;
+
+        $this->_set_sort_order($sort_field, $sort_order);
+
+        $msg_index = $this->sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
+
+        return $this->_fetch_thread_headers($mailbox,
+            $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
+    }
+
+
+    /**
+     * Helper function to get first and last index of the requested set
+     *
+     * @param  int     $max  Messages count
+     * @param  mixed   $page Page number to show, or string 'all'
+     * @return array   Array with two values: first index, last index
+     * @access private
+     */
+    private function _get_message_range($max, $page)
+    {
+        $start_msg = ($page-1) * $this->page_size;
+
+        if ($page=='all') {
+            $begin  = 0;
+            $end    = $max;
+        }
+        else if ($this->sort_order=='DESC') {
+            $begin  = $max - $this->page_size - $start_msg;
+            $end    = $max - $start_msg;
+        }
+        else {
+            $begin  = $start_msg;
+            $end    = $start_msg + $this->page_size;
+        }
+
+        if ($begin < 0) $begin = 0;
+        if ($end < 0) $end = $max;
+        if ($end > $max) $end = $max;
+
+        return array($begin, $end);
+    }
+
+
+    /**
+     * Fetches messages headers
+     *
+     * @param  string  $mailbox  Mailbox name
+     * @param  array   $msgs     Messages sequence numbers
+     * @param  bool    $is_uid   Enable if $msgs numbers are UIDs
+     * @param  bool    $force    Disables cache use
+     *
+     * @return array Messages headers indexed by UID
+     * @access private
+     */
+    function fetch_headers($mailbox, $msgs, $is_uid = false, $force = false)
+    {
+        if (empty($msgs))
+            return array();
+
+        if (!$force && ($mcache = $this->get_mcache_engine())) {
+            return $mcache->get_messages($mailbox, $msgs, $is_uid);
+        }
+
+        // fetch reqested headers from server
+        $index = $this->conn->fetchHeaders(
+            $mailbox, $msgs, $is_uid, false, $this->get_fetch_headers());
+
+        if (empty($index))
+            return array();
+
+        foreach ($index as $headers) {
+            $a_msg_headers[$headers->uid] = $headers;
+        }
+
+        return $a_msg_headers;
+    }
+
+
+    /**
+     * Returns current status of mailbox
+     *
+     * We compare the maximum UID to determine the number of
+     * new messages because the RECENT flag is not reliable.
+     *
+     * @param string $mailbox Mailbox/folder name
+     * @return int   Folder status
+     */
+    function mailbox_status($mailbox = null)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+        $old = $this->get_folder_stats($mailbox);
+
+        // refresh message count -> will update
+        $this->_messagecount($mailbox, 'ALL', true);
+
+        $result = 0;
+        $new = $this->get_folder_stats($mailbox);
+
+        // got new messages
+        if ($new['maxuid'] > $old['maxuid'])
+            $result += 1;
+        // some messages has been deleted
+        if ($new['cnt'] < $old['cnt'])
+            $result += 2;
+
+        // @TODO: optional checking for messages flags changes (?)
+        // @TODO: UIDVALIDITY checking
+
+        return $result;
+    }
+
+
+    /**
+     * Stores folder statistic data in session
+     * @TODO: move to separate DB table (cache?)
+     *
+     * @param string $mailbox Mailbox name
+     * @param string $name    Data name
+     * @param mixed  $data    Data value
+     */
+    private function set_folder_stats($mailbox, $name, $data)
+    {
+        $_SESSION['folders'][$mailbox][$name] = $data;
+    }
+
+
+    /**
+     * Gets folder statistic data
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return array Stats data
+     */
+    private function get_folder_stats($mailbox)
+    {
+        if ($_SESSION['folders'][$mailbox])
+            return (array) $_SESSION['folders'][$mailbox];
+        else
+            return array();
+    }
+
+
+    /**
+     * Return sorted array of message IDs (not UIDs)
+     *
+     * @param string $mailbox    Mailbox to get index from
+     * @param string $sort_field Sort column
+     * @param string $sort_order Sort order [ASC, DESC]
+     * @return array Indexed array with message IDs
+     */
+    function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
+    {
+        if ($this->threading)
+            return $this->thread_index($mailbox, $sort_field, $sort_order);
+
+        $this->_set_sort_order($sort_field, $sort_order);
+
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+        $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
+
+        // we have a saved search result, get index from there
+        if (!isset($this->icache[$key]) && $this->search_string
+            && !$this->search_threads && $mailbox == $this->mailbox) {
+            // use message index sort as default sorting
+            if (!$this->sort_field) {
+                $msgs = $this->search_set;
+
+                if ($this->search_sort_field != 'date')
+                    sort($msgs);
+
+                if ($this->sort_order == 'DESC')
+                    $this->icache[$key] = array_reverse($msgs);
+                else
+                    $this->icache[$key] = $msgs;
+            }
+            // sort with SORT command
+            else if ($this->search_sorted) {
+                if ($this->sort_field && $this->search_sort_field != $this->sort_field)
+                    $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
+
+                if ($this->sort_order == 'DESC')
+                    $this->icache[$key] = array_reverse($this->search_set);
+                else
+                    $this->icache[$key] = $this->search_set;
+            }
+            else {
+                $a_index = $this->conn->fetchHeaderIndex($mailbox,
+                    join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
+
+                if (is_array($a_index)) {
+                    if ($this->sort_order=="ASC")
+                        asort($a_index);
+                    else if ($this->sort_order=="DESC")
+                        arsort($a_index);
+
+                    $this->icache[$key] = array_keys($a_index);
+                }
+                else {
+                    $this->icache[$key] = array();
+                }
+            }
+        }
+
+        // have stored it in RAM
+        if (isset($this->icache[$key]))
+            return $this->icache[$key];
+
+        // check local cache
+        if ($mcache = $this->get_mcache_engine()) {
+            $a_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
+            $this->icache[$key] = array_keys($a_index);
+        }
+        // fetch from IMAP server
+        else {
+            $this->icache[$key] = $this->message_index_direct(
+                $mailbox, $this->sort_field, $this->sort_order);
+        }
+
+        return $this->icache[$key];
+    }
+
+
+    /**
+     * Return sorted array of message IDs (not UIDs) directly from IMAP server.
+     * Doesn't use cache and ignores current search settings.
+     *
+     * @param string $mailbox    Mailbox to get index from
+     * @param string $sort_field Sort column
+     * @param string $sort_order Sort order [ASC, DESC]
+     *
+     * @return array Indexed array with message IDs
+     */
+    function message_index_direct($mailbox, $sort_field = null, $sort_order = null)
+    {
+        // use message index sort as default sorting
+        if (!$sort_field) {
+            if ($this->skip_deleted) {
+                $a_index = $this->conn->search($mailbox, 'ALL UNDELETED');
+                // I didn't found that SEARCH should return sorted IDs
+                if (is_array($a_index))
+                    sort($a_index);
+            } else if ($max = $this->_messagecount($mailbox, 'ALL', true, false)) {
+                $a_index = range(1, $max);
+            }
+
+            if ($a_index !== false && $sort_order == 'DESC')
+                $a_index = array_reverse($a_index);
+        }
+        // fetch complete message index
+        else if ($this->get_capability('SORT') &&
+            ($a_index = $this->conn->sort($mailbox,
+                $sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
+        ) {
+            if ($sort_order == 'DESC')
+                $a_index = array_reverse($a_index);
+        }
+        else if ($a_index = $this->conn->fetchHeaderIndex(
+            $mailbox, "1:*", $sort_field, $skip_deleted)) {
+            if ($sort_order=="ASC")
+                asort($a_index);
+            else if ($sort_order=="DESC")
+                arsort($a_index);
+
+            $a_index = array_keys($a_index);
+        }
+
+        return $a_index !== false ? $a_index : array();
+    }
+
+
+    /**
+     * Return sorted array of threaded message IDs (not UIDs)
+     *
+     * @param string $mailbox    Mailbox to get index from
+     * @param string $sort_field Sort column
+     * @param string $sort_order Sort order [ASC, DESC]
+     * @return array Indexed array with message IDs
+     */
+    function thread_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
+    {
+        $this->_set_sort_order($sort_field, $sort_order);
+
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+        $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi";
+
+        // we have a saved search result, get index from there
+        if (!isset($this->icache[$key]) && $this->search_string
+            && $this->search_threads && $mailbox == $this->mailbox) {
+            // use message IDs for better performance
+            $ids = array_keys_recursive($this->search_set['tree']);
+            $this->icache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
+        }
+
+        // have stored it in RAM
+        if (isset($this->icache[$key]))
+            return $this->icache[$key];
+
+        // get all threads (default sort order)
+        list ($thread_tree) = $this->fetch_threads($mailbox);
+
+        $this->icache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
+
+        return $this->icache[$key];
+    }
+
+
+    /**
+     * Return array of threaded messages (all, not only roots)
+     *
+     * @param string $mailbox     Mailbox to get index from
+     * @param array  $thread_tree Threaded messages array (see fetch_threads())
+     * @param array  $ids         Message IDs if we know what we need (e.g. search result)
+     *                            for better performance
+     * @return array Indexed array with message IDs
+     *
+     * @access private
+     */
+    private function _flatten_threads($mailbox, $thread_tree, $ids=null)
+    {
+        if (empty($thread_tree))
+            return array();
+
+        $msg_index = $this->sort_threads($mailbox, $thread_tree, $ids);
+
+        if ($this->sort_order == 'DESC')
+            $msg_index = array_reverse($msg_index);
+
+        // flatten threads array
+        $all_ids = array();
+        foreach ($msg_index as $root) {
+            $all_ids[] = $root;
+            if (!empty($thread_tree[$root])) {
+                foreach (array_keys_recursive($thread_tree[$root]) as $val)
+                    $all_ids[] = $val;
+            }
+        }
+
+        return $all_ids;
+    }
+
+
+    /**
+     * Invoke search request to IMAP server
+     *
+     * @param  string  $mailbox    Mailbox name to search in
+     * @param  string  $str        Search criteria
+     * @param  string  $charset    Search charset
+     * @param  string  $sort_field Header field to sort by
+     * @return array   search results as list of message IDs
+     * @access public
+     */
+    function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL)
+    {
+        if (!$str)
+            return false;
+
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
+
+        $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading,
+            $this->threading || $this->search_sorted ? true : false);
+
+        return $results;
+    }
+
+
+    /**
+     * Private search method
+     *
+     * @param string $mailbox    Mailbox name
+     * @param string $criteria   Search criteria
+     * @param string $charset    Charset
+     * @param string $sort_field Sorting field
+     *
+     * @return array   search results as list of message ids
+     * @see rcube_imap::search()
+     */
+    private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
+    {
+        $orig_criteria = $criteria;
+
+        if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
+            $criteria = 'UNDELETED '.$criteria;
+
+        if ($this->threading) {
+            $a_messages = $this->conn->thread($mailbox, $this->threading, $criteria, $charset);
+
+            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+            // but I've seen that Courier doesn't support UTF-8)
+            if ($a_messages === false && $charset && $charset != 'US-ASCII')
+                $a_messages = $this->conn->thread($mailbox, $this->threading,
+                    $this->convert_criteria($criteria, $charset), 'US-ASCII');
+
+            if ($a_messages !== false) {
+                list ($thread_tree, $msg_depth, $has_children) = $a_messages;
+                $a_messages = array(
+                    'tree' => $thread_tree,
+                    'depth'=> $msg_depth,
+                    'children' => $has_children
+                );
+            }
+
+            return $a_messages;
+        }
+
+        if ($sort_field && $this->get_capability('SORT')) {
+            $charset = $charset ? $charset : $this->default_charset;
+            $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
+
+            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+            // but I've seen Courier with disabled UTF-8 support)
+            if ($a_messages === false && $charset && $charset != 'US-ASCII')
+                $a_messages = $this->conn->sort($mailbox, $sort_field,
+                    $this->convert_criteria($criteria, $charset), false, 'US-ASCII');
+
+            if ($a_messages !== false) {
+                $this->search_sorted = true;
+                return $a_messages;
+            }
+        }
+
+        if ($orig_criteria == 'ALL') {
+            $max = $this->_messagecount($mailbox, 'ALL', true, false);
+            $a_messages = $max ? range(1, $max) : array();
+        }
+        else {
+            $a_messages = $this->conn->search($mailbox,
+                ($charset ? "CHARSET $charset " : '') . $criteria);
+
+            // Error, try with US-ASCII (some servers may support only US-ASCII)
+            if ($a_messages === false && $charset && $charset != 'US-ASCII')
+                $a_messages = $this->conn->search($mailbox,
+                    'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset));
+
+            // I didn't found that SEARCH should return sorted IDs
+            if (is_array($a_messages) && !$this->sort_field)
+                sort($a_messages);
+        }
+
+        $this->search_sorted = false;
+
+        return $a_messages;
+    }
+
+
+    /**
+     * Direct (real and simple) SEARCH request to IMAP server,
+     * without result sorting and caching
+     *
+     * @param  string  $mailbox Mailbox name to search in
+     * @param  string  $str     Search string
+     * @param  boolean $ret_uid True if UIDs should be returned
+     *
+     * @return array   Search results as list of message IDs or UIDs
+     */
+    function search_once($mailbox='', $str=NULL, $ret_uid=false)
+    {
+        if (!$str)
+            return false;
+
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        return $this->conn->search($mailbox, $str, $ret_uid);
+    }
+
+
+    /**
+     * Converts charset of search criteria string
+     *
+     * @param  string  $str          Search string
+     * @param  string  $charset      Original charset
+     * @param  string  $dest_charset Destination charset (default US-ASCII)
+     * @return string  Search string
+     * @access private
+     */
+    private function convert_criteria($str, $charset, $dest_charset='US-ASCII')
+    {
+        // convert strings to US_ASCII
+        if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
+            $last = 0; $res = '';
+            foreach ($matches[1] as $m) {
+                $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
+                $string = substr($str, $string_offset - 1, $m[0]);
+                $string = rcube_charset_convert($string, $charset, $dest_charset);
+                if (!$string)
+                    continue;
+                $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
+                $last = $m[0] + $string_offset - 1;
+            }
+            if ($last < strlen($str))
+                $res .= substr($str, $last, strlen($str)-$last);
+        }
+        else // strings for conversion not found
+            $res = $str;
+
+        return $res;
+    }
+
+
+    /**
+     * Sort thread
+     *
+     * @param string $mailbox     Mailbox name
+     * @param  array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result)
+     * @param  array $ids         Message IDs if we know what we need (e.g. search result)
+     *
+     * @return array Sorted roots IDs
+     */
+    function sort_threads($mailbox, $thread_tree, $ids = null)
+    {
+        // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
+        // THREAD=REFERENCES:     sorting by sent date of root message
+        // THREAD=REFS:           sorting by the most recent date in each thread
+
+        // default sorting
+        if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
+            return array_keys((array)$thread_tree);
+        }
+        // here we'll implement REFS sorting
+        else {
+            if ($mcache = $this->get_mcache_engine()) {
+                $a_index = $mcache->get_index($mailbox, $this->sort_field, 'ASC');
+                if (is_array($a_index)) {
+                    $a_index = array_keys($a_index);
+                    // now we must remove IDs that doesn't exist in $ids
+                    if (!empty($ids))
+                        $a_index = array_intersect($a_index, $ids);
+                }
+            }
+            // use SORT command
+            else if ($this->get_capability('SORT') &&
+                ($a_index = $this->conn->sort($mailbox, $this->sort_field,
+                    !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
+            ) {
+                // do nothing
+            }
+            else {
+                // fetch specified headers for all messages and sort them
+                $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
+                    $this->sort_field, $this->skip_deleted);
+
+                // return unsorted tree if we've got no index data
+                if (!empty($a_index)) {
+                    asort($a_index); // ASC
+                    $a_index = array_values($a_index);
+                }
+            }
+
+            if (empty($a_index))
+                return array_keys((array)$thread_tree);
+
+            return $this->_sort_thread_refs($thread_tree, $a_index);
+        }
+    }
+
+
+    /**
+     * THREAD=REFS sorting implementation
+     *
+     * @param  array $tree   Thread tree array (message identifiers as keys)
+     * @param  array $index  Array of sorted message identifiers
+     *
+     * @return array   Array of sorted roots messages
+     */
+    private function _sort_thread_refs($tree, $index)
+    {
+        if (empty($tree))
+            return array();
+
+        $index = array_combine(array_values($index), $index);
+
+        // assign roots
+        foreach ($tree as $idx => $val) {
+            $index[$idx] = $idx;
+            if (!empty($val)) {
+                $idx_arr = array_keys_recursive($tree[$idx]);
+                foreach ($idx_arr as $subidx)
+                    $index[$subidx] = $idx;
+            }
+        }
+
+        $index = array_values($index);
+
+        // create sorted array of roots
+        $msg_index = array();
+        if ($this->sort_order != 'DESC') {
+            foreach ($index as $idx)
+                if (!isset($msg_index[$idx]))
+                    $msg_index[$idx] = $idx;
+            $msg_index = array_values($msg_index);
+        }
+        else {
+            for ($x=count($index)-1; $x>=0; $x--)
+                if (!isset($msg_index[$index[$x]]))
+                    $msg_index[$index[$x]] = $index[$x];
+            $msg_index = array_reverse($msg_index);
+        }
+
+        return $msg_index;
+    }
+
+
+    /**
+     * Refresh saved search set
+     *
+     * @return array Current search set
+     */
+    function refresh_search()
+    {
+        if (!empty($this->search_string))
+            $this->search_set = $this->search('', $this->search_string, $this->search_charset,
+                $this->search_sort_field, $this->search_threads, $this->search_sorted);
+
+        return $this->get_search_set();
+    }
+
+
+    /**
+     * Check if the given message ID is part of the current search set
+     *
+     * @param string $msgid Message id
+     * @return boolean True on match or if no search request is stored
+     */
+    function in_searchset($msgid)
+    {
+        if (!empty($this->search_string)) {
+            if ($this->search_threads)
+                return isset($this->search_set['depth']["$msgid"]);
+            else
+                return in_array("$msgid", (array)$this->search_set, true);
+        }
+        else
+            return true;
+    }
+
+
+    /**
+     * Return message headers object of a specific message
+     *
+     * @param int     $id       Message sequence ID or UID
+     * @param string  $mailbox  Mailbox to read from
+     * @param bool    $force    True to skip cache
+     *
+     * @return rcube_mail_header Message headers
+     */
+    function get_headers($uid, $mailbox = null, $force = false)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        // get cached headers
+        if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
+            $headers = $mcache->get_message($mailbox, $uid);
+        }
+        else {
+            $headers = $this->conn->fetchHeader(
+                $mailbox, $uid, true, true, $this->get_fetch_headers());
+        }
+
+        return $headers;
+    }
+
+
+    /**
+     * Fetch message headers and body structure from the IMAP server and build
+     * an object structure similar to the one generated by PEAR::Mail_mimeDecode
+     *
+     * @param int     $uid      Message UID to fetch
+     * @param string  $mailbox  Mailbox to read from
+     *
+     * @return object rcube_mail_header Message data
+     */
+    function get_message($uid, $mailbox = null)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        // Check internal cache
+        if (!empty($this->icache['message'])) {
+            if (($headers = $this->icache['message']) && $headers->uid == $uid) {
+                return $headers;
+            }
+        }
+
+        $headers = $this->get_headers($uid, $mailbox);
+
+        // message doesn't exist?
+        if (empty($headers))
+            return null; 
+
+        // structure might be cached
+        if (!empty($headers->structure))
+            return $headers;
+
+        $this->_msg_uid = $uid;
+
+        if (empty($headers->bodystructure)) {
+            $headers->bodystructure = $this->conn->getStructure($mailbox, $uid, true);
+        }
+
+        $structure = $headers->bodystructure;
+
+        if (empty($structure))
+            return $headers;
+
+        // set message charset from message headers
+        if ($headers->charset)
+            $this->struct_charset = $headers->charset;
+        else
+            $this->struct_charset = $this->_structure_charset($structure);
+
+        $headers->ctype = strtolower($headers->ctype);
+
+        // Here we can recognize malformed BODYSTRUCTURE and
+        // 1. [@TODO] parse the message in other way to create our own message structure
+        // 2. or just show the raw message body.
+        // Example of structure for malformed MIME message:
+        // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
+        if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
+            && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
+            // we can handle single-part messages, by simple fix in structure (#1486898)
+            if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
+                $structure[0] = $m[1];
+                $structure[1] = $m[2];
+            }
+            else
+                return $headers;
+        }
+
+        $struct = &$this->_structure_part($structure, 0, '', $headers);
+
+        // don't trust given content-type
+        if (empty($struct->parts) && !empty($headers->ctype)) {
+            $struct->mime_id = '1';
+            $struct->mimetype = strtolower($headers->ctype);
+            list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
+        }
+
+        $headers->structure = $struct;
+
+        return $this->icache['message'] = $headers;
+    }
+
+
+    /**
+     * Build message part object
+     *
+     * @param array  $part
+     * @param int    $count
+     * @param string $parent
+     * @access private
+     */
+    function &_structure_part($part, $count=0, $parent='', $mime_headers=null)
+    {
+        $struct = new rcube_message_part;
+        $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
+
+        // multipart
+        if (is_array($part[0])) {
+            $struct->ctype_primary = 'multipart';
+
+        /* RFC3501: BODYSTRUCTURE fields of multipart part
+            part1 array
+            part2 array
+            part3 array
+            ....
+            1. subtype
+            2. parameters (optional)
+            3. description (optional)
+            4. language (optional)
+            5. location (optional)
+        */
+
+            // find first non-array entry
+            for ($i=1; $i<count($part); $i++) {
+                if (!is_array($part[$i])) {
+                    $struct->ctype_secondary = strtolower($part[$i]);
+                    break;
+                }
+            }
+
+            $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
+
+            // build parts list for headers pre-fetching
+            for ($i=0; $i<count($part); $i++) {
+                if (!is_array($part[$i]))
+                    break;
+                // fetch message headers if message/rfc822
+                // or named part (could contain Content-Location header)
+                if (!is_array($part[$i][0])) {
+                    $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
+                    if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
+                        $mime_part_headers[] = $tmp_part_id;
+                    }
+                    else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
+                        $mime_part_headers[] = $tmp_part_id;
+                    }
+                }
+            }
+
+            // pre-fetch headers of all parts (in one command for better performance)
+            // @TODO: we could do this before _structure_part() call, to fetch
+            // headers for parts on all levels
+            if ($mime_part_headers) {
+                $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
+                    $this->_msg_uid, $mime_part_headers);
+            }
+
+            $struct->parts = array();
+            for ($i=0, $count=0; $i<count($part); $i++) {
+                if (!is_array($part[$i]))
+                    break;
+                $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
+                $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
+                    $mime_part_headers[$tmp_part_id]);
+            }
+
+            return $struct;
+        }
+
+        /* RFC3501: BODYSTRUCTURE fields of non-multipart part
+            0. type
+            1. subtype
+            2. parameters
+            3. id
+            4. description
+            5. encoding
+            6. size
+          -- text
+            7. lines
+          -- message/rfc822
+            7. envelope structure
+            8. body structure
+            9. lines
+          --
+            x. md5 (optional)
+            x. disposition (optional)
+            x. language (optional)
+            x. location (optional)
+        */
+
+        // regular part
+        $struct->ctype_primary = strtolower($part[0]);
+        $struct->ctype_secondary = strtolower($part[1]);
+        $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
+
+        // read content type parameters
+        if (is_array($part[2])) {
+            $struct->ctype_parameters = array();
+            for ($i=0; $i<count($part[2]); $i+=2)
+                $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
+
+            if (isset($struct->ctype_parameters['charset']))
+                $struct->charset = $struct->ctype_parameters['charset'];
+        }
+
+        // #1487700: workaround for lack of charset in malformed structure
+        if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
+            $struct->charset = $mime_headers->charset;
+        }
+
+        // read content encoding
+        if (!empty($part[5])) {
+            $struct->encoding = strtolower($part[5]);
+            $struct->headers['content-transfer-encoding'] = $struct->encoding;
+        }
+
+        // get part size
+        if (!empty($part[6]))
+            $struct->size = intval($part[6]);
+
+        // read part disposition
+        $di = 8;
+        if ($struct->ctype_primary == 'text') $di += 1;
+        else if ($struct->mimetype == 'message/rfc822') $di += 3;
+
+        if (is_array($part[$di]) && count($part[$di]) == 2) {
+            $struct->disposition = strtolower($part[$di][0]);
+
+            if (is_array($part[$di][1]))
+                for ($n=0; $n<count($part[$di][1]); $n+=2)
+                    $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
+        }
+
+        // get message/rfc822's child-parts
+        if (is_array($part[8]) && $di != 8) {
+            $struct->parts = array();
+            for ($i=0, $count=0; $i<count($part[8]); $i++) {
+                if (!is_array($part[8][$i]))
+                    break;
+                $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
+            }
+        }
+
+        // get part ID
+        if (!empty($part[3])) {
+            $struct->content_id = $part[3];
+            $struct->headers['content-id'] = $part[3];
+
+            if (empty($struct->disposition))
+                $struct->disposition = 'inline';
+        }
+
+        // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
+        if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
+            if (empty($mime_headers)) {
+                $mime_headers = $this->conn->fetchPartHeader(
+                    $this->mailbox, $this->_msg_uid, true, $struct->mime_id);
+            }
+
+            if (is_string($mime_headers))
+                $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
+            else if (is_object($mime_headers))
+                $struct->headers = get_object_vars($mime_headers) + $struct->headers;
+
+            // get real content-type of message/rfc822
+            if ($struct->mimetype == 'message/rfc822') {
+                // single-part
+                if (!is_array($part[8][0]))
+                    $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
+                // multi-part
+                else {
+                    for ($n=0; $n<count($part[8]); $n++)
+                        if (!is_array($part[8][$n]))
+                            break;
+                    $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
+                }
+            }
+
+            if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
+                if (is_array($part[8]) && $di != 8)
+                    $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
+            }
+        }
+
+        // normalize filename property
+        $this->_set_part_filename($struct, $mime_headers);
+
+        return $struct;
+    }
+
+
+    /**
+     * Set attachment filename from message part structure
+     *
+     * @param  rcube_message_part $part    Part object
+     * @param  string             $headers Part's raw headers
+     * @access private
+     */
+    private function _set_part_filename(&$part, $headers=null)
+    {
+        if (!empty($part->d_parameters['filename']))
+            $filename_mime = $part->d_parameters['filename'];
+        else if (!empty($part->d_parameters['filename*']))
+            $filename_encoded = $part->d_parameters['filename*'];
+        else if (!empty($part->ctype_parameters['name*']))
+            $filename_encoded = $part->ctype_parameters['name*'];
+        // RFC2231 value continuations
+        // TODO: this should be rewrited to support RFC2231 4.1 combinations
+        else if (!empty($part->d_parameters['filename*0'])) {
+            $i = 0;
+            while (isset($part->d_parameters['filename*'.$i])) {
+                $filename_mime .= $part->d_parameters['filename*'.$i];
+                $i++;
+            }
+            // some servers (eg. dovecot-1.x) have no support for parameter value continuations
+            // we must fetch and parse headers "manually"
+            if ($i<2) {
+                if (!$headers) {
+                    $headers = $this->conn->fetchPartHeader(
+                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
+                }
+                $filename_mime = '';
+                $i = 0;
+                while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
+                    $filename_mime .= $matches[1];
+                    $i++;
+                }
+            }
+        }
+        else if (!empty($part->d_parameters['filename*0*'])) {
+            $i = 0;
+            while (isset($part->d_parameters['filename*'.$i.'*'])) {
+                $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
+                $i++;
+            }
+            if ($i<2) {
+                if (!$headers) {
+                    $headers = $this->conn->fetchPartHeader(
+                            $this->mailbox, $this->_msg_uid, true, $part->mime_id);
+                }
+                $filename_encoded = '';
+                $i = 0; $matches = array();
+                while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
+                    $filename_encoded .= $matches[1];
+                    $i++;
+                }
+            }
+        }
+        else if (!empty($part->ctype_parameters['name*0'])) {
+            $i = 0;
+            while (isset($part->ctype_parameters['name*'.$i])) {
+                $filename_mime .= $part->ctype_parameters['name*'.$i];
+                $i++;
+            }
+            if ($i<2) {
+                if (!$headers) {
+                    $headers = $this->conn->fetchPartHeader(
+                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
+                }
+                $filename_mime = '';
+                $i = 0; $matches = array();
+                while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
+                    $filename_mime .= $matches[1];
+                    $i++;
+                }
+            }
+        }
+        else if (!empty($part->ctype_parameters['name*0*'])) {
+            $i = 0;
+            while (isset($part->ctype_parameters['name*'.$i.'*'])) {
+                $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
+                $i++;
+            }
+            if ($i<2) {
+                if (!$headers) {
+                    $headers = $this->conn->fetchPartHeader(
+                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
+                }
+                $filename_encoded = '';
+                $i = 0; $matches = array();
+                while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
+                    $filename_encoded .= $matches[1];
+                    $i++;
+                }
+            }
+        }
+        // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
+        else if (!empty($part->ctype_parameters['name']))
+            $filename_mime = $part->ctype_parameters['name'];
+        // Content-Disposition
+        else if (!empty($part->headers['content-description']))
+            $filename_mime = $part->headers['content-description'];
+        else
+            return;
+
+        // decode filename
+        if (!empty($filename_mime)) {
+            if (!empty($part->charset))
+                $charset = $part->charset;
+            else if (!empty($this->struct_charset))
+                $charset = $this->struct_charset;
+            else
+                $charset = rc_detect_encoding($filename_mime, $this->default_charset);
+
+            $part->filename = rcube_imap::decode_mime_string($filename_mime, $charset);
+        }
+        else if (!empty($filename_encoded)) {
+            // decode filename according to RFC 2231, Section 4
+            if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
+                $filename_charset = $fmatches[1];
+                $filename_encoded = $fmatches[2];
+            }
+
+            $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
+        }
+    }
+
+
+    /**
+     * Get charset name from message structure (first part)
+     *
+     * @param  array $structure Message structure
+     * @return string Charset name
+     * @access private
+     */
+    private function _structure_charset($structure)
+    {
+        while (is_array($structure)) {
+            if (is_array($structure[2]) && $structure[2][0] == 'charset')
+                return $structure[2][1];
+            $structure = $structure[0];
+        }
+    }
+
+
+    /**
+     * Fetch message body of a specific message from the server
+     *
+     * @param  int                $uid    Message UID
+     * @param  string             $part   Part number
+     * @param  rcube_message_part $o_part Part object created by get_structure()
+     * @param  mixed              $print  True to print part, ressource to write part contents in
+     * @param  resource           $fp     File pointer to save the message part
+     * @param  boolean            $skip_charset_conv Disables charset conversion
+     *
+     * @return string Message/part body if not printed
+     */
+    function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
+    {
+        // get part data if not provided
+        if (!is_object($o_part)) {
+            $structure = $this->conn->getStructure($this->mailbox, $uid, true);
+            $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
+
+            $o_part = new rcube_message_part;
+            $o_part->ctype_primary = $part_data['type'];
+            $o_part->encoding      = $part_data['encoding'];
+            $o_part->charset       = $part_data['charset'];
+            $o_part->size          = $part_data['size'];
+        }
+
+        if ($o_part && $o_part->size) {
+            $body = $this->conn->handlePartBody($this->mailbox, $uid, true,
+                $part ? $part : 'TEXT', $o_part->encoding, $print, $fp);
+        }
+
+        if ($fp || $print) {
+            return true;
+        }
+
+        // convert charset (if text or message part)
+        if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
+            // Remove NULL characters (#1486189)
+            $body = str_replace("\x00", '', $body);
+
+           if (!$skip_charset_conv) {
+                if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
+                    // try to extract charset information from HTML meta tag (#1488125)
+                    if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m))
+                        $o_part->charset = strtoupper($m[1]);
+                    else
+                        $o_part->charset = $this->default_charset;
+                }
+                $body = rcube_charset_convert($body, $o_part->charset);
+            }
+        }
+
+        return $body;
+    }
+
+
+    /**
+     * Fetch message body of a specific message from the server
+     *
+     * @param  int    $uid  Message UID
+     * @return string $part Message/part body
+     * @see    rcube_imap::get_message_part()
+     */
+    function &get_body($uid, $part=1)
+    {
+        $headers = $this->get_headers($uid);
+        return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
+            $headers->charset ? $headers->charset : $this->default_charset);
+    }
+
+
+    /**
+     * Returns the whole message source as string (or saves to a file)
+     *
+     * @param int      $uid Message UID
+     * @param resource $fp  File pointer to save the message
+     *
+     * @return string Message source string
+     */
+    function &get_raw_body($uid, $fp=null)
+    {
+        return $this->conn->handlePartBody($this->mailbox, $uid,
+            true, null, null, false, $fp);
+    }
+
+
+    /**
+     * Returns the message headers as string
+     *
+     * @param int $uid  Message UID
+     * @return string Message headers string
+     */
+    function &get_raw_headers($uid)
+    {
+        return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
+    }
+
+
+    /**
+     * Sends the whole message source to stdout
+     *
+     * @param int $uid Message UID
+     */
+    function print_raw_body($uid)
+    {
+        $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
+    }
+
+
+    /**
+     * Set message flag to one or several messages
+     *
+     * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
+     * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
+     * @param string  $mailbox    Folder name
+     * @param boolean $skip_cache True to skip message cache clean up
+     *
+     * @return boolean  Operation status
+     */
+    function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        $flag = strtoupper($flag);
+        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
+
+        if (strpos($flag, 'UN') === 0)
+            $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
+        else
+            $result = $this->conn->flag($mailbox, $uids, $flag);
+
+        if ($result) {
+            // reload message headers if cached
+            // @TODO: update flags instead removing from cache
+            if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
+                $status = strpos($flag, 'UN') !== 0;
+                $mflag  = preg_replace('/^UN/', '', $flag);
+                $mcache->change_flag($mailbox, $all_mode ? null : explode(',', $uids),
+                    $mflag, $status);
+            }
+
+            // clear cached counters
+            if ($flag == 'SEEN' || $flag == 'UNSEEN') {
+                $this->_clear_messagecount($mailbox, 'SEEN');
+                $this->_clear_messagecount($mailbox, 'UNSEEN');
+            }
+            else if ($flag == 'DELETED') {
+                $this->_clear_messagecount($mailbox, 'DELETED');
+            }
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Remove message flag for one or several messages
+     *
+     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
+     * @param string $flag    Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
+     * @param string $mailbox Folder name
+     *
+     * @return int   Number of flagged messages, -1 on failure
+     * @see set_flag
+     */
+    function unset_flag($uids, $flag, $mailbox=null)
+    {
+        return $this->set_flag($uids, 'UN'.$flag, $mailbox);
+    }
+
+
+    /**
+     * Append a mail message (source) to a specific mailbox
+     *
+     * @param string  $mailbox Target mailbox
+     * @param string  $message The message source string or filename
+     * @param string  $headers Headers string if $message contains only the body
+     * @param boolean $is_file True if $message is a filename
+     *
+     * @return int|bool Appended message UID or True on success, False on error
+     */
+    function save_message($mailbox, &$message, $headers='', $is_file=false)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        // make sure mailbox exists
+        if ($this->mailbox_exists($mailbox)) {
+            if ($is_file)
+                $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
+            else
+                $saved = $this->conn->append($mailbox, $message);
+        }
+
+        if ($saved) {
+            // increase messagecount of the target mailbox
+            $this->_set_messagecount($mailbox, 'ALL', 1);
+        }
+
+        return $saved;
+    }
+
+
+    /**
+     * Move a message from one mailbox to another
+     *
+     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
+     * @param string $to_mbox   Target mailbox
+     * @param string $from_mbox Source mailbox
+     * @return boolean True on success, False on error
+     */
+    function move_message($uids, $to_mbox, $from_mbox='')
+    {
+        if (!strlen($from_mbox)) {
+            $from_mbox = $this->mailbox;
+        }
+
+        if ($to_mbox === $from_mbox) {
+            return false;
+        }
+
+        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
+
+        // exit if no message uids are specified
+        if (empty($uids))
+            return false;
+
+        // make sure mailbox exists
+        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
+            if (in_array($to_mbox, $this->default_folders)) {
+                if (!$this->create_mailbox($to_mbox, true)) {
+                    return false;
+                }
+            }
+            else {
+                return false;
+            }
+        }
+
+        $config = rcmail::get_instance()->config;
+        $to_trash = $to_mbox == $config->get('trash_mbox');
+
+        // flag messages as read before moving them
+        if ($to_trash && $config->get('read_when_deleted')) {
+            // don't flush cache (4th argument)
+            $this->set_flag($uids, 'SEEN', $from_mbox, true);
+        }
+
+        // move messages
+        $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
+
+        // send expunge command in order to have the moved message
+        // really deleted from the source mailbox
+        if ($moved) {
+            $this->_expunge($from_mbox, false, $uids);
+            $this->_clear_messagecount($from_mbox);
+            $this->_clear_messagecount($to_mbox);
+        }
+        // moving failed
+        else if ($to_trash && $config->get('delete_always', false)) {
+            $moved = $this->delete_message($uids, $from_mbox);
+        }
+
+        if ($moved) {
+            // unset threads internal cache
+            unset($this->icache['threads']);
+
+            // remove message ids from search set
+            if ($this->search_set && $from_mbox == $this->mailbox) {
+                // threads are too complicated to just remove messages from set
+                if ($this->search_threads || $all_mode)
+                    $this->refresh_search();
+                else {
+                    $a_uids = explode(',', $uids);
+                    foreach ($a_uids as $uid)
+                        $a_mids[] = $this->uid2id($uid, $from_mbox);
+                    $this->search_set = array_diff($this->search_set, $a_mids);
+                }
+                unset($a_mids);
+                unset($a_uids);
+            }
+
+            // remove cached messages
+            // @TODO: do cache update instead of clearing it
+            $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
+        }
+
+        return $moved;
+    }
+
+
+    /**
+     * Copy a message from one mailbox to another
+     *
+     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
+     * @param string $to_mbox   Target mailbox
+     * @param string $from_mbox Source mailbox
+     * @return boolean True on success, False on error
+     */
+    function copy_message($uids, $to_mbox, $from_mbox='')
+    {
+        if (!strlen($from_mbox)) {
+            $from_mbox = $this->mailbox;
+        }
+
+        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
+
+        // exit if no message uids are specified
+        if (empty($uids)) {
+            return false;
+        }
+
+        // make sure mailbox exists
+        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
+            if (in_array($to_mbox, $this->default_folders)) {
+                if (!$this->create_mailbox($to_mbox, true)) {
+                    return false;
+                }
+            }
+            else {
+                return false;
+            }
+        }
+
+        // copy messages
+        $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
+
+        if ($copied) {
+            $this->_clear_messagecount($to_mbox);
+        }
+
+        return $copied;
+    }
+
+
+    /**
+     * Mark messages as deleted and expunge mailbox
+     *
+     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
+     * @param string $mailbox Source mailbox
+     *
+     * @return boolean True on success, False on error
+     */
+    function delete_message($uids, $mailbox='')
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
+
+        // exit if no message uids are specified
+        if (empty($uids))
+            return false;
+
+        $deleted = $this->conn->delete($mailbox, $uids);
+
+        if ($deleted) {
+            // send expunge command in order to have the deleted message
+            // really deleted from the mailbox
+            $this->_expunge($mailbox, false, $uids);
+            $this->_clear_messagecount($mailbox);
+            unset($this->uid_id_map[$mailbox]);
+
+            // unset threads internal cache
+            unset($this->icache['threads']);
+
+            // remove message ids from search set
+            if ($this->search_set && $mailbox == $this->mailbox) {
+                // threads are too complicated to just remove messages from set
+                if ($this->search_threads || $all_mode)
+                    $this->refresh_search();
+                else {
+                    $a_uids = explode(',', $uids);
+                    foreach ($a_uids as $uid)
+                        $a_mids[] = $this->uid2id($uid, $mailbox);
+                    $this->search_set = array_diff($this->search_set, $a_mids);
+                    unset($a_uids);
+                    unset($a_mids);
+                }
+            }
+
+            // remove cached messages
+            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
+        }
+
+        return $deleted;
+    }
+
+
+    /**
+     * Clear all messages in a specific mailbox
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return int Above 0 on success
+     */
+    function clear_mailbox($mailbox=null)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        // SELECT will set messages count for clearFolder()
+        if ($this->conn->select($mailbox)) {
+            $cleared = $this->conn->clearFolder($mailbox);
+        }
+
+        // make sure the cache is cleared as well
+        if ($cleared) {
+            $this->clear_message_cache($mailbox);
+            $a_mailbox_cache = $this->get_cache('messagecount');
+            unset($a_mailbox_cache[$mailbox]);
+            $this->update_cache('messagecount', $a_mailbox_cache);
+        }
+
+        return $cleared;
+    }
+
+
+    /**
+     * Send IMAP expunge command and clear cache
+     *
+     * @param string  $mailbox     Mailbox name
+     * @param boolean $clear_cache False if cache should not be cleared
+     *
+     * @return boolean True on success
+     */
+    function expunge($mailbox='', $clear_cache=true)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        return $this->_expunge($mailbox, $clear_cache);
+    }
+
+
+    /**
+     * Send IMAP expunge command and clear cache
+     *
+     * @param string  $mailbox     Mailbox name
+     * @param boolean $clear_cache False if cache should not be cleared
+     * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
+     * @return boolean True on success
+     * @access private
+     * @see rcube_imap::expunge()
+     */
+    private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
+    {
+        if ($uids && $this->get_capability('UIDPLUS'))
+            list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
+        else
+            $uids = null;
+
+        // force mailbox selection and check if mailbox is writeable
+        // to prevent a situation when CLOSE is executed on closed
+        // or EXPUNGE on read-only mailbox
+        $result = $this->conn->select($mailbox);
+        if (!$result) {
+            return false;
+        }
+        if (!$this->conn->data['READ-WRITE']) {
+            $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
+            return false;
+        }
+
+        // CLOSE(+SELECT) should be faster than EXPUNGE
+        if (empty($uids) || $all_mode)
+            $result = $this->conn->close();
+        else
+            $result = $this->conn->expunge($mailbox, $uids);
+
+        if ($result && $clear_cache) {
+            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
+            $this->_clear_messagecount($mailbox);
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Parse message UIDs input
+     *
+     * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
+     * @param string $mailbox Mailbox name
+     * @return array Two elements array with UIDs converted to list and ALL flag
+     * @access private
+     */
+    private function _parse_uids($uids, $mailbox)
+    {
+        if ($uids === '*' || $uids === '1:*') {
+            if (empty($this->search_set)) {
+                $uids = '1:*';
+                $all = true;
+            }
+            // get UIDs from current search set
+            // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
+            else {
+                if ($this->search_threads)
+                    $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
+                else
+                    $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
+
+                // save ID-to-UID mapping in local cache
+                if (is_array($uids))
+                    foreach ($uids as $id => $uid)
+                        $this->uid_id_map[$mailbox][$uid] = $id;
+
+                $uids = join(',', $uids);
+            }
+        }
+        else {
+            if (is_array($uids))
+                $uids = join(',', $uids);
+
+            if (preg_match('/[^0-9,]/', $uids))
+                $uids = '';
+        }
+
+        return array($uids, (bool) $all);
+    }
+
+
+    /**
+     * Translate UID to message ID
+     *
+     * @param int    $uid     Message UID
+     * @param string $mailbox Mailbox name
+     *
+     * @return int   Message ID
+     */
+    function get_id($uid, $mailbox=null)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        return $this->uid2id($uid, $mailbox);
+    }
+
+
+    /**
+     * Translate message number to UID
+     *
+     * @param int    $id      Message ID
+     * @param string $mailbox Mailbox name
+     *
+     * @return int   Message UID
+     */
+    function get_uid($id, $mailbox=null)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        return $this->id2uid($id, $mailbox);
+    }
+
+
+
+    /* --------------------------------
+     *        folder managment
+     * --------------------------------*/
+
+    /**
+     * Public method for listing subscribed folders
+     *
+     * @param   string  $root      Optional root folder
+     * @param   string  $name      Optional name pattern
+     * @param   string  $filter    Optional filter
+     * @param   string  $rights    Optional ACL requirements
+     * @param   bool    $skip_sort Enable to return unsorted list (for better performance)
+     *
+     * @return  array   List of folders
+     * @access  public
+     */
+    function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
+    {
+        $cache_key = $root.':'.$name;
+        if (!empty($filter)) {
+            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
+        }
+        $cache_key .= ':'.$rights;
+        $cache_key = 'mailboxes.'.md5($cache_key);
+
+        // get cached folder list
+        $a_mboxes = $this->get_cache($cache_key);
+        if (is_array($a_mboxes)) {
+            return $a_mboxes;
+        }
+
+        $a_mboxes = $this->_list_mailboxes($root, $name, $filter, $rights);
+
+        if (!is_array($a_mboxes)) {
+            return array();
+        }
+
+        // filter folders list according to rights requirements
+        if ($rights && $this->get_capability('ACL')) {
+            $a_mboxes = $this->filter_rights($a_mboxes, $rights);
+        }
+
+        // INBOX should always be available
+        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
+            array_unshift($a_mboxes, 'INBOX');
+        }
+
+        // sort mailboxes (always sort for cache)
+        if (!$skip_sort || $this->cache) {
+            $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
+        }
+
+        // write mailboxlist to cache
+        $this->update_cache($cache_key, $a_mboxes);
+
+        return $a_mboxes;
+    }
+
+
+    /**
+     * Private method for mailbox listing
+     *
+     * @param   string  $root   Optional root folder
+     * @param   string  $name   Optional name pattern
+     * @param   mixed   $filter Optional filter
+     * @param   string  $rights Optional ACL requirements
+     *
+     * @return  array   List of mailboxes/folders
+     * @see     rcube_imap::list_mailboxes()
+     * @access  private
+     */
+    private function _list_mailboxes($root='', $name='*', $filter=null, $rights=null)
+    {
+        $a_defaults = $a_out = array();
+
+        // Give plugins a chance to provide a list of mailboxes
+        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
+            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
+
+        if (isset($data['folders'])) {
+            $a_folders = $data['folders'];
+        }
+        else if (!$this->conn->connected()) {
+           return null;
+        }
+        else {
+            // Server supports LIST-EXTENDED, we can use selection options
+            $config = rcmail::get_instance()->config;
+            // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
+            if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
+                // This will also set mailbox options, LSUB doesn't do that
+                $a_folders = $this->conn->listMailboxes($root, $name,
+                    NULL, array('SUBSCRIBED'));
+
+                // unsubscribe non-existent folders, remove from the list
+                if (is_array($a_folders) && $name == '*') {
+                    foreach ($a_folders as $idx => $folder) {
+                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
+                            && in_array('\\NonExistent', $opts)
+                        ) {
+                            $this->conn->unsubscribe($folder);
+                            unset($a_folders[$idx]);
+                        }
+                    }
+                }
+            }
+            // retrieve list of folders from IMAP server using LSUB
+            else {
+                $a_folders = $this->conn->listSubscribed($root, $name);
+
+                // unsubscribe non-existent folders, remove from the list
+                if (is_array($a_folders) && $name == '*') {
+                    foreach ($a_folders as $idx => $folder) {
+                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
+                            && in_array('\\Noselect', $opts)
+                        ) {
+                            // Some servers returns \Noselect for existing folders
+                            if (!$this->mailbox_exists($folder)) {
+                                $this->conn->unsubscribe($folder);
+                                unset($a_folders[$idx]);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!is_array($a_folders) || !sizeof($a_folders)) {
+            $a_folders = array();
+        }
+
+        return $a_folders;
+    }
+
+
+    /**
+     * Get a list of all folders available on the IMAP server
+     *
+     * @param string  $root      IMAP root dir
+     * @param string  $name      Optional name pattern
+     * @param mixed   $filter    Optional filter
+     * @param string  $rights    Optional ACL requirements
+     * @param bool    $skip_sort Enable to return unsorted list (for better performance)
+     *
+     * @return array Indexed array with folder names
+     */
+    function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
+    {
+        // @TODO: caching
+        // Give plugins a chance to provide a list of mailboxes
+        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
+            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
+
+        if (isset($data['folders'])) {
+            $a_mboxes = $data['folders'];
+        }
+        else {
+            // retrieve list of folders from IMAP server
+            $a_mboxes = $this->conn->listMailboxes($root, $name);
+        }
+
+        if (!is_array($a_mboxes)) {
+            $a_mboxes = array();
+        }
+
+        // INBOX should always be available
+        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
+            array_unshift($a_mboxes, 'INBOX');
+        }
+
+        // filter folders list according to rights requirements
+        if ($rights && $this->get_capability('ACL')) {
+            $a_folders = $this->filter_rights($a_folders, $rights);
+        }
+
+        // filter folders and sort them
+        if (!$skip_sort) {
+            $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
+        }
+
+        return $a_mboxes;
+    }
+
+
+    /**
+     * Filter the given list of folders according to access rights
+     */
+    private function filter_rights($a_folders, $rights)
+    {
+        $regex = '/('.$rights.')/';
+        foreach ($a_folders as $idx => $folder) {
+            $myrights = join('', (array)$this->my_rights($folder));
+            if ($myrights !== null && !preg_match($regex, $myrights))
+                unset($a_folders[$idx]);
+        }
+
+        return $a_folders;
+    }
+
+
+    /**
+     * Get mailbox quota information
+     * added by Nuny
+     *
+     * @return mixed Quota info or False if not supported
+     */
+    function get_quota()
+    {
+        if ($this->get_capability('QUOTA'))
+            return $this->conn->getQuota();
+
+        return false;
+    }
+
+
+    /**
+     * Get mailbox size (size of all messages in a mailbox)
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return int Mailbox size in bytes, False on error
+     */
+    function get_mailbox_size($mailbox)
+    {
+        // @TODO: could we try to use QUOTA here?
+        $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
+
+        if (is_array($result))
+            $result = array_sum($result);
+
+        return $result;
+    }
+
+
+    /**
+     * Subscribe to a specific mailbox(es)
+     *
+     * @param array $a_mboxes Mailbox name(s)
+     * @return boolean True on success
+     */
+    function subscribe($a_mboxes)
+    {
+        if (!is_array($a_mboxes))
+            $a_mboxes = array($a_mboxes);
+
+        // let this common function do the main work
+        return $this->_change_subscription($a_mboxes, 'subscribe');
+    }
+
+
+    /**
+     * Unsubscribe mailboxes
+     *
+     * @param array $a_mboxes Mailbox name(s)
+     * @return boolean True on success
+     */
+    function unsubscribe($a_mboxes)
+    {
+        if (!is_array($a_mboxes))
+            $a_mboxes = array($a_mboxes);
+
+        // let this common function do the main work
+        return $this->_change_subscription($a_mboxes, 'unsubscribe');
+    }
+
+
+    /**
+     * Create a new mailbox on the server and register it in local cache
+     *
+     * @param string  $mailbox   New mailbox name
+     * @param boolean $subscribe True if the new mailbox should be subscribed
+     *
+     * @return boolean True on success
+     */
+    function create_mailbox($mailbox, $subscribe=false)
+    {
+        $result = $this->conn->createFolder($mailbox);
+
+        // try to subscribe it
+        if ($result) {
+            // clear cache
+            $this->clear_cache('mailboxes', true);
+
+            if ($subscribe)
+                $this->subscribe($mailbox);
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Set a new name to an existing mailbox
+     *
+     * @param string $mailbox  Mailbox to rename
+     * @param string $new_name New mailbox name
+     *
+     * @return boolean True on success
+     */
+    function rename_mailbox($mailbox, $new_name)
+    {
+        if (!strlen($new_name)) {
+            return false;
+        }
+
+        $delm = $this->get_hierarchy_delimiter();
+
+        // get list of subscribed folders
+        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
+            $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
+            $subscribed   = $this->mailbox_exists($mailbox, true);
+        }
+        else {
+            $a_subscribed = $this->_list_mailboxes();
+            $subscribed   = in_array($mailbox, $a_subscribed);
+        }
+
+        $result = $this->conn->renameFolder($mailbox, $new_name);
+
+        if ($result) {
+            // unsubscribe the old folder, subscribe the new one
+            if ($subscribed) {
+                $this->conn->unsubscribe($mailbox);
+                $this->conn->subscribe($new_name);
+            }
+
+            // check if mailbox children are subscribed
+            foreach ($a_subscribed as $c_subscribed) {
+                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
+                    $this->conn->unsubscribe($c_subscribed);
+                    $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
+                        $new_name, $c_subscribed));
+
+                    // clear cache
+                    $this->clear_message_cache($c_subscribed);
+                }
+            }
+
+            // clear cache
+            $this->clear_message_cache($mailbox);
+            $this->clear_cache('mailboxes', true);
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Remove mailbox from server
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return boolean True on success
+     */
+    function delete_mailbox($mailbox)
+    {
+        $delm = $this->get_hierarchy_delimiter();
+
+        // get list of folders
+        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
+            $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
+        else
+            $sub_mboxes = $this->list_unsubscribed();
+
+        // send delete command to server
+        $result = $this->conn->deleteFolder($mailbox);
+
+        if ($result) {
+            // unsubscribe mailbox
+            $this->conn->unsubscribe($mailbox);
+
+            foreach ($sub_mboxes as $c_mbox) {
+                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
+                    $this->conn->unsubscribe($c_mbox);
+                    if ($this->conn->deleteFolder($c_mbox)) {
+	                    $this->clear_message_cache($c_mbox);
+                    }
+                }
+            }
+
+            // clear mailbox-related cache
+            $this->clear_message_cache($mailbox);
+            $this->clear_cache('mailboxes', true);
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Create all folders specified as default
+     */
+    function create_default_folders()
+    {
+        // create default folders if they do not exist
+        foreach ($this->default_folders as $folder) {
+            if (!$this->mailbox_exists($folder))
+                $this->create_mailbox($folder, true);
+            else if (!$this->mailbox_exists($folder, true))
+                $this->subscribe($folder);
+        }
+    }
+
+
+    /**
+     * Checks if folder exists and is subscribed
+     *
+     * @param string   $mailbox      Folder name
+     * @param boolean  $subscription Enable subscription checking
+     *
+     * @return boolean TRUE or FALSE
+     */
+    function mailbox_exists($mailbox, $subscription=false)
+    {
+        if ($mailbox == 'INBOX') {
+            return true;
+        }
+
+        $key  = $subscription ? 'subscribed' : 'existing';
+
+        if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
+            return true;
+
+        if ($subscription) {
+            $a_folders = $this->conn->listSubscribed('', $mailbox);
+        }
+        else {
+            $a_folders = $this->conn->listMailboxes('', $mailbox);
+        }
+
+        if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
+            $this->icache[$key][] = $mailbox;
+            return true;
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Returns the namespace where the folder is in
+     *
+     * @param string $mailbox Folder name
+     *
+     * @return string One of 'personal', 'other' or 'shared'
+     * @access public
+     */
+    function mailbox_namespace($mailbox)
+    {
+        if ($mailbox == 'INBOX') {
+            return 'personal';
+        }
+
+        foreach ($this->namespace as $type => $namespace) {
+            if (is_array($namespace)) {
+                foreach ($namespace as $ns) {
+                    if (strlen($ns[0])) {
+                        if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
+                            || strpos($mailbox, $ns[0]) === 0
+                        ) {
+                            return $type;
+                        }
+                    }
+                }
+            }
+        }
+
+        return 'personal';
+    }
+
+
+    /**
+     * Modify folder name according to namespace.
+     * For output it removes prefix of the personal namespace if it's possible.
+     * For input it adds the prefix. Use it before creating a folder in root
+     * of the folders tree.
+     *
+     * @param string $mailbox Folder name
+     * @param string $mode    Mode name (out/in)
+     *
+     * @return string Folder name
+     */
+    function mod_mailbox($mailbox, $mode = 'out')
+    {
+        if (!strlen($mailbox)) {
+            return $mailbox;
+        }
+
+        $prefix     = $this->namespace['prefix']; // see set_env()
+        $prefix_len = strlen($prefix);
+
+        if (!$prefix_len) {
+            return $mailbox;
+        }
+
+        // remove prefix for output
+        if ($mode == 'out') {
+            if (substr($mailbox, 0, $prefix_len) === $prefix) {
+                return substr($mailbox, $prefix_len);
+            }
+        }
+        // add prefix for input (e.g. folder creation)
+        else {
+            return $prefix . $mailbox;
+        }
+
+        return $mailbox;
+    }
+
+
+    /**
+     * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
+     *
+     * @param string $mailbox Folder name
+     * @param bool   $force   Set to True if options should be refreshed
+     *                        Options are available after LIST command only
+     *
+     * @return array Options list
+     */
+    function mailbox_options($mailbox, $force=false)
+    {
+        if ($mailbox == 'INBOX') {
+            return array();
+        }
+
+        if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
+            if ($force) {
+                $this->conn->listMailboxes('', $mailbox);
+            }
+            else {
+                return array();
+            }
+        }
+
+        $opts = $this->conn->data['LIST'][$mailbox];
+
+        return is_array($opts) ? $opts : array();
+    }
+
+
+    /**
+     * Gets connection (and current mailbox) data: UIDVALIDITY, EXISTS, RECENT,
+     * PERMANENTFLAGS, UIDNEXT, UNSEEN
+     *
+     * @param string $mailbox Folder name
+     *
+     * @return array Data
+     */
+    function mailbox_data($mailbox)
+    {
+        if (!strlen($mailbox))
+            $mailbox = $this->mailbox !== null ? $this->mailbox : 'INBOX';
+
+        if ($this->conn->selected != $mailbox) {
+            if ($this->conn->select($mailbox))
+                $this->mailbox = $mailbox;
+            else
+                return null;
+        }
+
+        $data = $this->conn->data;
+
+        // add (E)SEARCH result for ALL UNDELETED query
+        if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx'][0] == $mailbox) {
+            $data['ALL_UNDELETED']   = $this->icache['undeleted_idx'][1];
+            $data['COUNT_UNDELETED'] = $this->icache['undeleted_idx'][2];
+        }
+
+        return $data;
+    }
+
+
+    /**
+     * Returns extended information about the folder
+     *
+     * @param string $mailbox Folder name
+     *
+     * @return array Data
+     */
+    function mailbox_info($mailbox)
+    {
+        if ($this->icache['options'] && $this->icache['options']['name'] == $mailbox) {
+            return $this->icache['options'];
+        }
+
+        $acl       = $this->get_capability('ACL');
+        $namespace = $this->get_namespace();
+        $options   = array();
+
+        // check if the folder is a namespace prefix
+        if (!empty($namespace)) {
+            $mbox = $mailbox . $this->delimiter;
+            foreach ($namespace as $ns) {
+                if (!empty($ns)) {
+                    foreach ($ns as $item) {
+                        if ($item[0] === $mbox) {
+                            $options['is_root'] = true;
+                            break 2;
+                        }
+                    }
+                }
+            }
+        }
+        // check if the folder is other user virtual-root
+        if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
+            $parts = explode($this->delimiter, $mailbox);
+            if (count($parts) == 2) {
+                $mbox = $parts[0] . $this->delimiter;
+                foreach ($namespace['other'] as $item) {
+                    if ($item[0] === $mbox) {
+                        $options['is_root'] = true;
+                        break;
+                    }
+                }
+            }
+        }
+
+        $options['name']      = $mailbox;
+        $options['options']   = $this->mailbox_options($mailbox, true);
+        $options['namespace'] = $this->mailbox_namespace($mailbox);
+        $options['rights']    = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
+        $options['special']   = in_array($mailbox, $this->default_folders);
+
+        // Set 'noselect' and 'norename' flags
+        if (is_array($options['options'])) {
+            foreach ($options['options'] as $opt) {
+                $opt = strtolower($opt);
+                if ($opt == '\noselect' || $opt == '\nonexistent') {
+                    $options['noselect'] = true;
+                }
+            }
+        }
+        else {
+            $options['noselect'] = true;
+        }
+
+        if (!empty($options['rights'])) {
+            $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
+
+            if (!$options['noselect']) {
+                $options['noselect'] = !in_array('r', $options['rights']);
+            }
+        }
+        else {
+            $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
+        }
+
+        $this->icache['options'] = $options;
+
+        return $options;
+    }
+
+
+    /**
+     * Synchronizes messages cache.
+     *
+     * @param string $mailbox Folder name
+     */
+    public function mailbox_sync($mailbox)
+    {
+        if ($mcache = $this->get_mcache_engine()) {
+            $mcache->synchronize($mailbox);
+        }
+    }
+
+
+    /**
+     * Get message header names for rcube_imap_generic::fetchHeader(s)
+     *
+     * @return string Space-separated list of header names
+     */
+    private function get_fetch_headers()
+    {
+        $headers = explode(' ', $this->fetch_add_headers);
+        $headers = array_map('strtoupper', $headers);
+
+        if ($this->messages_caching || $this->get_all_headers)
+            $headers = array_merge($headers, $this->all_headers);
+
+        return implode(' ', array_unique($headers));
+    }
+
+
+    /* -----------------------------------------
+     *   ACL and METADATA/ANNOTATEMORE methods
+     * ----------------------------------------*/
+
+    /**
+     * Changes the ACL on the specified mailbox (SETACL)
+     *
+     * @param string $mailbox Mailbox name
+     * @param string $user    User name
+     * @param string $acl     ACL string
+     *
+     * @return boolean True on success, False on failure
+     *
+     * @access public
+     * @since 0.5-beta
+     */
+    function set_acl($mailbox, $user, $acl)
+    {
+        if ($this->get_capability('ACL'))
+            return $this->conn->setACL($mailbox, $user, $acl);
+
+        return false;
+    }
+
+
+    /**
+     * Removes any <identifier,rights> pair for the
+     * specified user from the ACL for the specified
+     * mailbox (DELETEACL)
+     *
+     * @param string $mailbox Mailbox name
+     * @param string $user    User name
+     *
+     * @return boolean True on success, False on failure
+     *
+     * @access public
+     * @since 0.5-beta
+     */
+    function delete_acl($mailbox, $user)
+    {
+        if ($this->get_capability('ACL'))
+            return $this->conn->deleteACL($mailbox, $user);
+
+        return false;
+    }
+
+
+    /**
+     * Returns the access control list for mailbox (GETACL)
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return array User-rights array on success, NULL on error
+     * @access public
+     * @since 0.5-beta
+     */
+    function get_acl($mailbox)
+    {
+        if ($this->get_capability('ACL'))
+            return $this->conn->getACL($mailbox);
+
+        return NULL;
+    }
+
+
+    /**
+     * Returns information about what rights can be granted to the
+     * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
+     *
+     * @param string $mailbox Mailbox name
+     * @param string $user    User name
+     *
+     * @return array List of user rights
+     * @access public
+     * @since 0.5-beta
+     */
+    function list_rights($mailbox, $user)
+    {
+        if ($this->get_capability('ACL'))
+            return $this->conn->listRights($mailbox, $user);
+
+        return NULL;
+    }
+
+
+    /**
+     * Returns the set of rights that the current user has to
+     * mailbox (MYRIGHTS)
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return array MYRIGHTS response on success, NULL on error
+     * @access public
+     * @since 0.5-beta
+     */
+    function my_rights($mailbox)
+    {
+        if ($this->get_capability('ACL'))
+            return $this->conn->myRights($mailbox);
+
+        return NULL;
+    }
+
+
+    /**
+     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+     *
+     * @param string $mailbox Mailbox name (empty for server metadata)
+     * @param array  $entries Entry-value array (use NULL value as NIL)
+     *
+     * @return boolean True on success, False on failure
+     * @access public
+     * @since 0.5-beta
+     */
+    function set_metadata($mailbox, $entries)
+    {
+        if ($this->get_capability('METADATA') ||
+            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
+        ) {
+            return $this->conn->setMetadata($mailbox, $entries);
+        }
+        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
+            foreach ((array)$entries as $entry => $value) {
+                list($ent, $attr) = $this->md2annotate($entry);
+                $entries[$entry] = array($ent, $attr, $value);
+            }
+            return $this->conn->setAnnotation($mailbox, $entries);
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+     *
+     * @param string $mailbox Mailbox name (empty for server metadata)
+     * @param array  $entries Entry names array
+     *
+     * @return boolean True on success, False on failure
+     *
+     * @access public
+     * @since 0.5-beta
+     */
+    function delete_metadata($mailbox, $entries)
+    {
+        if ($this->get_capability('METADATA') || 
+            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
+        ) {
+            return $this->conn->deleteMetadata($mailbox, $entries);
+        }
+        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
+            foreach ((array)$entries as $idx => $entry) {
+                list($ent, $attr) = $this->md2annotate($entry);
+                $entries[$idx] = array($ent, $attr, NULL);
+            }
+            return $this->conn->setAnnotation($mailbox, $entries);
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
+     *
+     * @param string $mailbox Mailbox name (empty for server metadata)
+     * @param array  $entries Entries
+     * @param array  $options Command options (with MAXSIZE and DEPTH keys)
+     *
+     * @return array Metadata entry-value hash array on success, NULL on error
+     *
+     * @access public
+     * @since 0.5-beta
+     */
+    function get_metadata($mailbox, $entries, $options=array())
+    {
+        if ($this->get_capability('METADATA') || 
+            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
+        ) {
+            return $this->conn->getMetadata($mailbox, $entries, $options);
+        }
+        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
+            $queries = array();
+            $res     = array();
+
+            // Convert entry names
+            foreach ((array)$entries as $entry) {
+                list($ent, $attr) = $this->md2annotate($entry);
+                $queries[$attr][] = $ent;
+            }
+
+            // @TODO: Honor MAXSIZE and DEPTH options
+            foreach ($queries as $attrib => $entry)
+                if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
+                    $res = array_merge_recursive($res, $result);
+
+            return $res;
+        }
+
+        return NULL;
+    }
+
+
+    /**
+     * Converts the METADATA extension entry name into the correct
+     * entry-attrib names for older ANNOTATEMORE version.
+     *
+     * @param string $entry Entry name
+     *
+     * @return array Entry-attribute list, NULL if not supported (?)
+     */
+    private function md2annotate($entry)
+    {
+        if (substr($entry, 0, 7) == '/shared') {
+            return array(substr($entry, 7), 'value.shared');
+        }
+        else if (substr($entry, 0, 8) == '/private') {
+            return array(substr($entry, 8), 'value.priv');
+        }
+
+        // @TODO: log error
+        return NULL;
+    }
+
+
+    /* --------------------------------
+     *   internal caching methods
+     * --------------------------------*/
+
+    /**
+     * Enable or disable indexes caching
+     *
+     * @param string $type Cache type (@see rcmail::get_cache)
+     * @access public
+     */
+    function set_caching($type)
+    {
+        if ($type) {
+            $this->caching = $type;
+        }
+        else {
+            if ($this->cache)
+                $this->cache->close();
+            $this->cache   = null;
+            $this->caching = false;
+        }
+    }
+
+    /**
+     * Getter for IMAP cache object
+     */
+    private function get_cache_engine()
+    {
+        if ($this->caching && !$this->cache) {
+            $rcmail = rcmail::get_instance();
+            $this->cache = $rcmail->get_cache('IMAP', $this->caching);
+        }
+
+        return $this->cache;
+    }
+
+    /**
+     * Returns cached value
+     *
+     * @param string $key Cache key
+     * @return mixed
+     * @access public
+     */
+    function get_cache($key)
+    {
+        if ($cache = $this->get_cache_engine()) {
+            return $cache->get($key);
+        }
+    }
+
+    /**
+     * Update cache
+     *
+     * @param string $key  Cache key
+     * @param mixed  $data Data
+     * @access public
+     */
+    function update_cache($key, $data)
+    {
+        if ($cache = $this->get_cache_engine()) {
+            $cache->set($key, $data);
+        }
+    }
+
+    /**
+     * Clears the cache.
+     *
+     * @param string  $key         Cache key name or pattern
+     * @param boolean $prefix_mode Enable it to clear all keys starting
+     *                             with prefix specified in $key
+     * @access public
+     */
+    function clear_cache($key=null, $prefix_mode=false)
+    {
+        if ($cache = $this->get_cache_engine()) {
+            $cache->remove($key, $prefix_mode);
+        }
+    }
+
+
+    /* --------------------------------
+     *   message caching methods
+     * --------------------------------*/
+
+    /**
+     * Enable or disable messages caching
+     *
+     * @param boolean $set Flag
+     */
+    function set_messages_caching($set)
+    {
+        if ($set) {
+            $this->messages_caching = true;
+        }
+        else {
+            if ($this->mcache)
+                $this->mcache->close();
+            $this->mcache = null;
+            $this->messages_caching = false;
+        }
+    }
+
+    /**
+     * Getter for messages cache object
+     */
+    private function get_mcache_engine()
+    {
+        if ($this->messages_caching && !$this->mcache) {
+            $rcmail = rcmail::get_instance();
+            if ($dbh = $rcmail->get_dbh()) {
+                $this->mcache = new rcube_imap_cache(
+                    $dbh, $this, $rcmail->user->ID, $this->skip_deleted);
+            }
+        }
+
+        return $this->mcache;
+    }
+
+    /**
+     * Clears the messages cache.
+     *
+     * @param string $mailbox Folder name
+     * @param array  $uids    Optional message UIDs to remove from cache
+     */
+    function clear_message_cache($mailbox = null, $uids = null)
+    {
+        if ($mcache = $this->get_mcache_engine()) {
+            $mcache->clear($mailbox, $uids);
+        }
+    }
+
+
+
+    /* --------------------------------
+     *   encoding/decoding methods
+     * --------------------------------*/
+
+    /**
+     * Split an address list into a structured array list
+     *
+     * @param string  $input  Input string
+     * @param int     $max    List only this number of addresses
+     * @param boolean $decode Decode address strings
+     * @return array  Indexed list of addresses
+     */
+    function decode_address_list($input, $max=null, $decode=true)
+    {
+        $a = $this->_parse_address_list($input, $decode);
+        $out = array();
+        // Special chars as defined by RFC 822 need to in quoted string (or escaped).
+        $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
+
+        if (!is_array($a))
+            return $out;
+
+        $c = count($a);
+        $j = 0;
+
+        foreach ($a as $val) {
+            $j++;
+            $address = trim($val['address']);
+            $name    = trim($val['name']);
+
+            if ($name && $address && $name != $address)
+                $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
+            else if ($address)
+                $string = $address;
+            else if ($name)
+                $string = $name;
+
+            $out[$j] = array(
+                'name'   => $name,
+                'mailto' => $address,
+                'string' => $string
+            );
+
+            if ($max && $j==$max)
+                break;
+        }
+
+        return $out;
+    }
+
+
+    /**
+     * Decode a message header value
+     *
+     * @param string  $input         Header value
+     * @param boolean $remove_quotas Remove quotes if necessary
+     * @return string Decoded string
+     */
+    function decode_header($input, $remove_quotes=false)
+    {
+        $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
+        if ($str[0] == '"' && $remove_quotes)
+            $str = str_replace('"', '', $str);
+
+        return $str;
+    }
+
+
+    /**
+     * Decode a mime-encoded string to internal charset
+     *
+     * @param string $input    Header value
+     * @param string $fallback Fallback charset if none specified
+     *
+     * @return string Decoded string
+     * @static
+     */
+    public static function decode_mime_string($input, $fallback=null)
+    {
+        if (!empty($fallback)) {
+            $default_charset = $fallback;
+        }
+        else {
+            $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
+        }
+
+        // rfc: all line breaks or other characters not found
+        // in the Base64 Alphabet must be ignored by decoding software
+        // delete all blanks between MIME-lines, differently we can
+        // receive unnecessary blanks and broken utf-8 symbols
+        $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
+
+        // encoded-word regexp
+        $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
+
+        // Find all RFC2047's encoded words
+        if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
+            // Initialize variables
+            $tmp   = array();
+            $out   = '';
+            $start = 0;
+
+            foreach ($matches as $idx => $m) {
+                $pos      = $m[0][1];
+                $charset  = $m[1][0];
+                $encoding = $m[2][0];
+                $text     = $m[3][0];
+                $length   = strlen($m[0][0]);
+
+                // Append everything that is before the text to be decoded
+                if ($start != $pos) {
+                    $substr = substr($input, $start, $pos-$start);
+                    $out   .= rcube_charset_convert($substr, $default_charset);
+                    $start  = $pos;
+                }
+                $start += $length;
+
+                // Per RFC2047, each string part "MUST represent an integral number
+                // of characters . A multi-octet character may not be split across
+                // adjacent encoded-words." However, some mailers break this, so we
+                // try to handle characters spanned across parts anyway by iterating
+                // through and aggregating sequential encoded parts with the same
+                // character set and encoding, then perform the decoding on the
+                // aggregation as a whole.
+
+                $tmp[] = $text;
+                if ($next_match = $matches[$idx+1]) {
+                    if ($next_match[0][1] == $start
+                        && $next_match[1][0] == $charset
+                        && $next_match[2][0] == $encoding
+                    ) {
+                        continue;
+                    }
+                }
+
+                $count = count($tmp);
+                $text  = '';
+
+                // Decode and join encoded-word's chunks
+                if ($encoding == 'B' || $encoding == 'b') {
+                    // base64 must be decoded a segment at a time
+                    for ($i=0; $i<$count; $i++)
+                        $text .= base64_decode($tmp[$i]);
+                }
+                else { //if ($encoding == 'Q' || $encoding == 'q') {
+                    // quoted printable can be combined and processed at once
+                    for ($i=0; $i<$count; $i++)
+                        $text .= $tmp[$i];
+
+                    $text = str_replace('_', ' ', $text);
+                    $text = quoted_printable_decode($text);
+                }
+
+                $out .= rcube_charset_convert($text, $charset);
+                $tmp = array();
+            }
+
+            // add the last part of the input string
+            if ($start != strlen($input)) {
+                $out .= rcube_charset_convert(substr($input, $start), $default_charset);
+            }
+
+            // return the results
+            return $out;
+        }
+
+        // no encoding information, use fallback
+        return rcube_charset_convert($input, $default_charset);
+    }
+
+
+    /**
+     * Decode a mime part
+     *
+     * @param string $input    Input string
+     * @param string $encoding Part encoding
+     * @return string Decoded string
+     */
+    function mime_decode($input, $encoding='7bit')
+    {
+        switch (strtolower($encoding)) {
+        case 'quoted-printable':
+            return quoted_printable_decode($input);
+        case 'base64':
+            return base64_decode($input);
+        case 'x-uuencode':
+        case 'x-uue':
+        case 'uue':
+        case 'uuencode':
+            return convert_uudecode($input);
+        case '7bit':
+        default:
+            return $input;
+        }
+    }
+
+
+    /**
+     * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
+     *
+     * @param string $body        Part body to decode
+     * @param string $ctype_param Charset to convert from
+     * @return string Content converted to internal charset
+     */
+    function charset_decode($body, $ctype_param)
+    {
+        if (is_array($ctype_param) && !empty($ctype_param['charset']))
+            return rcube_charset_convert($body, $ctype_param['charset']);
+
+        // defaults to what is specified in the class header
+        return rcube_charset_convert($body,  $this->default_charset);
+    }
+
+
+    /* --------------------------------
+     *         private methods
+     * --------------------------------*/
+
+    /**
+     * Validate the given input and save to local properties
+     *
+     * @param string $sort_field Sort column
+     * @param string $sort_order Sort order
+     * @access private
+     */
+    private function _set_sort_order($sort_field, $sort_order)
+    {
+        if ($sort_field != null)
+            $this->sort_field = asciiwords($sort_field);
+        if ($sort_order != null)
+            $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
+    }
+
+
+    /**
+     * Sort mailboxes first by default folders and then in alphabethical order
+     *
+     * @param array $a_folders Mailboxes list
+     * @access private
+     */
+    private function _sort_mailbox_list($a_folders)
+    {
+        $a_out = $a_defaults = $folders = array();
+
+        $delimiter = $this->get_hierarchy_delimiter();
+
+        // find default folders and skip folders starting with '.'
+        foreach ($a_folders as $i => $folder) {
+            if ($folder[0] == '.')
+                continue;
+
+            if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
+                $a_defaults[$p] = $folder;
+            else
+                $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
+        }
+
+        // sort folders and place defaults on the top
+        asort($folders, SORT_LOCALE_STRING);
+        ksort($a_defaults);
+        $folders = array_merge($a_defaults, array_keys($folders));
+
+        // finally we must rebuild the list to move
+        // subfolders of default folders to their place...
+        // ...also do this for the rest of folders because
+        // asort() is not properly sorting case sensitive names
+        while (list($key, $folder) = each($folders)) {
+            // set the type of folder name variable (#1485527)
+            $a_out[] = (string) $folder;
+            unset($folders[$key]);
+            $this->_rsort($folder, $delimiter, $folders, $a_out);
+        }
+
+        return $a_out;
+    }
+
+
+    /**
+     * @access private
+     */
+    private function _rsort($folder, $delimiter, &$list, &$out)
+    {
+        while (list($key, $name) = each($list)) {
+	        if (strpos($name, $folder.$delimiter) === 0) {
+	            // set the type of folder name variable (#1485527)
+    	        $out[] = (string) $name;
+	            unset($list[$key]);
+	            $this->_rsort($name, $delimiter, $list, $out);
+	        }
+        }
+        reset($list);
+    }
+
+
+    /**
+     * Finds message sequence ID for specified UID
+     *
+     * @param int    $uid      Message UID
+     * @param string $mailbox  Mailbox name
+     * @param bool   $force    True to skip cache
+     *
+     * @return int Message (sequence) ID
+     */
+    function uid2id($uid, $mailbox = null, $force = false)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        if (!empty($this->uid_id_map[$mailbox][$uid])) {
+            return $this->uid_id_map[$mailbox][$uid];
+        }
+
+        if (!$force && ($mcache = $this->get_mcache_engine()))
+            $id = $mcache->uid2id($mailbox, $uid);
+
+        if (empty($id))
+            $id = $this->conn->UID2ID($mailbox, $uid);
+
+        $this->uid_id_map[$mailbox][$uid] = $id;
+
+        return $id;
+    }
+
+
+    /**
+     * Find UID of the specified message sequence ID
+     *
+     * @param int    $id       Message (sequence) ID
+     * @param string $mailbox  Mailbox name
+     * @param bool   $force    True to skip cache
+     *
+     * @return int Message UID
+     */
+    function id2uid($id, $mailbox = null, $force = false)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = $this->mailbox;
+        }
+
+        if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
+            return $uid;
+        }
+
+        if (!$force && ($mcache = $this->get_mcache_engine()))
+            $uid = $mcache->id2uid($mailbox, $id);
+
+        if (empty($uid))
+            $uid = $this->conn->ID2UID($mailbox, $id);
+
+        $this->uid_id_map[$mailbox][$uid] = $id;
+
+        return $uid;
+    }
+
+
+    /**
+     * Subscribe/unsubscribe a list of mailboxes and update local cache
+     * @access private
+     */
+    private function _change_subscription($a_mboxes, $mode)
+    {
+        $updated = false;
+
+        if (is_array($a_mboxes))
+            foreach ($a_mboxes as $i => $mailbox) {
+                $a_mboxes[$i] = $mailbox;
+
+                if ($mode == 'subscribe')
+                    $updated = $this->conn->subscribe($mailbox);
+                else if ($mode == 'unsubscribe')
+                    $updated = $this->conn->unsubscribe($mailbox);
+            }
+
+        // clear cached mailbox list(s)
+        if ($updated) {
+            $this->clear_cache('mailboxes', true);
+        }
+
+        return $updated;
+    }
+
+
+    /**
+     * Increde/decrese messagecount for a specific mailbox
+     * @access private
+     */
+    private function _set_messagecount($mailbox, $mode, $increment)
+    {
+        $mode = strtoupper($mode);
+        $a_mailbox_cache = $this->get_cache('messagecount');
+
+        if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
+            return false;
+
+        // add incremental value to messagecount
+        $a_mailbox_cache[$mailbox][$mode] += $increment;
+
+        // there's something wrong, delete from cache
+        if ($a_mailbox_cache[$mailbox][$mode] < 0)
+            unset($a_mailbox_cache[$mailbox][$mode]);
+
+        // write back to cache
+        $this->update_cache('messagecount', $a_mailbox_cache);
+
+        return true;
+    }
+
+
+    /**
+     * Remove messagecount of a specific mailbox from cache
+     * @access private
+     */
+    private function _clear_messagecount($mailbox, $mode=null)
+    {
+        $a_mailbox_cache = $this->get_cache('messagecount');
+
+        if (is_array($a_mailbox_cache[$mailbox])) {
+            if ($mode) {
+                unset($a_mailbox_cache[$mailbox][$mode]);
+            }
+            else {
+                unset($a_mailbox_cache[$mailbox]);
+            }
+            $this->update_cache('messagecount', $a_mailbox_cache);
+        }
+    }
+
+
+    /**
+     * Split RFC822 header string into an associative array
+     * @access private
+     */
+    private function _parse_headers($headers)
+    {
+        $a_headers = array();
+        $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
+        $lines = explode("\n", $headers);
+        $c = count($lines);
+
+        for ($i=0; $i<$c; $i++) {
+            if ($p = strpos($lines[$i], ': ')) {
+                $field = strtolower(substr($lines[$i], 0, $p));
+                $value = trim(substr($lines[$i], $p+1));
+                if (!empty($value))
+                    $a_headers[$field] = $value;
+            }
+        }
+
+        return $a_headers;
+    }
+
+
+    /**
+     * @access private
+     */
+    private function _parse_address_list($str, $decode=true)
+    {
+        // remove any newlines and carriage returns before
+        $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
+
+        // extract list items, remove comments
+        $str = self::explode_header_string(',;', $str, true);
+        $result = array();
+
+        // simplified regexp, supporting quoted local part
+        $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
+
+        foreach ($str as $key => $val) {
+            $name    = '';
+            $address = '';
+            $val     = trim($val);
+
+            if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
+                $address = $m[2];
+                $name    = trim($m[1]);
+            }
+            else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
+                $address = $m[1];
+                $name    = '';
+            }
+            else {
+                $name = $val;
+            }
+
+            // dequote and/or decode name
+            if ($name) {
+                if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
+                    $name = substr($name, 1, -1);
+                    $name = stripslashes($name);
+                }
+                if ($decode) {
+                    $name = $this->decode_header($name);
+                }
+            }
+
+            if (!$address && $name) {
+                $address = $name;
+            }
+
+            if ($address) {
+                $result[$key] = array('name' => $name, 'address' => $address);
+            }
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Explodes header (e.g. address-list) string into array of strings
+     * using specified separator characters with proper handling
+     * of quoted-strings and comments (RFC2822)
+     *
+     * @param string $separator       String containing separator characters
+     * @param string $str             Header string
+     * @param bool   $remove_comments Enable to remove comments
+     *
+     * @return array Header items
+     */
+    static function explode_header_string($separator, $str, $remove_comments=false)
+    {
+        $length  = strlen($str);
+        $result  = array();
+        $quoted  = false;
+        $comment = 0;
+        $out     = '';
+
+        for ($i=0; $i<$length; $i++) {
+            // we're inside a quoted string
+            if ($quoted) {
+                if ($str[$i] == '"') {
+                    $quoted = false;
+                }
+                else if ($str[$i] == '\\') {
+                    if ($comment <= 0) {
+                        $out .= '\\';
+                    }
+                    $i++;
+                }
+            }
+            // we're inside a comment string
+            else if ($comment > 0) {
+                    if ($str[$i] == ')') {
+                        $comment--;
+                    }
+                    else if ($str[$i] == '(') {
+                        $comment++;
+                    }
+                    else if ($str[$i] == '\\') {
+                        $i++;
+                    }
+                    continue;
+            }
+            // separator, add to result array
+            else if (strpos($separator, $str[$i]) !== false) {
+                    if ($out) {
+                        $result[] = $out;
+                    }
+                    $out = '';
+                    continue;
+            }
+            // start of quoted string
+            else if ($str[$i] == '"') {
+                    $quoted = true;
+            }
+            // start of comment
+            else if ($remove_comments && $str[$i] == '(') {
+                    $comment++;
+            }
+
+            if ($comment <= 0) {
+                $out .= $str[$i];
+            }
+        }
+
+        if ($out && $comment <= 0) {
+            $result[] = $out;
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * This is our own debug handler for the IMAP connection
+     * @access public
+     */
+    public function debug_handler(&$imap, $message)
+    {
+        write_log('imap', $message);
+    }
+
+}  // end class rcube_imap
+
+
+/**
+ * Class representing a message part
+ *
+ * @package Mail
+ */
+class rcube_message_part
+{
+    var $mime_id = '';
+    var $ctype_primary = 'text';
+    var $ctype_secondary = 'plain';
+    var $mimetype = 'text/plain';
+    var $disposition = '';
+    var $filename = '';
+    var $encoding = '8bit';
+    var $charset = '';
+    var $size = 0;
+    var $headers = array();
+    var $d_parameters = array();
+    var $ctype_parameters = array();
+
+    function __clone()
+    {
+        if (isset($this->parts))
+            foreach ($this->parts as $idx => $part)
+                if (is_object($part))
+	                $this->parts[$idx] = clone $part;
+    }
+}
+
+
+/**
+ * Class for sorting an array of rcube_mail_header objects in a predetermined order.
+ *
+ * @package Mail
+ * @author Eric Stadtherr
+ */
+class rcube_header_sorter
+{
+    private $seqs = array();
+    private $uids = array();
+
+
+    /**
+     * Set the predetermined sort order.
+     *
+     * @param array $index  Numerically indexed array of IMAP ID or UIDs
+     * @param bool  $is_uid Set to true if $index contains UIDs
+     */
+    function set_index($index, $is_uid = false)
+    {
+        $index = array_flip($index);
+
+        if ($is_uid)
+            $this->uids = $index;
+        else
+            $this->seqs = $index;
+    }
+
+    /**
+     * Sort the array of header objects
+     *
+     * @param array $headers Array of rcube_mail_header objects indexed by UID
+     */
+    function sort_headers(&$headers)
+    {
+        if (!empty($this->uids))
+            uksort($headers, array($this, "compare_uids"));
+        else
+            uasort($headers, array($this, "compare_seqnums"));
+    }
+
+    /**
+     * Sort method called by uasort()
+     *
+     * @param rcube_mail_header $a
+     * @param rcube_mail_header $b
+     */
+    function compare_seqnums($a, $b)
+    {
+        // First get the sequence number from the header object (the 'id' field).
+        $seqa = $a->id;
+        $seqb = $b->id;
+
+        // then find each sequence number in my ordered list
+        $posa = isset($this->seqs[$seqa]) ? intval($this->seqs[$seqa]) : -1;
+        $posb = isset($this->seqs[$seqb]) ? intval($this->seqs[$seqb]) : -1;
+
+        // return the relative position as the comparison value
+        return $posa - $posb;
+    }
+
+    /**
+     * Sort method called by uksort()
+     *
+     * @param int $a Array key (UID)
+     * @param int $b Array key (UID)
+     */
+    function compare_uids($a, $b)
+    {
+        // then find each sequence number in my ordered list
+        $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
+        $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
+
+        // return the relative position as the comparison value
+        return $posa - $posb;
+    }
+}
Index: /branches/devel-composer/program/include/rcube_imap_cache.php
===================================================================
--- /branches/devel-composer/program/include/rcube_imap_cache.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_imap_cache.php	(revision 5386)
@@ -0,0 +1,1212 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_imap_cache.php                                  |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Caching of IMAP folder contents (messages and index)                |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ | Author: Aleksander Machniak <alec@alec.pl>                            |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Interface class for accessing Roundcube messages cache
+ *
+ * @package    Cache
+ * @author     Thomas Bruederli <roundcube@gmail.com>
+ * @author     Aleksander Machniak <alec@alec.pl>
+ * @version    1.0
+ */
+class rcube_imap_cache
+{
+    /**
+     * Instance of rcube_imap
+     *
+     * @var rcube_imap
+     */
+    private $imap;
+
+    /**
+     * Instance of rcube_mdb2
+     *
+     * @var rcube_mdb2
+     */
+    private $db;
+
+    /**
+     * User ID
+     *
+     * @var int
+     */
+    private $userid;
+
+    /**
+     * Internal (in-memory) cache
+     *
+     * @var array
+     */
+    private $icache = array();
+
+    private $skip_deleted = false;
+
+    /**
+     * List of known flags. Thanks to this we can handle flag changes
+     * with good performance. Bad thing is we need to know used flags.
+     */
+    public $flags = array(
+        1       => 'SEEN',          // RFC3501
+        2       => 'DELETED',       // RFC3501
+        4       => 'ANSWERED',      // RFC3501
+        8       => 'FLAGGED',       // RFC3501
+        16      => 'DRAFT',         // RFC3501
+        32      => 'MDNSENT',       // RFC3503
+        64      => 'FORWARDED',     // RFC5550
+        128     => 'SUBMITPENDING', // RFC5550
+        256     => 'SUBMITTED',     // RFC5550
+        512     => 'JUNK',
+        1024    => 'NONJUNK',
+        2048    => 'LABEL1',
+        4096    => 'LABEL2',
+        8192    => 'LABEL3',
+        16384   => 'LABEL4',
+        32768   => 'LABEL5',
+    );
+
+
+    /**
+     * Object constructor.
+     */
+    function __construct($db, $imap, $userid, $skip_deleted)
+    {
+        $this->db           = $db;
+        $this->imap         = $imap;
+        $this->userid       = (int)$userid;
+        $this->skip_deleted = $skip_deleted;
+    }
+
+
+    /**
+     * Cleanup actions (on shutdown).
+     */
+    public function close()
+    {
+        $this->save_icache();
+        $this->icache = null;
+    }
+
+
+    /**
+     * Return (sorted) messages index.
+     * If index doesn't exist or is invalid, will be updated.
+     *
+     * @param string  $mailbox     Folder name
+     * @param string  $sort_field  Sorting column
+     * @param string  $sort_order  Sorting order (ASC|DESC)
+     * @param bool    $exiting     Skip index initialization if it doesn't exist in DB
+     *
+     * @return array Messages index
+     */
+    function get_index($mailbox, $sort_field = null, $sort_order = null, $existing = false)
+    {
+        if (empty($this->icache[$mailbox]))
+            $this->icache[$mailbox] = array();
+
+        $sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC';
+
+        // Seek in internal cache
+        if (array_key_exists('index', $this->icache[$mailbox])) {
+            // The index was fetched from database already, but not validated yet
+            if (!array_key_exists('result', $this->icache[$mailbox]['index'])) {
+                $index = $this->icache[$mailbox]['index'];
+            }
+            // We've got a valid index
+            else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field
+            ) {
+                if ($this->icache[$mailbox]['index']['sort_order'] == $sort_order)
+                    return $this->icache[$mailbox]['index']['result'];
+                else
+                    return array_reverse($this->icache[$mailbox]['index']['result'], true);
+            }
+        }
+
+        // Get index from DB (if DB wasn't already queried)
+        if (empty($index) && empty($this->icache[$mailbox]['index_queried'])) {
+            $index = $this->get_index_row($mailbox);
+
+            // set the flag that DB was already queried for index
+            // this way we'll be able to skip one SELECT, when
+            // get_index() is called more than once
+            $this->icache[$mailbox]['index_queried'] = true;
+        }
+
+        $data = null;
+
+        // @TODO: Think about skipping validation checks.
+        // If we could check only every 10 minutes, we would be able to skip
+        // expensive checks, mailbox selection or even IMAP connection, this would require
+        // additional logic to force cache invalidation in some cases
+        // and many rcube_imap changes to connect when needed
+
+        // Entry exists, check cache status
+        if (!empty($index)) {
+            $exists = true;
+
+            if ($sort_field == 'ANY') {
+                $sort_field = $index['sort_field'];
+            }
+
+            if ($sort_field != $index['sort_field']) {
+                $is_valid = false;
+            }
+            else {
+                $is_valid = $this->validate($mailbox, $index, $exists);
+            }
+
+            if ($is_valid) {
+                // build index, assign sequence IDs to unique IDs
+                $data = array_combine($index['seq'], $index['uid']);
+                // revert the order if needed
+                if ($index['sort_order'] != $sort_order)
+                    $data = array_reverse($data, true);
+            }
+        }
+        else {
+            if ($existing) {
+                return null;
+            }
+            else if ($sort_field == 'ANY') {
+                $sort_field = '';
+            }
+
+            // Got it in internal cache, so the row already exist
+            $exists = array_key_exists('index', $this->icache[$mailbox]);
+        }
+
+        // Index not found, not valid or sort field changed, get index from IMAP server
+        if ($data === null) {
+            // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
+            $mbox_data = $this->imap->mailbox_data($mailbox);
+            $data      = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
+
+            // insert/update
+            $this->add_index_row($mailbox, $sort_field, $sort_order, $data, $mbox_data,
+                $exists, $index['modseq']);
+        }
+
+        $this->icache[$mailbox]['index'] = array(
+            'result'     => $data,
+            'sort_field' => $sort_field,
+            'sort_order' => $sort_order,
+            'modseq'     => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
+        );
+
+        return $data;
+    }
+
+
+    /**
+     * Return messages thread.
+     * If threaded index doesn't exist or is invalid, will be updated.
+     *
+     * @param string  $mailbox     Folder name
+     * @param string  $sort_field  Sorting column
+     * @param string  $sort_order  Sorting order (ASC|DESC)
+     *
+     * @return array Messages threaded index
+     */
+    function get_thread($mailbox)
+    {
+        if (empty($this->icache[$mailbox]))
+            $this->icache[$mailbox] = array();
+
+        // Seek in internal cache
+        if (array_key_exists('thread', $this->icache[$mailbox])) {
+            return array(
+                $this->icache[$mailbox]['thread']['tree'],
+                $this->icache[$mailbox]['thread']['depth'],
+                $this->icache[$mailbox]['thread']['children'],
+            );
+        }
+
+        // Get thread from DB (if DB wasn't already queried)
+        if (empty($this->icache[$mailbox]['thread_queried'])) {
+            $index = $this->get_thread_row($mailbox);
+
+            // set the flag that DB was already queried for thread
+            // this way we'll be able to skip one SELECT, when
+            // get_thread() is called more than once or after clear()
+            $this->icache[$mailbox]['thread_queried'] = true;
+        }
+
+        $data = null;
+
+        // Entry exist, check cache status
+        if (!empty($index)) {
+            $exists   = true;
+            $is_valid = $this->validate($mailbox, $index, $exists);
+
+            if (!$is_valid) {
+                $index = null;
+            }
+        }
+
+        // Index not found or not valid, get index from IMAP server
+        if ($index === null) {
+            // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
+            $mbox_data = $this->imap->mailbox_data($mailbox);
+
+            if ($mbox_data['EXISTS']) {
+                // get all threads (default sort order)
+                list ($thread_tree, $msg_depth, $has_children) = $this->imap->fetch_threads($mailbox, true);
+            }
+
+            $index = array(
+                'tree'     => !empty($thread_tree) ? $thread_tree : array(),
+                'depth'    => !empty($msg_depth) ? $msg_depth : array(),
+                'children' => !empty($has_children) ? $has_children : array(),
+            );
+
+            // insert/update
+            $this->add_thread_row($mailbox, $index, $mbox_data, $exists);
+        }
+
+        $this->icache[$mailbox]['thread'] = $index;
+
+        return array($index['tree'], $index['depth'], $index['children']);
+    }
+
+
+    /**
+     * Returns list of messages (headers). See rcube_imap::fetch_headers().
+     *
+     * @param string $mailbox  Folder name
+     * @param array  $msgs     Message sequence numbers
+     * @param bool   $is_uid   True if $msgs contains message UIDs
+     *
+     * @return array The list of messages (rcube_mail_header) indexed by UID
+     */
+    function get_messages($mailbox, $msgs = array(), $is_uid = true)
+    {
+        if (empty($msgs)) {
+            return array();
+        }
+
+        // @TODO: it would be nice if we could work with UIDs only
+        // then index would be not needed. For now we need it to
+        // map id to uid here and to update message id for cached message
+
+        // Convert IDs to UIDs
+        $index = $this->get_index($mailbox, 'ANY');
+        if (!$is_uid) {
+            foreach ($msgs as $idx => $msgid)
+                if ($uid = $index[$msgid])
+                    $msgs[$idx] = $uid;
+        }
+
+        // Fetch messages from cache
+        $sql_result = $this->db->query(
+            "SELECT uid, data, flags"
+            ." FROM ".get_table_name('cache_messages')
+            ." WHERE user_id = ?"
+                ." AND mailbox = ?"
+                ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
+            $this->userid, $mailbox);
+
+        $msgs   = array_flip($msgs);
+        $result = array();
+
+        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+            $uid          = intval($sql_arr['uid']);
+            $result[$uid] = $this->build_message($sql_arr);
+
+            // save memory, we don't need message body here (?)
+            $result[$uid]->body = null;
+
+            // update message ID according to index data
+            if (!empty($index) && ($id = array_search($uid, $index)))
+                $result[$uid]->id = $id;
+
+            if (!empty($result[$uid])) {
+                unset($msgs[$uid]);
+            }
+        }
+
+        // Fetch not found messages from IMAP server
+        if (!empty($msgs)) {
+            $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), true, true);
+
+            // Insert to DB and add to result list
+            if (!empty($messages)) {
+                foreach ($messages as $msg) {
+                    $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
+                    $result[$msg->uid] = $msg;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Returns message data.
+     *
+     * @param string $mailbox  Folder name
+     * @param int    $uid      Message UID
+     * @param bool   $update   If message doesn't exists in cache it will be fetched
+     *                         from IMAP server
+     * @param bool   $no_cache Enables internal cache usage
+     *
+     * @return rcube_mail_header Message data
+     */
+    function get_message($mailbox, $uid, $update = true, $cache = true)
+    {
+        // Check internal cache
+        if (($message = $this->icache['message'])
+            && $message['mailbox'] == $mailbox && $message['object']->uid == $uid
+        ) {
+            return $this->icache['message']['object'];
+        }
+
+        $sql_result = $this->db->query(
+            "SELECT flags, data"
+            ." FROM ".get_table_name('cache_messages')
+            ." WHERE user_id = ?"
+                ." AND mailbox = ?"
+                ." AND uid = ?",
+                $this->userid, $mailbox, (int)$uid);
+
+        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+            $message = $this->build_message($sql_arr);
+            $found   = true;
+
+            // update message ID according to index data
+            $index = $this->get_index($mailbox, 'ANY');
+            if (!empty($index) && ($id = array_search($uid, $index)))
+                $message->id = $id;
+        }
+
+        // Get the message from IMAP server
+        if (empty($message) && $update) {
+            $message = $this->imap->get_headers($uid, $mailbox, true);
+            // cache will be updated in close(), see below
+        }
+
+        // Save the message in internal cache, will be written to DB in close()
+        // Common scenario: user opens unseen message
+        // - get message (SELECT)
+        // - set message headers/structure (INSERT or UPDATE)
+        // - set \Seen flag (UPDATE)
+        // This way we can skip one UPDATE
+        if (!empty($message) && $cache) {
+            // Save current message from internal cache
+            $this->save_icache();
+
+            $this->icache['message'] = array(
+                'object'  => $message,
+                'mailbox' => $mailbox,
+                'exists'  => $found,
+                'md5sum'  => md5(serialize($message)),
+            );
+        }
+
+        return $message;
+    }
+
+
+    /**
+     * Saves the message in cache.
+     *
+     * @param string            $mailbox  Folder name
+     * @param rcube_mail_header $message  Message data
+     * @param bool              $force    Skips message in-cache existance check
+     */
+    function add_message($mailbox, $message, $force = false)
+    {
+        if (!is_object($message) || empty($message->uid))
+            return;
+
+        $msg   = serialize($this->db->encode(clone $message));
+        $flags = 0;
+
+        if (!empty($message->flags)) {
+            foreach ($this->flags as $idx => $flag)
+                if (!empty($message->flags[$flag]))
+                    $flags += $idx;
+        }
+        unset($msg->flags);
+
+        // update cache record (even if it exists, the update
+        // here will work as select, assume row exist if affected_rows=0)
+        if (!$force) {
+            $res = $this->db->query(
+                "UPDATE ".get_table_name('cache_messages')
+                ." SET flags = ?, data = ?, changed = ".$this->db->now()
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ?"
+                    ." AND uid = ?",
+                $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
+
+            if ($this->db->affected_rows())
+                return;
+        }
+
+        // insert new record
+        $this->db->query(
+            "INSERT INTO ".get_table_name('cache_messages')
+            ." (user_id, mailbox, uid, flags, changed, data)"
+            ." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)",
+            $this->userid, $mailbox, (int) $message->uid, $flags, $msg);
+    }
+
+
+    /**
+     * Sets the flag for specified message.
+     *
+     * @param string  $mailbox  Folder name
+     * @param array   $uids     Message UIDs or null to change flag
+     *                          of all messages in a folder
+     * @param string  $flag     The name of the flag
+     * @param bool    $enabled  Flag state
+     */
+    function change_flag($mailbox, $uids, $flag, $enabled = false)
+    {
+        $flag = strtoupper($flag);
+        $idx  = (int) array_search($flag, $this->flags);
+
+        if (!$idx) {
+            return;
+        }
+
+        // Internal cache update
+        if ($uids && count($uids) == 1 && ($uid = current($uids))
+            && ($message = $this->icache['message'])
+            && $message['mailbox'] == $mailbox && $message['object']->uid == $uid
+        ) {
+            $message['object']->flags[$flag] = $enabled;
+            return;
+        }
+
+        $this->db->query(
+            "UPDATE ".get_table_name('cache_messages')
+            ." SET changed = ".$this->db->now()
+            .", flags = flags ".($enabled ? "+ $idx" : "- $idx")
+            ." WHERE user_id = ?"
+                ." AND mailbox = ?"
+                .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : "")
+                ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),
+            $this->userid, $mailbox);
+    }
+
+
+    /**
+     * Removes message(s) from cache.
+     *
+     * @param string $mailbox  Folder name
+     * @param array  $uids     Message UIDs, NULL removes all messages
+     */
+    function remove_message($mailbox = null, $uids = null)
+    {
+        if (!strlen($mailbox)) {
+            $this->db->query(
+                "DELETE FROM ".get_table_name('cache_messages')
+                ." WHERE user_id = ?",
+                $this->userid);
+        }
+        else {
+            // Remove the message from internal cache
+            if (!empty($uids) && !is_array($uids) && ($message = $this->icache['message'])
+                && $message['mailbox'] == $mailbox && $message['object']->uid == $uids
+            ) {
+                $this->icache['message'] = null;
+            }
+
+            $this->db->query(
+                "DELETE FROM ".get_table_name('cache_messages')
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ".$this->db->quote($mailbox)
+                    .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
+                $this->userid);
+        }
+
+    }
+
+
+    /**
+     * Clears index cache.
+     *
+     * @param string  $mailbox     Folder name
+     * @param bool    $remove      Enable to remove the DB row
+     */
+    function remove_index($mailbox = null, $remove = false)
+    {
+        // The index should be only removed from database when
+        // UIDVALIDITY was detected or the mailbox is empty
+        // otherwise use 'valid' flag to not loose HIGHESTMODSEQ value
+        if ($remove)
+            $this->db->query(
+                "DELETE FROM ".get_table_name('cache_index')
+                ." WHERE user_id = ".intval($this->userid)
+                    .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
+            );
+        else
+            $this->db->query(
+                "UPDATE ".get_table_name('cache_index')
+                ." SET valid = 0"
+                ." WHERE user_id = ".intval($this->userid)
+                    .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
+            );
+
+        if (strlen($mailbox)) {
+            unset($this->icache[$mailbox]['index']);
+            // Index removed, set flag to skip SELECT query in get_index()
+            $this->icache[$mailbox]['index_queried'] = true;
+        }
+        else
+            $this->icache = array();
+    }
+
+
+    /**
+     * Clears thread cache.
+     *
+     * @param string  $mailbox     Folder name
+     */
+    function remove_thread($mailbox = null)
+    {
+        $this->db->query(
+            "DELETE FROM ".get_table_name('cache_thread')
+            ." WHERE user_id = ".intval($this->userid)
+                .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
+        );
+
+        if (strlen($mailbox)) {
+            unset($this->icache[$mailbox]['thread']);
+            // Thread data removed, set flag to skip SELECT query in get_thread()
+            $this->icache[$mailbox]['thread_queried'] = true;
+        }
+        else
+            $this->icache = array();
+    }
+
+
+    /**
+     * Clears the cache.
+     *
+     * @param string $mailbox  Folder name
+     * @param array  $uids     Message UIDs, NULL removes all messages in a folder
+     */
+    function clear($mailbox = null, $uids = null)
+    {
+        $this->remove_index($mailbox, true);
+        $this->remove_thread($mailbox);
+        $this->remove_message($mailbox, $uids);
+    }
+
+
+    /**
+     * @param string $mailbox Folder name
+     * @param int    $id      Message (sequence) ID
+     *
+     * @return int Message UID
+     */
+    function id2uid($mailbox, $id)
+    {
+        if (!empty($this->icache['pending_index_update']))
+            return null;
+
+        // get index if it exists
+        $index = $this->get_index($mailbox, 'ANY', null, true);
+
+        return $index[$id];
+    }
+
+
+    /**
+     * @param string $mailbox Folder name
+     * @param int    $uid     Message UID
+     *
+     * @return int Message (sequence) ID
+     */
+    function uid2id($mailbox, $uid)
+    {
+        if (!empty($this->icache['pending_index_update']))
+            return null;
+
+        // get index if it exists
+        $index = $this->get_index($mailbox, 'ANY', null, true);
+
+        return array_search($uid, (array)$index);
+    }
+
+    /**
+     * Fetches index data from database
+     */
+    private function get_index_row($mailbox)
+    {
+        // Get index from DB
+        $sql_result = $this->db->query(
+            "SELECT data, valid"
+            ." FROM ".get_table_name('cache_index')
+            ." WHERE user_id = ?"
+                ." AND mailbox = ?",
+            $this->userid, $mailbox);
+
+        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+            $data = explode('@', $sql_arr['data']);
+
+            return array(
+                'valid'      => $sql_arr['valid'],
+                'seq'        => explode(',', $data[0]),
+                'uid'        => explode(',', $data[1]),
+                'sort_field' => $data[2],
+                'sort_order' => $data[3],
+                'deleted'    => $data[4],
+                'validity'   => $data[5],
+                'uidnext'    => $data[6],
+                'modseq'     => $data[7],
+            );
+        }
+
+        return null;
+    }
+
+
+    /**
+     * Fetches thread data from database
+     */
+    private function get_thread_row($mailbox)
+    {
+        // Get thread from DB
+        $sql_result = $this->db->query(
+            "SELECT data"
+            ." FROM ".get_table_name('cache_thread')
+            ." WHERE user_id = ?"
+                ." AND mailbox = ?",
+            $this->userid, $mailbox);
+
+        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+            $data = explode('@', $sql_arr['data']);
+
+            // Uncompress data, see add_thread_row()
+  //          $data[0] = str_replace(array('*', '^', '#'), array(';a:0:{}', 'i:', ';a:1:'), $data[0]);
+            $data[0] = unserialize($data[0]);
+
+            // build 'depth' and 'children' arrays
+            $depth = $children = array();
+            $this->build_thread_data($data[0], $depth, $children);
+
+            return array(
+                'tree'     => $data[0],
+                'depth'    => $depth,
+                'children' => $children,
+                'deleted'  => $data[1],
+                'validity' => $data[2],
+                'uidnext'  => $data[3],
+            );
+        }
+
+        return null;
+    }
+
+
+    /**
+     * Saves index data into database
+     */
+    private function add_index_row($mailbox, $sort_field, $sort_order,
+        $data = array(), $mbox_data = array(), $exists = false, $modseq = null)
+    {
+        $data = array(
+            implode(',', array_keys($data)),
+            implode(',', array_values($data)),
+            $sort_field,
+            $sort_order,
+            (int) $this->skip_deleted,
+            (int) $mbox_data['UIDVALIDITY'],
+            (int) $mbox_data['UIDNEXT'],
+            $modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
+        );
+        $data = implode('@', $data);
+
+        if ($exists)
+            $sql_result = $this->db->query(
+                "UPDATE ".get_table_name('cache_index')
+                ." SET data = ?, valid = 1, changed = ".$this->db->now()
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ?",
+                $data, $this->userid, $mailbox);
+        else
+            $sql_result = $this->db->query(
+                "INSERT INTO ".get_table_name('cache_index')
+                ." (user_id, mailbox, data, valid, changed)"
+                ." VALUES (?, ?, ?, 1, ".$this->db->now().")",
+                $this->userid, $mailbox, $data);
+    }
+
+
+    /**
+     * Saves thread data into database
+     */
+    private function add_thread_row($mailbox, $data = array(), $mbox_data = array(), $exists = false)
+    {
+        $tree = serialize($data['tree']);
+        // This significantly reduces data length
+//        $tree = str_replace(array(';a:0:{}', 'i:', ';a:1:'), array('*', '^', '#'), $tree);
+
+        $data = array(
+            $tree,
+            (int) $this->skip_deleted,
+            (int) $mbox_data['UIDVALIDITY'],
+            (int) $mbox_data['UIDNEXT'],
+        );
+        $data = implode('@', $data);
+
+        if ($exists)
+            $sql_result = $this->db->query(
+                "UPDATE ".get_table_name('cache_thread')
+                ." SET data = ?, changed = ".$this->db->now()
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ?",
+                $data, $this->userid, $mailbox);
+        else
+            $sql_result = $this->db->query(
+                "INSERT INTO ".get_table_name('cache_thread')
+                ." (user_id, mailbox, data, changed)"
+                ." VALUES (?, ?, ?, ".$this->db->now().")",
+                $this->userid, $mailbox, $data);
+    }
+
+
+    /**
+     * Checks index/thread validity
+     */
+    private function validate($mailbox, $index, &$exists = true)
+    {
+        $is_thread = isset($index['tree']);
+
+        // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
+        $mbox_data = $this->imap->mailbox_data($mailbox);
+
+        // @TODO: Think about skipping validation checks.
+        // If we could check only every 10 minutes, we would be able to skip
+        // expensive checks, mailbox selection or even IMAP connection, this would require
+        // additional logic to force cache invalidation in some cases
+        // and many rcube_imap changes to connect when needed
+
+        // Check UIDVALIDITY
+        if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
+            $this->clear($mailbox);
+            $exists = false;
+            return false;
+        }
+
+        // Folder is empty but cache isn't
+        if (empty($mbox_data['EXISTS'])) {
+            if (!empty($index['seq']) || !empty($index['tree'])) {
+                $this->clear($mailbox);
+                $exists = false;
+                return false;
+            }
+        }
+        // Folder is not empty but cache is
+        else if (empty($index['seq']) && empty($index['tree'])) {
+            unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
+            return false;
+        }
+
+        // Validation flag
+        if (!$is_thread && empty($index['valid'])) {
+            unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
+            return false;
+        }
+
+        // Index was created with different skip_deleted setting
+        if ($this->skip_deleted != $index['deleted']) {
+            return false;
+        }
+
+        // Check HIGHESTMODSEQ
+        if (!empty($index['modseq']) && !empty($mbox_data['HIGHESTMODSEQ'])
+            && $index['modseq'] == $mbox_data['HIGHESTMODSEQ']
+        ) {
+            return true;
+        }
+
+        // Check UIDNEXT
+        if ($index['uidnext'] != $mbox_data['UIDNEXT']) {
+            unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
+            return false;
+        }
+
+        // @TODO: find better validity check for threaded index
+        if ($is_thread) {
+            // check messages number...
+            if (!$this->skip_deleted && $mbox_data['EXISTS'] != @max(array_keys($index['depth']))) {
+                return false;
+            }
+            return true;
+        }
+
+        // The rest of checks, more expensive
+        if (!empty($this->skip_deleted)) {
+            // compare counts if available
+            if ($mbox_data['COUNT_UNDELETED'] != null
+                && $mbox_data['COUNT_UNDELETED'] != count($index['uid'])) {
+                return false;
+            }
+            // compare UID sets
+            if ($mbox_data['ALL_UNDELETED'] != null) {
+                $uids_new = rcube_imap_generic::uncompressMessageSet($mbox_data['ALL_UNDELETED']);
+                $uids_old = $index['uid'];
+
+                if (count($uids_new) != count($uids_old)) {
+                    return false;
+                }
+
+                sort($uids_new, SORT_NUMERIC);
+                sort($uids_old, SORT_NUMERIC);
+
+                if ($uids_old != $uids_new)
+                    return false;
+            }
+            else {
+                // get all undeleted messages excluding cached UIDs
+                $ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '.
+                    rcube_imap_generic::compressMessageSet($index['uid']));
+
+                if (!empty($ids)) {
+                    return false;
+                }
+            }
+        }
+        else {
+            // check messages number...
+            if ($mbox_data['EXISTS'] != max($index['seq'])) {
+                return false;
+            }
+            // ... and max UID
+            if (max($index['uid']) != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+
+    /**
+     * Synchronizes the mailbox.
+     *
+     * @param string $mailbox Folder name
+     */
+    function synchronize($mailbox)
+    {
+        // RFC4549: Synchronization Operations for Disconnected IMAP4 Clients
+        // RFC4551: IMAP Extension for Conditional STORE Operation
+        //          or Quick Flag Changes Resynchronization
+        // RFC5162: IMAP Extensions for Quick Mailbox Resynchronization
+
+        // @TODO: synchronize with other methods?
+        $qresync   = $this->imap->get_capability('QRESYNC');
+        $condstore = $qresync ? true : $this->imap->get_capability('CONDSTORE');
+
+        if (!$qresync && !$condstore) {
+            return;
+        }
+
+        // Get stored index
+        $index = $this->get_index_row($mailbox);
+
+        // database is empty
+        if (empty($index)) {
+            // set the flag that DB was already queried for index
+            // this way we'll be able to skip one SELECT in get_index()
+            $this->icache[$mailbox]['index_queried'] = true;
+            return;
+        }
+
+        $this->icache[$mailbox]['index'] = $index;
+
+        // no last HIGHESTMODSEQ value
+        if (empty($index['modseq'])) {
+            return;
+        }
+
+        // NOTE: make sure the mailbox isn't selected, before
+        // enabling QRESYNC and invoking SELECT
+        if ($this->imap->conn->selected !== null) {
+            $this->imap->conn->close();
+        }
+
+        // Enable QRESYNC
+        $res = $this->imap->conn->enable($qresync ? 'QRESYNC' : 'CONDSTORE');
+        if (!is_array($res)) {
+            return;
+        }
+
+        // Get mailbox data (UIDVALIDITY, HIGHESTMODSEQ, counters, etc.)
+        $mbox_data = $this->imap->mailbox_data($mailbox);
+
+        if (empty($mbox_data)) {
+             return;
+        }
+
+        // Check UIDVALIDITY
+        if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
+            $this->clear($mailbox);
+            return;
+        }
+
+        // QRESYNC not supported on specified mailbox
+        if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) {
+            return;
+        }
+
+        // Nothing new
+        if ($mbox_data['HIGHESTMODSEQ'] == $index['modseq']) {
+            return;
+        }
+
+        // Get known uids
+        $uids = array();
+        $sql_result = $this->db->query(
+            "SELECT uid"
+            ." FROM ".get_table_name('cache_messages')
+            ." WHERE user_id = ?"
+                ." AND mailbox = ?",
+            $this->userid, $mailbox);
+
+        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+          $uids[] = $sql_arr['uid'];
+        }
+
+        // No messages in database, nothing to sync
+        if (empty($uids)) {
+            return;
+        }
+
+        // Get modified flags and vanished messages
+        // UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED)
+        $result = $this->imap->conn->fetch($mailbox,
+            !empty($uids) ? $uids : '1:*', true, array('FLAGS'),
+            $index['modseq'], $qresync);
+
+        $invalidated = false;
+
+        if (!empty($result)) {
+            foreach ($result as $id => $msg) {
+                $uid = $msg->uid;
+                // Remove deleted message
+                if ($this->skip_deleted && !empty($msg->flags['DELETED'])) {
+                    $this->remove_message($mailbox, $uid);
+
+                    if (!$invalidated) {
+                        $invalidated = true;
+                        // Invalidate thread indexes (?)
+                        $this->remove_thread($mailbox);
+                        // Invalidate index
+                        $index['valid'] = false;
+                    }
+                    continue;
+                }
+
+                $flags = 0;
+                if (!empty($msg->flags)) {
+                    foreach ($this->flags as $idx => $flag)
+                        if (!empty($msg->flags[$flag]))
+                            $flags += $idx;
+                }
+
+                $this->db->query(
+                    "UPDATE ".get_table_name('cache_messages')
+                    ." SET flags = ?, changed = ".$this->db->now()
+                    ." WHERE user_id = ?"
+                        ." AND mailbox = ?"
+                        ." AND uid = ?"
+                        ." AND flags <> ?",
+                    $flags, $this->userid, $mailbox, $uid, $flags);
+            }
+        }
+
+        // Get VANISHED
+        if ($qresync) {
+            $mbox_data = $this->imap->mailbox_data($mailbox);
+
+            // Removed messages
+            if (!empty($mbox_data['VANISHED'])) {
+                $uids = rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED']);
+                if (!empty($uids)) {
+                    // remove messages from database
+                    $this->remove_message($mailbox, $uids);
+
+                    // Invalidate thread indexes (?)
+                    $this->remove_thread($mailbox);
+                    // Invalidate index
+                    $index['valid'] = false;
+                }
+            }
+        }
+
+        $sort_field = $index['sort_field'];
+        $sort_order = $index['sort_order'];
+        $exists     = true;
+
+        // Validate index
+        if (!$this->validate($mailbox, $index, $exists)) {
+            // Update index
+            $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
+        }
+        else {
+            $data = array_combine($index['seq'], $index['uid']);
+        }
+
+        // update index and/or HIGHESTMODSEQ value
+        $this->add_index_row($mailbox, $sort_field, $sort_order, $data, $mbox_data, $exists);
+
+        // update internal cache for get_index()
+        $this->icache[$mailbox]['index']['result'] = $data;
+    }
+
+
+    /**
+     * Converts cache row into message object.
+     *
+     * @param array $sql_arr Message row data
+     *
+     * @return rcube_mail_header Message object
+     */
+    private function build_message($sql_arr)
+    {
+        $message = $this->db->decode(unserialize($sql_arr['data']));
+
+        if ($message) {
+            $message->flags = array();
+            foreach ($this->flags as $idx => $flag)
+                if (($sql_arr['flags'] & $idx) == $idx)
+                    $message->flags[$flag] = true;
+        }
+
+        return $message;
+    }
+
+
+    /**
+     * Creates 'depth' and 'children' arrays from stored thread 'tree' data.
+     */
+    private function build_thread_data($data, &$depth, &$children, $level = 0)
+    {
+        foreach ((array)$data as $key => $val) {
+            $children[$key] = !empty($val);
+            $depth[$key] = $level;
+            if (!empty($val))
+                $this->build_thread_data($val, $depth, $children, $level + 1);
+        }
+    }
+
+
+    /**
+     * Saves message stored in internal cache
+     */
+    private function save_icache()
+    {
+        // Save current message from internal cache
+        if ($message = $this->icache['message']) {
+            // clean up some object's data
+            $object = $this->message_object_prepare($message['object']);
+
+            // calculate current md5 sum
+            $md5sum = md5(serialize($object));
+
+            if ($message['md5sum'] != $md5sum) {
+                $this->add_message($message['mailbox'], $object, !$message['exists']);
+            }
+
+            $this->icache['message']['md5sum'] = $md5sum;
+        }
+    }
+
+
+    /**
+     * Prepares message object to be stored in database.
+     */
+    private function message_object_prepare($msg)
+    {
+        // Remove body too big (>25kB)
+        if ($msg->body && strlen($msg->body) > 25 * 1024) {
+            unset($msg->body);
+        }
+
+        // Fix mimetype which might be broken by some code when message is displayed
+        // Another solution would be to use object's copy in rcube_message class
+        // to prevent related issues, however I'm not sure which is better
+        if ($msg->mimetype) {
+            list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
+        }
+
+        if (is_array($msg->structure->parts)) {
+            foreach ($msg->structure->parts as $idx => $part) {
+                $msg->structure->parts[$idx] = $this->message_object_prepare($part);
+            }
+        }
+
+        return $msg;
+    }
+
+
+    /**
+     * Fetches index data from IMAP server
+     */
+    private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array())
+    {
+        $data = array();
+
+        if (empty($mbox_data)) {
+            $mbox_data = $this->imap->mailbox_data($mailbox);
+        }
+
+        // Prevent infinite loop.
+        // It happens when rcube_imap::message_index_direct() is called.
+        // There id2uid() is called which will again call get_index() and so on.
+        if (!$sort_field && !$this->skip_deleted)
+            $this->icache['pending_index_update'] = true;
+
+        if ($mbox_data['EXISTS']) {
+            // fetch sorted sequence numbers
+            $data_seq = $this->imap->message_index_direct($mailbox, $sort_field, $sort_order);
+            // fetch UIDs
+            if (!empty($data_seq)) {
+                // Seek in internal cache
+                if (array_key_exists('index', (array)$this->icache[$mailbox])
+                    && array_key_exists('result', (array)$this->icache[$mailbox]['index'])
+                )
+                    $data_uid = $this->icache[$mailbox]['index']['result'];
+                else
+                    $data_uid = $this->imap->conn->fetchUIDs($mailbox, $data_seq);
+
+                // build index
+                if (!empty($data_uid)) {
+                    foreach ($data_seq as $seq)
+                        if ($uid = $data_uid[$seq])
+                            $data[$seq] = $uid;
+                }
+            }
+        }
+
+        // Reset internal flags
+        $this->icache['pending_index_update'] = false;
+
+        return $data;
+    }
+}
Index: /branches/devel-composer/program/include/rcube_imap_generic.php
===================================================================
--- /branches/devel-composer/program/include/rcube_imap_generic.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_imap_generic.php	(revision 5386)
@@ -0,0 +1,3555 @@
+<?php
+
+/**
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_imap_generic.php                                |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2010, The Roundcube Dev Team                       |
+ | Copyright (C) 2011, Kolab Systems AG                                  |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Provide alternative IMAP library that doesn't rely on the standard  |
+ |   C-Client based version. This allows to function regardless          |
+ |   of whether or not the PHP build it's running on has IMAP            |
+ |   functionality built-in.                                             |
+ |                                                                       |
+ |   Based on Iloha IMAP Library. See http://ilohamail.org/ for details  |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <alec@alec.pl>                            |
+ | Author: Ryo Chijiiwa <Ryo@IlohaMail.org>                              |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Struct representing an e-mail message header
+ *
+ * @package Mail
+ * @author  Aleksander Machniak <alec@alec.pl>
+ */
+class rcube_mail_header
+{
+    public $id;
+    public $uid;
+    public $subject;
+    public $from;
+    public $to;
+    public $cc;
+    public $replyto;
+    public $in_reply_to;
+    public $date;
+    public $messageID;
+    public $size;
+    public $encoding;
+    public $charset;
+    public $ctype;
+    public $timestamp;
+    public $bodystructure;
+    public $internaldate;
+    public $references;
+    public $priority;
+    public $mdn_to;
+    public $others = array();
+    public $flags = array();
+}
+
+// For backward compatibility with cached messages (#1486602)
+class iilBasicHeader extends rcube_mail_header
+{
+}
+
+/**
+ * PHP based wrapper class to connect to an IMAP server
+ *
+ * @package Mail
+ * @author  Aleksander Machniak <alec@alec.pl>
+ */
+class rcube_imap_generic
+{
+    public $error;
+    public $errornum;
+    public $result;
+    public $resultcode;
+    public $selected;
+    public $data = array();
+    public $flags = array(
+        'SEEN'     => '\\Seen',
+        'DELETED'  => '\\Deleted',
+        'ANSWERED' => '\\Answered',
+        'DRAFT'    => '\\Draft',
+        'FLAGGED'  => '\\Flagged',
+        'FORWARDED' => '$Forwarded',
+        'MDNSENT'  => '$MDNSent',
+        '*'        => '\\*',
+    );
+
+    private $fp;
+    private $host;
+    private $logged = false;
+    private $capability = array();
+    private $capability_readed = false;
+    private $prefs;
+    private $cmd_tag;
+    private $cmd_num = 0;
+    private $resourceid;
+    private $_debug = false;
+    private $_debug_handler = false;
+
+    const ERROR_OK = 0;
+    const ERROR_NO = -1;
+    const ERROR_BAD = -2;
+    const ERROR_BYE = -3;
+    const ERROR_UNKNOWN = -4;
+    const ERROR_COMMAND = -5;
+    const ERROR_READONLY = -6;
+
+    const COMMAND_NORESPONSE = 1;
+    const COMMAND_CAPABILITY = 2;
+    const COMMAND_LASTLINE   = 4;
+
+    /**
+     * Object constructor
+     */
+    function __construct()
+    {
+    }
+
+    /**
+     * Send simple (one line) command to the connection stream
+     *
+     * @param string $string Command string
+     * @param bool   $endln  True if CRLF need to be added at the end of command
+     *
+     * @param int Number of bytes sent, False on error
+     */
+    function putLine($string, $endln=true)
+    {
+        if (!$this->fp)
+            return false;
+
+        if ($this->_debug) {
+            $this->debug('C: '. rtrim($string));
+        }
+
+        $res = fwrite($this->fp, $string . ($endln ? "\r\n" : ''));
+
+        if ($res === false) {
+            @fclose($this->fp);
+            $this->fp = null;
+        }
+
+        return $res;
+    }
+
+    /**
+     * Send command to the connection stream with Command Continuation
+     * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support
+     *
+     * @param string $string Command string
+     * @param bool   $endln  True if CRLF need to be added at the end of command
+     *
+     * @param int Number of bytes sent, False on error
+     */
+    function putLineC($string, $endln=true)
+    {
+        if (!$this->fp)
+            return false;
+
+        if ($endln)
+            $string .= "\r\n";
+
+
+        $res = 0;
+        if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) {
+            for ($i=0, $cnt=count($parts); $i<$cnt; $i++) {
+                if (preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i+1], $matches)) {
+                    // LITERAL+ support
+                    if ($this->prefs['literal+']) {
+                        $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
+                    }
+
+                    $bytes = $this->putLine($parts[$i].$parts[$i+1], false);
+                    if ($bytes === false)
+                        return false;
+                    $res += $bytes;
+
+                    // don't wait if server supports LITERAL+ capability
+                    if (!$this->prefs['literal+']) {
+                        $line = $this->readLine(1000);
+                        // handle error in command
+                        if ($line[0] != '+')
+                            return false;
+                    }
+                    $i++;
+                }
+                else {
+                    $bytes = $this->putLine($parts[$i], false);
+                    if ($bytes === false)
+                        return false;
+                    $res += $bytes;
+                }
+            }
+        }
+        return $res;
+    }
+
+    function readLine($size=1024)
+    {
+        $line = '';
+
+        if (!$size) {
+            $size = 1024;
+        }
+
+        do {
+            if ($this->eof()) {
+                return $line ? $line : NULL;
+            }
+
+            $buffer = fgets($this->fp, $size);
+
+            if ($buffer === false) {
+                $this->closeSocket();
+                break;
+            }
+            if ($this->_debug) {
+                $this->debug('S: '. rtrim($buffer));
+            }
+            $line .= $buffer;
+        } while (substr($buffer, -1) != "\n");
+
+        return $line;
+    }
+
+    function multLine($line, $escape = false)
+    {
+        $line = rtrim($line);
+        if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
+            $out   = '';
+            $str   = substr($line, 0, -strlen($m[0]));
+            $bytes = $m[1];
+
+            while (strlen($out) < $bytes) {
+                $line = $this->readBytes($bytes);
+                if ($line === NULL)
+                    break;
+                $out .= $line;
+            }
+
+            $line = $str . ($escape ? $this->escape($out) : $out);
+        }
+
+        return $line;
+    }
+
+    function readBytes($bytes)
+    {
+        $data = '';
+        $len  = 0;
+        while ($len < $bytes && !$this->eof())
+        {
+            $d = fread($this->fp, $bytes-$len);
+            if ($this->_debug) {
+                $this->debug('S: '. $d);
+            }
+            $data .= $d;
+            $data_len = strlen($data);
+            if ($len == $data_len) {
+                break; // nothing was read -> exit to avoid apache lockups
+            }
+            $len = $data_len;
+        }
+
+        return $data;
+    }
+
+    function readReply(&$untagged=null)
+    {
+        do {
+            $line = trim($this->readLine(1024));
+            // store untagged response lines
+            if ($line[0] == '*')
+                $untagged[] = $line;
+        } while ($line[0] == '*');
+
+        if ($untagged)
+            $untagged = join("\n", $untagged);
+
+        return $line;
+    }
+
+    function parseResult($string, $err_prefix='')
+    {
+        if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) {
+            $res = strtoupper($matches[1]);
+            $str = trim($matches[2]);
+
+            if ($res == 'OK') {
+                $this->errornum = self::ERROR_OK;
+            } else if ($res == 'NO') {
+                $this->errornum = self::ERROR_NO;
+            } else if ($res == 'BAD') {
+                $this->errornum = self::ERROR_BAD;
+            } else if ($res == 'BYE') {
+                $this->closeSocket();
+                $this->errornum = self::ERROR_BYE;
+            }
+
+            if ($str) {
+                $str = trim($str);
+                // get response string and code (RFC5530)
+                if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) {
+                    $this->resultcode = strtoupper($m[1]);
+                    $str = trim(substr($str, strlen($m[1]) + 2));
+                }
+                else {
+                    $this->resultcode = null;
+                    // parse response for [APPENDUID 1204196876 3456]
+                    if (preg_match("/^\[APPENDUID [0-9]+ ([0-9,:*]+)\]/i", $str, $m)) {
+                        $this->data['APPENDUID'] = $m[1];
+                    }
+                }
+                $this->result = $str;
+
+                if ($this->errornum != self::ERROR_OK) {
+                    $this->error = $err_prefix ? $err_prefix.$str : $str;
+                }
+            }
+
+            return $this->errornum;
+        }
+        return self::ERROR_UNKNOWN;
+    }
+
+    private function eof()
+    {
+        if (!is_resource($this->fp)) {
+            return true;
+        }
+
+        // If a connection opened by fsockopen() wasn't closed
+        // by the server, feof() will hang.
+        $start = microtime(true);
+
+        if (feof($this->fp) || 
+            ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout']))
+        ) {
+            $this->closeSocket();
+            return true;
+        }
+
+        return false;
+    }
+
+    private function closeSocket()
+    {
+        @fclose($this->fp);
+        $this->fp = null;
+    }
+
+    function setError($code, $msg='')
+    {
+        $this->errornum = $code;
+        $this->error    = $msg;
+    }
+
+    // check if $string starts with $match (or * BYE/BAD)
+    function startsWith($string, $match, $error=false, $nonempty=false)
+    {
+        $len = strlen($match);
+        if ($len == 0) {
+            return false;
+        }
+        if (!$this->fp) {
+            return true;
+        }
+        if (strncmp($string, $match, $len) == 0) {
+            return true;
+        }
+        if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
+            if (strtoupper($m[1]) == 'BYE') {
+                $this->closeSocket();
+            }
+            return true;
+        }
+        if ($nonempty && !strlen($string)) {
+            return true;
+        }
+        return false;
+    }
+
+    private function hasCapability($name)
+    {
+        if (empty($this->capability) || $name == '') {
+            return false;
+        }
+
+        if (in_array($name, $this->capability)) {
+            return true;
+        }
+        else if (strpos($name, '=')) {
+            return false;
+        }
+
+        $result = array();
+        foreach ($this->capability as $cap) {
+            $entry = explode('=', $cap);
+            if ($entry[0] == $name) {
+                $result[] = $entry[1];
+            }
+        }
+
+        return !empty($result) ? $result : false;
+    }
+
+    /**
+     * Capabilities checker
+     *
+     * @param string $name Capability name
+     *
+     * @return mixed Capability values array for key=value pairs, true/false for others
+     */
+    function getCapability($name)
+    {
+        $result = $this->hasCapability($name);
+
+        if (!empty($result)) {
+            return $result;
+        }
+        else if ($this->capability_readed) {
+            return false;
+        }
+
+        // get capabilities (only once) because initial
+        // optional CAPABILITY response may differ
+        $result = $this->execute('CAPABILITY');
+
+        if ($result[0] == self::ERROR_OK) {
+            $this->parseCapability($result[1]);
+        }
+
+        $this->capability_readed = true;
+
+        return $this->hasCapability($name);
+    }
+
+    function clearCapability()
+    {
+        $this->capability = array();
+        $this->capability_readed = false;
+    }
+
+    /**
+     * DIGEST-MD5/CRAM-MD5/PLAIN Authentication
+     *
+     * @param string $user
+     * @param string $pass
+     * @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5)
+     *
+     * @return resource Connection resourse on success, error code on error
+     */
+    function authenticate($user, $pass, $type='PLAIN')
+    {
+        if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') {
+            if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) {
+                $this->setError(self::ERROR_BYE,
+                    "The Auth_SASL package is required for DIGEST-MD5 authentication");
+                return self::ERROR_BAD;
+            }
+
+            $this->putLine($this->nextTag() . " AUTHENTICATE $type");
+            $line = trim($this->readReply());
+
+            if ($line[0] == '+') {
+                $challenge = substr($line, 2);
+            }
+            else {
+                return $this->parseResult($line);
+            }
+
+            if ($type == 'CRAM-MD5') {
+                // RFC2195: CRAM-MD5
+                $ipad = '';
+                $opad = '';
+
+                // initialize ipad, opad
+                for ($i=0; $i<64; $i++) {
+                    $ipad .= chr(0x36);
+                    $opad .= chr(0x5C);
+                }
+
+                // pad $pass so it's 64 bytes
+                $padLen = 64 - strlen($pass);
+                for ($i=0; $i<$padLen; $i++) {
+                    $pass .= chr(0);
+                }
+
+                // generate hash
+                $hash  = md5($this->_xor($pass, $opad) . pack("H*",
+                    md5($this->_xor($pass, $ipad) . base64_decode($challenge))));
+                $reply = base64_encode($user . ' ' . $hash);
+
+                // send result
+                $this->putLine($reply);
+            }
+            else {
+                // RFC2831: DIGEST-MD5
+                // proxy authorization
+                if (!empty($this->prefs['auth_cid'])) {
+                    $authc = $this->prefs['auth_cid'];
+                    $pass  = $this->prefs['auth_pw'];
+                }
+                else {
+                    $authc = $user;
+                }
+                $auth_sasl = Auth_SASL::factory('digestmd5');
+                $reply = base64_encode($auth_sasl->getResponse($authc, $pass,
+                    base64_decode($challenge), $this->host, 'imap', $user));
+
+                // send result
+                $this->putLine($reply);
+                $line = trim($this->readReply());
+
+                if ($line[0] == '+') {
+                    $challenge = substr($line, 2);
+                }
+                else {
+                    return $this->parseResult($line);
+                }
+
+                // check response
+                $challenge = base64_decode($challenge);
+                if (strpos($challenge, 'rspauth=') === false) {
+                    $this->setError(self::ERROR_BAD,
+                        "Unexpected response from server to DIGEST-MD5 response");
+                    return self::ERROR_BAD;
+                }
+
+                $this->putLine('');
+            }
+
+            $line = $this->readReply();
+            $result = $this->parseResult($line);
+        }
+        else { // PLAIN
+            // proxy authorization
+            if (!empty($this->prefs['auth_cid'])) {
+                $authc = $this->prefs['auth_cid'];
+                $pass  = $this->prefs['auth_pw'];
+            }
+            else {
+                $authc = $user;
+            }
+
+            $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass);
+
+            // RFC 4959 (SASL-IR): save one round trip
+            if ($this->getCapability('SASL-IR')) {
+                list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
+                    self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY);
+            }
+            else {
+                $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
+                $line = trim($this->readReply());
+
+                if ($line[0] != '+') {
+                    return $this->parseResult($line);
+                }
+
+                // send result, get reply and process it
+                $this->putLine($reply);
+                $line = $this->readReply();
+                $result = $this->parseResult($line);
+            }
+        }
+
+        if ($result == self::ERROR_OK) {
+            // optional CAPABILITY response
+            if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
+                $this->parseCapability($matches[1], true);
+            }
+            return $this->fp;
+        }
+        else {
+            $this->setError($result, "AUTHENTICATE $type: $line");
+        }
+
+        return $result;
+    }
+
+    /**
+     * LOGIN Authentication
+     *
+     * @param string $user
+     * @param string $pass
+     *
+     * @return resource Connection resourse on success, error code on error
+     */
+    function login($user, $password)
+    {
+        list($code, $response) = $this->execute('LOGIN', array(
+            $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY);
+
+        // re-set capabilities list if untagged CAPABILITY response provided
+        if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
+            $this->parseCapability($matches[1], true);
+        }
+
+        if ($code == self::ERROR_OK) {
+            return $this->fp;
+        }
+
+        return $code;
+    }
+
+    /**
+     * Gets the delimiter
+     *
+     * @return string The delimiter
+     */
+    function getHierarchyDelimiter()
+    {
+        if ($this->prefs['delimiter']) {
+            return $this->prefs['delimiter'];
+        }
+
+        // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
+        list($code, $response) = $this->execute('LIST',
+            array($this->escape(''), $this->escape('')));
+
+        if ($code == self::ERROR_OK) {
+            $args = $this->tokenizeResponse($response, 4);
+            $delimiter = $args[3];
+
+            if (strlen($delimiter) > 0) {
+                return ($this->prefs['delimiter'] = $delimiter);
+            }
+        }
+
+        return NULL;
+    }
+
+    /**
+     * NAMESPACE handler (RFC 2342)
+     *
+     * @return array Namespace data hash (personal, other, shared)
+     */
+    function getNamespace()
+    {
+        if (array_key_exists('namespace', $this->prefs)) {
+            return $this->prefs['namespace'];
+        }
+
+        if (!$this->getCapability('NAMESPACE')) {
+            return self::ERROR_BAD;
+        }
+
+        list($code, $response) = $this->execute('NAMESPACE');
+
+        if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) {
+            $data = $this->tokenizeResponse(substr($response, 11));
+        }
+
+        if (!is_array($data)) {
+            return $code;
+        }
+
+        $this->prefs['namespace'] = array(
+            'personal' => $data[0],
+            'other'    => $data[1],
+            'shared'   => $data[2],
+        );
+
+        return $this->prefs['namespace'];
+    }
+
+    function connect($host, $user, $password, $options=null)
+    {
+        // set options
+        if (is_array($options)) {
+            $this->prefs = $options;
+        }
+        // set auth method
+        if (!empty($this->prefs['auth_type'])) {
+            $auth_method = strtoupper($this->prefs['auth_type']);
+        } else {
+            $auth_method = 'CHECK';
+        }
+
+        $result = false;
+
+        // initialize connection
+        $this->error    = '';
+        $this->errornum = self::ERROR_OK;
+        $this->selected = null;
+        $this->user     = $user;
+        $this->host     = $host;
+        $this->logged   = false;
+
+        // check input
+        if (empty($host)) {
+            $this->setError(self::ERROR_BAD, "Empty host");
+            return false;
+        }
+        if (empty($user)) {
+            $this->setError(self::ERROR_NO, "Empty user");
+            return false;
+        }
+        if (empty($password)) {
+            $this->setError(self::ERROR_NO, "Empty password");
+            return false;
+        }
+
+        if (!$this->prefs['port']) {
+            $this->prefs['port'] = 143;
+        }
+        // check for SSL
+        if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
+            $host = $this->prefs['ssl_mode'] . '://' . $host;
+        }
+
+        if ($this->prefs['timeout'] <= 0) {
+            $this->prefs['timeout'] = ini_get('default_socket_timeout');
+        }
+
+        // Connect
+        $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
+
+        if (!$this->fp) {
+            $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr));
+            return false;
+        }
+
+        if ($this->prefs['timeout'] > 0)
+            stream_set_timeout($this->fp, $this->prefs['timeout']);
+
+        $line = trim(fgets($this->fp, 8192));
+
+        if ($this->_debug) {
+            // set connection identifier for debug output
+            preg_match('/#([0-9]+)/', (string)$this->fp, $m);
+            $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
+
+            if ($line)
+                $this->debug('S: '. $line);
+        }
+
+        // Connected to wrong port or connection error?
+        if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
+            if ($line)
+                $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
+            else
+                $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
+
+            $this->setError(self::ERROR_BAD, $error);
+            $this->closeConnection();
+            return false;
+        }
+
+        // RFC3501 [7.1] optional CAPABILITY response
+        if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
+            $this->parseCapability($matches[1], true);
+        }
+
+        // TLS connection
+        if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
+            if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
+                $res = $this->execute('STARTTLS');
+
+                if ($res[0] != self::ERROR_OK) {
+                    $this->closeConnection();
+                    return false;
+                }
+
+                if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+                    $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
+                    $this->closeConnection();
+                    return false;
+                }
+
+                // Now we're secure, capabilities need to be reread
+                $this->clearCapability();
+            }
+        }
+
+        // Send ID info
+        if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
+            $this->id($this->prefs['ident']);
+        }
+
+        $auth_methods = array();
+        $result       = null;
+
+        // check for supported auth methods
+        if ($auth_method == 'CHECK') {
+            if ($auth_caps = $this->getCapability('AUTH')) {
+                $auth_methods = $auth_caps;
+            }
+            // RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure
+            $login_disabled = $this->getCapability('LOGINDISABLED');
+            if (($key = array_search('LOGIN', $auth_methods)) !== false) {
+                if ($login_disabled) {
+                    unset($auth_methods[$key]);
+                }
+            }
+            else if (!$login_disabled) {
+                $auth_methods[] = 'LOGIN';
+            }
+
+            // Use best (for security) supported authentication method
+            foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) {
+                if (in_array($auth_method, $auth_methods)) {
+                    break;
+                }
+            }
+        }
+        else {
+            // Prevent from sending credentials in plain text when connection is not secure
+            if ($auth_method == 'LOGIN' && $this->getCapability('LOGINDISABLED')) {
+                $this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
+                $this->closeConnection();
+                return false;
+            }
+            // replace AUTH with CRAM-MD5 for backward compat.
+            if ($auth_method == 'AUTH') {
+                $auth_method = 'CRAM-MD5';
+            }
+        }
+
+        // pre-login capabilities can be not complete
+        $this->capability_readed = false;
+
+        // Authenticate
+        switch ($auth_method) {
+            case 'CRAM_MD5':
+                $auth_method = 'CRAM-MD5';
+            case 'CRAM-MD5':
+            case 'DIGEST-MD5':
+            case 'PLAIN':
+                $result = $this->authenticate($user, $password, $auth_method);
+                break;
+            case 'LOGIN':
+                $result = $this->login($user, $password);
+                break;
+            default:
+                $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method");
+        }
+
+        // Connected and authenticated
+        if (is_resource($result)) {
+            if ($this->prefs['force_caps']) {
+                $this->clearCapability();
+            }
+            $this->logged = true;
+
+            return true;
+        }
+
+        $this->closeConnection();
+
+        return false;
+    }
+
+    function connected()
+    {
+        return ($this->fp && $this->logged) ? true : false;
+    }
+
+    function closeConnection()
+    {
+        if ($this->putLine($this->nextTag() . ' LOGOUT')) {
+            $this->readReply();
+        }
+
+        $this->closeSocket();
+    }
+
+    /**
+     * Executes SELECT command (if mailbox is already not in selected state)
+     *
+     * @param string $mailbox      Mailbox name
+     * @param array  $qresync_data QRESYNC data (RFC5162)
+     *
+     * @return boolean True on success, false on error
+     */
+    function select($mailbox, $qresync_data = null)
+    {
+        if (!strlen($mailbox)) {
+            return false;
+        }
+
+        if ($this->selected === $mailbox) {
+            return true;
+        }
+/*
+    Temporary commented out because Courier returns \Noselect for INBOX
+    Requires more investigation
+
+        if (is_array($this->data['LIST']) && is_array($opts = $this->data['LIST'][$mailbox])) {
+            if (in_array('\\Noselect', $opts)) {
+                return false;
+            }
+        }
+*/
+        $params = array($this->escape($mailbox));
+
+        // QRESYNC data items
+        //    0. the last known UIDVALIDITY,
+        //    1. the last known modification sequence,
+        //    2. the optional set of known UIDs, and
+        //    3. an optional parenthesized list of known sequence ranges and their
+        //       corresponding UIDs.
+        if (!empty($qresync_data)) {
+            if (!empty($qresync_data[2]))
+                $qresync_data[2] = self::compressMessageSet($qresync_data[2]);
+            $params[] = array('QRESYNC', $qresync_data);
+        }
+
+        list($code, $response) = $this->execute('SELECT', $params);
+
+        if ($code == self::ERROR_OK) {
+            $response = explode("\r\n", $response);
+            foreach ($response as $line) {
+                if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/i', $line, $m)) {
+                    $this->data[strtoupper($m[2])] = (int) $m[1];
+                }
+                else if (preg_match('/^\* OK \[/i', $line, $match)) {
+                    $line = substr($line, 6);
+                    if (preg_match('/^(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)/i', $line, $match)) {
+                        $this->data[strtoupper($match[1])] = (int) $match[2];
+                    }
+                    else if (preg_match('/^(HIGHESTMODSEQ) ([0-9]+)/i', $line, $match)) {
+                        $this->data[strtoupper($match[1])] = (string) $match[2];
+                    }
+                    else if (preg_match('/^(NOMODSEQ)/i', $line, $match)) {
+                        $this->data[strtoupper($match[1])] = true;
+                    }
+                    else if (preg_match('/^PERMANENTFLAGS \(([^\)]+)\)/iU', $line, $match)) {
+                        $this->data['PERMANENTFLAGS'] = explode(' ', $match[1]);
+                    }
+                }
+                // QRESYNC FETCH response (RFC5162)
+                else if (preg_match('/^\* ([0-9+]) FETCH/i', $line, $match)) {
+                    $line       = substr($line, strlen($match[0]));
+                    $fetch_data = $this->tokenizeResponse($line, 1);
+                    $data       = array('id' => $match[1]);
+
+                    for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) {
+                        $data[strtolower($fetch_data[$i])] = $fetch_data[$i+1];
+                    }
+
+                    $this->data['QRESYNC'][$data['uid']] = $data;
+                }
+                // QRESYNC VANISHED response (RFC5162)
+                else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
+                    $line   = substr($line, strlen($match[0]));
+                    $v_data = $this->tokenizeResponse($line, 1);
+
+                    $this->data['VANISHED'] = $v_data;
+                }
+            }
+
+            $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
+
+            $this->selected = $mailbox;
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Executes STATUS command
+     *
+     * @param string $mailbox Mailbox name
+     * @param array  $items   Additional requested item names. By default
+     *                        MESSAGES and UNSEEN are requested. Other defined
+     *                        in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
+     *
+     * @return array Status item-value hash
+     * @since 0.5-beta
+     */
+    function status($mailbox, $items=array())
+    {
+        if (!strlen($mailbox)) {
+            return false;
+        }
+
+        if (!in_array('MESSAGES', $items)) {
+            $items[] = 'MESSAGES';
+        }
+        if (!in_array('UNSEEN', $items)) {
+            $items[] = 'UNSEEN';
+        }
+
+        list($code, $response) = $this->execute('STATUS', array($this->escape($mailbox),
+            '(' . implode(' ', (array) $items) . ')'));
+
+        if ($code == self::ERROR_OK && preg_match('/\* STATUS /i', $response)) {
+            $result   = array();
+            $response = substr($response, 9); // remove prefix "* STATUS "
+
+            list($mbox, $items) = $this->tokenizeResponse($response, 2);
+
+            // Fix for #1487859. Some buggy server returns not quoted
+            // folder name with spaces. Let's try to handle this situation
+            if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
+                $response = substr($response, $pos);
+                $items = $this->tokenizeResponse($response, 1);
+                if (!is_array($items)) {
+                    return $result;
+                }
+            }
+
+            for ($i=0, $len=count($items); $i<$len; $i += 2) {
+                $result[$items[$i]] = $items[$i+1];
+            }
+
+            $this->data['STATUS:'.$mailbox] = $result;
+
+            return $result;
+        }
+
+        return false;
+    }
+
+    /**
+     * Executes EXPUNGE command
+     *
+     * @param string $mailbox  Mailbox name
+     * @param string $messages Message UIDs to expunge
+     *
+     * @return boolean True on success, False on error
+     */
+    function expunge($mailbox, $messages=NULL)
+    {
+        if (!$this->select($mailbox)) {
+            return false;
+        }
+
+        if (!$this->data['READ-WRITE']) {
+            $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE');
+            return false;
+        }
+
+        // Clear internal status cache
+        unset($this->data['STATUS:'.$mailbox]);
+
+        if ($messages)
+            $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
+        else
+            $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
+
+        if ($result == self::ERROR_OK) {
+            $this->selected = null; // state has changed, need to reselect
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Executes CLOSE command
+     *
+     * @return boolean True on success, False on error
+     * @since 0.5
+     */
+    function close()
+    {
+        $result = $this->execute('CLOSE', NULL, self::COMMAND_NORESPONSE);
+
+        if ($result == self::ERROR_OK) {
+            $this->selected = null;
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Executes SUBSCRIBE command
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return boolean True on success, False on error
+     */
+    function subscribe($mailbox)
+    {
+        $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    /**
+     * Executes UNSUBSCRIBE command
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return boolean True on success, False on error
+     */
+    function unsubscribe($mailbox)
+    {
+        $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    /**
+     * Executes DELETE command
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return boolean True on success, False on error
+     */
+    function deleteFolder($mailbox)
+    {
+        $result = $this->execute('DELETE', array($this->escape($mailbox)),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    /**
+     * Removes all messages in a folder
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return boolean True on success, False on error
+     */
+    function clearFolder($mailbox)
+    {
+        $num_in_trash = $this->countMessages($mailbox);
+        if ($num_in_trash > 0) {
+            $res = $this->delete($mailbox, '1:*');
+        }
+
+        if ($res) {
+            if ($this->selected === $mailbox)
+                $res = $this->close();
+            else
+                $res = $this->expunge($mailbox);
+        }
+
+        return $res;
+    }
+
+    /**
+     * Returns count of all messages in a folder
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return int Number of messages, False on error
+     */
+    function countMessages($mailbox, $refresh = false)
+    {
+        if ($refresh) {
+            $this->selected = null;
+        }
+
+        if ($this->selected === $mailbox) {
+            return $this->data['EXISTS'];
+        }
+
+        // Check internal cache
+        $cache = $this->data['STATUS:'.$mailbox];
+        if (!empty($cache) && isset($cache['MESSAGES'])) {
+            return (int) $cache['MESSAGES'];
+        }
+
+        // Try STATUS (should be faster than SELECT)
+        $counts = $this->status($mailbox);
+        if (is_array($counts)) {
+            return (int) $counts['MESSAGES'];
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns count of messages with \Recent flag in a folder
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return int Number of messages, False on error
+     */
+    function countRecent($mailbox)
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = 'INBOX';
+        }
+
+        $this->select($mailbox);
+
+        if ($this->selected === $mailbox) {
+            return $this->data['RECENT'];
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns count of messages without \Seen flag in a specified folder
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return int Number of messages, False on error
+     */
+    function countUnseen($mailbox)
+    {
+        // Check internal cache
+        $cache = $this->data['STATUS:'.$mailbox];
+        if (!empty($cache) && isset($cache['UNSEEN'])) {
+            return (int) $cache['UNSEEN'];
+        }
+
+        // Try STATUS (should be faster than SELECT+SEARCH)
+        $counts = $this->status($mailbox);
+        if (is_array($counts)) {
+            return (int) $counts['UNSEEN'];
+        }
+
+        // Invoke SEARCH as a fallback
+        $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT'));
+        if (is_array($index)) {
+            return (int) $index['COUNT'];
+        }
+
+        return false;
+    }
+
+    /**
+     * Executes ID command (RFC2971)
+     *
+     * @param array $items Client identification information key/value hash
+     *
+     * @return array Server identification information key/value hash
+     * @since 0.6
+     */
+    function id($items=array())
+    {
+        if (is_array($items) && !empty($items)) {
+            foreach ($items as $key => $value) {
+                $args[] = $this->escape($key, true);
+                $args[] = $this->escape($value, true);
+            }
+        }
+
+        list($code, $response) = $this->execute('ID', array(
+            !empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)
+        ));
+
+
+        if ($code == self::ERROR_OK && preg_match('/\* ID /i', $response)) {
+            $response = substr($response, 5); // remove prefix "* ID "
+            $items    = $this->tokenizeResponse($response, 1);
+            $result   = null;
+
+            for ($i=0, $len=count($items); $i<$len; $i += 2) {
+                $result[$items[$i]] = $items[$i+1];
+            }
+
+            return $result;
+        }
+
+        return false;
+    }
+
+    /**
+     * Executes ENABLE command (RFC5161)
+     *
+     * @param mixed $extension Extension name to enable (or array of names)
+     *
+     * @return array|bool List of enabled extensions, False on error
+     * @since 0.6
+     */
+    function enable($extension)
+    {
+        if (empty($extension))
+            return false;
+
+        if (!$this->hasCapability('ENABLE'))
+            return false;
+
+        if (!is_array($extension))
+            $extension = array($extension);
+
+        list($code, $response) = $this->execute('ENABLE', $extension);
+
+        if ($code == self::ERROR_OK && preg_match('/\* ENABLED /i', $response)) {
+            $response = substr($response, 10); // remove prefix "* ENABLED "
+            $result   = (array) $this->tokenizeResponse($response);
+
+            return $result;
+        }
+
+        return false;
+    }
+
+    function sort($mailbox, $field, $add='', $is_uid=FALSE, $encoding = 'US-ASCII')
+    {
+        $field = strtoupper($field);
+        if ($field == 'INTERNALDATE') {
+            $field = 'ARRIVAL';
+        }
+
+        $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
+            'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
+
+        if (!$fields[$field]) {
+            return false;
+        }
+
+        if (!$this->select($mailbox)) {
+            return false;
+        }
+
+        // message IDs
+        if (!empty($add))
+            $add = $this->compressMessageSet($add);
+
+        list($code, $response) = $this->execute($is_uid ? 'UID SORT' : 'SORT',
+            array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : '')));
+
+        if ($code == self::ERROR_OK) {
+            // remove prefix and unilateral untagged server responses
+            $response = substr($response, stripos($response, '* SORT') + 7);
+            if ($pos = strpos($response, '*')) {
+                $response = substr($response, 0, $pos);
+            }
+            return preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY);
+        }
+
+        return false;
+    }
+
+    function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true, $uidfetch=false)
+    {
+        if (is_array($message_set)) {
+            if (!($message_set = $this->compressMessageSet($message_set)))
+                return false;
+        } else {
+            list($from_idx, $to_idx) = explode(':', $message_set);
+            if (empty($message_set) ||
+                (isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)) {
+                return false;
+            }
+        }
+
+        $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
+
+        $fields_a['DATE']         = 1;
+        $fields_a['INTERNALDATE'] = 4;
+        $fields_a['ARRIVAL']      = 4;
+        $fields_a['FROM']         = 1;
+        $fields_a['REPLY-TO']     = 1;
+        $fields_a['SENDER']       = 1;
+        $fields_a['TO']           = 1;
+        $fields_a['CC']           = 1;
+        $fields_a['SUBJECT']      = 1;
+        $fields_a['UID']          = 2;
+        $fields_a['SIZE']         = 2;
+        $fields_a['SEEN']         = 3;
+        $fields_a['RECENT']       = 3;
+        $fields_a['DELETED']      = 3;
+
+        if (!($mode = $fields_a[$index_field])) {
+            return false;
+        }
+
+        /*  Do "SELECT" command */
+        if (!$this->select($mailbox)) {
+            return false;
+        }
+
+        // build FETCH command string
+        $key     = $this->nextTag();
+        $cmd     = $uidfetch ? 'UID FETCH' : 'FETCH';
+        $deleted = $skip_deleted ? ' FLAGS' : '';
+
+        if ($mode == 1 && $index_field == 'DATE')
+            $request = " $cmd $message_set (INTERNALDATE BODY.PEEK[HEADER.FIELDS (DATE)]$deleted)";
+        else if ($mode == 1)
+            $request = " $cmd $message_set (BODY.PEEK[HEADER.FIELDS ($index_field)]$deleted)";
+        else if ($mode == 2) {
+            if ($index_field == 'SIZE')
+                $request = " $cmd $message_set (RFC822.SIZE$deleted)";
+            else
+                $request = " $cmd $message_set ($index_field$deleted)";
+        } else if ($mode == 3)
+            $request = " $cmd $message_set (FLAGS)";
+        else // 4
+            $request = " $cmd $message_set (INTERNALDATE$deleted)";
+
+        $request = $key . $request;
+
+        if (!$this->putLine($request)) {
+            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+            return false;
+        }
+
+        $result = array();
+
+        do {
+            $line = rtrim($this->readLine(200));
+            $line = $this->multLine($line);
+
+            if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
+                $id     = $m[1];
+                $flags  = NULL;
+
+                if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
+                    $flags = explode(' ', strtoupper($matches[1]));
+                    if (in_array('\\DELETED', $flags)) {
+                        $deleted[$id] = $id;
+                        continue;
+                    }
+                }
+
+                if ($mode == 1 && $index_field == 'DATE') {
+                    if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) {
+                        $value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]);
+                        $value = trim($value);
+                        $result[$id] = $this->strToTime($value);
+                    }
+                    // non-existent/empty Date: header, use INTERNALDATE
+                    if (empty($result[$id])) {
+                        if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches))
+                            $result[$id] = $this->strToTime($matches[1]);
+                        else
+                            $result[$id] = 0;
+                    }
+                } else if ($mode == 1) {
+                    if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) {
+                        $value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]);
+                        $result[$id] = trim($value);
+                    } else {
+                        $result[$id] = '';
+                    }
+                } else if ($mode == 2) {
+                    if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
+                        $result[$id] = trim($matches[2]);
+                    } else {
+                        $result[$id] = 0;
+                    }
+                } else if ($mode == 3) {
+                    if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
+                        $flags = explode(' ', $matches[1]);
+                    }
+                    $result[$id] = in_array('\\'.$index_field, $flags) ? 1 : 0;
+                } else if ($mode == 4) {
+                    if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
+                        $result[$id] = $this->strToTime($matches[1]);
+                    } else {
+                        $result[$id] = 0;
+                    }
+                }
+            }
+        } while (!$this->startsWith($line, $key, true, true));
+
+        return $result;
+    }
+
+    static function compressMessageSet($messages, $force=false)
+    {
+        // given a comma delimited list of independent mid's,
+        // compresses by grouping sequences together
+
+        if (!is_array($messages)) {
+            // if less than 255 bytes long, let's not bother
+            if (!$force && strlen($messages)<255) {
+                return $messages;
+           }
+
+            // see if it's already been compressed
+            if (strpos($messages, ':') !== false) {
+                return $messages;
+            }
+
+            // separate, then sort
+            $messages = explode(',', $messages);
+        }
+
+        sort($messages);
+
+        $result = array();
+        $start  = $prev = $messages[0];
+
+        foreach ($messages as $id) {
+            $incr = $id - $prev;
+            if ($incr > 1) { // found a gap
+                if ($start == $prev) {
+                    $result[] = $prev; // push single id
+                } else {
+                    $result[] = $start . ':' . $prev; // push sequence as start_id:end_id
+                }
+                $start = $id; // start of new sequence
+            }
+            $prev = $id;
+        }
+
+        // handle the last sequence/id
+        if ($start == $prev) {
+            $result[] = $prev;
+        } else {
+            $result[] = $start.':'.$prev;
+        }
+
+        // return as comma separated string
+        return implode(',', $result);
+    }
+
+    static function uncompressMessageSet($messages)
+    {
+        $result   = array();
+        $messages = explode(',', $messages);
+
+        foreach ($messages as $part) {
+            $items = explode(':', $part);
+            $max   = max($items[0], $items[1]);
+
+            for ($x=$items[0]; $x<=$max; $x++) {
+                $result[] = $x;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns message sequence identifier
+     *
+     * @param string $mailbox Mailbox name
+     * @param int    $uid     Message unique identifier (UID)
+     *
+     * @return int Message sequence identifier
+     */
+    function UID2ID($mailbox, $uid)
+    {
+        if ($uid > 0) {
+            $id_a = $this->search($mailbox, "UID $uid");
+            if (is_array($id_a) && count($id_a) == 1) {
+                return (int) $id_a[0];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns message unique identifier (UID)
+     *
+     * @param string $mailbox Mailbox name
+     * @param int    $uid     Message sequence identifier
+     *
+     * @return int Message unique identifier
+     */
+    function ID2UID($mailbox, $id)
+    {
+        if (empty($id) || $id < 0) {
+            return null;
+        }
+
+        if (!$this->select($mailbox)) {
+            return null;
+        }
+
+        list($code, $response) = $this->execute('FETCH', array($id, '(UID)'));
+
+        if ($code == self::ERROR_OK && preg_match("/^\* $id FETCH \(UID (.*)\)/i", $response, $m)) {
+            return (int) $m[1];
+        }
+
+        return null;
+    }
+
+    function fetchUIDs($mailbox, $message_set=null)
+    {
+        if (empty($message_set))
+            $message_set = '1:*';
+
+        return $this->fetchHeaderIndex($mailbox, $message_set, 'UID', false);
+    }
+
+    /**
+     * FETCH command (RFC3501)
+     *
+     * @param string $mailbox     Mailbox name
+     * @param mixed  $message_set Message(s) sequence identifier(s) or UID(s)
+     * @param bool   $is_uid      True if $message_set contains UIDs
+     * @param array  $query_items FETCH command data items
+     * @param string $mod_seq     Modification sequence for CHANGEDSINCE (RFC4551) query
+     * @param bool   $vanished    Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query
+     *
+     * @return array List of rcube_mail_header elements, False on error
+     * @since 0.6
+     */
+    function fetch($mailbox, $message_set, $is_uid = false, $query_items = array(),
+        $mod_seq = null, $vanished = false)
+    {
+        if (!$this->select($mailbox)) {
+            return false;
+        }
+
+        $message_set = $this->compressMessageSet($message_set);
+        $result      = array();
+
+        $key      = $this->nextTag();
+        $request  = $key . ($is_uid ? ' UID' : '') . " FETCH $message_set ";
+        $request .= "(" . implode(' ', $query_items) . ")";
+
+        if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) {
+            $request .= " (CHANGEDSINCE $mod_seq" . ($vanished ? " VANISHED" : '') .")";
+        }
+
+        if (!$this->putLine($request)) {
+            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+            return false;
+        }
+
+        do {
+            $line = $this->readLine(4096);
+
+            if (!$line)
+                break;
+
+            // Sample reply line:
+            // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
+            // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
+            // BODY[HEADER.FIELDS ...
+
+            if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
+                $id = intval($m[1]);
+
+                $result[$id]            = new rcube_mail_header;
+                $result[$id]->id        = $id;
+                $result[$id]->subject   = '';
+                $result[$id]->messageID = 'mid:' . $id;
+
+                $lines = array();
+                $line  = substr($line, strlen($m[0]) + 2);
+                $ln    = 0;
+
+                // get complete entry
+                while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
+                    $bytes = $m[1];
+                    $out   = '';
+
+                    while (strlen($out) < $bytes) {
+                        $out = $this->readBytes($bytes);
+                        if ($out === NULL)
+                            break;
+                        $line .= $out;
+                    }
+
+                    $str = $this->readLine(4096);
+                    if ($str === false)
+                        break;
+
+                    $line .= $str;
+                }
+
+                // Tokenize response and assign to object properties
+                while (list($name, $value) = $this->tokenizeResponse($line, 2)) {
+                    if ($name == 'UID') {
+                        $result[$id]->uid = intval($value);
+                    }
+                    else if ($name == 'RFC822.SIZE') {
+                        $result[$id]->size = intval($value);
+                    }
+                    else if ($name == 'RFC822.TEXT') {
+                        $result[$id]->body = $value;
+                    }
+                    else if ($name == 'INTERNALDATE') {
+                        $result[$id]->internaldate = $value;
+                        $result[$id]->date         = $value;
+                        $result[$id]->timestamp    = $this->StrToTime($value);
+                    }
+                    else if ($name == 'FLAGS') {
+                        if (!empty($value)) {
+                            foreach ((array)$value as $flag) {
+                                $flag = str_replace(array('$', '\\'), '', $flag);
+                                $flag = strtoupper($flag);
+
+                                $result[$id]->flags[$flag] = true;
+                            }
+                        }
+                    }
+                    else if ($name == 'MODSEQ') {
+                        $result[$id]->modseq = $value[0];
+                    }
+                    else if ($name == 'ENVELOPE') {
+                        $result[$id]->envelope = $value;
+                    }
+                    else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) {
+                        if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) {
+                            $value = array($value);
+                        }
+                        $result[$id]->bodystructure = $value;
+                    }
+                    else if ($name == 'RFC822') {
+                        $result[$id]->body = $value;
+                    }
+                    else if ($name == 'BODY') {
+                        $body = $this->tokenizeResponse($line, 1);
+                        if ($value[0] == 'HEADER.FIELDS')
+                            $headers = $body;
+                        else if (!empty($value))
+                            $result[$id]->bodypart[$value[0]] = $body;
+                        else
+                            $result[$id]->body = $body;
+                    }
+                }
+
+                // create array with header field:data
+                if (!empty($headers)) {
+                    $headers = explode("\n", trim($headers));
+                    foreach ($headers as $hid => $resln) {
+                        if (ord($resln[0]) <= 32) {
+                            $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
+                        } else {
+                            $lines[++$ln] = trim($resln);
+                        }
+                    }
+
+                    while (list($lines_key, $str) = each($lines)) {
+                        list($field, $string) = explode(':', $str, 2);
+
+                        $field  = strtolower($field);
+                        $string = preg_replace('/\n[\t\s]*/', ' ', trim($string));
+
+                        switch ($field) {
+                        case 'date';
+                            $result[$id]->date = $string;
+                            $result[$id]->timestamp = $this->strToTime($string);
+                            break;
+                        case 'from':
+                            $result[$id]->from = $string;
+                            break;
+                        case 'to':
+                            $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
+                            break;
+                        case 'subject':
+                            $result[$id]->subject = $string;
+                            break;
+                        case 'reply-to':
+                            $result[$id]->replyto = $string;
+                            break;
+                        case 'cc':
+                            $result[$id]->cc = $string;
+                            break;
+                        case 'bcc':
+                            $result[$id]->bcc = $string;
+                            break;
+                        case 'content-transfer-encoding':
+                            $result[$id]->encoding = $string;
+                        break;
+                        case 'content-type':
+                            $ctype_parts = preg_split('/[; ]/', $string);
+                            $result[$id]->ctype = strtolower(array_shift($ctype_parts));
+                            if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
+                                $result[$id]->charset = $regs[1];
+                            }
+                            break;
+                        case 'in-reply-to':
+                            $result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string);
+                            break;
+                        case 'references':
+                            $result[$id]->references = $string;
+                            break;
+                        case 'return-receipt-to':
+                        case 'disposition-notification-to':
+                        case 'x-confirm-reading-to':
+                            $result[$id]->mdn_to = $string;
+                            break;
+                        case 'message-id':
+                            $result[$id]->messageID = $string;
+                            break;
+                        case 'x-priority':
+                            if (preg_match('/^(\d+)/', $string, $matches)) {
+                                $result[$id]->priority = intval($matches[1]);
+                            }
+                            break;
+                        default:
+                            if (strlen($field) > 2) {
+                                $result[$id]->others[$field] = $string;
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+
+            // VANISHED response (QRESYNC RFC5162)
+            // Sample: * VANISHED (EARLIER) 300:310,405,411
+
+            else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
+                $line   = substr($line, strlen($match[0]));
+                $v_data = $this->tokenizeResponse($line, 1);
+
+                $this->data['VANISHED'] = $v_data;
+            }
+
+        } while (!$this->startsWith($line, $key, true));
+
+        return $result;
+    }
+
+    function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add = '')
+    {
+        $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE');
+        if ($bodystr)
+            $query_items[] = 'BODYSTRUCTURE';
+        $query_items[] = 'BODY.PEEK[HEADER.FIELDS ('
+            . 'DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY'
+            . ($add ? ' ' . trim($add) : '')
+            . ')]';
+
+        $result = $this->fetch($mailbox, $message_set, $is_uid, $query_items);
+
+        return $result;
+    }
+
+    function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='')
+    {
+        $a = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add);
+        if (is_array($a)) {
+            return array_shift($a);
+        }
+        return false;
+    }
+
+    function sortHeaders($a, $field, $flag)
+    {
+        if (empty($field)) {
+            $field = 'uid';
+        }
+        else {
+            $field = strtolower($field);
+        }
+
+        if ($field == 'date' || $field == 'internaldate') {
+            $field = 'timestamp';
+        }
+
+        if (empty($flag)) {
+            $flag = 'ASC';
+        } else {
+            $flag = strtoupper($flag);
+        }
+
+        $c = count($a);
+        if ($c > 0) {
+            // Strategy:
+            // First, we'll create an "index" array.
+            // Then, we'll use sort() on that array,
+            // and use that to sort the main array.
+
+            // create "index" array
+            $index = array();
+            reset($a);
+            while (list($key, $val) = each($a)) {
+                if ($field == 'timestamp') {
+                    $data = $this->strToTime($val->date);
+                    if (!$data) {
+                        $data = $val->timestamp;
+                    }
+                } else {
+                    $data = $val->$field;
+                    if (is_string($data)) {
+                        $data = str_replace('"', '', $data);
+                        if ($field == 'subject') {
+                            $data = preg_replace('/^(Re: \s*|Fwd:\s*|Fw:\s*)+/i', '', $data);
+                        }
+                        $data = strtoupper($data);
+                    }
+                }
+                $index[$key] = $data;
+            }
+
+            // sort index
+            if ($flag == 'ASC') {
+                asort($index);
+            } else {
+                arsort($index);
+            }
+
+            // form new array based on index
+            $result = array();
+            reset($index);
+            while (list($key, $val) = each($index)) {
+                $result[$key] = $a[$key];
+            }
+        }
+
+        return $result;
+    }
+
+
+    function modFlag($mailbox, $messages, $flag, $mod)
+    {
+        if ($mod != '+' && $mod != '-') {
+            $mod = '+';
+        }
+
+        if (!$this->select($mailbox)) {
+            return false;
+        }
+
+        if (!$this->data['READ-WRITE']) {
+            $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
+            return false;
+        }
+
+        // Clear internal status cache
+        if ($flag == 'SEEN') {
+            unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
+        }
+
+        $flag   = $this->flags[strtoupper($flag)];
+        $result = $this->execute('UID STORE', array(
+            $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    function flag($mailbox, $messages, $flag) {
+        return $this->modFlag($mailbox, $messages, $flag, '+');
+    }
+
+    function unflag($mailbox, $messages, $flag) {
+        return $this->modFlag($mailbox, $messages, $flag, '-');
+    }
+
+    function delete($mailbox, $messages) {
+        return $this->modFlag($mailbox, $messages, 'DELETED', '+');
+    }
+
+    function copy($messages, $from, $to)
+    {
+        if (!$this->select($from)) {
+            return false;
+        }
+
+        // Clear internal status cache
+        unset($this->data['STATUS:'.$to]);
+
+        $result = $this->execute('UID COPY', array(
+            $this->compressMessageSet($messages), $this->escape($to)),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    function move($messages, $from, $to)
+    {
+        if (!$this->select($from)) {
+            return false;
+        }
+
+        if (!$this->data['READ-WRITE']) {
+            $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
+            return false;
+        }
+
+        $r = $this->copy($messages, $from, $to);
+
+        if ($r) {
+            // Clear internal status cache
+            unset($this->data['STATUS:'.$from]);
+
+            return $this->delete($from, $messages);
+        }
+        return $r;
+    }
+
+    // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
+    // 7 times instead :-) See comments on http://uk2.php.net/references and this article:
+    // http://derickrethans.nl/files/phparch-php-variables-article.pdf
+    private function parseThread($str, $begin, $end, $root, $parent, $depth, &$depthmap, &$haschildren)
+    {
+        $node = array();
+        if ($str[$begin] != '(') {
+            $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin);
+            $msg = substr($str, $begin, $stop - $begin);
+            if ($msg == 0)
+                return $node;
+            if (is_null($root))
+                $root = $msg;
+            $depthmap[$msg] = $depth;
+            $haschildren[$msg] = false;
+            if (!is_null($parent))
+                $haschildren[$parent] = true;
+            if ($stop + 1 < $end)
+                $node[$msg] = $this->parseThread($str, $stop + 1, $end, $root, $msg, $depth + 1, $depthmap, $haschildren);
+            else
+                $node[$msg] = array();
+        } else {
+            $off = $begin;
+            while ($off < $end) {
+                $start = $off;
+                $off++;
+                $n = 1;
+                while ($n > 0) {
+                    $p = strpos($str, ')', $off);
+                    if ($p === false) {
+                        error_log("Mismatched brackets parsing IMAP THREAD response:");
+                        error_log(substr($str, ($begin < 10) ? 0 : ($begin - 10), $end - $begin + 20));
+                        error_log(str_repeat(' ', $off - (($begin < 10) ? 0 : ($begin - 10))));
+                        return $node;
+                    }
+                    $p1 = strpos($str, '(', $off);
+                    if ($p1 !== false && $p1 < $p) {
+                        $off = $p1 + 1;
+                        $n++;
+                    } else {
+                        $off = $p + 1;
+                        $n--;
+                    }
+                }
+                $node += $this->parseThread($str, $start + 1, $off - 1, $root, $parent, $depth, $depthmap, $haschildren);
+            }
+        }
+
+        return $node;
+    }
+
+    function thread($mailbox, $algorithm='REFERENCES', $criteria='', $encoding='US-ASCII')
+    {
+        $old_sel = $this->selected;
+
+        if (!$this->select($mailbox)) {
+            return false;
+        }
+
+        // return empty result when folder is empty and we're just after SELECT
+        if ($old_sel != $mailbox && !$this->data['EXISTS']) {
+            return array(array(), array(), array());
+        }
+
+        $encoding  = $encoding ? trim($encoding) : 'US-ASCII';
+        $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
+        $criteria  = $criteria ? 'ALL '.trim($criteria) : 'ALL';
+        $data      = '';
+
+        list($code, $response) = $this->execute('THREAD', array(
+            $algorithm, $encoding, $criteria));
+
+        if ($code == self::ERROR_OK) {
+            // remove prefix...
+            $response = substr($response, stripos($response, '* THREAD') + 9);
+            // ...unilateral untagged server responses
+            if ($pos = strpos($response, '*')) {
+                $response = substr($response, 0, $pos);
+            }
+
+            $response    = str_replace("\r\n", '', $response);
+            $depthmap    = array();
+            $haschildren = array();
+
+            $tree = $this->parseThread($response, 0, strlen($response),
+                null, null, 0, $depthmap, $haschildren);
+
+            return array($tree, $depthmap, $haschildren);
+        }
+
+        return false;
+    }
+
+    /**
+     * Executes SEARCH command
+     *
+     * @param string $mailbox    Mailbox name
+     * @param string $criteria   Searching criteria
+     * @param bool   $return_uid Enable UID in result instead of sequence ID
+     * @param array  $items      Return items (MIN, MAX, COUNT, ALL)
+     *
+     * @return array Message identifiers or item-value hash 
+     */
+    function search($mailbox, $criteria, $return_uid=false, $items=array())
+    {
+        $old_sel = $this->selected;
+
+        if (!$this->select($mailbox)) {
+            return false;
+        }
+
+        // return empty result when folder is empty and we're just after SELECT
+        if ($old_sel != $mailbox && !$this->data['EXISTS']) {
+            if (!empty($items))
+                return array_combine($items, array_fill(0, count($items), 0));
+            else
+                return array();
+        }
+
+        $esearch  = empty($items) ? false : $this->getCapability('ESEARCH');
+        $criteria = trim($criteria);
+        $params   = '';
+
+        // RFC4731: ESEARCH
+        if (!empty($items) && $esearch) {
+            $params .= 'RETURN (' . implode(' ', $items) . ')';
+        }
+        if (!empty($criteria)) {
+            $modseq = stripos($criteria, 'MODSEQ') !== false;
+            $params .= ($params ? ' ' : '') . $criteria;
+        }
+        else {
+            $params .= 'ALL';
+        }
+
+        list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH',
+            array($params));
+
+        if ($code == self::ERROR_OK) {
+            // remove prefix...
+            $response = substr($response, stripos($response,
+                $esearch ? '* ESEARCH' : '* SEARCH') + ($esearch ? 10 : 9));
+            // ...and unilateral untagged server responses
+            if ($pos = strpos($response, '*')) {
+                $response = rtrim(substr($response, 0, $pos));
+            }
+
+            // remove MODSEQ response
+            if ($modseq) {
+                if (preg_match('/\(MODSEQ ([0-9]+)\)$/', $response, $m)) {
+                    $response = substr($response, 0, -strlen($m[0]));
+                }
+            }
+
+            if ($esearch) {
+                // Skip prefix: ... (TAG "A285") UID ...
+                $this->tokenizeResponse($response, $return_uid ? 2 : 1);
+
+                $result = array();
+                for ($i=0; $i<count($items); $i++) {
+                    // If the SEARCH returns no matches, the server MUST NOT
+                    // include the item result option in the ESEARCH response
+                    if ($ret = $this->tokenizeResponse($response, 2)) {
+                        list ($name, $value) = $ret;
+                        $result[$name] = $value;
+                    }
+                }
+
+                return $result;
+            }
+            else {
+                $response = preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY);
+
+                if (!empty($items)) {
+                    $result = array();
+                    if (in_array('COUNT', $items)) {
+                        $result['COUNT'] = count($response);
+                    }
+                    if (in_array('MIN', $items)) {
+                        $result['MIN'] = !empty($response) ? min($response) : 0;
+                    }
+                    if (in_array('MAX', $items)) {
+                        $result['MAX'] = !empty($response) ? max($response) : 0;
+                    }
+                    if (in_array('ALL', $items)) {
+                        $result['ALL'] = $this->compressMessageSet($response, true);
+                    }
+
+                    return $result;
+                }
+                else {
+                    return $response;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns list of mailboxes
+     *
+     * @param string $ref         Reference name
+     * @param string $mailbox     Mailbox name
+     * @param array  $status_opts (see self::_listMailboxes)
+     * @param array  $select_opts (see self::_listMailboxes)
+     *
+     * @return array List of mailboxes or hash of options if $status_opts argument
+     *               is non-empty.
+     */
+    function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
+    {
+        return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts);
+    }
+
+    /**
+     * Returns list of subscribed mailboxes
+     *
+     * @param string $ref         Reference name
+     * @param string $mailbox     Mailbox name
+     * @param array  $status_opts (see self::_listMailboxes)
+     *
+     * @return array List of mailboxes or hash of options if $status_opts argument
+     *               is non-empty.
+     */
+    function listSubscribed($ref, $mailbox, $status_opts=array())
+    {
+        return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL);
+    }
+
+    /**
+     * IMAP LIST/LSUB command
+     *
+     * @param string $ref         Reference name
+     * @param string $mailbox     Mailbox name
+     * @param bool   $subscribed  Enables returning subscribed mailboxes only
+     * @param array  $status_opts List of STATUS options (RFC5819: LIST-STATUS)
+     *                            Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
+     * @param array  $select_opts List of selection options (RFC5258: LIST-EXTENDED)
+     *                            Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
+     *
+     * @return array List of mailboxes or hash of options if $status_ops argument
+     *               is non-empty.
+     */
+    private function _listMailboxes($ref, $mailbox, $subscribed=false,
+        $status_opts=array(), $select_opts=array())
+    {
+        if (!strlen($mailbox)) {
+            $mailbox = '*';
+        }
+
+        $args = array();
+
+        if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
+            $select_opts = (array) $select_opts;
+
+            $args[] = '(' . implode(' ', $select_opts) . ')';
+        }
+
+        $args[] = $this->escape($ref);
+        $args[] = $this->escape($mailbox);
+
+        if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
+            $status_opts = (array) $status_opts;
+            $lstatus = true;
+
+            $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
+        }
+
+        list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
+
+        if ($code == self::ERROR_OK) {
+            $folders = array();
+            while ($this->tokenizeResponse($response, 1) == '*') {
+                $cmd = strtoupper($this->tokenizeResponse($response, 1));
+                // * LIST (<options>) <delimiter> <mailbox>
+                if ($cmd == 'LIST' || $cmd == 'LSUB') {
+                    list($opts, $delim, $mailbox) = $this->tokenizeResponse($response, 3);
+
+                    // Add to result array
+                    if (!$lstatus) {
+                        $folders[] = $mailbox;
+                    }
+                    else {
+                        $folders[$mailbox] = array();
+                    }
+
+                    // Add to options array
+                    if (!empty($opts)) {
+                        if (empty($this->data['LIST'][$mailbox]))
+                            $this->data['LIST'][$mailbox] = $opts;
+                        else
+                            $this->data['LIST'][$mailbox] = array_unique(array_merge(
+                                $this->data['LIST'][$mailbox], $opts));
+                    }
+                }
+                // * STATUS <mailbox> (<result>)
+                else if ($cmd == 'STATUS') {
+                    list($mailbox, $status) = $this->tokenizeResponse($response, 2);
+
+                    for ($i=0, $len=count($status); $i<$len; $i += 2) {
+                        list($name, $value) = $this->tokenizeResponse($status, 2);
+                        $folders[$mailbox][$name] = $value;
+                    }
+                }
+                // other untagged response line, skip it
+                else {
+                    $response = ltrim($response);
+                    if (($position = strpos($response, "\n")) !== false)
+                        $response = substr($response, $position+1);
+                    else
+                        $response = '';
+                }
+            }
+
+            return $folders;
+        }
+
+        return false;
+    }
+
+    function fetchMIMEHeaders($mailbox, $uid, $parts, $mime=true)
+    {
+        if (!$this->select($mailbox)) {
+            return false;
+        }
+
+        $result = false;
+        $parts  = (array) $parts;
+        $key    = $this->nextTag();
+        $peeks  = array();
+        $type   = $mime ? 'MIME' : 'HEADER';
+
+        // format request
+        foreach ($parts as $part) {
+            $peeks[] = "BODY.PEEK[$part.$type]";
+        }
+
+        $request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')';
+
+        // send request
+        if (!$this->putLine($request)) {
+            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+            return false;
+        }
+
+        do {
+            $line = $this->readLine(1024);
+
+            if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
+                $idx     = $matches[1];
+                $headers = '';
+
+                // get complete entry
+                if (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
+                    $bytes = $m[1];
+                    $out   = '';
+
+                    while (strlen($out) < $bytes) {
+                        $out = $this->readBytes($bytes);
+                        if ($out === null)
+                            break;
+                        $headers .= $out;
+                    }
+                }
+
+                $result[$idx] = trim($headers);
+            }
+        } while (!$this->startsWith($line, $key, true));
+
+        return $result;
+    }
+
+    function fetchPartHeader($mailbox, $id, $is_uid=false, $part=NULL)
+    {
+        $part = empty($part) ? 'HEADER' : $part.'.MIME';
+
+        return $this->handlePartBody($mailbox, $id, $is_uid, $part);
+    }
+
+    function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL)
+    {
+        if (!$this->select($mailbox)) {
+            return false;
+        }
+
+        switch ($encoding) {
+        case 'base64':
+            $mode = 1;
+            break;
+        case 'quoted-printable':
+            $mode = 2;
+            break;
+        case 'x-uuencode':
+        case 'x-uue':
+        case 'uue':
+        case 'uuencode':
+            $mode = 3;
+            break;
+        default:
+            $mode = 0;
+        }
+
+        // format request
+        $reply_key = '* ' . $id;
+        $key       = $this->nextTag();
+        $request   = $key . ($is_uid ? ' UID' : '') . " FETCH $id (BODY.PEEK[$part])";
+
+        // send request
+        if (!$this->putLine($request)) {
+            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+            return false;
+        }
+
+        // receive reply line
+        do {
+            $line = rtrim($this->readLine(1024));
+            $a    = explode(' ', $line);
+        } while (!($end = $this->startsWith($line, $key, true)) && $a[2] != 'FETCH');
+
+        $len    = strlen($line);
+        $result = false;
+
+        if ($a[2] != 'FETCH') {
+        }
+        // handle empty "* X FETCH ()" response
+        else if ($line[$len-1] == ')' && $line[$len-2] != '(') {
+            // one line response, get everything between first and last quotes
+            if (substr($line, -4, 3) == 'NIL') {
+                // NIL response
+                $result = '';
+            } else {
+                $from = strpos($line, '"') + 1;
+                $to   = strrpos($line, '"');
+                $len  = $to - $from;
+                $result = substr($line, $from, $len);
+            }
+
+            if ($mode == 1) {
+                $result = base64_decode($result);
+            }
+            else if ($mode == 2) {
+                $result = quoted_printable_decode($result);
+            }
+            else if ($mode == 3) {
+                $result = convert_uudecode($result);
+            }
+
+        } else if ($line[$len-1] == '}') {
+            // multi-line request, find sizes of content and receive that many bytes
+            $from     = strpos($line, '{') + 1;
+            $to       = strrpos($line, '}');
+            $len      = $to - $from;
+            $sizeStr  = substr($line, $from, $len);
+            $bytes    = (int)$sizeStr;
+            $prev     = '';
+
+            while ($bytes > 0) {
+                $line = $this->readLine(4096);
+
+                if ($line === NULL) {
+                    break;
+                }
+
+                $len  = strlen($line);
+
+                if ($len > $bytes) {
+                    $line = substr($line, 0, $bytes);
+                    $len = strlen($line);
+                }
+                $bytes -= $len;
+
+                // BASE64
+                if ($mode == 1) {
+                    $line = rtrim($line, "\t\r\n\0\x0B");
+                    // create chunks with proper length for base64 decoding
+                    $line = $prev.$line;
+                    $length = strlen($line);
+                    if ($length % 4) {
+                        $length = floor($length / 4) * 4;
+                        $prev = substr($line, $length);
+                        $line = substr($line, 0, $length);
+                    }
+                    else
+                        $prev = '';
+                    $line = base64_decode($line);
+                // QUOTED-PRINTABLE
+                } else if ($mode == 2) {
+                    $line = rtrim($line, "\t\r\0\x0B");
+                    $line = quoted_printable_decode($line);
+                // UUENCODE
+                } else if ($mode == 3) {
+                    $line = rtrim($line, "\t\r\n\0\x0B");
+                    if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line))
+                        continue;
+                    $line = convert_uudecode($line);
+                // default
+                } else {
+                    $line = rtrim($line, "\t\r\n\0\x0B") . "\n";
+                }
+
+                if ($file)
+                    fwrite($file, $line);
+                else if ($print)
+                    echo $line;
+                else
+                    $result .= $line;
+            }
+        }
+
+        // read in anything up until last line
+        if (!$end)
+            do {
+                $line = $this->readLine(1024);
+            } while (!$this->startsWith($line, $key, true));
+
+        if ($result !== false) {
+            if ($file) {
+                fwrite($file, $result);
+            } else if ($print) {
+                echo $result;
+            } else
+                return $result;
+            return true;
+        }
+
+        return false;
+    }
+
+    function createFolder($mailbox)
+    {
+        $result = $this->execute('CREATE', array($this->escape($mailbox)),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    function renameFolder($from, $to)
+    {
+        $result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    /**
+     * Handler for IMAP APPEND command
+     *
+     * @param string $mailbox Mailbox name
+     * @param string $message Message content
+     *
+     * @return string|bool On success APPENDUID response (if available) or True, False on failure
+     */
+    function append($mailbox, &$message)
+    {
+        unset($this->data['APPENDUID']);
+
+        if (!$mailbox) {
+            return false;
+        }
+
+        $message = str_replace("\r", '', $message);
+        $message = str_replace("\n", "\r\n", $message);
+
+        $len = strlen($message);
+        if (!$len) {
+            return false;
+        }
+
+        $key = $this->nextTag();
+        $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
+            $len, ($this->prefs['literal+'] ? '+' : ''));
+
+        if ($this->putLine($request)) {
+            // Don't wait when LITERAL+ is supported
+            if (!$this->prefs['literal+']) {
+                $line = $this->readReply();
+
+                if ($line[0] != '+') {
+                    $this->parseResult($line, 'APPEND: ');
+                    return false;
+                }
+            }
+
+            if (!$this->putLine($message)) {
+                return false;
+            }
+
+            do {
+                $line = $this->readLine();
+            } while (!$this->startsWith($line, $key, true, true));
+
+            // Clear internal status cache
+            unset($this->data['STATUS:'.$mailbox]);
+
+            if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
+                return false;
+            else if (!empty($this->data['APPENDUID']))
+                return $this->data['APPENDUID'];
+            else
+                return true;
+        }
+        else {
+            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+        }
+
+        return false;
+    }
+
+    /**
+     * Handler for IMAP APPEND command.
+     *
+     * @param string $mailbox Mailbox name
+     * @param string $path    Path to the file with message body
+     * @param string $headers Message headers
+     *
+     * @return string|bool On success APPENDUID response (if available) or True, False on failure
+     */
+    function appendFromFile($mailbox, $path, $headers=null)
+    {
+        unset($this->data['APPENDUID']);
+
+        if (!$mailbox) {
+            return false;
+        }
+
+        // open message file
+        $in_fp = false;
+        if (file_exists(realpath($path))) {
+            $in_fp = fopen($path, 'r');
+        }
+        if (!$in_fp) {
+            $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
+            return false;
+        }
+
+        $body_separator = "\r\n\r\n";
+        $len = filesize($path);
+
+        if (!$len) {
+            return false;
+        }
+
+        if ($headers) {
+            $headers = preg_replace('/[\r\n]+$/', '', $headers);
+            $len += strlen($headers) + strlen($body_separator);
+        }
+
+        // send APPEND command
+        $key = $this->nextTag();
+        $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
+            $len, ($this->prefs['literal+'] ? '+' : ''));
+
+        if ($this->putLine($request)) {
+            // Don't wait when LITERAL+ is supported
+            if (!$this->prefs['literal+']) {
+                $line = $this->readReply();
+
+                if ($line[0] != '+') {
+                    $this->parseResult($line, 'APPEND: ');
+                    return false;
+                }
+            }
+
+            // send headers with body separator
+            if ($headers) {
+                $this->putLine($headers . $body_separator, false);
+            }
+
+            // send file
+            while (!feof($in_fp) && $this->fp) {
+                $buffer = fgets($in_fp, 4096);
+                $this->putLine($buffer, false);
+            }
+            fclose($in_fp);
+
+            if (!$this->putLine('')) { // \r\n
+                return false;
+            }
+
+            // read response
+            do {
+                $line = $this->readLine();
+            } while (!$this->startsWith($line, $key, true, true));
+
+            // Clear internal status cache
+            unset($this->data['STATUS:'.$mailbox]);
+
+            if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
+                return false;
+            else if (!empty($this->data['APPENDUID']))
+                return $this->data['APPENDUID'];
+            else
+                return true;
+        }
+        else {
+            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+        }
+
+        return false;
+    }
+
+    function getQuota()
+    {
+        /*
+         * GETQUOTAROOT "INBOX"
+         * QUOTAROOT INBOX user/rchijiiwa1
+         * QUOTA user/rchijiiwa1 (STORAGE 654 9765)
+         * OK Completed
+         */
+        $result      = false;
+        $quota_lines = array();
+        $key         = $this->nextTag();
+        $command     = $key . ' GETQUOTAROOT INBOX';
+
+        // get line(s) containing quota info
+        if ($this->putLine($command)) {
+            do {
+                $line = rtrim($this->readLine(5000));
+                if (preg_match('/^\* QUOTA /', $line)) {
+                    $quota_lines[] = $line;
+                }
+            } while (!$this->startsWith($line, $key, true, true));
+        }
+        else {
+            $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
+        }
+
+        // return false if not found, parse if found
+        $min_free = PHP_INT_MAX;
+        foreach ($quota_lines as $key => $quota_line) {
+            $quota_line   = str_replace(array('(', ')'), '', $quota_line);
+            $parts        = explode(' ', $quota_line);
+            $storage_part = array_search('STORAGE', $parts);
+
+            if (!$storage_part) {
+                continue;
+            }
+
+            $used  = intval($parts[$storage_part+1]);
+            $total = intval($parts[$storage_part+2]);
+            $free  = $total - $used;
+
+            // return lowest available space from all quotas
+            if ($free < $min_free) {
+                $min_free          = $free;
+                $result['used']    = $used;
+                $result['total']   = $total;
+                $result['percent'] = min(100, round(($used/max(1,$total))*100));
+                $result['free']    = 100 - $result['percent'];
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Send the SETACL command (RFC4314)
+     *
+     * @param string $mailbox Mailbox name
+     * @param string $user    User name
+     * @param mixed  $acl     ACL string or array
+     *
+     * @return boolean True on success, False on failure
+     *
+     * @since 0.5-beta
+     */
+    function setACL($mailbox, $user, $acl)
+    {
+        if (is_array($acl)) {
+            $acl = implode('', $acl);
+        }
+
+        $result = $this->execute('SETACL', array(
+            $this->escape($mailbox), $this->escape($user), strtolower($acl)),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    /**
+     * Send the DELETEACL command (RFC4314)
+     *
+     * @param string $mailbox Mailbox name
+     * @param string $user    User name
+     *
+     * @return boolean True on success, False on failure
+     *
+     * @since 0.5-beta
+     */
+    function deleteACL($mailbox, $user)
+    {
+        $result = $this->execute('DELETEACL', array(
+            $this->escape($mailbox), $this->escape($user)),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    /**
+     * Send the GETACL command (RFC4314)
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return array User-rights array on success, NULL on error
+     * @since 0.5-beta
+     */
+    function getACL($mailbox)
+    {
+        list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox)));
+
+        if ($code == self::ERROR_OK && preg_match('/^\* ACL /i', $response)) {
+            // Parse server response (remove "* ACL ")
+            $response = substr($response, 6);
+            $ret  = $this->tokenizeResponse($response);
+            $mbox = array_shift($ret);
+            $size = count($ret);
+
+            // Create user-rights hash array
+            // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
+            // so we could return only standard rights defined in RFC4314,
+            // excluding 'c' and 'd' defined in RFC2086.
+            if ($size % 2 == 0) {
+                for ($i=0; $i<$size; $i++) {
+                    $ret[$ret[$i]] = str_split($ret[++$i]);
+                    unset($ret[$i-1]);
+                    unset($ret[$i]);
+                }
+                return $ret;
+            }
+
+            $this->setError(self::ERROR_COMMAND, "Incomplete ACL response");
+            return NULL;
+        }
+
+        return NULL;
+    }
+
+    /**
+     * Send the LISTRIGHTS command (RFC4314)
+     *
+     * @param string $mailbox Mailbox name
+     * @param string $user    User name
+     *
+     * @return array List of user rights
+     * @since 0.5-beta
+     */
+    function listRights($mailbox, $user)
+    {
+        list($code, $response) = $this->execute('LISTRIGHTS', array(
+            $this->escape($mailbox), $this->escape($user)));
+
+        if ($code == self::ERROR_OK && preg_match('/^\* LISTRIGHTS /i', $response)) {
+            // Parse server response (remove "* LISTRIGHTS ")
+            $response = substr($response, 13);
+
+            $ret_mbox = $this->tokenizeResponse($response, 1);
+            $ret_user = $this->tokenizeResponse($response, 1);
+            $granted  = $this->tokenizeResponse($response, 1);
+            $optional = trim($response);
+
+            return array(
+                'granted'  => str_split($granted),
+                'optional' => explode(' ', $optional),
+            );
+        }
+
+        return NULL;
+    }
+
+    /**
+     * Send the MYRIGHTS command (RFC4314)
+     *
+     * @param string $mailbox Mailbox name
+     *
+     * @return array MYRIGHTS response on success, NULL on error
+     * @since 0.5-beta
+     */
+    function myRights($mailbox)
+    {
+        list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox)));
+
+        if ($code == self::ERROR_OK && preg_match('/^\* MYRIGHTS /i', $response)) {
+            // Parse server response (remove "* MYRIGHTS ")
+            $response = substr($response, 11);
+
+            $ret_mbox = $this->tokenizeResponse($response, 1);
+            $rights   = $this->tokenizeResponse($response, 1);
+
+            return str_split($rights);
+        }
+
+        return NULL;
+    }
+
+    /**
+     * Send the SETMETADATA command (RFC5464)
+     *
+     * @param string $mailbox Mailbox name
+     * @param array  $entries Entry-value array (use NULL value as NIL)
+     *
+     * @return boolean True on success, False on failure
+     * @since 0.5-beta
+     */
+    function setMetadata($mailbox, $entries)
+    {
+        if (!is_array($entries) || empty($entries)) {
+            $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
+            return false;
+        }
+
+        foreach ($entries as $name => $value) {
+            $entries[$name] = $this->escape($name) . ' ' . $this->escape($value);
+        }
+
+        $entries = implode(' ', $entries);
+        $result = $this->execute('SETMETADATA', array(
+            $this->escape($mailbox), '(' . $entries . ')'),
+            self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    /**
+     * Send the SETMETADATA command with NIL values (RFC5464)
+     *
+     * @param string $mailbox Mailbox name
+     * @param array  $entries Entry names array
+     *
+     * @return boolean True on success, False on failure
+     *
+     * @since 0.5-beta
+     */
+    function deleteMetadata($mailbox, $entries)
+    {
+        if (!is_array($entries) && !empty($entries)) {
+            $entries = explode(' ', $entries);
+        }
+
+        if (empty($entries)) {
+            $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
+            return false;
+        }
+
+        foreach ($entries as $entry) {
+            $data[$entry] = NULL;
+        }
+
+        return $this->setMetadata($mailbox, $data);
+    }
+
+    /**
+     * Send the GETMETADATA command (RFC5464)
+     *
+     * @param string $mailbox Mailbox name
+     * @param array  $entries Entries
+     * @param array  $options Command options (with MAXSIZE and DEPTH keys)
+     *
+     * @return array GETMETADATA result on success, NULL on error
+     *
+     * @since 0.5-beta
+     */
+    function getMetadata($mailbox, $entries, $options=array())
+    {
+        if (!is_array($entries)) {
+            $entries = array($entries);
+        }
+
+        // create entries string
+        foreach ($entries as $idx => $name) {
+            $entries[$idx] = $this->escape($name);
+        }
+
+        $optlist = '';
+        $entlist = '(' . implode(' ', $entries) . ')';
+
+        // create options string
+        if (is_array($options)) {
+            $options = array_change_key_case($options, CASE_UPPER);
+            $opts = array();
+
+            if (!empty($options['MAXSIZE'])) {
+                $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
+            }
+            if (!empty($options['DEPTH'])) {
+                $opts[] = 'DEPTH '.intval($options['DEPTH']);
+            }
+
+            if ($opts) {
+                $optlist = '(' . implode(' ', $opts) . ')';
+            }
+        }
+
+        $optlist .= ($optlist ? ' ' : '') . $entlist;
+
+        list($code, $response) = $this->execute('GETMETADATA', array(
+            $this->escape($mailbox), $optlist));
+
+        if ($code == self::ERROR_OK) {
+            $result = array();
+            $data   = $this->tokenizeResponse($response);
+
+            // The METADATA response can contain multiple entries in a single
+            // response or multiple responses for each entry or group of entries
+            if (!empty($data) && ($size = count($data))) {
+                for ($i=0; $i<$size; $i++) {
+                    if (isset($mbox) && is_array($data[$i])) {
+                        $size_sub = count($data[$i]);
+                        for ($x=0; $x<$size_sub; $x++) {
+                            $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
+                        }
+                        unset($data[$i]);
+                    }
+                    else if ($data[$i] == '*') {
+                        if ($data[$i+1] == 'METADATA') {
+                            $mbox = $data[$i+2];
+                            unset($data[$i]);   // "*"
+                            unset($data[++$i]); // "METADATA"
+                            unset($data[++$i]); // Mailbox
+                        }
+                        // get rid of other untagged responses
+                        else {
+                            unset($mbox);
+                            unset($data[$i]);
+                        }
+                    }
+                    else if (isset($mbox)) {
+                        $result[$mbox][$data[$i]] = $data[++$i];
+                        unset($data[$i]);
+                        unset($data[$i-1]);
+                    }
+                    else {
+                        unset($data[$i]);
+                    }
+                }
+            }
+
+            return $result;
+        }
+
+        return NULL;
+    }
+
+    /**
+     * Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
+     *
+     * @param string $mailbox Mailbox name
+     * @param array  $data    Data array where each item is an array with
+     *                        three elements: entry name, attribute name, value
+     *
+     * @return boolean True on success, False on failure
+     * @since 0.5-beta
+     */
+    function setAnnotation($mailbox, $data)
+    {
+        if (!is_array($data) || empty($data)) {
+            $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
+            return false;
+        }
+
+        foreach ($data as $entry) {
+            // ANNOTATEMORE drafts before version 08 require quoted parameters
+            $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true),
+                $this->escape($entry[1], true), $this->escape($entry[2], true));
+        }
+
+        $entries = implode(' ', $entries);
+        $result  = $this->execute('SETANNOTATION', array(
+            $this->escape($mailbox), $entries), self::COMMAND_NORESPONSE);
+
+        return ($result == self::ERROR_OK);
+    }
+
+    /**
+     * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
+     *
+     * @param string $mailbox Mailbox name
+     * @param array  $data    Data array where each item is an array with
+     *                        two elements: entry name and attribute name
+     *
+     * @return boolean True on success, False on failure
+     *
+     * @since 0.5-beta
+     */
+    function deleteAnnotation($mailbox, $data)
+    {
+        if (!is_array($data) || empty($data)) {
+            $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
+            return false;
+        }
+
+        return $this->setAnnotation($mailbox, $data);
+    }
+
+    /**
+     * Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
+     *
+     * @param string $mailbox Mailbox name
+     * @param array  $entries Entries names
+     * @param array  $attribs Attribs names
+     *
+     * @return array Annotations result on success, NULL on error
+     *
+     * @since 0.5-beta
+     */
+    function getAnnotation($mailbox, $entries, $attribs)
+    {
+        if (!is_array($entries)) {
+            $entries = array($entries);
+        }
+        // create entries string
+        // ANNOTATEMORE drafts before version 08 require quoted parameters
+        foreach ($entries as $idx => $name) {
+            $entries[$idx] = $this->escape($name, true);
+        }
+        $entries = '(' . implode(' ', $entries) . ')';
+
+        if (!is_array($attribs)) {
+            $attribs = array($attribs);
+        }
+        // create entries string
+        foreach ($attribs as $idx => $name) {
+            $attribs[$idx] = $this->escape($name, true);
+        }
+        $attribs = '(' . implode(' ', $attribs) . ')';
+
+        list($code, $response) = $this->execute('GETANNOTATION', array(
+            $this->escape($mailbox), $entries, $attribs));
+
+        if ($code == self::ERROR_OK) {
+            $result = array();
+            $data   = $this->tokenizeResponse($response);
+
+            // Here we returns only data compatible with METADATA result format
+            if (!empty($data) && ($size = count($data))) {
+                for ($i=0; $i<$size; $i++) {
+                    $entry = $data[$i];
+                    if (isset($mbox) && is_array($entry)) {
+                        $attribs = $entry;
+                        $entry   = $last_entry;
+                    }
+                    else if ($entry == '*') {
+                        if ($data[$i+1] == 'ANNOTATION') {
+                            $mbox = $data[$i+2];
+                            unset($data[$i]);   // "*"
+                            unset($data[++$i]); // "ANNOTATION"
+                            unset($data[++$i]); // Mailbox
+                        }
+                        // get rid of other untagged responses
+                        else {
+                            unset($mbox);
+                            unset($data[$i]);
+                        }
+                        continue;
+                    }
+                    else if (isset($mbox)) {
+                        $attribs = $data[++$i];
+                    }
+                    else {
+                        unset($data[$i]);
+                        continue;
+                    }
+
+                    if (!empty($attribs)) {
+                        for ($x=0, $len=count($attribs); $x<$len;) {
+                            $attr  = $attribs[$x++];
+                            $value = $attribs[$x++];
+                            if ($attr == 'value.priv') {
+                                $result[$mbox]['/private' . $entry] = $value;
+                            }
+                            else if ($attr == 'value.shared') {
+                                $result[$mbox]['/shared' . $entry] = $value;
+                            }
+                        }
+                    }
+                    $last_entry = $entry;
+                    unset($data[$i]);
+                }
+            }
+
+            return $result;
+        }
+
+        return NULL;
+    }
+
+    /**
+     * Returns BODYSTRUCTURE for the specified message.
+     *
+     * @param string $mailbox Folder name
+     * @param int    $id      Message sequence number or UID
+     * @param bool   $is_uid  True if $id is an UID
+     *
+     * @return array/bool Body structure array or False on error.
+     * @since 0.6
+     */
+    function getStructure($mailbox, $id, $is_uid = false)
+    {
+        $result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE'));
+        if (is_array($result)) {
+            $result = array_shift($result);
+            return $result->bodystructure;
+        }
+        return false;
+    }
+
+    /**
+     * Returns data of a message part according to specified structure.
+     *
+     * @param array  $structure Message structure (getStructure() result)
+     * @param string $part      Message part identifier
+     *
+     * @return array Part data as hash array (type, encoding, charset, size)
+     */
+    static function getStructurePartData($structure, $part)
+    {
+	    $part_a = self::getStructurePartArray($structure, $part);
+	    $data   = array();
+
+	    if (empty($part_a)) {
+            return $data;
+        }
+
+        // content-type
+        if (is_array($part_a[0])) {
+            $data['type'] = 'multipart';
+        }
+        else {
+            $data['type'] = strtolower($part_a[0]);
+
+            // encoding
+            $data['encoding'] = strtolower($part_a[5]);
+
+            // charset
+            if (is_array($part_a[2])) {
+               while (list($key, $val) = each($part_a[2])) {
+                    if (strcasecmp($val, 'charset') == 0) {
+                        $data['charset'] = $part_a[2][$key+1];
+                        break;
+                    }
+                }
+            }
+        }
+
+        // size
+        $data['size'] = intval($part_a[6]);
+
+        return $data;
+    }
+
+    static function getStructurePartArray($a, $part)
+    {
+	    if (!is_array($a)) {
+            return false;
+        }
+	    if (strpos($part, '.') > 0) {
+		    $original_part = $part;
+		    $pos = strpos($part, '.');
+		    $rest = substr($original_part, $pos+1);
+		    $part = substr($original_part, 0, $pos);
+		    if ((strcasecmp($a[0], 'message') == 0) && (strcasecmp($a[1], 'rfc822') == 0)) {
+			    $a = $a[8];
+		    }
+		    return self::getStructurePartArray($a[$part-1], $rest);
+	    }
+        else if ($part>0) {
+		    if (!is_array($a[0]) && (strcasecmp($a[0], 'message') == 0)
+                && (strcasecmp($a[1], 'rfc822') == 0)) {
+			    $a = $a[8];
+		    }
+		    if (is_array($a[$part-1]))
+                return $a[$part-1];
+		    else
+                return $a;
+	    }
+        else if (($part == 0) || (empty($part))) {
+		    return $a;
+	    }
+    }
+
+    /**
+     * Creates next command identifier (tag)
+     *
+     * @return string Command identifier
+     * @since 0.5-beta
+     */
+    function nextTag()
+    {
+        $this->cmd_num++;
+        $this->cmd_tag = sprintf('A%04d', $this->cmd_num);
+
+        return $this->cmd_tag;
+    }
+
+    /**
+     * Sends IMAP command and parses result
+     *
+     * @param string $command   IMAP command
+     * @param array  $arguments Command arguments
+     * @param int    $options   Execution options
+     *
+     * @return mixed Response code or list of response code and data
+     * @since 0.5-beta
+     */
+    function execute($command, $arguments=array(), $options=0)
+    {
+        $tag      = $this->nextTag();
+        $query    = $tag . ' ' . $command;
+        $noresp   = ($options & self::COMMAND_NORESPONSE);
+        $response = $noresp ? null : '';
+
+        if (!empty($arguments)) {
+            foreach ($arguments as $arg) {
+                $query .= ' ' . self::r_implode($arg);
+            }
+        }
+
+        // Send command
+        if (!$this->putLineC($query)) {
+            $this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
+            return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
+        }
+
+        // Parse response
+        do {
+            $line = $this->readLine(4096);
+            if ($response !== null) {
+                $response .= $line;
+            }
+        } while (!$this->startsWith($line, $tag . ' ', true, true));
+
+        $code = $this->parseResult($line, $command . ': ');
+
+        // Remove last line from response
+        if ($response) {
+            $line_len = min(strlen($response), strlen($line) + 2);
+            $response = substr($response, 0, -$line_len);
+        }
+
+        // optional CAPABILITY response
+        if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK
+            && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)
+        ) {
+            $this->parseCapability($matches[1], true);
+        }
+
+        // return last line only (without command tag, result and response code)
+        if ($line && ($options & self::COMMAND_LASTLINE)) {
+            $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
+        }
+
+        return $noresp ? $code : array($code, $response);
+    }
+
+    /**
+     * Splits IMAP response into string tokens
+     *
+     * @param string &$str The IMAP's server response
+     * @param int    $num  Number of tokens to return
+     *
+     * @return mixed Tokens array or string if $num=1
+     * @since 0.5-beta
+     */
+    static function tokenizeResponse(&$str, $num=0)
+    {
+        $result = array();
+
+        while (!$num || count($result) < $num) {
+            // remove spaces from the beginning of the string
+            $str = ltrim($str);
+
+            switch ($str[0]) {
+
+            // String literal
+            case '{':
+                if (($epos = strpos($str, "}\r\n", 1)) == false) {
+                    // error
+                }
+                if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
+                    // error
+                }
+                $result[] = $bytes ? substr($str, $epos + 3, $bytes) : '';
+                // Advance the string
+                $str = substr($str, $epos + 3 + $bytes);
+                break;
+
+            // Quoted string
+            case '"':
+                $len = strlen($str);
+
+                for ($pos=1; $pos<$len; $pos++) {
+                    if ($str[$pos] == '"') {
+                        break;
+                    }
+                    if ($str[$pos] == "\\") {
+                        if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
+                            $pos++;
+                        }
+                    }
+                }
+                if ($str[$pos] != '"') {
+                    // error
+                }
+                // we need to strip slashes for a quoted string
+                $result[] = stripslashes(substr($str, 1, $pos - 1));
+                $str      = substr($str, $pos + 1);
+                break;
+
+            // Parenthesized list
+            case '(':
+            case '[':
+                $str = substr($str, 1);
+                $result[] = self::tokenizeResponse($str);
+                break;
+            case ')':
+            case ']':
+                $str = substr($str, 1);
+                return $result;
+                break;
+
+            // String atom, number, NIL, *, %
+            default:
+                // empty or one character
+                if ($str === '') {
+                    break 2;
+                }
+                if (strlen($str) < 2) {
+                    $result[] = $str;
+                    $str = '';
+                    break;
+                }
+
+                // excluded chars: SP, CTL, ), [, ]
+                if (preg_match('/^([^\x00-\x20\x29\x5B\x5D\x7F]+)/', $str, $m)) {
+                    $result[] = $m[1] == 'NIL' ? NULL : $m[1];
+                    $str = substr($str, strlen($m[1]));
+                }
+                break;
+            }
+        }
+
+        return $num == 1 ? $result[0] : $result;
+    }
+
+    static function r_implode($element)
+    {
+        $string = '';
+
+        if (is_array($element)) {
+            reset($element);
+            while (list($key, $value) = each($element)) {
+                $string .= ' ' . self::r_implode($value);
+            }
+        }
+        else {
+            return $element;
+        }
+
+        return '(' . trim($string) . ')';
+    }
+
+    private function _xor($string, $string2)
+    {
+        $result = '';
+        $size   = strlen($string);
+
+        for ($i=0; $i<$size; $i++) {
+            $result .= chr(ord($string[$i]) ^ ord($string2[$i]));
+        }
+
+        return $result;
+    }
+
+    /**
+     * Converts datetime string into unix timestamp
+     *
+     * @param string $date Date string
+     *
+     * @return int Unix timestamp
+     */
+    static function strToTime($date)
+    {
+        // support non-standard "GMTXXXX" literal
+        $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
+
+        // if date parsing fails, we have a date in non-rfc format
+        // remove token from the end and try again
+        while (($ts = intval(@strtotime($date))) <= 0) {
+            $d = explode(' ', $date);
+            array_pop($d);
+            if (empty($d)) {
+                break;
+            }
+            $date = implode(' ', $d);
+        }
+
+        return $ts < 0 ? 0 : $ts;
+    }
+
+    private function parseCapability($str, $trusted=false)
+    {
+        $str = preg_replace('/^\* CAPABILITY /i', '', $str);
+
+        $this->capability = explode(' ', strtoupper($str));
+
+        if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
+            $this->prefs['literal+'] = true;
+        }
+
+        if ($trusted) {
+            $this->capability_readed = true;
+        }
+    }
+
+    /**
+     * Escapes a string when it contains special characters (RFC3501)
+     *
+     * @param string  $string       IMAP string
+     * @param boolean $force_quotes Forces string quoting (for atoms)
+     *
+     * @return string String atom, quoted-string or string literal
+     * @todo lists
+     */
+    static function escape($string, $force_quotes=false)
+    {
+        if ($string === null) {
+            return 'NIL';
+        }
+        if ($string === '') {
+            return '""';
+        }
+        // atom-string (only safe characters)
+        if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) {
+            return $string;
+        }
+        // quoted-string
+        if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) {
+            return '"' . addcslashes($string, '\\"') . '"';
+        }
+
+        // literal-string
+        return sprintf("{%d}\r\n%s", strlen($string), $string);
+    }
+
+    static function unEscape($string)
+    {
+        return stripslashes($string);
+    }
+
+    /**
+     * Set the value of the debugging flag.
+     *
+     * @param   boolean $debug      New value for the debugging flag.
+     *
+     * @since   0.5-stable
+     */
+    function setDebug($debug, $handler = null)
+    {
+        $this->_debug = $debug;
+        $this->_debug_handler = $handler;
+    }
+
+    /**
+     * Write the given debug text to the current debug output handler.
+     *
+     * @param   string  $message    Debug mesage text.
+     *
+     * @since   0.5-stable
+     */
+    private function debug($message)
+    {
+        if ($this->resourceid) {
+            $message = sprintf('[%s] %s', $this->resourceid, $message);
+        }
+
+        if ($this->_debug_handler) {
+            call_user_func_array($this->_debug_handler, array(&$this, $message));
+        } else {
+            echo "DEBUG: $message\n";
+        }
+    }
+
+}
Index: /branches/devel-composer/program/include/rcube_json_output.php
===================================================================
--- /branches/devel-composer/program/include/rcube_json_output.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_json_output.php	(revision 5386)
@@ -0,0 +1,295 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_json_output.php                                 |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2008-2010, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Class to handle HTML page output using a skin template.             |
+ |   Extends rcube_html_page class from rcube_shared.inc                 |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * View class to produce JSON responses
+ *
+ * @package View
+ */
+class rcube_json_output
+{
+    /**
+     * Stores configuration object.
+     *
+     * @var rcube_config
+     */
+    private $config;
+    private $charset = RCMAIL_CHARSET;
+    private $texts = array();
+    private $commands = array();
+    private $callbacks = array();
+    private $message = null;
+
+    public $browser;
+    public $env = array();
+    public $type = 'js';
+    public $ajax_call = true;
+
+
+    /**
+     * Constructor
+     */
+    public function __construct($task=null)
+    {
+        $this->config  = rcmail::get_instance()->config;
+        $this->browser = new rcube_browser();
+    }
+
+
+    /**
+     * Set environment variable
+     *
+     * @param string $name Property name
+     * @param mixed $value Property value
+     */
+    public function set_env($name, $value)
+    {
+        $this->env[$name] = $value;
+    }
+
+
+    /**
+     * Issue command to set page title
+     *
+     * @param string $title New page title
+     */
+    public function set_pagetitle($title)
+    {
+        if ($this->config->get('devel_mode') && !empty($_SESSION['username']))
+            $name = $_SESSION['username'];
+        else
+            $name = $this->config->get('product_name');
+
+        $this->command('set_pagetitle', empty($name) ? $title : $name.' :: '.$title);
+    }
+
+
+    /**
+     * @ignore
+     */
+    function set_charset($charset)
+    {
+        // ignore: $this->charset = $charset;
+    }
+
+
+    /**
+     * Get charset for output
+     *
+     * @return string Output charset
+     */
+    function get_charset()
+    {
+        return $this->charset;
+    }
+
+
+    /**
+     * Register a template object handler
+     *
+     * @param  string $obj Object name
+     * @param  string $func Function name to call
+     * @return void
+     */
+    public function add_handler($obj, $func)
+    {
+        // ignore
+    }
+
+
+    /**
+     * Register a list of template object handlers
+     *
+     * @param  array $arr Hash array with object=>handler pairs
+     * @return void
+     */
+    public function add_handlers($arr)
+    {
+        // ignore
+    }
+
+
+    /**
+     * Call a client method
+     *
+     * @param string Method to call
+     * @param ... Additional arguments
+     */
+    public function command()
+    {
+        $cmd = func_get_args();
+
+        if (strpos($cmd[0], 'plugin.') === 0)
+          $this->callbacks[] = $cmd;
+        else
+          $this->commands[] = $cmd;
+    }
+
+
+    /**
+     * Add a localized label to the client environment
+     */
+    public function add_label()
+    {
+        $args = func_get_args();
+        if (count($args) == 1 && is_array($args[0]))
+            $args = $args[0];
+
+        foreach ($args as $name) {
+            $this->texts[$name] = rcube_label($name);
+        }
+    }
+
+
+    /**
+     * Invoke display_message command
+     *
+     * @param string  $message  Message to display
+     * @param string  $type     Message type [notice|confirm|error]
+     * @param array   $vars     Key-value pairs to be replaced in localized text
+     * @param boolean $override Override last set message
+     * @param int     $timeout  Message displaying time in seconds
+     * @uses self::command()
+     */
+    public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0)
+    {
+        if ($override || !$this->message) {
+            if (rcube_label_exists($message)) {
+                if (!empty($vars))
+                    $vars = array_map('Q', $vars);
+                $msgtext = rcube_label(array('name' => $message, 'vars' => $vars));
+            }
+            else
+                $msgtext = $message;
+
+            $this->message = $message;
+            $this->command('display_message', $msgtext, $type, $timeout * 1000);
+        }
+    }
+
+
+    /**
+     * Delete all stored env variables and commands
+     */
+    public function reset()
+    {
+        $this->env = array();
+        $this->texts = array();
+        $this->commands = array();
+    }
+
+
+    /**
+     * Redirect to a certain url
+     *
+     * @param mixed $p Either a string with the action or url parameters as key-value pairs
+     * @param int $delay Delay in seconds
+     * @see rcmail::url()
+     */
+    public function redirect($p = array(), $delay = 1)
+    {
+        $location = rcmail::get_instance()->url($p);
+        $this->remote_response("window.setTimeout(\"location.href='{$location}'\", $delay);");
+        exit;
+    }
+
+
+    /**
+     * Send an AJAX response to the client.
+     */
+    public function send()
+    {
+        $this->remote_response();
+        exit;
+    }
+
+
+    /**
+     * Send an AJAX response with executable JS code
+     *
+     * @param  string  $add Additional JS code
+     * @param  boolean True if output buffer should be flushed
+     * @return void
+     * @deprecated
+     */
+    public function remote_response($add='')
+    {
+        static $s_header_sent = false;
+
+        if (!$s_header_sent) {
+            $s_header_sent = true;
+            send_nocacheing_headers();
+            header('Content-Type: text/plain; charset=' . $this->get_charset());
+        }
+
+        // unset default env vars
+        unset($this->env['task'], $this->env['action'], $this->env['comm_path']);
+
+        $rcmail = rcmail::get_instance();
+        $response['action'] = $rcmail->action;
+
+        if ($unlock = get_input_value('_unlock', RCUBE_INPUT_GPC)) {
+            $response['unlock'] = $unlock;
+        }
+
+        if (!empty($this->env))
+            $response['env'] = $this->env;
+
+        if (!empty($this->texts))
+            $response['texts'] = $this->texts;
+
+        // send function calls
+        $response['exec'] = $this->get_js_commands() . $add;
+
+        if (!empty($this->callbacks))
+            $response['callbacks'] = $this->callbacks;
+
+        echo json_serialize($response);
+    }
+
+
+    /**
+     * Return executable javascript code for all registered commands
+     *
+     * @return string $out
+     */
+    private function get_js_commands()
+    {
+        $out = '';
+
+        foreach ($this->commands as $i => $args) {
+            $method = array_shift($args);
+            foreach ($args as $i => $arg) {
+                $args[$i] = json_serialize($arg);
+            }
+
+            $out .= sprintf(
+                "this.%s(%s);\n",
+                preg_replace('/^parent\./', '', $method),
+                implode(',', $args)
+            );
+        }
+
+        return $out;
+    }
+}
Index: /branches/devel-composer/program/include/rcube_ldap.php
===================================================================
--- /branches/devel-composer/program/include/rcube_ldap.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_ldap.php	(revision 5386)
@@ -0,0 +1,1914 @@
+<?php
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_ldap.php                                        |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2006-2011, The Roundcube Dev Team                       |
+ | Copyright (C) 2011, Kolab Systems AG                                  |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Interface to an LDAP address directory                              |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ |         Andreas Dick <andudi (at) gmx (dot) ch>                       |
+ |         Aleksander Machniak <machniak@kolabsys.com>                   |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Model class to access an LDAP address directory
+ *
+ * @package Addressbook
+ */
+class rcube_ldap extends rcube_addressbook
+{
+    /** public properties */
+    public $primary_key = 'ID';
+    public $groups = false;
+    public $readonly = true;
+    public $ready = false;
+    public $group_id = 0;
+    public $list_page = 1;
+    public $page_size = 10;
+    public $coltypes = array();
+
+    /** private properties */
+    protected $conn;
+    protected $prop = array();
+    protected $fieldmap = array();
+
+    protected $filter = '';
+    protected $result = null;
+    protected $ldap_result = null;
+    protected $sort_col = '';
+    protected $mail_domain = '';
+    protected $debug = false;
+
+    private $base_dn = '';
+    private $groups_base_dn = '';
+    private $group_url = null;
+    private $cache;
+
+    private $vlv_active = false;
+    private $vlv_count = 0;
+
+
+    /**
+    * Object constructor
+    *
+    * @param array 	LDAP connection properties
+    * @param boolean 	Enables debug mode
+    * @param string 	Current user mail domain name
+    * @param integer User-ID
+    */
+    function __construct($p, $debug=false, $mail_domain=NULL)
+    {
+        $this->prop = $p;
+
+        if (isset($p['searchonly']))
+            $this->searchonly = $p['searchonly'];
+
+        // check if groups are configured
+        if (is_array($p['groups']) && count($p['groups'])) {
+            $this->groups = true;
+            // set member field
+            if (!empty($p['groups']['member_attr']))
+                $this->prop['member_attr'] = strtolower($p['groups']['member_attr']);
+            else if (empty($p['member_attr']))
+                $this->prop['member_attr'] = 'member';
+            // set default name attribute to cn
+            if (empty($this->prop['groups']['name_attr']))
+                $this->prop['groups']['name_attr'] = 'cn';
+            if (empty($this->prop['groups']['scope']))
+                $this->prop['groups']['scope'] = 'sub';
+        }
+
+        // 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' => 1, 'subtypes' => $subtypes);
+            }
+            elseif ($type) {
+                $this->coltypes[$col]['subtypes'][] = $type;
+                $this->coltypes[$col]['limit']++;
+            }
+            if ($type && !$this->fieldmap[$col])
+                $this->fieldmap[$col] = $lf;
+        }
+
+        // support for composite address
+        if ($this->fieldmap['street'] && $this->fieldmap['locality']) {
+            $this->coltypes['address'] = array('limit' => max(1, $this->coltypes['locality']['limit']), 'subtypes' => $this->coltypes['locality']['subtypes'], 'childs' => array());
+            foreach (array('street','locality','zipcode','region','country') as $childcol) {
+                if ($this->fieldmap[$childcol]) {
+                    $this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
+                    unset($this->coltypes[$childcol]);  // remove address child col from global coltypes list
+                }
+            }
+        }
+        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']))
+            $this->prop['required_fields'] = (array) $this->prop['required_fields'];
+
+        foreach ($this->prop['required_fields'] as $key => $val)
+            $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
+
+        $this->sort_col    = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
+        $this->debug       = $debug;
+        $this->mail_domain = $mail_domain;
+
+        // initialize cache
+        $rcmail = rcmail::get_instance();
+        $this->cache = $rcmail->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
+
+        $this->_connect();
+    }
+
+
+    /**
+    * Establish a connection to the LDAP server
+    */
+    private function _connect()
+    {
+        global $RCMAIL;
+
+        if (!function_exists('ldap_connect'))
+            raise_error(array('code' => 100, 'type' => 'ldap',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "No ldap support in this installation of PHP"),
+                true, true);
+
+        if (is_resource($this->conn))
+            return true;
+
+        if (!is_array($this->prop['hosts']))
+            $this->prop['hosts'] = array($this->prop['hosts']);
+
+        if (empty($this->prop['ldap_version']))
+            $this->prop['ldap_version'] = 3;
+
+        foreach ($this->prop['hosts'] as $host)
+        {
+            $host     = idn_to_ascii(rcube_parse_host($host));
+            $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
+
+            $this->_debug("C: Connect [$hostname]");
+
+            if ($lc = @ldap_connect($host, $this->prop['port']))
+            {
+                if ($this->prop['use_tls'] === true)
+                    if (!ldap_start_tls($lc))
+                        continue;
+
+                $this->_debug("S: OK");
+
+                ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
+                $this->prop['host'] = $host;
+                $this->conn = $lc;
+                break;
+            }
+            $this->_debug("S: NOT OK");
+        }
+
+        // See if the directory is writeable.
+        if ($this->prop['writable']) {
+            $this->readonly = false;
+        }
+
+        if (!is_resource($this->conn)) {
+            raise_error(array('code' => 100, 'type' => 'ldap',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
+
+            return false;
+        }
+
+        $bind_pass = $this->prop['bind_pass'];
+        $bind_user = $this->prop['bind_user'];
+        $bind_dn   = $this->prop['bind_dn'];
+
+        $this->base_dn        = $this->prop['base_dn'];
+        $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
+        $this->prop['groups']['base_dn'] : $this->base_dn;
+
+        // User specific access, generate the proper values to use.
+        if ($this->prop['user_specific']) {
+            // No password set, use the session password
+            if (empty($bind_pass)) {
+                $bind_pass = $RCMAIL->decrypt($_SESSION['password']);
+            }
+
+            // Get the pieces needed for variable replacement.
+            if ($fu = $RCMAIL->user->get_username())
+                list($u, $d) = explode('@', $fu);
+            else
+                $d = $this->mail_domain;
+
+            $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
+
+            $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
+
+            if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
+                // Search for the dn to use to authenticate
+                $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
+                $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
+
+                $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
+
+                $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
+                if ($res) {
+                    if (($entry = ldap_first_entry($this->conn, $res))
+                        && ($bind_dn = ldap_get_dn($this->conn, $entry))
+                    ) {
+                        $this->_debug("S: search returned dn: $bind_dn");
+                        $dn = ldap_explode_dn($bind_dn, 1);
+                        $replaces['%dn'] = $dn[0];
+                    }
+                }
+                else {
+                    $this->_debug("S: ".ldap_error($this->conn));
+                }
+
+                // DN not found
+                if (empty($replaces['%dn'])) {
+                    if (!empty($this->prop['search_dn_default']))
+                        $replaces['%dn'] = $this->prop['search_dn_default'];
+                    else {
+                        raise_error(array(
+                            'code' => 100, 'type' => 'ldap',
+                            'file' => __FILE__, 'line' => __LINE__,
+                            'message' => "DN not found using LDAP search."), true);
+                        return false;
+                    }
+                }
+            }
+
+            // Replace the bind_dn and base_dn variables.
+            $bind_dn              = strtr($bind_dn, $replaces);
+            $this->base_dn        = strtr($this->base_dn, $replaces);
+            $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
+
+            if (empty($bind_user)) {
+                $bind_user = $u;
+            }
+        }
+
+        if (empty($bind_pass)) {
+            $this->ready = true;
+        }
+        else {
+            if (!empty($bind_dn)) {
+                $this->ready = $this->bind($bind_dn, $bind_pass);
+            }
+            else if (!empty($this->prop['auth_cid'])) {
+                $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
+            }
+            else {
+                $this->ready = $this->sasl_bind($bind_user, $bind_pass);
+            }
+        }
+
+        return $this->ready;
+    }
+
+
+    /**
+     * Bind connection with (SASL-) user and password
+     *
+     * @param string $authc Authentication user
+     * @param string $pass  Bind password
+     * @param string $authz Autorization user
+     *
+     * @return boolean True on success, False on error
+     */
+    public function sasl_bind($authc, $pass, $authz=null)
+    {
+        if (!$this->conn) {
+            return false;
+        }
+
+        if (!function_exists('ldap_sasl_bind')) {
+            raise_error(array('code' => 100, 'type' => 'ldap',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Unable to bind: ldap_sasl_bind() not exists"),
+                true, true);
+        }
+
+        if (!empty($authz)) {
+            $authz = 'u:' . $authz;
+        }
+
+        if (!empty($this->prop['auth_method'])) {
+            $method = $this->prop['auth_method'];
+        }
+        else {
+            $method = 'DIGEST-MD5';
+        }
+
+        $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]");
+
+        if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
+            $this->_debug("S: OK");
+            return true;
+        }
+
+        $this->_debug("S: ".ldap_error($this->conn));
+
+        raise_error(array(
+            'code' => ldap_errno($this->conn), 'type' => 'ldap',
+            'file' => __FILE__, 'line' => __LINE__,
+            'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)),
+            true);
+
+        return false;
+    }
+
+
+    /**
+     * Bind connection with DN and password
+     *
+     * @param string Bind DN
+     * @param string Bind password
+     *
+     * @return boolean True on success, False on error
+     */
+    public function bind($dn, $pass)
+    {
+        if (!$this->conn) {
+            return false;
+        }
+
+        $this->_debug("C: Bind [dn: $dn] [pass: $pass]");
+
+        if (@ldap_bind($this->conn, $dn, $pass)) {
+            $this->_debug("S: OK");
+            return true;
+        }
+
+        $this->_debug("S: ".ldap_error($this->conn));
+
+        raise_error(array(
+            'code' => ldap_errno($this->conn), 'type' => 'ldap',
+            'file' => __FILE__, 'line' => __LINE__,
+            'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
+            true);
+
+        return false;
+    }
+
+
+    /**
+     * Close connection to LDAP server
+     */
+    function close()
+    {
+        if ($this->conn)
+        {
+            $this->_debug("C: Close");
+            ldap_unbind($this->conn);
+            $this->conn = null;
+        }
+    }
+
+
+    /**
+     * Returns address book name
+     *
+     * @return string Address book name
+     */
+    function get_name()
+    {
+        return $this->prop['name'];
+    }
+
+
+    /**
+     * Set internal list page
+     *
+     * @param number $page Page number to list
+     */
+    function set_page($page)
+    {
+        $this->list_page = (int)$page;
+    }
+
+
+    /**
+     * Set internal page size
+     *
+     * @param number $size Number of messages to display on one page
+     */
+    function set_pagesize($size)
+    {
+        $this->page_size = (int)$size;
+    }
+
+
+    /**
+     * Save a search string for future listings
+     *
+     * @param string $filter Filter string
+     */
+    function set_search_set($filter)
+    {
+        $this->filter = $filter;
+    }
+
+
+    /**
+     * Getter for saved search properties
+     *
+     * @return mixed Search properties used by this class
+     */
+    function get_search_set()
+    {
+        return $this->filter;
+    }
+
+
+    /**
+     * Reset all saved results and search parameters
+     */
+    function reset()
+    {
+        $this->result = null;
+        $this->ldap_result = null;
+        $this->filter = '';
+    }
+
+
+    /**
+     * List the current set of contact records
+     *
+     * @param  array  List of cols to show
+     * @param  int    Only return this number of records
+     *
+     * @return array  Indexed list of contact records, each a hash array
+     */
+    function list_records($cols=null, $subset=0)
+    {
+        if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id)
+        {
+            $this->result = new rcube_result_set(0);
+            $this->result->searchonly = true;
+            return $this->result;
+        }
+
+        // fetch group members recursively
+        if ($this->group_id && $this->group_data['dn'])
+        {
+            $entries = $this->list_group_members($this->group_data['dn']);
+
+            // make list of entries unique and sort it
+            $seen = array();
+            foreach ($entries as $i => $rec) {
+                if ($seen[$rec['dn']]++)
+                    unset($entries[$i]);
+            }
+            usort($entries, array($this, '_entry_sort_cmp'));
+
+            $entries['count'] = count($entries);
+            $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
+        }
+        else
+        {
+            // add general filter to query
+            if (!empty($this->prop['filter']) && empty($this->filter))
+                $this->set_search_set($this->prop['filter']);
+
+            // exec LDAP search if no result resource is stored
+            if ($this->conn && !$this->ldap_result)
+                $this->_exec_search();
+
+            // count contacts for this user
+            $this->result = $this->count();
+
+            // we have a search result resource
+            if ($this->ldap_result && $this->result->count > 0)
+            {
+                // sorting still on the ldap server
+                if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
+                    ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
+
+                // get all entries from the ldap server
+                $entries = ldap_get_entries($this->conn, $this->ldap_result);
+            }
+
+        }  // end else
+
+        // start and end of the page
+        $start_row = $this->vlv_active ? 0 : $this->result->first;
+        $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
+        $last_row = $this->result->first + $this->page_size;
+        $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
+
+        // filter entries for this page
+        for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
+            $this->result->add($this->_ldap2result($entries[$i]));
+
+        return $this->result;
+    }
+
+    /**
+    * Get all members of the given group
+    *
+    * @param string Group DN
+    * @param array  Group entries (if called recursively)
+    * @return array Accumulated group members
+     */
+    function list_group_members($dn, $count = false, $entries = null)
+    {
+        $group_members = array();
+
+        // fetch group object
+        if (empty($entries)) {
+            $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
+            if ($result === false)
+            {
+                $this->_debug("S: ".ldap_error($this->conn));
+                return $group_members;
+            }
+
+            $entries = @ldap_get_entries($this->conn, $result);
+        }
+
+        for ($i=0; $i < $entries["count"]; $i++)
+        {
+            $entry = $entries[$i];
+
+            if (empty($entry['objectclass']))
+                continue;
+
+            foreach ((array)$entry['objectclass'] as $objectclass)
+            {
+                switch (strtolower($objectclass)) {
+                    case "groupofnames":
+                    case "kolabgroupofnames":
+                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
+                        break;
+                    case "groupofuniquenames":
+                    case "kolabgroupofuniquenames":
+                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
+                        break;
+                    case "groupofurls":
+                        $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
+                        break;
+                }
+            }
+            
+            if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
+              break;
+        }
+
+        return array_filter($group_members);
+    }
+
+    /**
+     * Fetch members of the given group entry from server
+     *
+     * @param string Group DN
+     * @param array  Group entry
+     * @param string Member attribute to use
+     * @return array Accumulated group members
+     */
+    private function _list_group_members($dn, $entry, $attr, $count)
+    {
+        // Use the member attributes to return an array of member ldap objects
+        // NOTE that the member attribute is supposed to contain a DN
+        $group_members = array();
+        if (empty($entry[$attr]))
+            return $group_members;
+
+        // read these attributes for all members
+        $attrib = $count ? array('dn') : array_values($this->fieldmap);
+        $attrib[] = 'objectClass';
+        $attrib[] = 'member';
+        $attrib[] = 'uniqueMember';
+        $attrib[] = 'memberURL';
+
+        for ($i=0; $i < $entry[$attr]['count']; $i++)
+        {
+            $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
+                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
+
+            $members = @ldap_get_entries($this->conn, $result);
+            if ($members == false)
+            {
+                $this->_debug("S: ".ldap_error($this->conn));
+                $members = array();
+            }
+
+            // for nested groups, call recursively
+            $nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
+
+            unset($members['count']);
+            $group_members = array_merge($group_members, array_filter($members), $nested_group_members);
+        }
+
+        return $group_members;
+    }
+
+    /**
+     * List members of group class groupOfUrls
+     *
+     * @param string Group DN
+     * @param array  Group entry
+     * @param boolean True if only used for counting
+     * @return array Accumulated group members
+     */
+    private function _list_group_memberurl($dn, $entry, $count)
+    {
+        $group_members = array();
+
+        for ($i=0; $i < $entry['memberurl']['count']; $i++)
+        {
+            // extract components from url
+            if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
+                continue;
+
+            // add search filter if any
+            $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
+            $func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list');
+
+            $attrib = $count ? array('dn') : array_values($this->fieldmap);
+            if ($result = @$func($this->conn, $m[1], $filter,
+                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']))
+            {
+                $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
+                if ($err = ldap_errno($this->conn))
+                    $this->_debug("S: Error: " .ldap_err2str($err));
+            }
+            else
+            {
+                $this->_debug("S: ".ldap_error($this->conn));
+                return $group_members;
+            }
+
+            $entries = @ldap_get_entries($this->conn, $result);
+            for ($j = 0; $j < $entries['count']; $j++)
+            {
+                if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))
+                    $group_members = array_merge($group_members, $nested_group_members);
+                else
+                    $group_members[] = $entries[$j];
+            }
+        }
+
+        return $group_members;
+    }
+
+    /**
+     * Callback for sorting entries
+     */
+    function _entry_sort_cmp($a, $b)
+    {
+        return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
+    }
+
+
+    /**
+     * Search contacts
+     *
+     * @param mixed   $fields   The field name of array of field names to search in
+     * @param mixed   $value    Search value (or array of values when $fields is array)
+     * @param boolean $strict   True for strict, False for partial (fuzzy) matching
+     * @param boolean $select   True if results are requested, False if count only
+     * @param boolean $nocount  (Not used)
+     * @param array   $required List of fields that cannot be empty
+     *
+     * @return array  Indexed list of contact records and 'count' value
+     */
+    function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
+    {
+        // special treatment for ID-based search
+        if ($fields == 'ID' || $fields == $this->primary_key)
+        {
+            $ids = !is_array($value) ? explode(',', $value) : $value;
+            $result = new rcube_result_set();
+            foreach ($ids as $id)
+            {
+                if ($rec = $this->get_record($id, true))
+                {
+                    $result->add($rec);
+                    $result->count++;
+                }
+            }
+            return $result;
+        }
+
+        // use VLV pseudo-search for autocompletion
+        if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == 'email,name')
+        {
+            // add general filter to query
+            if (!empty($this->prop['filter']) && empty($this->filter))
+                $this->set_search_set($this->prop['filter']);
+
+            // set VLV controls with encoded search string
+            $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value);
+
+            $function = $this->_scope2func($this->prop['scope']);
+            $this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)',
+                array_values($this->fieldmap), 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
+
+            // get all entries of this page and post-filter those that really match the query
+            $this->result = new rcube_result_set(0);
+            $entries = ldap_get_entries($this->conn, $this->ldap_result);
+            for ($i = 0; $i < $entries['count']; $i++) {
+                $rec = $this->_ldap2result($entries[$i]);
+                if (stripos($rec['name'] . $rec['email'], $value) !== false) {
+                    $this->result->add($rec);
+                    $this->result->count++;
+                }
+            }
+
+            return $this->result;
+        }
+
+        // use AND operator for advanced searches
+        $filter = is_array($value) ? '(&' : '(|';
+        $wc     = !$strict && $this->prop['fuzzy_search'] ? '*' : '';
+
+        if ($fields == '*')
+        {
+            // search_fields are required for fulltext search
+            if (empty($this->prop['search_fields']))
+            {
+                $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
+                $this->result = new rcube_result_set();
+                return $this->result;
+            }
+            if (is_array($this->prop['search_fields']))
+            {
+                foreach ($this->prop['search_fields'] as $field) {
+                    $filter .= "($field=$wc" . $this->_quote_string($value) . "$wc)";
+                }
+            }
+        }
+        else
+        {
+            foreach ((array)$fields as $idx => $field) {
+                $val = is_array($value) ? $value[$idx] : $value;
+                if ($f = $this->_map_field($field)) {
+                    $filter .= "($f=$wc" . $this->_quote_string($val) . "$wc)";
+                }
+            }
+        }
+        $filter .= ')';
+
+        // add required (non empty) fields filter
+        $req_filter = '';
+        foreach ((array)$required as $field)
+            if ($f = $this->_map_field($field))
+                $req_filter .= "($f=*)";
+
+        if (!empty($req_filter))
+            $filter = '(&' . $req_filter . $filter . ')';
+
+        // avoid double-wildcard if $value is empty
+        $filter = preg_replace('/\*+/', '*', $filter);
+
+        // add general filter to query
+        if (!empty($this->prop['filter']))
+            $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
+
+        // set filter string and execute search
+        $this->set_search_set($filter);
+        $this->_exec_search();
+
+        if ($select)
+            $this->list_records();
+        else
+            $this->result = $this->count();
+
+        return $this->result;
+    }
+
+
+    /**
+     * Count number of available contacts in database
+     *
+     * @return object rcube_result_set Resultset with values for 'count' and 'first'
+     */
+    function count()
+    {
+        $count = 0;
+        if ($this->conn && $this->ldap_result) {
+            $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
+        }
+        else if ($this->group_id && $this->group_data['dn']) {
+            $count = count($this->list_group_members($this->group_data['dn'], true));
+        }
+        else if ($this->conn) {
+            // We have a connection but no result set, attempt to get one.
+            if (empty($this->filter)) {
+                // The filter is not set, set it.
+                $this->filter = $this->prop['filter'];
+            }
+            $this->_exec_search(true);
+            if ($this->ldap_result) {
+                $count = ldap_count_entries($this->conn, $this->ldap_result);
+            }
+        }
+
+        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
+    }
+
+
+    /**
+     * Return the last result set
+     *
+     * @return object rcube_result_set Current resultset or NULL if nothing selected yet
+     */
+    function get_result()
+    {
+        return $this->result;
+    }
+
+
+    /**
+     * Get a specific contact record
+     *
+     * @param mixed   Record identifier
+     * @param boolean Return as associative array
+     *
+     * @return mixed  Hash array or rcube_result_set with all record fields
+     */
+    function get_record($dn, $assoc=false)
+    {
+        $res = null;
+        if ($this->conn && $dn)
+        {
+            $dn = self::dn_decode($dn);
+
+            $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
+
+            if ($this->ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap)))
+                $entry = ldap_first_entry($this->conn, $this->ldap_result);
+            else
+                $this->_debug("S: ".ldap_error($this->conn));
+
+            if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
+            {
+                $this->_debug("S: OK"/* . print_r($rec, true)*/);
+
+                $rec = array_change_key_case($rec, CASE_LOWER);
+
+                // Add in the dn for the entry.
+                $rec['dn'] = $dn;
+                $res = $this->_ldap2result($rec);
+                $this->result = new rcube_result_set(1);
+                $this->result->add($res);
+            }
+        }
+
+        return $assoc ? $res : $this->result;
+    }
+
+
+    /**
+     * Check the given data before saving.
+     * If input not valid, the message to display can be fetched using get_error()
+     *
+     * @param array Assoziative array with data to save
+     * @param boolean Try to fix/complete record automatically
+     * @return boolean True if input is valid, False if not.
+     */
+    public function validate(&$save_data, $autofix = false)
+    {
+        // check for name input
+        if (empty($save_data['name'])) {
+            $this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
+            return false;
+        }
+
+        // Verify that the required fields are set.
+        $missing = null;
+        $ldap_data = $this->_map_data($save_data);
+        foreach ($this->prop['required_fields'] as $fld) {
+            if (!isset($ldap_data[$fld])) {
+                $missing[$fld] = 1;
+            }
+        }
+
+        if ($missing) {
+            // try to complete record automatically
+            if ($autofix) {
+                $reverse_map = array_flip($this->fieldmap);
+                $name_parts = preg_split('/[\s,.]+/', $save_data['name']);
+                if ($missing['sn']) {
+                    $sn_field = $reverse_map['sn'];
+                    $save_data[$sn_field] = array_pop ($name_parts);
+                }
+                if ($missing[($fn_field = $this->fieldmap['firstname'])]) {
+                    $save_data['firstname'] = array_shift($name_parts);
+                }
+
+                return $this->validate($save_data, false);
+            }
+
+            // TODO: generate message saying which fields are missing
+            $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
+            return false;
+        }
+
+        // validate e-mail addresses
+        return parent::validate($save_data, $autofix);
+    }
+
+
+    /**
+     * Create a new contact record
+     *
+     * @param array    Hash array with save data
+     *
+     * @return encoded record ID on success, False on error
+     */
+    function insert($save_cols)
+    {
+        // Map out the column names to their LDAP ones to build the new entry.
+        $newentry = $this->_map_data($save_cols);
+        $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
+
+        // Verify that the required fields are set.
+        $missing = null;
+        foreach ($this->prop['required_fields'] as $fld) {
+            if (!isset($newentry[$fld])) {
+                $missing[] = $fld;
+            }
+        }
+
+        // abort process if requiered fields are missing
+        // TODO: generate message saying which fields are missing
+        if ($missing) {
+            $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
+            return false;
+        }
+
+        // Build the new entries DN.
+        $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->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
+
+        $this->_debug("S: OK");
+
+        $dn = self::dn_encode($dn);
+
+        // add new contact to the selected group
+        if ($this->groups)
+            $this->add_to_group($this->group_id, $dn);
+
+        return $dn;
+    }
+
+
+    /**
+     * Update a specific contact record
+     *
+     * @param mixed Record identifier
+     * @param array Hash array with save data
+     *
+     * @return boolean True on success, False on error
+     */
+    function update($id, $save_cols)
+    {
+        $record = $this->get_record($id, true);
+        $result = $this->get_result();
+        $record = $result->first();
+
+        $newdata = array();
+        $replacedata = array();
+        $deletedata = array();
+
+        // flatten composite fields in $record
+        if (is_array($record['address'])) {
+          foreach ($record['address'] as $i => $struct) {
+            foreach ($struct as $col => $val) {
+              $record[$col][$i] = $val;
+            }
+          }
+        }
+
+        foreach ($this->fieldmap as $col => $fld) {
+            $val = $save_cols[$col];
+            if ($fld) {
+                // remove empty array values
+                if (is_array($val))
+                    $val = array_filter($val);
+                // The field does exist compare it to the ldap record.
+                if ($record[$col] != $val) {
+                    // Changed, but find out how.
+                    if (!isset($record[$col])) {
+                        // Field was not set prior, need to add it.
+                        $newdata[$fld] = $val;
+                    } // end if
+                    elseif ($val == '') {
+                        // Field supplied is empty, verify that it is not required.
+                        if (!in_array($fld, $this->prop['required_fields'])) {
+                            // It is not, safe to clear.
+                            $deletedata[$fld] = $record[$col];
+                        } // end if
+                    } // end elseif
+                    else {
+                        // The data was modified, save it out.
+                        $replacedata[$fld] = $val;
+                    } // end else
+                } // end if
+            } // end if
+        } // end foreach
+
+        $dn = self::dn_decode($id);
+
+        // Update the entry as required.
+        if (!empty($deletedata)) {
+            // Delete the fields.
+            $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");
+        } // end if
+
+        if (!empty($replacedata)) {
+            // Handle RDN change
+            if ($replacedata[$this->prop['LDAP_rdn']]) {
+                $newdn = $this->prop['LDAP_rdn'].'='
+                    .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
+                    .','.$this->base_dn;
+                if ($dn != $newdn) {
+                    $newrdn = $this->prop['LDAP_rdn'].'='
+                    .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
+                    unset($replacedata[$this->prop['LDAP_rdn']]);
+                }
+            }
+            // Replace the fields.
+            if (!empty($replacedata)) {
+                $this->_debug("C: Replace [dn: $dn]: ".print_r($replacedata, true));
+                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
+
+        if (!empty($newdata)) {
+            // Add the fields.
+            $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");
+        } // end if
+
+        // Handle RDN change
+        if (!empty($newrdn)) {
+            $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
+            if (!ldap_rename($this->conn, $dn, $newrdn, NULL, TRUE)) {
+                $this->_debug("S: ".ldap_error($this->conn));
+                return false;
+            }
+            $this->_debug("S: OK");
+
+            $dn    = self::dn_encode($dn);
+            $newdn = self::dn_encode($newdn);
+
+            // change the group membership of the contact
+            if ($this->groups)
+            {
+                $group_ids = $this->get_record_groups($dn);
+                foreach ($group_ids as $group_id)
+                {
+                    $this->remove_from_group($group_id, $dn);
+                    $this->add_to_group($group_id, $newdn);
+                }
+            }
+
+            return $newdn;
+        }
+
+        return true;
+    }
+
+
+    /**
+     * Mark one or more contact records as deleted
+     *
+     * @param array   Record identifiers
+     * @param boolean Remove record(s) irreversible (unsupported)
+     *
+     * @return boolean True on success, False on error
+     */
+    function delete($ids, $force=true)
+    {
+        if (!is_array($ids)) {
+            // Not an array, break apart the encoded DNs.
+            $ids = explode(',', $ids);
+        } // end if
+
+        foreach ($ids as $id) {
+            $dn = self::dn_decode($id);
+            $this->_debug("C: Delete [dn: $dn]");
+            // Delete the record.
+            $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");
+
+            // remove contact from all groups where he was member
+            if ($this->groups) {
+                $dn = self::dn_encode($dn);
+                $group_ids = $this->get_record_groups($dn);
+                foreach ($group_ids as $group_id) {
+                    $this->remove_from_group($group_id, $dn);
+                }
+            }
+        } // end foreach
+
+        return count($ids);
+    }
+
+
+    /**
+     * Execute the LDAP search based on the stored credentials
+     */
+    private function _exec_search($count = false)
+    {
+        if ($this->ready)
+        {
+            $filter = $this->filter ? $this->filter : '(objectclass=*)';
+            $function = $this->_scope2func($this->prop['scope'], $ns_function);
+
+            $this->_debug("C: Search [$filter][dn: $this->base_dn]");
+
+            // when using VLV, we get the total count by...
+            if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
+                // ...either reading numSubOrdinates attribute
+                if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
+                    $counts = ldap_get_entries($this->conn, $result_count);
+                    for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
+                        $this->vlv_count += $counts[$j]['numsubordinates'][0];
+                    $this->_debug("D: total numsubordinates = " . $this->vlv_count);
+                }
+                else if (!function_exists('ldap_parse_virtuallist_control'))  // ...or by fetching all records dn and count them
+                    $this->vlv_count = $this->_exec_search(true);
+
+                $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
+            }
+
+            // only fetch dn for count (should keep the payload low)
+            $attrs = $count ? array('dn') : array_values($this->fieldmap);
+            if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
+                $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']))
+            {
+                // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
+                if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control') &&
+                    ldap_parse_result($this->conn, $this->ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) {
+                    ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $this->vlv_count, $vresult);
+                    $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count");
+                }
+
+                $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
+                if ($err = ldap_errno($this->conn))
+                    $this->_debug("S: Error: " .ldap_err2str($err));
+
+                return $count ? ldap_count_entries($this->conn, $this->ldap_result) : true;
+            }
+            else
+            {
+                $this->_debug("S: ".ldap_error($this->conn));
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Choose the right PHP function according to scope property
+     */
+    private function _scope2func($scope, &$ns_function = null)
+    {
+        switch ($scope) {
+          case 'sub':
+            $function = $ns_function  = 'ldap_search';
+            break;
+          case 'base':
+            $function = $ns_function = 'ldap_read';
+            break;
+          default:
+            $function = 'ldap_list';
+            $ns_function = 'ldap_read';
+            break;
+        }
+        
+        return $function;
+    }
+
+    /**
+     * Set server controls for Virtual List View (paginated listing)
+     */
+    private function _vlv_set_controls($prop, $list_page, $page_size, $search = null)
+    {
+        $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => $this->_sort_ber_encode((array)$prop['sort']));
+        $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
+
+        $sort = (array)$prop['sort'];
+        $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
+            . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
+
+        if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
+            $this->_debug("S: ".ldap_error($this->conn));
+            $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
+            return false;
+        }
+
+        return true;
+    }
+
+
+    /**
+     * Converts LDAP entry into an array
+     */
+    private function _ldap2result($rec)
+    {
+        $out = array();
+
+        if ($rec['dn'])
+            $out[$this->primary_key] = self::dn_encode($rec['dn']);
+
+        foreach ($this->fieldmap as $rf => $lf)
+        {
+            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] = $value;
+            }
+
+            // Make sure name fields aren't arrays (#1488108)
+            if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
+                $out[$rf] = $out[$rf][0];
+            }
+        }
+
+        return $out;
+    }
+
+
+    /**
+     * Return real field name (from fields map)
+     */
+    private function _map_field($field)
+    {
+        return $this->fieldmap[$field];
+    }
+
+
+    /**
+     * Convert a record data set into LDAP field attributes
+     */
+    private function _map_data($save_cols)
+    {
+        $ldap_data = array();
+        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.
+                $ldap_data[$fld] = $val;
+            }
+        }
+        
+        return $ldap_data;
+    }
+
+
+    /**
+     * Returns unified attribute name (resolving aliases)
+     */
+    private static function _attr_name($name)
+    {
+        // list of known attribute aliases
+        $aliases = array(
+            'gn' => 'givenname',
+            'rfc822mailbox' => 'email',
+            'userid' => 'uid',
+            'emailaddress' => 'email',
+            'pkcs9email' => 'email',
+        );
+        return isset($aliases[$name]) ? $aliases[$name] : $name;
+    }
+
+
+    /**
+     * Prints debug info to the log
+     */
+    private function _debug($str)
+    {
+        if ($this->debug)
+            write_log('ldap', $str);
+    }
+
+
+    /**
+     * Quotes attribute value string
+     *
+     * @param string $str Attribute value
+     * @param bool   $dn  True if the attribute is a DN
+     *
+     * @return string Quoted string
+     */
+    private static 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');
+        else
+            $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
+                '/'=>'\2f');
+
+        return strtr($str, $replace);
+    }
+
+
+    /**
+     * Setter for the current group
+     * (empty, has to be re-implemented by extending class)
+     */
+    function set_group($group_id)
+    {
+        if ($group_id)
+        {
+            if (($group_cache = $this->cache->get('groups')) === null)
+                $group_cache = $this->_fetch_groups();
+
+            $this->group_id = $group_id;
+            $this->group_data = $group_cache[$group_id];
+        }
+        else
+        {
+            $this->group_id = 0;
+            $this->group_data = null;
+        }
+    }
+
+    /**
+     * 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($search = null)
+    {
+        if (!$this->groups)
+            return array();
+
+        // use cached list for searching
+        $this->cache->expunge();
+        if (!$search || ($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
+
+        $groups = array();
+        if ($search) {
+            $search = strtolower($search);
+            foreach ($group_cache as $group) {
+                if (strstr(strtolower($group['name']), $search))
+                    $groups[] = $group;
+            }
+        }
+        else
+            $groups = $group_cache;
+
+        return array_values($groups);
+    }
+
+    /**
+     * Fetch groups from server
+     */
+    private function _fetch_groups($vlv_page = 0)
+    {
+        $base_dn = $this->groups_base_dn;
+        $filter = $this->prop['groups']['filter'];
+        $name_attr = $this->prop['groups']['name_attr'];
+        $email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail';
+        $sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
+        $sort_attr = $sort_attrs[0];
+
+        $this->_debug("C: Search [$filter][dn: $base_dn]");
+
+        // use vlv to list groups
+        if ($this->prop['groups']['vlv']) {
+            $page_size = 200;
+            if (!$this->prop['groups']['sort'])
+                $this->prop['groups']['sort'] = $sort_attrs;
+            $vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size);
+        }
+
+        $function = $this->_scope2func($this->prop['groups']['scope'], $ns_function);
+        $res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)));
+        if ($res === false)
+        {
+            $this->_debug("S: ".ldap_error($this->conn));
+            return array();
+        }
+
+        $ldap_data = ldap_get_entries($this->conn, $res);
+        $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
+
+        $groups = array();
+        $group_sortnames = array();
+        $group_count = $ldap_data["count"];
+        for ($i=0; $i < $group_count; $i++)
+        {
+            $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
+            $group_id = self::dn_encode($group_name);
+            $groups[$group_id]['ID'] = $group_id;
+            $groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
+            $groups[$group_id]['name'] = $group_name;
+            $groups[$group_id]['member_attr'] = $this->prop['member_attr'];
+
+            // check objectClass attributes of group and act accordingly
+            for ($j=0; $j < $ldap_data[$i]['objectclass']['count']; $j++) {
+                switch (strtolower($ldap_data[$i]['objectclass'][$j])) {
+                    case 'groupofnames':
+                    case 'kolabgroupofnames':
+                        $groups[$group_id]['member_attr'] = 'member';
+                        break;
+
+                    case 'groupofuniquenames':
+                    case 'kolabgroupofuniquenames':
+                        $groups[$group_id]['member_attr'] = 'uniqueMember';
+                        break;
+                }
+            }
+
+            // list email attributes of a group
+            for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) {
+                if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0)
+                    $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j];
+            }
+
+            $group_sortnames[] = strtolower($ldap_data[$i][$sort_attr][0]);
+        }
+
+        // recursive call can exit here
+        if ($vlv_page > 0)
+            return $groups;
+
+        // call recursively until we have fetched all groups
+        while ($vlv_active && $group_count == $page_size)
+        {
+            $next_page = $this->_fetch_groups(++$vlv_page);
+            $groups = array_merge($groups, $next_page);
+            $group_count = count($next_page);
+        }
+
+        // when using VLV the list of groups is already sorted
+        if (!$this->prop['groups']['vlv'])
+            array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
+
+        // cache this
+        $this->cache->set('groups', $groups);
+
+        return $groups;
+    }
+
+    /**
+     * Get group properties such as name and email address(es)
+     *
+     * @param string Group identifier
+     * @return array Group properties as hash array
+     */
+    function get_group($group_id)
+    {
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
+
+        $group_data = $group_cache[$group_id];
+        unset($group_data['dn'], $group_data['member_attr']);
+
+        return $group_data;
+    }
+
+    /**
+     * Create a contact group with the given name
+     *
+     * @param string The group name
+     * @return mixed False on error, array with record props in success
+     */
+    function create_group($group_name)
+    {
+        $base_dn = $this->groups_base_dn;
+        $new_dn = "cn=$group_name,$base_dn";
+        $new_gid = self::dn_encode($group_name);
+        $name_attr = $this->prop['groups']['name_attr'];
+
+        $new_entry = array(
+            'objectClass' => $this->prop['groups']['object_classes'],
+            $name_attr => $group_name,
+        );
+
+        $this->_debug("C: Add [dn: $new_dn]: ".print_r($new_entry, true));
+
+        $res = ldap_add($this->conn, $new_dn, $new_entry);
+        if ($res === false)
+        {
+            $this->_debug("S: ".ldap_error($this->conn));
+            $this->set_error(self::ERROR_SAVING, 'errorsaving');
+            return false;
+        }
+
+        $this->_debug("S: OK");
+        $this->cache->remove('groups');
+
+        return array('id' => $new_gid, 'name' => $group_name);
+    }
+
+    /**
+     * Delete the given group and all linked group members
+     *
+     * @param string Group identifier
+     * @return boolean True on success, false if no data was changed
+     */
+    function delete_group($group_id)
+    {
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
+
+        $base_dn = $this->groups_base_dn;
+        $group_name = $group_cache[$group_id]['name'];
+        $del_dn = "cn=$group_name,$base_dn";
+
+        $this->_debug("C: Delete [dn: $del_dn]");
+
+        $res = ldap_delete($this->conn, $del_dn);
+        if ($res === false)
+        {
+            $this->_debug("S: ".ldap_error($this->conn));
+            $this->set_error(self::ERROR_SAVING, 'errorsaving');
+            return false;
+        }
+
+        $this->_debug("S: OK");
+        $this->cache->remove('groups');
+
+        return true;
+    }
+
+    /**
+     * Rename a specific contact group
+     *
+     * @param string Group identifier
+     * @param string New name to set for this group
+     * @param string New group identifier (if changed, otherwise don't set)
+     * @return boolean New name on success, false if no data was changed
+     */
+    function rename_group($group_id, $new_name, &$new_gid)
+    {
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
+
+        $base_dn = $this->groups_base_dn;
+        $group_name = $group_cache[$group_id]['name'];
+        $old_dn = "cn=$group_name,$base_dn";
+        $new_rdn = "cn=$new_name";
+        $new_gid = self::dn_encode($new_name);
+
+        $this->_debug("C: Rename [dn: $old_dn] [dn: $new_rdn]");
+
+        $res = ldap_rename($this->conn, $old_dn, $new_rdn, NULL, TRUE);
+        if ($res === false)
+        {
+            $this->_debug("S: ".ldap_error($this->conn));
+            $this->set_error(self::ERROR_SAVING, 'errorsaving');
+            return false;
+        }
+
+        $this->_debug("S: OK");
+        $this->cache->remove('groups');
+
+        return $new_name;
+    }
+
+    /**
+     * Add the given contact records the a certain group
+     *
+     * @param string  Group identifier
+     * @param array   List of contact identifiers to be added
+     * @return int    Number of contacts added
+     */
+    function add_to_group($group_id, $contact_ids)
+    {
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
+
+        $base_dn     = $this->groups_base_dn;
+        $group_name  = $group_cache[$group_id]['name'];
+        $member_attr = $group_cache[$group_id]['member_attr'];
+        $group_dn    = "cn=$group_name,$base_dn";
+
+        $new_attrs = array();
+        foreach (explode(",", $contact_ids) as $id)
+            $new_attrs[$member_attr][] = self::dn_decode($id);
+
+        $this->_debug("C: Add [dn: $group_dn]: ".print_r($new_attrs, true));
+
+        $res = ldap_mod_add($this->conn, $group_dn, $new_attrs);
+        if ($res === false)
+        {
+            $this->_debug("S: ".ldap_error($this->conn));
+            $this->set_error(self::ERROR_SAVING, 'errorsaving');
+            return 0;
+        }
+
+        $this->_debug("S: OK");
+        $this->cache->remove('groups');
+
+        return count($new_attrs['member']);
+    }
+
+    /**
+     * Remove the given contact records from a certain group
+     *
+     * @param string  Group identifier
+     * @param array   List of contact identifiers to be removed
+     * @return int    Number of deleted group members
+     */
+    function remove_from_group($group_id, $contact_ids)
+    {
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
+
+        $base_dn     = $this->groups_base_dn;
+        $group_name  = $group_cache[$group_id]['name'];
+        $member_attr = $group_cache[$group_id]['member_attr'];
+        $group_dn    = "cn=$group_name,$base_dn";
+
+        $del_attrs = array();
+        foreach (explode(",", $contact_ids) as $id)
+            $del_attrs[$member_attr][] = self::dn_decode($id);
+
+        $this->_debug("C: Delete [dn: $group_dn]: ".print_r($del_attrs, true));
+
+        $res = ldap_mod_del($this->conn, $group_dn, $del_attrs);
+        if ($res === false)
+        {
+            $this->_debug("S: ".ldap_error($this->conn));
+            $this->set_error(self::ERROR_SAVING, 'errorsaving');
+            return 0;
+        }
+
+        $this->_debug("S: OK");
+        $this->cache->remove('groups');
+
+        return count($del_attrs['member']);
+    }
+
+    /**
+     * Get group assignments of a specific contact record
+     *
+     * @param mixed Record identifier
+     *
+     * @return array List of assigned groups as ID=>Name pairs
+     * @since 0.5-beta
+     */
+    function get_record_groups($contact_id)
+    {
+        if (!$this->groups)
+            return array();
+
+        $base_dn     = $this->groups_base_dn;
+        $contact_dn  = self::dn_decode($contact_id);
+        $name_attr   = $this->prop['groups']['name_attr'];
+        $member_attr = $this->prop['member_attr'];
+        $add_filter  = '';
+        if ($member_attr != 'member' && $member_attr != 'uniqueMember')
+            $add_filter = "($member_attr=$contact_dn)";
+        $filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\'));
+
+        $this->_debug("C: Search [$filter][dn: $base_dn]");
+
+        $res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr));
+        if ($res === false)
+        {
+            $this->_debug("S: ".ldap_error($this->conn));
+            return array();
+        }
+        $ldap_data = ldap_get_entries($this->conn, $res);
+        $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
+
+        $groups = array();
+        for ($i=0; $i<$ldap_data["count"]; $i++)
+        {
+            $group_name = $ldap_data[$i][$name_attr][0];
+            $group_id = self::dn_encode($group_name);
+            $groups[$group_id] = $group_id;
+        }
+        return $groups;
+    }
+
+
+    /**
+     * Generate BER encoded string for Virtual List View option
+     *
+     * @param integer List offset (first record)
+     * @param integer Records per page
+     * @return string BER encoded option value
+     */
+    private function _vlv_ber_encode($offset, $rpp, $search = '')
+    {
+        # this string is ber-encoded, php will prefix this value with:
+        # 04 (octet string) and 10 (length of 16 bytes)
+        # the code behind this string is broken down as follows:
+        # 30 = ber sequence with a length of 0e (14) bytes following
+        # 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
+        # 02 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
+        # a0 = type context-specific/constructed with a length of 06 (6) bytes following
+        # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
+        # 02 = type integer with 2 bytes following (contentCount):  01 00
+        
+        # whith a search string present:
+        # 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
+        # 81 indicates a user string is present where as a a0 indicates just a offset search
+        # 81 = type context-specific/constructed with a length of 06 (6) bytes following
+        
+        # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
+        # encoding of integer values (note: these values are in
+        # two-complement form so since offset will never be negative bit 8 of the
+        # leftmost octet should never by set to 1):
+        # 8.3.2: If the contents octets of an integer value encoding consist
+        # of more than one octet, then the bits of the first octet (rightmost) and bit 8
+        # of the second (to the left of first octet) octet:
+        # a) shall not all be ones; and
+        # b) shall not all be zero
+        
+        if ($search)
+        {
+            $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
+            $ber_val = self::_string2hex($search);
+            $str = self::_ber_addseq($ber_val, '81');
+        }
+        else
+        {
+            # construct the string from right to left
+            $str = "020100"; # contentCount
+
+            $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format
+
+            // calculate octet length of $ber_val
+            $str = self::_ber_addseq($ber_val, '02') . $str;
+
+            // now compute length over $str
+            $str = self::_ber_addseq($str, 'a0');
+        }
+        
+        // now tack on records per page
+        $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
+
+        // now tack on sequence identifier and length
+        $str = self::_ber_addseq($str, '30');
+
+        return pack('H'.strlen($str), $str);
+    }
+
+
+    /**
+     * create ber encoding for sort control
+     *
+     * @param array List of cols to sort by
+     * @return string BER encoded option value
+     */
+    private function _sort_ber_encode($sortcols)
+    {
+        $str = '';
+        foreach (array_reverse((array)$sortcols) as $col) {
+            $ber_val = self::_string2hex($col);
+
+            # 30 = ber sequence with a length of octet value
+            # 04 = octet string with a length of the ascii value
+            $oct = self::_ber_addseq($ber_val, '04');
+            $str = self::_ber_addseq($oct, '30') . $str;
+        }
+
+        // now tack on sequence identifier and length
+        $str = self::_ber_addseq($str, '30');
+
+        return pack('H'.strlen($str), $str);
+    }
+
+    /**
+     * Add BER sequence with correct length and the given identifier
+     */
+    private static function _ber_addseq($str, $identifier)
+    {
+        $len = dechex(strlen($str)/2);
+        if (strlen($len) % 2 != 0)
+            $len = '0'.$len;
+
+        return $identifier . $len . $str;
+    }
+
+    /**
+     * Returns BER encoded integer value in hex format
+     */
+    private static function _ber_encode_int($offset)
+    {
+        $val = dechex($offset);
+        $prefix = '';
+
+        // check if bit 8 of high byte is 1
+        if (preg_match('/^[89abcdef]/', $val))
+            $prefix = '00';
+
+        if (strlen($val)%2 != 0)
+            $prefix .= '0';
+
+        return $prefix . $val;
+    }
+
+    /**
+     * Returns ascii string encoded in hex
+     */
+    private static function _string2hex($str)
+    {
+        $hex = '';
+        for ($i=0; $i < strlen($str); $i++)
+            $hex .= dechex(ord($str[$i]));
+        return $hex;
+    }
+
+    /**
+     * HTML-safe DN string encoding
+     *
+     * @param string $str DN string
+     *
+     * @return string Encoded HTML identifier string
+     */
+    static function dn_encode($str)
+    {
+        // @TODO: to make output string shorter we could probably
+        //        remove dc=* items from it
+        return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
+    }
+
+    /**
+     * Decodes DN string encoded with _dn_encode()
+     *
+     * @param string $str Encoded HTML identifier string
+     *
+     * @return string DN string
+     */
+    static function dn_decode($str)
+    {
+        $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
+        return base64_decode($str);
+    }
+}
Index: /branches/devel-composer/program/include/rcube_mdb2.php
===================================================================
--- /branches/devel-composer/program/include/rcube_mdb2.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_mdb2.php	(revision 5386)
@@ -0,0 +1,803 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_mdb2.php                                        |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2009, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   PEAR:DB wrapper class that implements PEAR MDB2 functions           |
+ |   See http://pear.php.net/package/MDB2                                |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Lukas Kahwe Smith <smith@pooteeweet.org>                      |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Database independent query interface
+ *
+ * This is a wrapper for the PEAR::MDB2 class
+ *
+ * @package    Database
+ * @author     David Saez Padros <david@ols.es>
+ * @author     Thomas Bruederli <roundcube@gmail.com>
+ * @author     Lukas Kahwe Smith <smith@pooteeweet.org>
+ * @version    1.18
+ * @link       http://pear.php.net/package/MDB2
+ */
+class rcube_mdb2
+{
+    var $db_dsnw;               // DSN for write operations
+    var $db_dsnr;               // DSN for read operations
+    var $db_connected = false;  // Already connected ?
+    var $db_mode = '';          // Connection mode
+    var $db_handle = 0;         // Connection handle
+    var $db_error = false;
+    var $db_error_msg = '';
+
+    private $debug_mode = false;
+    private $write_failure = false;
+    private $a_query_results = array('dummy');
+    private $last_res_id = 0;
+    private $tables;
+
+
+    /**
+     * Object constructor
+     *
+     * @param  string $db_dsnw DSN for read/write operations
+     * @param  string $db_dsnr Optional DSN for read only operations
+     */
+    function __construct($db_dsnw, $db_dsnr='', $pconn=false)
+    {
+        if ($db_dsnr == '')
+            $db_dsnr = $db_dsnw;
+
+        $this->db_dsnw = $db_dsnw;
+        $this->db_dsnr = $db_dsnr;
+        $this->db_pconn = $pconn;
+
+        $dsn_array = MDB2::parseDSN($db_dsnw);
+        $this->db_provider = $dsn_array['phptype'];
+    }
+
+
+    /**
+     * Connect to specific database
+     *
+     * @param  string $dsn  DSN for DB connections
+     * @return MDB2 PEAR database handle
+     * @access private
+     */
+    private function dsn_connect($dsn)
+    {
+        // Use persistent connections if available
+        $db_options = array(
+            'persistent'       => $this->db_pconn,
+            'emulate_prepared' => $this->debug_mode,
+            'debug'            => $this->debug_mode,
+            'debug_handler'    => array($this, 'debug_handler'),
+            'portability'      => MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_EMPTY_TO_NULL);
+
+        if ($this->db_provider == 'pgsql') {
+            $db_options['disable_smart_seqname'] = true;
+            $db_options['seqname_format'] = '%s';
+        }
+
+        $dbh = MDB2::connect($dsn, $db_options);
+
+        if (MDB2::isError($dbh)) {
+            $this->db_error = true;
+            $this->db_error_msg = $dbh->getMessage();
+
+            raise_error(array('code' => 500, 'type' => 'db',
+                'line' => __LINE__, 'file' => __FILE__,
+                'message' => $dbh->getUserInfo()), true, false);
+        }
+        else if ($this->db_provider == 'sqlite') {
+            $dsn_array = MDB2::parseDSN($dsn);
+            if (!filesize($dsn_array['database']) && !empty($this->sqlite_initials))
+                $this->_sqlite_create_database($dbh, $this->sqlite_initials);
+        }
+        else if ($this->db_provider!='mssql' && $this->db_provider!='sqlsrv')
+            $dbh->setCharset('utf8');
+
+        return $dbh;
+    }
+
+
+    /**
+     * Connect to appropiate database depending on the operation
+     *
+     * @param  string $mode Connection mode (r|w)
+     * @access public
+     */
+    function db_connect($mode)
+    {
+        // Already connected
+        if ($this->db_connected) {
+            // connected to read-write db, current connection is ok
+            if ($this->db_mode == 'w' && !$this->write_failure)
+                return;
+
+            // no replication, current connection is ok for read and write
+            if (empty($this->db_dsnr) || $this->db_dsnw == $this->db_dsnr) {
+                $this->db_mode = 'w';
+                return;
+            }
+
+            // Same mode, current connection is ok
+            if ($this->db_mode == $mode)
+                return;
+        }
+
+        $dsn = ($mode == 'r') ? $this->db_dsnr : $this->db_dsnw;
+
+        $this->db_handle = $this->dsn_connect($dsn);
+        $this->db_connected = !PEAR::isError($this->db_handle);
+
+        if ($this->db_connected)
+          $this->db_mode = $mode;
+    }
+
+
+    /**
+     * Activate/deactivate debug mode
+     *
+     * @param boolean $dbg True if SQL queries should be logged
+     * @access public
+     */
+    function set_debug($dbg = true)
+    {
+        $this->debug_mode = $dbg;
+        if ($this->db_connected) {
+            $this->db_handle->setOption('debug', $dbg);
+            $this->db_handle->setOption('emulate_prepared', $dbg);
+        }
+    }
+
+
+    /**
+     * Getter for error state
+     *
+     * @param  boolean  True on error
+     * @access public
+     */
+    function is_error()
+    {
+        return $this->db_error ? $this->db_error_msg : false;
+    }
+
+
+    /**
+     * Connection state checker
+     *
+     * @param  boolean  True if in connected state
+     * @access public
+     */
+    function is_connected()
+    {
+        return PEAR::isError($this->db_handle) ? false : $this->db_connected;
+    }
+
+
+    /**
+     * Is database replication configured?
+     * This returns true if dsnw != dsnr
+     */
+    function is_replicated()
+    {
+      return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr;
+    }
+
+
+    /**
+     * Execute a SQL query
+     *
+     * @param  string  SQL query to execute
+     * @param  mixed   Values to be inserted in query
+     * @return number  Query handle identifier
+     * @access public
+     */
+    function query()
+    {
+        $params = func_get_args();
+        $query = array_shift($params);
+
+        // Support one argument of type array, instead of n arguments
+        if (count($params) == 1 && is_array($params[0]))
+            $params = $params[0];
+
+        return $this->_query($query, 0, 0, $params);
+    }
+
+
+    /**
+     * Execute a SQL query with limits
+     *
+     * @param  string  SQL query to execute
+     * @param  number  Offset for LIMIT statement
+     * @param  number  Number of rows for LIMIT statement
+     * @param  mixed   Values to be inserted in query
+     * @return number  Query handle identifier
+     * @access public
+     */
+    function limitquery()
+    {
+        $params  = func_get_args();
+        $query   = array_shift($params);
+        $offset  = array_shift($params);
+        $numrows = array_shift($params);
+
+        return $this->_query($query, $offset, $numrows, $params);
+    }
+
+
+    /**
+     * Execute a SQL query with limits
+     *
+     * @param  string $query   SQL query to execute
+     * @param  number $offset  Offset for LIMIT statement
+     * @param  number $numrows Number of rows for LIMIT statement
+     * @param  array  $params  Values to be inserted in query
+     * @return number  Query handle identifier
+     * @access private
+     */
+    private function _query($query, $offset, $numrows, $params)
+    {
+        // Read or write ?
+        $mode = (strtolower(substr(trim($query),0,6)) == 'select') ? 'r' : 'w';
+
+        // don't event attempt to connect if previous write-operation failed
+        if ($this->write_failure && $mode == 'w')
+            return false;
+
+        $this->db_connect($mode);
+
+        // check connection before proceeding
+        if (!$this->is_connected())
+            return null;
+
+        if ($this->db_provider == 'sqlite')
+            $this->_sqlite_prepare();
+
+        if ($numrows || $offset)
+            $result = $this->db_handle->setLimit($numrows,$offset);
+
+        if (empty($params))
+            $result = $mode == 'r' ? $this->db_handle->query($query) : $this->db_handle->exec($query);
+        else {
+            $params = (array)$params;
+            $q = $this->db_handle->prepare($query, null, $mode=='w' ? MDB2_PREPARE_MANIP : null);
+            if ($this->db_handle->isError($q)) {
+                $this->db_error = true;
+                $this->db_error_msg = $q->userinfo;
+
+                raise_error(array('code' => 500, 'type' => 'db',
+                    'line' => __LINE__, 'file' => __FILE__,
+                    'message' => $this->db_error_msg), true, false);
+                
+                $result = false;
+            }
+            else {
+                $result = $q->execute($params);
+                $q->free();
+            }
+        }
+
+        // remember that write-operation failed
+        if ($mode == 'w' && ($result === false || PEAR::isError($result)))
+            $this->write_failure = true;
+
+        // add result, even if it's an error
+        return $this->_add_result($result);
+    }
+
+
+    /**
+     * Get number of rows for a SQL query
+     * If no query handle is specified, the last query will be taken as reference
+     *
+     * @param  number $res_id  Optional query handle identifier
+     * @return mixed   Number of rows or false on failure
+     * @access public
+     */
+    function num_rows($res_id=null)
+    {
+        if (!$this->db_connected)
+            return false;
+
+        if ($result = $this->_get_result($res_id))
+            return $result->numRows();
+        else
+            return false;
+    }
+
+
+    /**
+     * Get number of affected rows for the last query
+     *
+     * @param  number $res_id Optional query handle identifier
+     * @return mixed   Number of rows or false on failure
+     * @access public
+     */
+    function affected_rows($res_id = null)
+    {
+        if (!$this->db_connected)
+            return false;
+
+        return $this->_get_result($res_id);
+    }
+
+
+    /**
+     * Get last inserted record ID
+     * For Postgres databases, a sequence name is required
+     *
+     * @param  string $table  Table name (to find the incremented sequence)
+     * @return mixed   ID or false on failure
+     * @access public
+     */
+    function insert_id($table = '')
+    {
+        if (!$this->db_connected || $this->db_mode == 'r')
+            return false;
+
+        if ($table) {
+            if ($this->db_provider == 'pgsql')
+                // find sequence name
+                $table = get_sequence_name($table);
+            else
+                // resolve table name
+                $table = get_table_name($table);
+        }
+
+        $id = $this->db_handle->lastInsertID($table);
+
+        return $this->db_handle->isError($id) ? null : $id;
+    }
+
+
+    /**
+     * Get an associative array for one row
+     * If no query handle is specified, the last query will be taken as reference
+     *
+     * @param  number $res_id Optional query handle identifier
+     * @return mixed   Array with col values or false on failure
+     * @access public
+     */
+    function fetch_assoc($res_id=null)
+    {
+        $result = $this->_get_result($res_id);
+        return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
+    }
+
+
+    /**
+     * Get an index array for one row
+     * If no query handle is specified, the last query will be taken as reference
+     *
+     * @param  number $res_id  Optional query handle identifier
+     * @return mixed   Array with col values or false on failure
+     * @access public
+     */
+    function fetch_array($res_id=null)
+    {
+        $result = $this->_get_result($res_id);
+        return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
+    }
+
+
+    /**
+     * Get col values for a result row
+     *
+     * @param  MDB2_Result_Common Query $result result handle
+     * @param  number                   $mode   Fetch mode identifier
+     * @return mixed   Array with col values or false on failure
+     * @access private
+     */
+    private function _fetch_row($result, $mode)
+    {
+        if ($result === false || PEAR::isError($result) || !$this->is_connected())
+            return false;
+
+        return $result->fetchRow($mode);
+    }
+
+
+    /**
+     * Wrapper for the SHOW TABLES command
+     *
+     * @return array List of all tables of the current database
+     * @access public
+     * @since 0.4-beta
+     */
+    function list_tables()
+    {
+        // get tables if not cached
+        if (!$this->tables) {
+            $this->db_handle->loadModule('Manager');
+            if (!PEAR::isError($result = $this->db_handle->listTables()))
+                $this->tables = $result;
+            else
+                $this->tables = array();
+        }
+
+        return $this->tables;
+    }
+
+
+    /**
+     * Wrapper for SHOW COLUMNS command
+     *
+     * @param string Table name
+     * @return array List of table cols
+     */
+    function list_cols($table)
+    {
+        $this->db_handle->loadModule('Manager');
+        if (!PEAR::isError($result = $this->db_handle->listTableFields($table))) {
+            return $result;
+        }
+        
+        return null;
+    }
+
+
+    /**
+     * Formats input so it can be safely used in a query
+     *
+     * @param  mixed  $input  Value to quote
+     * @param  string $type   Type of data
+     * @return string  Quoted/converted string for use in query
+     * @access public
+     */
+    function quote($input, $type = null)
+    {
+        // handle int directly for better performance
+        if ($type == 'integer')
+            return intval($input);
+
+        // create DB handle if not available
+        if (!$this->db_handle)
+            $this->db_connect('r');
+
+        return $this->db_connected ? $this->db_handle->quote($input, $type) : addslashes($input);
+    }
+
+
+    /**
+     * Quotes a string so it can be safely used as a table or column name
+     *
+     * @param  string $str Value to quote
+     * @return string  Quoted string for use in query
+     * @deprecated     Replaced by rcube_MDB2::quote_identifier
+     * @see            rcube_mdb2::quote_identifier
+     * @access public
+     */
+    function quoteIdentifier($str)
+    {
+        return $this->quote_identifier($str);
+    }
+
+
+    /**
+     * Quotes a string so it can be safely used as a table or column name
+     *
+     * @param  string $str Value to quote
+     * @return string  Quoted string for use in query
+     * @access public
+     */
+    function quote_identifier($str)
+    {
+        if (!$this->db_handle)
+            $this->db_connect('r');
+
+        return $this->db_connected ? $this->db_handle->quoteIdentifier($str) : $str;
+    }
+
+
+    /**
+     * Escapes a string
+     *
+     * @param  string $str The string to be escaped
+     * @return string  The escaped string
+     * @access public
+     * @since  0.1.1
+     */
+    function escapeSimple($str)
+    {
+        if (!$this->db_handle)
+            $this->db_connect('r');
+
+        return $this->db_handle->escape($str);
+    }
+
+
+    /**
+     * Return SQL function for current time and date
+     *
+     * @return string SQL function to use in query
+     * @access public
+     */
+    function now()
+    {
+        switch($this->db_provider) {
+            case 'mssql':
+            case 'sqlsrv':
+                return "getdate()";
+
+            default:
+                return "now()";
+        }
+    }
+
+
+    /**
+     * Return list of elements for use with SQL's IN clause
+     *
+     * @param  array  $arr  Input array
+     * @param  string $type Type of data
+     * @return string Comma-separated list of quoted values for use in query
+     * @access public
+     */
+    function array2list($arr, $type = null)
+    {
+        if (!is_array($arr))
+            return $this->quote($arr, $type);
+
+        foreach ($arr as $idx => $item)
+            $arr[$idx] = $this->quote($item, $type);
+
+        return implode(',', $arr);
+    }
+
+
+    /**
+     * Return SQL statement to convert a field value into a unix timestamp
+     *
+     * This method is deprecated and should not be used anymore due to limitations
+     * of timestamp functions in Mysql (year 2038 problem)
+     *
+     * @param  string $field Field name
+     * @return string  SQL statement to use in query
+     * @deprecated
+     */
+    function unixtimestamp($field)
+    {
+        switch($this->db_provider) {
+            case 'pgsql':
+                return "EXTRACT (EPOCH FROM $field)";
+
+            case 'mssql':
+            case 'sqlsrv':
+                return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
+
+            default:
+                return "UNIX_TIMESTAMP($field)";
+        }
+    }
+
+
+    /**
+     * Return SQL statement to convert from a unix timestamp
+     *
+     * @param  string $timestamp Field name
+     * @return string  SQL statement to use in query
+     * @access public
+     */
+    function fromunixtime($timestamp)
+    {
+        return date("'Y-m-d H:i:s'", $timestamp);
+    }
+
+
+    /**
+     * Return SQL statement for case insensitive LIKE
+     *
+     * @param  string $column  Field name
+     * @param  string $value   Search value
+     * @return string  SQL statement to use in query
+     * @access public
+     */
+    function ilike($column, $value)
+    {
+        // TODO: use MDB2's matchPattern() function
+        switch($this->db_provider) {
+            case 'pgsql':
+                return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
+            default:
+                return $this->quote_identifier($column).' LIKE '.$this->quote($value);
+        }
+    }
+
+    /**
+     * Abstract SQL statement for value concatenation
+     *
+     * @return string SQL statement to be used in query
+     * @access public
+     */
+    function concat(/* col1, col2, ... */)
+    {
+        $func = '';
+        $args = func_get_args();
+
+        switch($this->db_provider) {
+            case 'mysql':
+            case 'mysqli':
+                $func = 'CONCAT';
+                $delim = ', ';
+                break;
+            case 'mssql':
+            case 'sqlsrv':
+                $delim = ' + ';
+                break;
+            default:
+                $delim = ' || ';
+        }
+
+        return $func . '(' . join($delim, $args) . ')';
+    }
+
+
+    /**
+     * Encodes non-UTF-8 characters in string/array/object (recursive)
+     *
+     * @param  mixed  $input Data to fix
+     * @return mixed  Properly UTF-8 encoded data
+     * @access public
+     */
+    function encode($input)
+    {
+        if (is_object($input)) {
+            foreach (get_object_vars($input) as $idx => $value)
+                $input->$idx = $this->encode($value);
+            return $input;
+        }
+        else if (is_array($input)) {
+            foreach ($input as $idx => $value)
+                $input[$idx] = $this->encode($value);
+            return $input;	
+        }
+
+        return utf8_encode($input);
+    }
+
+
+    /**
+     * Decodes encoded UTF-8 string/object/array (recursive)
+     *
+     * @param  mixed $input Input data
+     * @return mixed  Decoded data
+     * @access public
+     */
+    function decode($input)
+    {
+        if (is_object($input)) {
+            foreach (get_object_vars($input) as $idx => $value)
+                $input->$idx = $this->decode($value);
+            return $input;
+        }
+        else if (is_array($input)) {
+            foreach ($input as $idx => $value)
+                $input[$idx] = $this->decode($value);
+            return $input;	
+        }
+
+        return utf8_decode($input);
+    }
+
+
+    /**
+     * Adds a query result and returns a handle ID
+     *
+     * @param  object $res Query handle
+     * @return mixed   Handle ID
+     * @access private
+     */
+    private function _add_result($res)
+    {
+        // sql error occured
+        if (PEAR::isError($res)) {
+            $this->db_error = true;
+            $this->db_error_msg = $res->getMessage();
+            raise_error(array('code' => 500, 'type' => 'db',
+                'line' => __LINE__, 'file' => __FILE__,
+                'message' => $res->getMessage() . " Query: " 
+                . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
+                true, false);
+        }
+
+        $res_id = sizeof($this->a_query_results);
+        $this->last_res_id = $res_id;
+        $this->a_query_results[$res_id] = $res;
+        return $res_id;
+    }
+
+
+    /**
+     * Resolves a given handle ID and returns the according query handle
+     * If no ID is specified, the last resource handle will be returned
+     *
+     * @param  number $res_id Handle ID
+     * @return mixed   Resource handle or false on failure
+     * @access private
+     */
+    private function _get_result($res_id = null)
+    {
+        if ($res_id == null)
+            $res_id = $this->last_res_id;
+
+        if (isset($this->a_query_results[$res_id]))
+            if (!PEAR::isError($this->a_query_results[$res_id]))
+                return $this->a_query_results[$res_id];
+
+        return false;
+    }
+
+
+    /**
+     * Create a sqlite database from a file
+     *
+     * @param  MDB2   $dbh       SQLite database handle
+     * @param  string $file_name File path to use for DB creation
+     * @access private
+     */
+    private function _sqlite_create_database($dbh, $file_name)
+    {
+        if (empty($file_name) || !is_string($file_name))
+            return;
+
+        $data = file_get_contents($file_name);
+
+        if (strlen($data))
+            if (!sqlite_exec($dbh->connection, $data, $error) || MDB2::isError($dbh)) 
+                raise_error(array('code' => 500, 'type' => 'db',
+                    'line' => __LINE__, 'file' => __FILE__,
+                    'message' => $error), true, false); 
+    }
+
+
+    /**
+     * Add some proprietary database functions to the current SQLite handle
+     * in order to make it MySQL compatible
+     *
+     * @access private
+     */
+    private function _sqlite_prepare()
+    {
+        include_once(INSTALL_PATH . 'program/include/rcube_sqlite.inc');
+
+        // we emulate via callback some missing MySQL function
+        sqlite_create_function($this->db_handle->connection,
+            'from_unixtime', 'rcube_sqlite_from_unixtime');
+        sqlite_create_function($this->db_handle->connection,
+            'unix_timestamp', 'rcube_sqlite_unix_timestamp');
+        sqlite_create_function($this->db_handle->connection,
+            'now', 'rcube_sqlite_now');
+        sqlite_create_function($this->db_handle->connection,
+            'md5', 'rcube_sqlite_md5');
+    }
+
+
+    /**
+     * Debug handler for the MDB2
+     */
+    function debug_handler(&$db, $scope, $message, $context = array())
+    {
+        if ($scope != 'prepare') {
+            $debug_output = sprintf('%s(%d): %s;',
+                $scope, $db->db_index, rtrim($message, ';'));
+            write_log('sql', $debug_output);
+        }
+    }
+
+}  // end class rcube_db
Index: /branches/devel-composer/program/include/rcube_message.php
===================================================================
--- /branches/devel-composer/program/include/rcube_message.php	(revision 5386)
+++ /branches/devel-composer/program/include/rcube_message.php	(revision 5386)
@@ -0,0 +1,748 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_message.php                                     |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2008-2010, The Roundcube Dev Team                       |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Logical representation of a mail message with all its data          |
+ |   and related functions                                               |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+
+/**
+ * Logical representation of a mail message with all its data
+ * and related functions
+ *
+ * @package    Mail
+ * @author     Thomas Bruederli <roundcube@gmail.com>
+ */
+class rcube_message
+{
+    /**
+     * Instace of rcmail.
+     *
+     * @var rcmail
+     */
+    private $app;
+
+    /**
+     * Instance of imap class
+     *
+     * @var rcube_imap
+     */
+    private $imap;
+    private $opt = array();
+    private $inline_parts = array();
+    private $parse_alternative = false;
+
+    public $uid = null;
+    public $headers;
+    public $parts = array();
+    public $mime_parts = array();
+    public $attachments = array();
+    public $subject = '';
+    public $sender = null;
+    public $is_safe = false;
+
+
+    /**
+     * __construct
+     *
+     * Provide a uid, and parse message structure.
+     *
+     * @param string $uid The message UID.
+     *
+     * @uses rcmail::get_instance()
+     * @uses rcube_imap::decode_mime_string()
+     * @uses self::set_safe()
+     *
+     * @see self::$app, self::$imap, self::$opt, self::$structure
+     */
+    function __construct($uid)
+    {
+        $this->app = rcmail::get_instance();
+        $this->imap = $this->app->imap;
+        $this->imap->get_all_headers = true;
+
+        $this->uid = $uid;
+        $this->headers = $this->imap->get_message($uid);
+
+        if (!$this->headers)
+            return;
+
+        $this->subject = rcube_imap::decode_mime_string(
+            $this->headers->subject, $this->headers->charset);
+        list(, $this->sender) = each($this->imap->decode_address_list($this->headers->from));
+
+        $this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$uid]));
+        $this->opt = array(
+            'safe' => $this->is_safe,
+            'prefer_html' => $this->app->config->get('prefer_html'),
+            'get_url' => rcmail_url('get', array(
+                '_mbox' => $this->imap->get_mailbox_name(), '_uid' => $uid))
+        );
+
+        if (!empty($this->headers->structure)) {
+            $this->get_mime_numbers($this->headers->structure);
+            $this->parse_structure($this->headers->structure);
+        }
+        else {
+            $this->body = $this->imap->get_body($uid);
+        }
+
+        // notify plugins and let them analyze this structured message object
+        $this->app->plugins->exec_hook('message_load', array('object' => $this));
+    }
+
+
+    /**
+     * Return a (decoded) message header
+     *
+     * @param string $name Header name
+     * @param bool   $row  Don't mime-decode the value
+     * @return string Header value
+     */
+    public function get_header($name, $raw = false)
+    {
+        if ($this->headers->$name)
+            $value = $this->headers->$name;
+        else if ($this->headers->others[$name])
+            $value = $this->headers->others[$name];
+
+        return $raw ? $value : $this->imap->decode_header($value);
+    }
+
+
+    /**
+     * Set is_safe var and session data
+     *
+     * @param bool $safe enable/disable
+     */
+    public function set_safe($safe = true)
+    {
+        $this->is_safe = $safe;
+        $_SESSION['safe_messages'][$this->uid] = $this->is_safe;
+    }
+
+
+    /**
+     * Compose a valid URL for getting a message part
+     *
+     * @param string $mime_id Part MIME-ID
+     * @return string URL or false if part does not exist
+     */
+    public function get_part_url($mime_id)
+    {
+        if ($this->mime_parts[$mime_id])
+            return $this->opt['get_url'] . '&_part=' . $mime_id;
+        else
+            return false;
+    }
+
+
+    /**
+     * Get content of a specific part of this message
+     *
+     * @param string $mime_id Part MIME-ID
+     * @param resource $fp File pointer to save the message part
+     * @return string Part content
+     */
+    public function get_part_content($mime_id, $fp=NULL)
+    {
+        if ($part = $this->mime_parts[$mime_id]) {
+            // stored in message structure (winmail/inline-uuencode)
+            if ($part->encoding == 'stream') {
+                if ($fp) {
+                    fwrite($fp, $part->body);
+                }
+                return $fp ? true : $part->body;
+            }
+            // get from IMAP
+            return $this->imap->get_message_part($this->uid, $mime_id, $part, NULL, $fp);
+        } else
+            return null;
+    }
+
+
+    /**
+     * Determine if the message contains a HTML part
+     *
+     * @return bool True if a HTML is available, False if not
+     */
+    function has_html_part()
+    {
+        // check all message parts
+        foreach ($this->parts as $pid => $part) {
+            $mimetype = strtolower($part->ctype_primary . '/' . $part->ctype_secondary);
+            if ($mimetype == 'text/html')
+                return true;
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Return the first HTML part of this message
+     *
+     * @return string HTML message part content
+     */
+    function first_html_part()
+    {
+        // check all message parts
+        foreach ($this->mime_parts as $mime_id => $part) {
+            $mimetype = strtolower($part->ctype_primary . '/' . $part->ctype_secondary);
+            if ($mimetype == 'text/html') {
+                return $this->imap->get_message_part($this->uid, $mime_id, $part);
+            }
+        }
+    }
+
+
+    /**
+     * Return the first text part of this message
+     *
+     * @param rcube_message_part $part Reference to the part if found
+     * @return string Plain text message/part content
+     */
+    function first_text_part(&$part=null)
+    {
+        // no message structure, return complete body
+        if (empty($this->parts))
+            return $this->body;
+
+        // check all message parts
+        foreach ($this->mime_parts as $mime_id => $part) {
+            $mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
+
+            if ($mimetype == 'text/plain') {
+                return $this->imap->get_message_part($this->uid, $mime_id, $part);
+            }
+            else if ($mimetype == 'text/html') {
+                $out = $this->imap->get_message_part($this->uid, $mime_id, $part);
+
+                // remove special chars encoding
+                $trans = array_flip(get_html_translation_table(HTML_ENTITIES));
+                $out = strtr($out, $trans);
+
+                // create instance of html2text class
+                $txt = new html2text($out);
+                return $txt->get_text();
+            }
+        }
+
+        $part = null;
+        return null;
+    }
+
+
+    /**
+     * Raad the message structure returend by the IMAP server
+     * and build flat lists of content parts and attachments
+     *
+     * @param rcube_message_part $structure Message structure node
+     * @param bool               $recursive True when called recursively
+     */
+    private function parse_structure($structure, $recursive = false)
+    {
+        // real content-type of message/rfc822 part
+        if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype)
+            $mimetype = $structure->real_mimetype;
+        else
+            $mimetype = $structure->mimetype;
+
+        // show message headers
+        if ($recursive && is_array($structure->headers) && isset($structure->headers['subject'])) {
+            $c = new stdClass;
+            $c->type = 'headers';
+            $c->headers = &$structure->headers;
+            $this->parts[] = $c;
+        }
+
+        // Allow plugins to handle message parts
+        $plugin = $this->app->plugins->exec_hook('message_part_structure',
+            array('object' => $this, 'structure' => $structure,
+                'mimetype' => $mimetype, 'recursive' => $recursive));
+
+        if ($plugin['abort'])
+            return;
+
+        $structure = $plugin['structure'];
+        list($message_ctype_primary, $message_ctype_secondary) = explode('/', $plugin['mimetype']);
+
+        // print body if message doesn't have multiple parts
+        if ($message_ctype_primary == 'text' && !$recursive) {
+            $structure->type = 'content';
+            $this->parts[] = &$structure;
+
+            // Parse simple (plain text) message body
+            if ($message_ctype_secondary == 'plain')
+                foreach ((array)$this->uu_decode($structure) as $uupart) {
+                    $this->mime_parts[$uupart->mime_id] = $uupart;
+                    $this->attachments[] = $uupart;
+                }
+        }
+        // the same for pgp signed messages
+        else if ($mimetype == 'application/pgp' && !$recursive) {
+            $structure->type = 'content';
+            $this->parts[] = &$structure;
+        }
+        // message contains (more than one!) alternative parts
+        else if ($mimetype == 'multipart/alternative'
+            && is_array($structure->parts) && count($structure->parts) > 1
+        ) {
+            // get html/plaintext parts
+            $plain_part = $html_part = $print_part = $related_part = null;
+
+            foreach ($structure->parts as $p => $sub_part) {
+                $sub_mimetype = $sub_part->mimetype;
+
+                // check if sub part is
+                if ($sub_mimetype == 'text/plain')
+                    $plain_part = $p;
+                else if ($sub_mimetype == 'text/html')
+                    $html_part = $p;
+                else if ($sub_mimetype == 'text/enriched')
+                    $enriched_part = $p;
+                else if (in_array($sub_mimetype, array('multipart/related', 'multipart/mixed', 'multipart/alternative')))
+                    $related_part = $p;
+            }
+
+            // parse related part (alternative part could be in here)
+            if ($related_part !== null && !$this->parse_alternative) {
+                $this->parse_alternative = true;
+                $this->parse_structure($structure->parts[$related_part], true);
+                $this->parse_alternative = false;
+
+                // if plain part was found, we should unset it if html is preferred
+                if ($this->opt['prefer_html'] && count($this->parts))
+                    $plain_part = null;
+            }
+
+            // choose html/plain part to print
+            if ($html_part !== null && $this->opt['prefer_html']) {
+                $print_part = &$structure->parts[$html_part];
+            }
+            else if ($enriched_part !== null) {
+                $print_part = &$structure->parts[$enriched_part];
+            }
+            else if ($plain_part !== null) {
+                $print_part = &$structure->parts[$plain_part];
+            }
+
+            // add the right message body
+            if (is_object($print_part)) {
+                $print_part->type = 'content';
+                $this->parts[] = $print_part;
+            }
+            // show plaintext warning
+            else if ($html_part !== null && empty($this->parts)) {
+                $c = new stdClass;
+                $c->type            = 'content';
+                $c->ctype_primary   = 'text';
+                $c->ctype_secondary = 'plain';
+                $c->body            = rcube_label('htmlmessage');
+
+                $this->parts[] = $c;
+            }
+
+            // add html part as attachment
+            if ($html_part !== null && $structure->parts[$html_part] !== $print_part) {
+                $html_part = &$structure->parts[$html_part];
+                $html_part->filename = rcube_label('htmlmessage');
+                $html_part->mimetype = 'text/html';
+
+                $this->attachments[] = $html_part;
+            }
+        }
+        // this is an ecrypted message -> create a plaintext body with the according message
+        else if ($mimetype == 'multipart/encrypted') {
+            $p = new stdClass;
+            $p->type            = 'content';
+            $p->ctype_primary   = 'text';
+            $p->ctype_secondary = 'plain';
+            $p->body            = rcube_label('encryptedmessage');
+            $p->size            = strlen($p->body);
+
+            $this->parts[] = $p;
+        }
+        // message contains multiple parts
+        else if (is_array($structure->parts) && !empty($structure->parts)) {
+            // iterate over parts
+            for ($i=0; $i < count($structure->parts); $i++) {
+                $mail_part      = &$structure->parts[$i];
+                $primary_type   = $mail_part->ctype_primary;
+                $secondary_type = $mail_part->ctype_secondary;
+
+                // real content-type of message/rfc822
+                if ($mail_part->real_mimetype) {
+                    $part_orig_mimetype = $mail_part->mimetype;
+                    $part_mimetype = $mail_part->real_mimetype;
+                    list($primary_type, $secondary_type) = explode('/', $part_mimetype);
+                }
+                else
+                    $part_mimetype = $mail_part->mimetype;
+
+                // multipart/alternative
+                if ($primary_type == 'multipart') {
+                    $this->parse_structure($mail_part, true);
+
+                    // list message/rfc822 as attachment as well (mostly .eml)
+                    if ($part_orig_mimetype == 'message/rfc822' && !empty($mail_part->filename))
+                        $this->attachments[] = $mail_part;
+                }
+                // part text/[plain|html] or delivery status
+                else if ((($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment') ||
+                    in_array($part_mimetype, array('message/delivery-status', 'text/rfc822-headers', 'message/disposition-notification'))
+                ) {
+                    // Allow plugins to handle also this part
+                    $plugin = $this->app->plugins->exec_hook('message_part_structure',
+                        array('object' => $this, 'structure' => $mail_part,
+                            'mimetype' => $part_mimetype, 'recursive' => true));
+
+                    if ($plugin['abort'])
+                        continue;
+
+                    if ($part_mimetype == 'text/html') {
+                        $got_html_part = true;
+                    }
+
+                    $mail_part = $plugin['structure'];
+                    list($primary_type, $secondary_type) = explode('/', $plugin['mimetype']);
+
+                    // add text part if it matches the prefs
+                    if (!$this->parse_alternative ||
+                        ($secondary_type == 'html' && $this->opt['prefer_html']) ||
+                        ($secondary_type == 'plain' && !$this->opt['prefer_html'])
+                    ) {
+                        $mail_part->type = 'content';
+                        $this->parts[] = $mail_part;
+                    }
+
+                    // list as attachment as well
+                    if (!empty($mail_part->filename))
+                        $this->attachments[] = $mail_part;
+                }
+                // part message/*
+                else if ($primary_type == 'message') {
+                    $this->parse_structure($mail_part, true);
+
+                    // list as attachment as well (mostly .eml)
+                    if (!empty($mail_part->filename))
+                        $this->attachments[] = $mail_part;
+                }
+                // ignore "virtual" protocol parts
+                else if ($primary_type == 'protocol') {
+                    continue;
+                }
+                // part is Microsoft Outlook TNEF (winmail.dat)
+                else if ($part_mimetype == 'application/ms-tnef') {
+                    foreach ((array)$this->tnef_decode($mail_part) as $tpart) {
+                        $this->mime_parts[$tpart->mime_id] = $tpart;
+                        $this->attachments[] = $tpart;
+                    }
+                }
+                // part is a file/attachment
+                else if (preg_match('/^(inline|attach)/', $mail_part->disposition) ||
+                    $mail_part->headers['content-id'] ||
+                    ($mail_part->filename &&
+                        (empty($mail_part->disposition) || preg_match('/^[a-z0-9!#$&.+^_-]+$/i', $mail_part->disposition)))
+                ) {
+                    // skip apple resource forks
+                    if ($message_ctype_secondary == 'appledouble' && $secondary_type == 'applefile')
+                        continue;
+
+                    // part belongs to a related message and is linked
+                    if ($mimetype == 'multipart/related'
+                        && ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])) {
+                        if ($mail_part->headers['content-id'])
+                            $mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']);
+                        if ($mail_part->headers['content-location'])
+                            $mail_part->content_location = $mail_part->headers['content-base'] . $mail_part->headers['content-location'];
+
+                        $this->inline_parts[] = $mail_part;
+                    }
+                    // attachment encapsulated within message/rfc822 part needs further decoding (#1486743)
+                    else if ($part_orig_mimetype == 'message/rfc822') {
+                        $this->parse_structure($mail_part, true);
+
+                        // list as attachment as well (mostly .eml)
+                        if (!empty($mail_part->filename))
+                            $this->attachments[] = $mail_part;
+                    }
+                    // regular attachment with valid content type
+                    // (content-type name regexp according to RFC4288.4.2)
+                    else if (preg_match('/^[a-z0-9!#$&.+^_-]+\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) {
+                        if (!$mail_part->filename)
+                            $mail_part->filename = 'Part '.$mail_part->mime_id;
+
+                        $this->attachments[] = $mail_part;
+                    }
+                    // attachment with invalid content type
+                    // replace malformed content type with application/octet-stream (#1487767)
+                    else if ($mail_part->filename) {
+                        $mail_part->ctype_primary   = 'application';
+                        $mail_part->ctype_secondary = 'octet-stream';
+                        $mail_part->mimetype        = 'application/octet-stream';
+
+                        $this->attachments[] = $mail_part;
+                    }
+                }
+                // attachment part as message/rfc822 (#1488026)
+                else if ($