Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
This document is not UTF8. It was detected as ISO-8859-1 (Latin 1) and converted to UTF8 for display.
diff --git a/CHANGELOG b/CHANGELOG
index eaffa7099..3d0d60cc1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,1667 +1,1675 @@
CHANGELOG Roundcube Webmail
===========================
+- Cache LDAP's user_specific search and use vlv for better performance (#1489186)
+- LDAP: auto-detect and use VLV indices for all search operations
+- LDAP: additional group configuration options for address books
+- LDAP: separated address book implementation from a generic LDAP wrapper class
+- Allow address books to browse a multi-level group hierarchy in the contacts list
+- Fix so install do not fail when one of DB driver checks fails but other drivers exist (#1489178)
+- Fix so exported vCard specifies encoding in v3-compatible format (#1489183)
- Fix session issues when local and database time differs (#1486132)
- Fix thread cache syncronization/validation (#1489028)
+- Added feature to import messages to the currently selected folder
- Add option show_real_foldernames to disable localization of special folders
- Fix database cache expunge issues (#1489149)
- Fix date format issues on MS SQL Server (#1488918)
- Add imap_cache_ttl option to configure TTL of imap_cache
- Make LDAP cache engine configurable via ldap_cache and ldap_cache_ttl options
- Fix "duplicate entry" errors on inserts to imap cache tables (#1489146)
- Improved handling of Reply-To/Bcc addresses of identity in compose form (#1489016)
- Added user preference to open all popups as standard windows
- Implemented shared cache (rcube_cache_shared)
- Change Reply-All button label/title when mailing list is detected (#1488938)
- Fix SMTP connection using IPv6 address in smtp_server option (#1489024)
- Added attachment_reminder plugin
- Make PHP code eval() free, use create_function()
- Add option to display email address together with a name in mail preview (#1488732)
- Support CSV import from Atmail (#1489045)
- Add db_prefix configuration option in place of db_table_*/db_sequence_* options
- Make possible to use db_prefix for schema initialization in Installer (#1489067)
- Fix updatedb.sh script so it recognizes also table prefix for external DDL files
- Fix parsing invalid date string (#1489035)
- Add "with attachment" option to messages list filter (#1485382)
- Call resize handler in intervals to prevent lags and double onresize calls in Chrome (#1489005)
- Add rel="noreferrer" for links in displayed messages (#1484686)
- Add ability to toggle between HTML and text while viewing a message (#1486939)
- Remove "HTML message" from attachments list while viewing a message in text mode (#1486939)
- Support IMAP MOVE extension [RFC 6851]
- Add attachment menu with Open and Download options (#1488975)
- Display user-friendly message on IMAP "over quota" errors (#1484164)
- Extended archive plugin with user-configurable options to store messages into subfolders
- Fix export of selected contacts from search result (#1488905)
- Feature to export only selected contacts from addressbook (by Phil Weir)
RELEASE 0.9.2
-------------
- Fix image thumbnails display in print mode (#1489134)
- Fix height of message headers block (#1489108)
- Fix timeout issue on drag&drop uploads (#1489170)
- Fix default sorting of threaded list when THREAD=REFS isn't supported
- Fix list mode switch to 'List' after saving list settings in Larry skin (#1489164)
- Fix error when there's no writeable addressbook source (#1489162)
- Fix zipdownload plugin issue with filenames charset (#1489156)
- Fix so non-inline images aren't skipped on forward (#1489150)
- Fix "null" instead of empty string on messages list in IE10 (#1489145)
- Fix legacy options handling
- Fix so bounces addresses in Sender headers are skipped on Reply-All (#1489011)
- Fix bug where serialized strings were truncated in PDO::quote() (#1489142)
- Fix displaying messages with invalid self-closing HTML tags (#1489137)
- Fix PHP warning when responding to a message with many Return-Path headers (#1489136)
- Fix unintentional compose window resize (#1489114)
- Fix performance regression in text wrapping function (#1489133)
- Fix connection to posgtres db using unix socket (#1489132)
- Fix handling of comma when adding contact from contacts widget (#1489107)
- Fix bug where a message was opened in both preview pane and new window on double-click (#1489122)
- Fix fatal error when xdebug.max_nesting_level was exceeded in rcube_washtml (#1489110)
- Fix PHP warning in html_table::set_row_attribs() in PHP 5.4 (#1489094)
- Fix invalid option selected in default_font selector when font is unset (#1489112)
- Fix displaying contact with ID divisible by 100 in sql addressbook (#1489121)
- Fix browser warnings on PDF plugin detection (#1489118)
- Fix fatal error when parsing UUencoded messages (#1489119)
RELEASE 0.9.1
-------------
- Better German labels for from/to to avoid conflicts with 'sender' (#1489084)
- Fix problem where security warning was displayed for valid images with image/jpg type (#1489097)
- Fix handling of invalid email addresses in headers (#1489092)
- Fix IMAP connection issue with default_socket_timeout < 0 and imap_timeout < 0 (#1489090)
- Fix various PHP code bugs found using static analysis (#1489086)
- Fix backslash character handling on vCard import (#1489085)
- Fix csv import from Thunderbird with French localization (#1489059)
- Fix messages list focus issue in Opera and Webkit (#1489058)
- Fix Reply-To header handling in Reply-All action (#1489037)
- Fix so Sender: address is added to Cc: field on reply to all (#1489011)
- Fix so addressbook_search_mode works also for group search (#1489079)
- Fix removal of a contact from a group in LDAP addressbook (#1489081)
- Inlcude SQL query in the log on SQL error (#1489064)
- Fix handling untagged responses in IMAP FETCH - "could not load message" error (#1489074)
- Fix very small window size in Chrome (#1488931)
- Fix list page reset when viewing a message in Larry skin (#1489076)
- Fix min_refresh_interval handling on preferences save (#1489073)
- Fix PDF support detection for Firefox PDF.js (#1488972)
- Fix possible collision in generated thumbnail cache key (#1489069)
- Fix exit code on bootsrap errors in CLI mode (#1489044)
- Fix error handling in CLI mode, use STDERR and non-empty exit code (#1489043)
- Fix error when using check_referer=true
- Fix incorrect handling of some specific links (#1489060)
- Fix incorrect handling of leading spaces in text wrapping
- Fix unintentional messages list jumps on click in Internet Explorer (#1489056)
- Fix list of required configuration options (#1489055)
- Fix DB error when creating a new contact and a group is selected (#1489051)
- Fix handling of deprecated boolean value of reply_mode option (#1489052)
RELEASE 0.9.0
-------------
- Fix display of HTML entities in protected folder name (#1489042)
- Set minimal permissions to temp files (#1488996)
- Improve content check for embedded images without filename (#1489029)
- Fix handling of invalid characters in message headers and output (#1489032)
- Fix selecting collapsed rows on select-all (#1489036)
- Avoid race-conditions with concurrent attachment uploads (#1488422)
- Fix possible header duplicates when using additional headers (#1489033)
- Fix session issues with use_https=true (#1488986)
- Fix blockquote width in sent mail (#1489031)
- Fix keyboard events on list widgets in Internet Explorer (#1489025)
RELEASE 0.9-rc2
---------------
- Fix security issue in save-pref command
- Remove sig_above configuration option, use reply_mode only (#1489001)
- Refresh current folder in opener window after draft save or message sent (#1488997)
- Fix saving draft just after entering compose window (#1489012)
- Fix javascript error in IE9 when loading form with placeholders into an iframe (#1489008)
- Fix handling of some conditional comment tags in HTML message (#1489004)
- Fix so forward as attachment works if additional attachment is added by message_compose hook (#1489000)
- Better handling of session errors in ajax requests (#1488960)
- Fix HTML part detection for some specific message structures (#1488992)
- Don't show fake address - phishing prevention (#1488981)
- Fix forward as attachment bug with editormode != 1 (#1488991)
- Fix LIMIT/OFFSET queries handling on MS SQL Server (#1488984)
- Fix so task name can really contain all from a-z0-9_- characters (#1488941)
- Fix javascript errors when working in a page opened with taget="_blank"
- Mention SQLite database format change in UPGRADING file (#1488983)
- Increase maxlength to 254 chars for email input fields in addressbook (#1488987)
- Fix thumbnail size when GD extension is used for image resize (#1488985)
- Display notice that message is encrypted also for application/pkcs7-mime messages (#1488526)
RELEASE 0.9-rc
--------------
- Fix plain text spellchecker incorrect highlighting in non-ASCII text (#1488973)
- Add workaround for invalid message charset detection by IMAP servers (#1488968)
- Fix NUL characters in content-type of ms-tnef attachment (#1488964)
- Fix regression in handling LDAP contact identifiers (#1488959)
- Updated translations from Transifex
- Fix buggy error template in a frame (#1488938)
- Add addressbook widget on compose page in classic skin
- Add search box to compose address book widget (#1488381)
- Fix login in case when default_host is an array with one element (#1488928)
- Use LDAP fallback hosts on connect + bind instead of ldap_connect() only.
- Add config option for LDAP bind timeout (sets LDAP_OPT_NETWORK_TIMEOUT option)
- Submit Addressbook advanced search form with Enter key (#1488568)
- Also block remote images in HTML part view (#1488827)
- Improved database schema upgrade procedure, added updatedb.sh script
- Force autocommit mode in mysql database driver (#1488902)
RELEASE 0.9-beta
----------------
- Fix searching by date in address book (#1488888)
- Improve charset detection by prioritizing charset according to user language (#1485669)
- Fix handling of escaped separator in vCard file (#1488896)
- Add option to use envelope From address for MDN responses (#1488880)
- Add possibility to search in message body only (#1488770)
- Support "multipart/relative" as an alias for "multipart/related" type (#1488886)
- Display PGP/MIME signature attachments as "Digital Signature" (#1488570)
- Workaround UW-IMAP bug where hierarchy separator is added to the shared folder name (#1488879)
- Fix version comparisons with -stable suffix (#1488876)
- Add unsupported alternative parts to attachments list (#1488870)
- Add Compose button on message view page (#1488747)
- Display 'Sender' header in message preview
- Plugin API: Added message_before_send hook
- Fix contact copy/add-to-group operations on search result (#1488862)
- Use matching identity in MDN response (#1488864)
- Fix handling of signatures on draft edit (#1488798)
- Fix so compacting of non-empty folder is possible also when messages list is empty (#1488858)
- Allow forwarding of multiple emails (#1486854)
- Fix big memory consumption of DB layer (#1488856)
- Fix broken message/part bodies when FETCH response contains more untagged lines (#1488836)
- Fix empty email on identities list after identity update (#1488834)
- Add new identities_level: (4) one identity with possibility to edit only signature
- Use Delivered-To and Envelope-To headers for identity selection (#1488840, #1488553)
- Fix XSS vulnerability using Flash files (#1488828)
- Always save drafts with format=flowed in order to keep original line wraps (#1488799)
- Select default_addressbook on the list in Address Book (#1488280)
- Fix so mobile phone has TYPE=CELL in exported vCard (#1488812)
- Support contacts import from CSV file (#1486399)
- Improved keep-alive action. Now the interval is based on session_lifetime (#1488507)
- Added cross-task 'refresh' request for system state updates (#1488507)
- Renamed config options: keep_alive to refresh_interval, min_keep_alive to min_refresh_interval
- Fix handling of text/enriched content on message reply/forward/edit
- Option to display attached images as thumbnails below message body
- Upgraded to jQuery 1.8.3 and jQuery UI 1.9.1
- Add config option to automatically generate LDAP attributes for new entries
- Add user settings to open message view and compose form in new windows (#1485486)
- Better client-side timezone detection using the jsTimezoneDetect library (#1488725)
- Add option to disable saving sent mail in Sent folder - no_save_sent_messages (#1488686)
- Fix handling dont_override with message_sort_col and message_sort_order settings (#1488760)
- Fix handling of URLs with asterisk characters (#1488759)
- Remove automatic to-lowercase conversion of usernames (#1488715)
- Plugin API: Add 'email_list' argument for identities data in user_create hook
- Integrated zipdownload plugin to download all attachments (#1445509)
- Fix HTML special characters handling in message list/header display (#1488523)
- List related text/html part as attachment in plain text mode (#1488677)
- Use IMAP BINARY (RFC3516) extension to fetch message/part bodies
- Fix folder creation under public namespace root (#1488665)
- Fix so "Edit as new" on draft creates a new message (#1488687)
- Fix invalid error message on deleting mail from read only folder (#1488694)
- Replace data URIs of images (pasted in HTML editor) with inline attachments (#1488502)
- Remove (too big) min-width on mail screen
- Added template object 'frame'
- Add option to enable HTML editor on forwarding (#1488517)
- Add option to not include original message on reply, rename option top_posting to reply_mode (#1485149)
- Added session_path config option and unified cookies settings in javascript
- Added "Undeleted" option to messages list filter
- Rewritten test scripts for PHPUnit
- Add new DB abstraction layer based on PHP PDO, supporting SQLite3 (#1488332)
- Removed PEAR::MDB2 package
- Removed users.alias column, added option ('user_aliases')
to use email address from identities as username (#1488581)
- Removed redundant cache.cache_id column (#1488528)
- Fix order of attachments in sent mail (#1488423)
- Fix Shift + delete button does not permanently delete messages (#1488243)
- Add Content-Length for attachments where possible (#1485478)
- Fix attachment sizes in message print page and attachment preview page (#1488515)
- Add mail attachments using drag & drop on HTML5 enabled browsers
- Add workaround for invalid BODYSTRUCTURE response - parse message with Mail_mimeDecode package (#1485585)
- Display Tiff as Jpeg in browsers without Tiff support (#1488452)
- Don't display Pdf/Tiff/Flash attachments inline without browser support (#1488452, #1487929)
- Add is_escaped attribute for html_select and html_textarea (#1488485)
- Fix issue where draft auto-save wasn't executed after some inactivity time
- Add vCard import from multiple files at once (#1488015)
- Roundcube Framework:
Add possibility to replace IMAP driver with custom class
Add IMAP auto-connection feature, improving performance with caching enabled
Replace imap_init hook with storage_init (with additional 'driver' argument)
Improved performance by caching IMAP server's capabilities in session
Unified global functions naming (rcube_ prefix)
Better classes separation
Framework files moved to lib/Roundcube
RELEASE 0.8.5
-------------
- Fix #countcontrols issue in IE<=8 when text is very long (#1488890)
- Fix unwanted horizontal scrollbar in message preview header (#1488866)
- Add workaround for IE<=8 bug where Content-Disposition:inline was ignored (#1488844)
- Fix XSS vulnerability in vbscript: and data:text links handling (#1488850)
- Fix absolute positioning in HTML messages (#1488819)
- Fix cache (in)validation after setting \Deleted flag
- Fix keybord events on messages list in opera browser (#1488823)
- Fix selection of collapsed thread rows (#1488772)
- Fix wrapping of quoted text with format=flowed (#1488177)
RELEASE 0.8.4
-------------
- Fix regression where unintentional page reload was done after request abort (#1488802)
- Fix XSS vulnerability in handling of text/enriched messages (#1488806)
- Fix handling of 'media' attribute on linked css (#1488789)
- Fix excessive LFs at the end of composed message with top_posting=true (#1488797)
- Fix bug where leading blanks were stripped from quoted lines (#1488795)
RELEASE 0.8.3
-------------
- Fix AREA links handling (#1488792)
- Fix possible HTTP DoS on error in keep-alive requests (#1488782)
- Fix compatybility with MDB2 2.5.0b4 (#1488779)
- Fix a bug where saving a message in INBOX wasn't possible
- Fix HTML part detection in messages with attachments (#1488769)
- Fix bug where wrong words were highlighted on spell-before-send check
- Fix scrolling quirk in email preview frame using Opera 12 (#1488763)
- Fix displaying of multipart/alternative messages with empty parts (#1488750)
- Fix threaded list sorting on PHP < 5.2.9 (#1488748)
- Fix Warning: htmlspecialchars(): charset `RCMAIL_CHARSET' not supported warning in Installer (#1488744)
RELEASE 0.8.2
-------------
- Fix XSS vulnerability from HTTP User-Agent header (#1488737)
- Force fonts in compose fields to be all the same (#1488690)
- Fix handling vCard entries with TEL;TYPE=CELL (#1488728)
- Fix error where session wasn't updated after folder rename/delete (#1488692)
- Fix PLAIN authentication for some IMAP servers (#1488674)
- Fix encoding vCard file when contains PHOTO;ENCODING=b (#1488683)
- Fix focus issue in IE when selecting message row (#1488620)
- Add full headers view in message preview window (#1488538)
- Fix message display page issues - unified with message preview (#1488590, #1488642)
- Fix displaying all headers when they contain malformed characters (#1488666)
- Fix decoding of HTML messages with UTF-16 charset specified (#1488654)
- Fix quota capability detection so it can be overwritten by a plugin (#1488655)
- Fix identity selection on reply (#1488101)
- Fix Larry's messages list filter in IE (#1488632)
- Fix more IE issues by disabling Compat. mode with X-UA-Compatible meta tag (#1488626)
- Fix setting locales under Solaris - use additional .UTF-8 suffix (#1488628)
- Fix email address validation for addresses with IP address in domain part
- Fix Larry skin issues in IE7 compat. mode (#1488618)
- Fix so subscribed non-existing/non-accessible shared folder can be unsubscribed
RELEASE 0.8.1
-------------
- Fix bug where domain name was converted to lower-case even with login_lc=false (#1488593)
- Fix lower-casing email address on replies (#1488598)
- Fix line separator in exported messages (#1488603)
- Fix XSS issue where plain signatures wasn't secured in HTML mode (#1488613)
- Fix XSS issue where href="javascript:" wasn't secured (#1488613)
- Fix impossible to create message with empty plain text part (#1488610)
- Fix stripped apostrophes when replying in plain text to HTML message (#1488606)
- Fix inactive Save search option after advanced search (#1488607)
- Fix Remove from group option is active for contact search result (#1488608)
- Disable autocapitalization in login form on iPad/iPhone (#1488609)
- Fix focus on the list when list row is clicked (#1488600)
- Added separate From and To columns apart from smart From/To column (#1486891)
- Fix fallback to Larry skin when configured skin isn't available (#1488591)
- Fix (workaround) delete operations with some versions of memcache (#1488592)
- Fix (disable) request validation for spell and spell_html actions
RELEASE 0.8.0
-------------
- Don't show product version on login screen (can be enabled by config)
- Renamed old default skin to 'classic'. Larry is the new default skin.
- Support connections to memcached socket file (#1488577)
- Enable TinyMCE inlinepopups plugin
- Update to TinyMCE 3.5.6
- Correctly escape localized labels in javascript variable (#1488567)
- Update Net_SMTP/Auth_SASL packages to fix Digest-MD5/Cram-MD5 authentication (#1488571)
- Don't add attachments content into reply/forward/draft message body (#1488557)
- Fix 'no connection' errors on page unloads (#1488547)
- Plugin API: Add 'unauthenticated' hook (#1488138)
- Show explicit error message when provided hostname is invalid (#1488550)
- Fix wrong compose screen elements focus in IE9 (#1488541)
- Fix fatal error when date.timezone isn't set (#1488546)
- Update to TinyMCE 3.5.4.1
- Better icons with distinct shapes for priority columns (#1488377)
- Show dedicated icon for multipart/report messages (#1488524)
- Properly hide text of icon links/buttons (#1488534)
- Fix handling of unitless CSS size values in HTML message (#1488535)
- Fix removing contact photo using LDAP addressbook (#1488420)
- Fix storing X-ANNIVERSARY date in vCard format (#1488527)
- Update to Mail_Mime-1.8.5 (#1488521)
- Fix XSS vulnerability in message subject handling using Larry skin (#1488519)
- Fix handling of links with various URI schemes e.g. "skype:" (#1488106)
- Fix handling of links inside PRE elements on html to text conversion
- Fix indexing of links on html to text conversion
- Decode header value in rcube_mime::get() by default (#1488511)
- Fix errors with enabled PHP magic_quotes_sybase option (#1488506)
- Fix SQL query for contacts listing on MS SQL Server (#1488505)
- Fix window.resize handler on IE8 and Opera (#1488453)
- Don't let error message popups cover the login form (#1488500)
- Update to TinyMCE 3.5.2
- Don't show errors when moving contacts into groups they are already in (#1488493)
- Make folders with unread messages in subfolders bold again (#1486793)
- Abbreviate long attachment file names with ellipsis (#1488499)
- Fix html2text conversion of strong|b|a|th|h tags when used in upper case
- Add listcontrols template container in Larry skin (#1488498)
- Fix host autoselection when default_host is an array (#1488495)
- Move messages forwarding mode setting into Preferences
- Fix HTML entities handling in HTML editor (#1488483)
- Fix listing shared folders on Courier IMAP (#1488466)
RELEASE 0.8-rc
--------------
- Added new translations in Belarusian, Interlingua and Malayalam
- Flipped compose options arrow (#1488474)
- Fix handling of large uuencode attachments (#1488473)
- Fix handling of "usemap" attribute (#1488472)
- Fix handling of some HTML tags e.g. IMG (#1488471)
- Use similar language as a fallback for plugin localization (#1488401)
- Fix issue where signature wasn't re-added on draft compose (#1488322)
- Update to TinyMCE 3.5 (#1488459)
- Fixed multi-threaded autocompletion when number of threads > number of sources
- Allow to configure the number of values allowed for each LDAP attribute
- Support for serialized LDAP address values (usually delimited with a $)
- Less restrictive session auth checks, repeat keep-alive requests on failure (#1488449)
- Fix redirect to mail/compose on re-login (#1488226)
- Add IE8 hack for messages list issue (#1487821)
- Fix handling errors on draft auto-save
- Fix importing vCard photo with ENCODING param specified (#1488432)
- Support mutliple name/email pairs for Bcc and Reply-To identity settings (#1488445)
- Set flexible width to login form fields (#1488418)
- Fix re-draw bug on list columns change in IE8 (#1487822)
- Allow mass-removal of addresses from a group (#1487748)
- Fix removing all contacts on import to LDAP addressbook
- Fix so "Back" from compose/show doesn't reset search request (#1488238)
- Add option to delete messages instead of moving to Trash when in Junk folder (#1486686)
- Fix invisible cursor when replying to a html message (#1487073)
- Reset IP stored in session when destroying session data (#1488056)
- Fix bug where memory_limit = -1 wasn't handled properly
- Support LDAP RFC2256's country object class read/write (#1488123)
- Upgraded to jQuery 1.7.2
- Image resize with GD extension (#1488383)
- Fix lack of warning when switching task in compose window (#1488399)
- Fix bug where it wasn't possible to enter ( or & characters in autocomplete fields
- Request all needed fields from address book backends (#1488394)
- Unified (single) spellchecker button
- Scroll long lists on drag&drop (#1485946)
- Copy all skins in installto script (#1488376)
RELEASE 0.8-beta
----------------
- Upgraded to jQuery 1.7.1 (#1488337) and jQuery UI 1.8.18
- Add Russian to the spellchecker languages list (#1488135)
- Remember custom skin selection after logout (#1488355)
- Make sure About tab is always the last tab (#1488257)
- Fix issue with folder creation under INBOX. namespace (#1488349)
- Added mailto: protocol handler registration link in User Preferences (#1486580)
- Handle identity details box with an iframe (#1487020)
- Fix issue where some text from original message was missing on reply (#1488340)
- Fix autoselect_host() for login (#1488297)
- Changed license to GNU GPLv3+ with exceptions for skins & plugins
- Added address book widget on compose screen
- Use proper timezones from PHP's internal timezonedb (#1485592)
- Add separate pagesize setting for mail messages and contacts (#1488269)
- Deprecate $DB, $USER, $IMAP global variables, Use $RCMAIL instead
- Add option to set default font for HTML message (#1484137)
- Fix issues with big memory allocation of IMAP results
- Prevent from memory_limit exceeding when trying to parse big messages bodies (#1487424)
- Add possibility to add SASL mechanisms for SMTP in smtp_connect hook (#1487937)
- Mark (with different color) folders with recent messages (#1486234)
- Added About tab in Settings
- TinyMCE updated to 3.4.6
RELEASE 0.7.2
-------------
- Fix encoding of attachment with comma in name (#1488389)
- Fix handling of % character in IMAP protocol (#1488382)
- Fix duplicate names handling in addressbook searches (#1488375)
- Fix displaying of HTML messages from Disqus (#1488372)
- Disable E_STRICT warnings on PHP 5.4
- Prevent from folder selection on virtual folder collapsing (#1488346)
- Fix automatic unsubscribe of non-existent folders
- Fix double-quotes handling in recipient names
- User configurable setting how to display contact names in list
- Make contacts list sorting configurable for the admin/user
- Fix parse errors in DDL files for MS SQL Server
- Revert SORT=DISPLAY support, removed by mistake (#1488327)
- Add lost translation label in de_DE (#1488315)
- Fix drafts update issues when edited from preview pane (#1488314)
- Fix wrong variable name in rcube_ldap.php (#1488302)
- Make mime type detection based on filename extension to be case-insensitive
- Fix failure on MySQL database upgrade from 0.7 - text column can't have default value (#1488300)
RELEASE 0.7.1
-------------
- Fix bug in handling of base href and inline content (#1488290)
- Fix SQL Error when saving a contact with many email addresses (#1488286)
- Fix strict email address searching if contact has more than one address
- Remove duplicated 'organization' label (#1488287)
- Fix so editor selector is hidden when 'htmleditor' is listed in 'dont_override'
- Fix wrong (long) label usage (#1488283)
- Fix handling of INBOX's subfolders in special folders config (#1488279)
- Add ifModule statement for setting Options -Indexes in .htaccess file (#1488274)
- Fix crashes with eAccelerator (#1488256)
- Fix searching on IMAP servers without CHARSET specifier support (#1488271)
- Fix expanding folders during drag&drop (#1488260)
- Fix wrong postgres sequence name in upgrade from 0.6
- Fix broken CREATE INDEX queries in SQLite DDL files (#1488255)
RELEASE 0.7
-----------
- Make Roundcube render the Email Standards Project Acid Test correctly
- Replace prompt() with jQuery UI dialog (#1485135)
- Fix navigation in messages search results
- Improved handling of some malformed values encoded with quoted-printable (#1488232)
- Add possibility to do LDAP bind before searching for bind DN
- Fix handling of empty <U> tags in HTML messages (#1488225)
- Add content filter for embedded attachments to protect from XSS on IE (#1487895)
- Use strpos() instead of strstr() when possible (#1488211)
- Fix handling HTML entities when converting HTML to text (#1488212)
- Fix fit_string_to_size() renders browser and ui unresponsive (#1488207)
- Fix handling of invalid characters in request (#1488124)
- Fix merging some configuration options in update.sh script (#1485864)
- Fix so TEXT key will remove all HEADER keys in IMAP SEARCH (#1488208)
- Fix handling contact photo url with https:// prefix (#1488202)
- Fix possible infinite redirect on attachment preview (#1488199)
- Improved clickjacking protection for browsers which don't support X-Frame-Options headers
- Fixed bug where similar folder names were highlighted wrong (#1487860)
- Fixed bug in handling link with '!' character in it (#1488195)
- Fixed bug where session ID's length was limited to 40 characters (#1488196)
- TinyMCE security issue: removed moxieplayer (embedding flv and mp4 is not supported anymore)
RELEASE 0.7-beta
----------------
- Fix handling of HTML form elements in messages (#1485137)
- Fix regression in setting recipient to self when replying to a Sent message (#1487074)
- Fix listing of folders in hidden namespaces (#1486796)
- Don't consider \Noselect flag when building folders tree (#1488004)
- Fix sorting autocomplete results (#1488084)
- Add option to set session name (#1486433)
- Add option to skip alternative email addresses in autocompletion
- Fix inconsistent behaviour of Compose button in Drafts folder, add Edit button for drafts
- Fix problem with parsing HTML message body with non-unicode characters (#1487813)
- Add option to define matching method for addressbook search (#1486564, #1487907)
- Make email recipients separator configurable
- Fix so folders with \Noinferiors attribute aren't listed in parent selector
- Fix handling of curly brackets in URLs (#1488168)
- Fix handling of dates (birthday/anniversary) in contact data (#1488147)
- Fix error on opening searched LDAP contact (#1488144)
- Fix redundant line break in flowed format (#1488146)
- 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
diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist
index 1c47c8170..1812105c1 100644
--- a/config/main.inc.php.dist
+++ b/config/main.inc.php.dist
@@ -1,915 +1,936 @@
<?php
/*
+-----------------------------------------------------------------------+
| Main configuration file |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2011, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
+-----------------------------------------------------------------------+
*/
$rcmail_config = array();
// ----------------------------------
// LOGGING/DEBUGGING
// ----------------------------------
// system error reporting, sum of: 1 = log; 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 - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %s - domain name after the '@' from e-mail address provided at login screen
// For example %n = mail.domain.tld, %t = domain.tld
// WARNING: After hostname change update of mail_host column in users table is
// required to match old user data records with the new host.
$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 null 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;
// Some server configurations (e.g. Courier) doesn't list folders in all namespaces
// Enable this option to force listing of folders in all namespaces
$rcmail_config['imap_force_ns'] = 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;
// Lifetime of IMAP indexes cache. Possible units: s, m, h, d, w
$rcmail_config['imap_cache_ttl'] = '10d';
// Lifetime of messages cache. Possible units: s, m, h, d, w
$rcmail_config['messages_cache_ttl'] = '10d';
// ----------------------------------
// 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 - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
$rcmail_config['smtp_server'] = '';
// SMTP port (default is 25; use 587 for STARTTLS or 465 for the
// deprecated SSL over SMTP (aka SMTPS))
$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)
// Note: There's a known issue where using ssl connection with
// timeout > 0 causes connection errors (https://bugs.php.net/bug.php?id=54511)
$rcmail_config['smtp_timeout'] = 0;
// ----------------------------------
// LDAP
// ----------------------------------
// Type of LDAP cache. Supported values: 'db', 'apc' and 'memcache'.
$rcmail_config['ldap_cache'] = 'db';
// Lifetime of LDAP cache. Possible units: s, m, h, d, w
$rcmail_config['ldap_cache_ttl'] = '10m';
// ----------------------------------
// 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;
// don't allow these settings to be overriden by the user
$rcmail_config['dont_override'] = array();
// provide an URL where a user can get support for this Roundcube installation
// PLEASE DO NOT LINK TO THE ROUNDCUBE.NET WEBSITE HERE!
$rcmail_config['support_url'] = '';
// replace Roundcube logo with this image
// specify an URL relative to the document root of this Roundcube installation
$rcmail_config['skin_logo'] = null;
// 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;
// Enables possibility to log in using email address from user identities
$rcmail_config['user_aliases'] = 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/';
// 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
// this option is mutually exclusive to 'force_https' and only either one of them should be set to true.
$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;
// Forces conversion of logins to lower case.
// 0 - disabled, 1 - only domain part, 2 - domain and local part.
// If users authentication is case-insensitive this must be enabled.
// Note: After enabling it all user records need to be updated, e.g. with query:
// UPDATE users SET username = LOWER(username);
$rcmail_config['login_lc'] = 2;
// Includes should be interpreted as PHP files
$rcmail_config['skin_include_php'] = false;
// display software version on login screen
$rcmail_config['display_version'] = false;
// Session lifetime in minutes
$rcmail_config['session_lifetime'] = 10;
// Session domain: .example.org
$rcmail_config['session_domain'] = '';
// Session name. Default: 'roundcube_sessid'
$rcmail_config['session_name'] = null;
// Session path. Defaults to PHP session.cookie_path setting.
$rcmail_config['session_path'] = null;
// Backend to use for session storage. Can either be 'db' (default), 'memcache' or 'php'
// 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
// Setting this value to 'php' will use the default session save handler configured in PHP
$rcmail_config['session_storage'] = 'db';
// Use these hosts for accessing memcached
// Define any number of hosts in the form of hostname:port or unix:///path/to/socket.file
$rcmail_config['memcache_hosts'] = null; // e.g. array( 'localhost:11211', '192.168.1.12:11211', 'unix:///var/tmp/memcached.sock' );
// 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 - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = 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, %t = 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;
// According to RFC2298, return receipt envelope sender address must be empty.
// If this option is true, Roundcube will use user's identity as envelope sender for MDN responses.
$rcmail_config['mdn_use_from'] = false;
// 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
// 4 - one identity with possibility to edit only signature
$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
// Path to a local mime magic database file for PHPs finfo extension.
// Set to null if the default path should be used.
$rcmail_config['mime_magic'] = null;
// Absolute path to a local mime.types mapping table file.
// This is used to derive mime-types from the filename extension or vice versa.
// Such a file is usually part of the apache webserver. If you don't find a file named mime.types on your system,
// download it from http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
$rcmail_config['mime_types'] = null;
// path to imagemagick identify binary
$rcmail_config['im_identify_path'] = null;
// path to imagemagick convert binary
$rcmail_config['im_convert_path'] = null;
// Size of thumbnails from image attachments displayed below the message content.
// Note: whether images are displayed at all depends on the 'inline_images' option.
// Set to 0 to display images in full size.
$rcmail_config['image_thumbnail_size'] = 240;
// 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;
// Disables saving sent messages in Sent folder (like gmail) (Default: false)
// Note: useful when SMTP server stores sent mail in user mailbox
$rcmail_config['no_save_sent_messages'] = 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', 'fromto', '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, fromto, cc, replyto, date, size, status, flag, attachment, 'priority'
$rcmail_config['list_cols'] = array('subject', 'status', 'fromto', '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_folders'] = array('INBOX', 'Drafts', 'Sent', 'Junk', 'Trash');
// Disable localization of the default folder names listed above
$rcmail_config['show_real_foldernames'] = false;
// automatically create the above listed default folders on first 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;
// Use this char/string to separate recipients when composing a new message
$rcmail_config['recipients_separator'] = ',';
// don't let users set pagesize to more than this value if set
$rcmail_config['max_pagesize'] = 200;
// Minimal value of user's 'refresh_interval' setting (in seconds)
$rcmail_config['min_refresh_interval'] = 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), 'ldap' and ''.
// 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.
// If set to '' then no address book will be displayed or only the
// addressbook which is created by a plugin (like CardDAV).
$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 - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
'hosts' => array('directory.verisign.com'),
'port' => 389,
- 'use_tls' => false,
+ 'use_tls' => false,
'ldap_version' => 3, // using LDAPv3
'network_timeout' => 10, // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
'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))'
// DN and password to bind as before searching for bind DN, if anonymous search is not allowed
'search_bind_dn' => '',
'search_bind_pw' => '',
// 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' => 'cn',
// 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
// for every attribute one can specify the number of values (limit) allowed.
// default is 1, a wildcard * means unlimited
'fieldmap' => array(
// Roundcube => LDAP:limit
'name' => 'cn',
'surname' => 'sn',
'firstname' => 'givenName',
'jobtitle' => 'title',
'email' => 'mail:*',
'phone:home' => 'homePhone',
'phone:work' => 'telephoneNumber',
'phone:mobile' => 'mobile',
'phone:pager' => 'pager',
+ 'phone:workfax' => 'facsimileTelephoneNumber',
'street' => 'street',
'zipcode' => 'postalCode',
'region' => 'st',
'locality' => 'l',
// if you country is a complex object, you need to configure 'sub_fields' below
'country' => 'c',
'organization' => 'o',
'department' => 'ou',
'jobtitle' => 'title',
'notes' => 'description',
+ 'photo' => 'jpegPhoto',
// these currently don't work:
- // 'phone:workfax' => 'facsimileTelephoneNumber',
- // 'photo' => 'jpegPhoto',
// 'manager' => 'manager',
// 'assistant' => 'secretary',
),
// Map of contact sub-objects (attribute name => objectClass(es)), e.g. 'c' => 'country'
'sub_fields' => array(),
// Generate values for the following LDAP attributes automatically when creating a new record
'autovalues' => array(
// 'uid' => 'md5(microtime())', // You may specify PHP code snippets which are then eval'ed
// 'mail' => '{givenname}.{sn}@mydomain.com', // or composite strings with placeholders for existing attributes
),
- '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.
- 'referrals' => true|false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
+ '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)
+ 'vlv_search' => false, // Use Virtual List View functions for autocompletion searches (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
+ 'config_root_dn' => 'cn=config', // Root DN to search config entries (e.g. vlv indexes)
+ '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.
+ 'referrals' => false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
// 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' => '',
- 'scope' => 'sub', // search mode: sub|base|list
- 'filter' => '(objectClass=groupOfNames)',
+ 'groups' => array(
+ 'base_dn' => '',
+ 'scope' => 'sub', // Search mode: sub|base|list
+ '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
+ 'member_attr' => 'member', // Name of the member attribute, e.g. uniqueMember
+ 'name_attr' => 'cn', // Attribute to be used as group name
+ 'member_filter' => '(objectclass=*)', // Optional filter to use when querying for group members
+ 'vlv' => false, // Use VLV controls to list groups
+ ),
+ // this configuration replaces the regular groups listing in the directory tree with
+ // a hard-coded list of groups, each listing entries with the configured base DN and filter.
+ // if the 'groups' option from above is set, it'll be shown as the first entry with the name 'Groups'
+ 'group_filters' => array(
+ 'departments' => array(
+ 'name' => 'Company Departments',
+ 'scope' => 'list',
+ 'base_dn' => 'ou=Groups,dc=mydomain,dc=com',
+ 'filter' => '(|(objectclass=groupofuniquenames)(objectclass=groupofurls))',
+ ),
+ 'customers' => array(
+ 'name' => 'Customers',
+ 'scope' => 'sub',
+ 'base_dn' => 'ou=Customers,dc=mydomain,dc=com',
+ 'filter' => '(objectClass=inetOrgPerson)',
+ ),
),
);
*/
// 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}';
// Matching mode for addressbook search (including autocompletion)
// 0 - partial (*abc*), default
// 1 - strict (abc)
// 2 - prefix (abc*)
// Note: For LDAP sources fuzzy_search must be enabled to use 'partial' or 'prefix' mode
$rcmail_config['addressbook_search_mode'] = 0;
// ----------------------------------
// 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'] = 'larry';
// Enables using standard browser windows (that can be handled as tabs)
// instead of popup windows
$rcmail_config['standard_windows'] = false;
// show up to X items in messages list view
$rcmail_config['mail_pagesize'] = 50;
// show up to X items in contacts list view
$rcmail_config['addressbook_pagesize'] = 50;
// sort contacts by this col (preferably either one of name, firstname, surname)
$rcmail_config['addressbook_sort_col'] = 'surname';
// the way how contact names are displayed in the list
// 0: display name
// 1: (prefix) firstname middlename surname (suffix)
// 2: (prefix) surname firstname middlename (suffix)
// 3: (prefix) surname, firstname middlename (suffix)
$rcmail_config['addressbook_name_listing'] = 0;
// use this timezone to display date/time
// valid timezone identifers are listed here: php.net/manual/en/timezones.php
// 'auto' will use the browser's timezone settings
$rcmail_config['timezone'] = 'auto';
// 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;
// open messages in new window
$rcmail_config['message_extwin'] = false;
// open message compose form in new window
$rcmail_config['compose_extwin'] = false;
// compose html formatted messages by default
// 0 - never, 1 - always, 2 - on reply to HTML message, 3 - on forward or reply to HTML message
$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 auto-refresh requests (in seconds)
// These are requests for system state updates e.g. checking for new messages, etc.
// Setting it to 0 disables the feature.
$rcmail_config['refresh_interval'] = 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'] = true;
// 0 - Do not expand threads
// 1 - Expand all threads automatically
// 2 - Expand only threads with unread messages
$rcmail_config['autoexpand_threads'] = 0;
// When replying:
// -1 - don't cite the original message
// 0 - place cursor below the original message
// 1 - place cursor above original message (top posting)
$rcmail_config['reply_mode'] = 0;
// 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;
// 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_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;
// Directly delete messages in Junk instead of moving to Trash
$rcmail_config['delete_junk'] = 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;
// Skip alternative email addresses in autocompletion (show one address per contact)
$rcmail_config['autocomplete_single'] = false;
// Default font for composed HTML message.
// Supported values: Andale Mono, Arial, Arial Black, Book Antiqua, Courier New,
// Georgia, Helvetica, Impact, Tahoma, Terminal, Times New Roman, Trebuchet MS, Verdana
$rcmail_config['default_font'] = 'Verdana';
// Enables display of email address with name instead of a name (and address in title)
$rcmail_config['message_show_email'] = false;
diff --git a/installer/check.php b/installer/check.php
index fcf35025e..bea8c42e0 100644
--- a/installer/check.php
+++ b/installer/check.php
@@ -1,246 +1,250 @@
<?php
if (!class_exists('rcube_install') || !is_object($RCI)) {
die("Not allowed! Please open installer/index.php instead.");
}
?>
<form action="index.php" method="get">
<?php
$required_php_exts = array(
'PCRE' => 'pcre',
'DOM' => 'dom',
'Session' => 'session',
'XML' => 'xml',
'JSON' => 'json',
'PDO' => 'PDO',
);
$optional_php_exts = array(
'FileInfo' => 'fileinfo',
'Libiconv' => 'iconv',
'Multibyte' => 'mbstring',
'OpenSSL' => 'openssl',
'Mcrypt' => 'mcrypt',
'Intl' => 'intl',
'Exif' => 'exif',
);
$required_libs = array(
'PEAR' => 'PEAR.php',
'Net_SMTP' => 'Net/SMTP.php',
'Net_IDNA2' => 'Net/IDNA2.php',
'Mail_mime' => 'Mail/mime.php',
);
$ini_checks = array(
'file_uploads' => 1,
'session.auto_start' => 0,
'zend.ze1_compatibility_mode' => 0,
'mbstring.func_overload' => 0,
'suhosin.session.encrypt' => 0,
'magic_quotes_runtime' => 0,
'magic_quotes_sybase' => 0,
'date.timezone' => '-NOTEMPTY-',
);
$optional_checks = array(
// required for utils/modcss.inc, should we require this?
'allow_url_fopen' => 1,
);
$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',
'Exif' => 'http://www.php.net/manual/en/book.exif.php',
'PDO' => 'http://www.php.net/manual/en/book.pdo.php',
'pdo_mysql' => 'http://www.php.net/manual/en/ref.pdo-mysql.php',
'pdo_pgsql' => 'http://www.php.net/manual/en/ref.pdo-pgsql.php',
'pdo_sqlite' => 'http://www.php.net/manual/en/ref.pdo-sqlite.php',
'pdo_sqlite2' => 'http://www.php.net/manual/en/ref.pdo-sqlite.php',
'pdo_sqlsrv' => 'http://www.php.net/manual/en/ref.pdo-sqlsrv.php',
'pdo_dblib' => 'http://www.php.net/manual/en/ref.pdo-dblib.php',
'PEAR' => 'http://pear.php.net',
'Net_SMTP' => 'http://pear.php.net/package/Net_SMTP',
'Mail_mime' => 'http://pear.php.net/package/Mail_mime',
'Net_IDNA2' => 'http://pear.php.net/package/Net_IDNA2',
);
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 ($RCI->supported_dbs as $database => $ext) {
if (extension_loaded($ext)) {
// MySQL driver requires PHP >= 5.3 (#1488875)
if ($ext == 'pdo_mysql' && version_compare(PHP_VERSION, '5.3.0', '<')) {
- $RCI->fail($database, 'PHP >= 5.3 required');
+ $RCI->fail($database, 'PHP >= 5.3 required', null, true);
}
else {
$RCI->pass($database);
+ $found_db_driver = true;
}
}
else {
$_ext = $ext_dir . '/' . $prefix . $ext . '.' . PHP_SHLIB_SUFFIX;
$msg = @is_readable($_ext) ? 'Could be loaded. Please add in php.ini' : '';
$RCI->na($database, $msg, $source_urls[$ext]);
}
echo '<br />';
}
+if (empty($found_db_driver)) {
+ $RCI->failures++;
+}
?>
<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, "empty value detected");
} else if ($var == 'date.timezone') {
try {
$tz = new DateTimeZone($status);
$RCI->pass($var);
}
catch (Exception $e) {
$RCI->fail($var, "invalid value detected: $status");
}
} 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>
diff --git a/installer/rcube_install.php b/installer/rcube_install.php
index c95d936d2..473fd2612 100644
--- a/installer/rcube_install.php
+++ b/installer/rcube_install.php
@@ -1,736 +1,739 @@
<?php
/*
+-----------------------------------------------------------------------+
| rcube_install.php |
| |
| This file is part of the Roundcube Webmail package |
| Copyright (C) 2008-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
+-----------------------------------------------------------------------+
*/
/**
* 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 $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',
'pagesize' => 'mail_pagesize',
'default_imap_folders' => 'default_folders',
'top_posting' => 'reply_mode',
);
// these config options are required for a working system
var $required_config = array(
'db_dsnw', 'des_key', 'session_lifetime',
);
// list of supported database drivers
var $supported_dbs = array(
'MySQL' => 'pdo_mysql',
'PostgreSQL' => 'pdo_pgsql',
'SQLite' => 'pdo_sqlite',
'SQLite (v2)' => 'pdo_sqlite2',
'SQL Server (SQLSRV)' => 'pdo_sqlsrv',
'SQL Server (DBLIB)' => 'pdo_dblib',
);
/**
* Constructor
*/
function __construct()
{
$this->step = intval($_REQUEST['_step']);
$this->is_post = $_SERVER['REQUEST_METHOD'] == 'POST';
}
/**
* Singleton getter
*/
static 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 = RCUBE_CONFIG_DIR . 'main.inc' . $suffix)) {
include($main_inc);
if (is_array($rcmail_config))
$this->config += $rcmail_config;
}
if (is_readable($db_inc = RCUBE_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(RCUBE_CONFIG_DIR . $which . '.inc.php.dist');
if (!$out)
return '[Warning: could not read the config template file]';
foreach ($this->config as $prop => $default) {
$is_default = !isset($_POST["_$prop"]);
$value = !$is_default || $this->bool_config_props[$prop] ? $_POST["_$prop"] : $default;
// convert some form data
if ($prop == 'debug_level' && !$is_default) {
if (is_array($value)) {
$val = 0;
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 if ($_POST['_dbtype'])
$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 == 'mail_pagesize' || $prop == 'addressbook_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_folders') {
$value = array();
foreach ($this->config['default_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, \$prop) . ';'",
$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;
}
}
// the old default mime_magic reference is obsolete
if ($this->config['mime_magic'] == '/usr/share/misc/magic') {
$out['obsolete'][] = array('prop' => 'mime_magic', 'explain' => "Set value to null in order to use system default");
}
// iterate over default config
foreach ($defaults as $prop => $value) {
if (!isset($seen[$prop]) && isset($required[$prop]) && !(is_bool($this->config[$prop]) || strlen($this->config[$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>syslog</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']);
}
}
}
$this->config = array_merge($this->config, $current);
foreach (array_keys((array)$current['ldap_public']) as $key) {
$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 rcube_db Database object
*
* @return boolean True if the schema is up-to-date, false if not or an error occured
*/
function db_schema_check($DB)
{
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 = $this->config['db_prefix'] . $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;
}
/**
* 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', '0.5.2', '0.5.3', '0.5.4',
'0.6-beta', '0.6',
'0.7-beta', '0.7', '0.7.1', '0.7.2', '0.7.3', '0.7.4',
'0.8-beta', '0.8-rc', '0.8.0', '0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5', '0.8.6',
'0.9-beta', '0.9-rc', '0.9-rc2',
// Note: Do not add newer versions here
));
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
+ * @param bool Do not count this failure
*/
- function fail($name, $message = '', $url = '')
+ function fail($name, $message = '', $url = '', $optional=false)
{
- $this->failures++;
+ if (!$optional) {
+ $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, $name=null) {
// special values
switch ($name) {
case 'syslog_facility':
$list = array(32 => 'LOG_AUTH', 80 => 'LOG_AUTHPRIV', 72 => ' LOG_CRON',
24 => 'LOG_DAEMON', 0 => 'LOG_KERN', 128 => 'LOG_LOCAL0',
136 => 'LOG_LOCAL1', 144 => 'LOG_LOCAL2', 152 => 'LOG_LOCAL3',
160 => 'LOG_LOCAL4', 168 => 'LOG_LOCAL5', 176 => 'LOG_LOCAL6',
184 => 'LOG_LOCAL7', 48 => 'LOG_LPR', 16 => 'LOG_MAIL',
56 => 'LOG_NEWS', 40 => 'LOG_SYSLOG', 8 => 'LOG_USER', 64 => 'LOG_UUCP');
if ($val = $list[$var])
return $val;
break;
}
if (is_array($var)) {
if (empty($var)) {
return 'array()';
}
else { // check if all keys are numeric
$isnum = true;
foreach (array_keys($var) as $key) {
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 = $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 schema
*
* @param string Version to update from
*
* @return boolen True on success, False on error
*/
function update_db($version)
{
system(INSTALL_PATH . "bin/updatedb.sh --package=roundcube"
. " --version=" . escapeshellarg($version)
. " --dir=" . INSTALL_PATH . "SQL"
. " 2>&1", $result);
return !$result;
}
/**
* 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)
{
$sql = $this->fix_table_names($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();
}
/**
* Parse SQL file and fix table names according to db_prefix
* Note: This need to be a complete database initial file
*/
private function fix_table_names($sql, $DB)
{
if (empty($this->config['db_prefix'])) {
return $sql;
}
// replace table names
if (preg_match_all('/CREATE TABLE (\[dbo\]\.|IF NOT EXISTS )?[`"\[\]]*([^`"\[\] \r\n]+)/i', $sql, $matches)) {
foreach ($matches[2] as $table) {
$real_table = $this->config['db_prefix'] . $table;
$sql = preg_replace("/([^a-zA-Z0-9_])$table([^a-zA-Z0-9_])/", "\\1$real_table\\2", $sql);
}
}
// replace sequence names
if ($DB->db_provider == 'postgres' && preg_match_all('/CREATE SEQUENCE (IF NOT EXISTS )?"?([^" \n\r]+)/i', $sql, $matches)) {
foreach ($matches[2] as $sequence) {
$real_sequence = $this->config['db_prefix'] . $sequence;
$sql = preg_replace("/([^a-zA-Z0-9_])$sequence([^a-zA-Z0-9_])/", "\\1$real_sequence\\2", $sql);
}
}
return $sql;
}
/**
* 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;
}
}
diff --git a/plugins/acl/acl.php b/plugins/acl/acl.php
index 28139e92c..59406262e 100644
--- a/plugins/acl/acl.php
+++ b/plugins/acl/acl.php
@@ -1,729 +1,730 @@
<?php
/**
* Folders Access Control Lists Management (RFC4314, RFC2086)
*
* @version @package_version@
* @author Aleksander Machniak <alec@alec.pl>
*
*
* Copyright (C) 2011-2012, Kolab Systems AG
*
* 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.
*/
class acl extends rcube_plugin
{
public $task = 'settings|addressbook|calendar';
private $rc;
private $supported = null;
private $mbox;
private $ldap;
private $specials = array('anyone', 'anonymous');
/**
* Plugin initialization
*/
function init()
{
$this->rc = rcmail::get_instance();
// Register hooks
$this->add_hook('folder_form', array($this, 'folder_form'));
// kolab_addressbook plugin
$this->add_hook('addressbook_form', array($this, 'folder_form'));
$this->add_hook('calendar_form_kolab', array($this, 'folder_form'));
// Plugin actions
$this->register_action('plugin.acl', array($this, 'acl_actions'));
$this->register_action('plugin.acl-autocomplete', array($this, 'acl_autocomplete'));
}
/**
* Handler for plugin actions (AJAX)
*/
function acl_actions()
{
$action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC));
// Connect to IMAP
$this->rc->storage_init();
// Load localization and configuration
$this->add_texts('localization/');
$this->load_config();
if ($action == 'save') {
$this->action_save();
}
else if ($action == 'delete') {
$this->action_delete();
}
else if ($action == 'list') {
$this->action_list();
}
// Only AJAX actions
$this->rc->output->send();
}
/**
* Handler for user login autocomplete request
*/
function acl_autocomplete()
{
$this->load_config();
$search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
$sid = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$users = array();
if ($this->init_ldap()) {
$max = (int) $this->rc->config->get('autocomplete_max', 15);
$mode = (int) $this->rc->config->get('addressbook_search_mode');
$this->ldap->set_pagesize($max);
$result = $this->ldap->search('*', $search, $mode);
foreach ($result->records as $record) {
$user = $record['uid'];
if (is_array($user)) {
$user = array_filter($user);
$user = $user[0];
}
if ($user) {
if ($record['name'])
$user = $record['name'] . ' (' . $user . ')';
$users[] = $user;
}
}
}
sort($users, SORT_LOCALE_STRING);
$this->rc->output->command('ksearch_query_results', $users, $search, $sid);
$this->rc->output->send();
}
/**
* Handler for 'folder_form' hook
*
* @param array $args Hook arguments array (form data)
*
* @return array Hook arguments array
*/
function folder_form($args)
{
$mbox_imap = $args['options']['name'];
$myrights = $args['options']['rights'];
// Edited folder name (empty in create-folder mode)
if (!strlen($mbox_imap)) {
return $args;
}
/*
// Do nothing on protected folders (?)
if ($args['options']['protected']) {
return $args;
}
*/
// Get MYRIGHTS
if (empty($myrights)) {
return $args;
}
// Load localization and include scripts
$this->load_config();
+ $this->specials = $this->rc->config->get('acl_specials', $this->specials);
$this->add_texts('localization/', array('deleteconfirm', 'norights',
'nouser', 'deleting', 'saving'));
$this->include_script('acl.js');
$this->rc->output->include_script('list.js');
$this->include_stylesheet($this->local_skin_path().'/acl.css');
// add Info fieldset if it doesn't exist
if (!isset($args['form']['props']['fieldsets']['info']))
$args['form']['props']['fieldsets']['info'] = array(
'name' => $this->rc->gettext('info'),
'content' => array());
// Display folder rights to 'Info' fieldset
$args['form']['props']['fieldsets']['info']['content']['myrights'] = array(
'label' => rcube::Q($this->gettext('myrights')),
'value' => $this->acl2text($myrights)
);
// Return if not folder admin
if (!in_array('a', $myrights)) {
return $args;
}
// The 'Sharing' tab
$this->mbox = $mbox_imap;
$this->rc->output->set_env('acl_users_source', (bool) $this->rc->config->get('acl_users_source'));
$this->rc->output->set_env('mailbox', $mbox_imap);
$this->rc->output->add_handlers(array(
'acltable' => array($this, 'templ_table'),
'acluser' => array($this, 'templ_user'),
'aclrights' => array($this, 'templ_rights'),
));
$this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
$this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
$this->rc->output->add_label('autocompletechars', 'autocompletemore');
$args['form']['sharing'] = array(
'name' => rcube::Q($this->gettext('sharing')),
'content' => $this->rc->output->parse('acl.table', false, false),
);
return $args;
}
/**
* Creates ACL rights table
*
* @param array $attrib Template object attributes
*
* @return string HTML Content
*/
function templ_table($attrib)
{
if (empty($attrib['id']))
$attrib['id'] = 'acl-table';
$out = $this->list_rights($attrib);
$this->rc->output->add_gui_object('acltable', $attrib['id']);
return $out;
}
/**
* Creates ACL rights form (rights list part)
*
* @param array $attrib Template object attributes
*
* @return string HTML Content
*/
function templ_rights($attrib)
{
// Get supported rights
$supported = $this->rights_supported();
// depending on server capability either use 'te' or 'd' for deleting msgs
$deleteright = implode(array_intersect(str_split('ted'), $supported));
$out = '';
$ul = '';
$input = new html_checkbox();
// Advanced rights
$attrib['id'] = 'advancedrights';
foreach ($supported as $key => $val) {
$id = "acl$val";
$ul .= html::tag('li', null,
$input->show('', array(
'name' => "acl[$val]", 'value' => $val, 'id' => $id))
. html::label(array('for' => $id, 'title' => $this->gettext('longacl'.$val)),
$this->gettext('acl'.$val)));
}
$out = html::tag('ul', $attrib, $ul, html::$common_attrib);
// Simple rights
$ul = '';
$attrib['id'] = 'simplerights';
$items = array(
'read' => 'lrs',
'write' => 'wi',
'delete' => $deleteright,
'other' => preg_replace('/[lrswi'.$deleteright.']/', '', implode($supported)),
);
foreach ($items as $key => $val) {
$id = "acl$key";
$ul .= html::tag('li', null,
$input->show('', array(
'name' => "acl[$val]", 'value' => $val, 'id' => $id))
. html::label(array('for' => $id, 'title' => $this->gettext('longacl'.$key)),
$this->gettext('acl'.$key)));
}
$out .= "\n" . html::tag('ul', $attrib, $ul, html::$common_attrib);
$this->rc->output->set_env('acl_items', $items);
return $out;
}
/**
* Creates ACL rights form (user part)
*
* @param array $attrib Template object attributes
*
* @return string HTML Content
*/
function templ_user($attrib)
{
// Create username input
$attrib['name'] = 'acluser';
$textfield = new html_inputfield($attrib);
$fields['user'] = html::label(array('for' => 'iduser'), $this->gettext('username'))
. ' ' . $textfield->show();
// Add special entries
if (!empty($this->specials)) {
foreach ($this->specials as $key) {
$fields[$key] = html::label(array('for' => 'id'.$key), $this->gettext($key));
}
}
$this->rc->output->set_env('acl_specials', $this->specials);
// Create list with radio buttons
if (count($fields) > 1) {
$ul = '';
$radio = new html_radiobutton(array('name' => 'usertype'));
foreach ($fields as $key => $val) {
$ul .= html::tag('li', null, $radio->show($key == 'user' ? 'user' : '',
array('value' => $key, 'id' => 'id'.$key))
. $val);
}
$out = html::tag('ul', array('id' => 'usertype'), $ul, html::$common_attrib);
}
// Display text input alone
else {
$out = $fields['user'];
}
return $out;
}
/**
* Creates ACL rights table
*
* @param array $attrib Template object attributes
*
* @return string HTML Content
*/
private function list_rights($attrib=array())
{
// Get ACL for the folder
$acl = $this->rc->storage->get_acl($this->mbox);
if (!is_array($acl)) {
$acl = array();
}
// Keep special entries (anyone/anonymous) on top of the list
if (!empty($this->specials) && !empty($acl)) {
foreach ($this->specials as $key) {
if (isset($acl[$key])) {
$acl_special[$key] = $acl[$key];
unset($acl[$key]);
}
}
}
// Sort the list by username
uksort($acl, 'strnatcasecmp');
if (!empty($acl_special)) {
$acl = array_merge($acl_special, $acl);
}
// Get supported rights and build column names
$supported = $this->rights_supported();
// depending on server capability either use 'te' or 'd' for deleting msgs
$deleteright = implode(array_intersect(str_split('ted'), $supported));
// Use advanced or simple (grouped) rights
$advanced = $this->rc->config->get('acl_advanced_mode');
if ($advanced) {
$items = array();
foreach ($supported as $sup) {
$items[$sup] = $sup;
}
}
else {
$items = array(
'read' => 'lrs',
'write' => 'wi',
'delete' => $deleteright,
'other' => preg_replace('/[lrswi'.$deleteright.']/', '', implode($supported)),
);
}
// Create the table
$attrib['noheader'] = true;
$table = new html_table($attrib);
// Create table header
$table->add_header('user', $this->gettext('identifier'));
foreach (array_keys($items) as $key) {
$label = $this->gettext('shortacl'.$key);
$table->add_header(array('class' => 'acl'.$key, 'title' => $label), $label);
}
$js_table = array();
foreach ($acl as $user => $rights) {
if ($this->rc->storage->conn->user == $user) {
continue;
}
// filter out virtual rights (c or d) the server may return
$userrights = array_intersect($rights, $supported);
$userid = rcube_utils::html_identifier($user);
if (!empty($this->specials) && in_array($user, $this->specials)) {
$user = $this->gettext($user);
}
$table->add_row(array('id' => 'rcmrow'.$userid));
$table->add('user', rcube::Q($user));
foreach ($items as $key => $right) {
$in = $this->acl_compare($userrights, $right);
switch ($in) {
case 2: $class = 'enabled'; break;
case 1: $class = 'partial'; break;
default: $class = 'disabled'; break;
}
$table->add('acl' . $key . ' ' . $class, '');
}
$js_table[$userid] = implode($userrights);
}
$this->rc->output->set_env('acl', $js_table);
$this->rc->output->set_env('acl_advanced', $advanced);
$out = $table->show();
return $out;
}
/**
* Handler for ACL update/create action
*/
private function action_save()
{
$mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP
$user = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC));
$acl = trim(rcube_utils::get_input_value('_acl', rcube_utils::INPUT_GPC));
$oldid = trim(rcube_utils::get_input_value('_old', rcube_utils::INPUT_GPC));
$acl = array_intersect(str_split($acl), $this->rights_supported());
$users = $oldid ? array($user) : explode(',', $user);
$result = 0;
foreach ($users as $user) {
$user = trim($user);
if (!empty($this->specials) && in_array($user, $this->specials)) {
$username = $this->gettext($user);
}
else if (!empty($user)) {
if (!strpos($user, '@') && ($realm = $this->get_realm())) {
$user .= '@' . rcube_utils::idn_to_ascii(preg_replace('/^@/', '', $realm));
}
$username = $user;
}
if (!$acl || !$user || !strlen($mbox)) {
continue;
}
$user = $this->mod_login($user);
$username = $this->mod_login($username);
if ($user != $_SESSION['username'] && $username != $_SESSION['username']) {
if ($this->rc->storage->set_acl($mbox, $user, $acl)) {
$ret = array('id' => rcube_utils::html_identifier($user),
'username' => $username, 'acl' => implode($acl), 'old' => $oldid);
$this->rc->output->command('acl_update', $ret);
$result++;
}
}
}
if ($result) {
$this->rc->output->show_message($oldid ? 'acl.updatesuccess' : 'acl.createsuccess', 'confirmation');
}
else {
$this->rc->output->show_message($oldid ? 'acl.updateerror' : 'acl.createerror', 'error');
}
}
/**
* Handler for ACL delete action
*/
private function action_delete()
{
$mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true)); //UTF7-IMAP
$user = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC));
$user = explode(',', $user);
foreach ($user as $u) {
$u = trim($u);
if ($this->rc->storage->delete_acl($mbox, $u)) {
$this->rc->output->command('acl_remove_row', rcube_utils::html_identifier($u));
}
else {
$error = true;
}
}
if (!$error) {
$this->rc->output->show_message('acl.deletesuccess', 'confirmation');
}
else {
$this->rc->output->show_message('acl.deleteerror', 'error');
}
}
/**
* Handler for ACL list update action (with display mode change)
*/
private function action_list()
{
if (in_array('acl_advanced_mode', (array)$this->rc->config->get('dont_override'))) {
return;
}
$this->mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP
$advanced = trim(rcube_utils::get_input_value('_mode', rcube_utils::INPUT_GPC));
$advanced = $advanced == 'advanced' ? true : false;
// Save state in user preferences
$this->rc->user->save_prefs(array('acl_advanced_mode' => $advanced));
$out = $this->list_rights();
$out = preg_replace(array('/^<table[^>]+>/', '/<\/table>$/'), '', $out);
$this->rc->output->command('acl_list_update', $out);
}
/**
* Creates <UL> list with descriptive access rights
*
* @param array $rights MYRIGHTS result
*
* @return string HTML content
*/
function acl2text($rights)
{
if (empty($rights)) {
return '';
}
$supported = $this->rights_supported();
$list = array();
$attrib = array(
'name' => 'rcmyrights',
'style' => 'margin:0; padding:0 15px;',
);
foreach ($supported as $right) {
if (in_array($right, $rights)) {
$list[] = html::tag('li', null, rcube::Q($this->gettext('acl' . $right)));
}
}
if (count($list) == count($supported))
return rcube::Q($this->gettext('aclfull'));
return html::tag('ul', $attrib, implode("\n", $list));
}
/**
* Compares two ACLs (according to supported rights)
*
* @param array $acl1 ACL rights array (or string)
* @param array $acl2 ACL rights array (or string)
*
* @param int Comparision result, 2 - full match, 1 - partial match, 0 - no match
*/
function acl_compare($acl1, $acl2)
{
if (!is_array($acl1)) $acl1 = str_split($acl1);
if (!is_array($acl2)) $acl2 = str_split($acl2);
$rights = $this->rights_supported();
$acl1 = array_intersect($acl1, $rights);
$acl2 = array_intersect($acl2, $rights);
$res = array_intersect($acl1, $acl2);
$cnt1 = count($res);
$cnt2 = count($acl2);
if ($cnt1 == $cnt2)
return 2;
else if ($cnt1)
return 1;
else
return 0;
}
/**
* Get list of supported access rights (according to RIGHTS capability)
*
* @return array List of supported access rights abbreviations
*/
function rights_supported()
{
if ($this->supported !== null) {
return $this->supported;
}
$capa = $this->rc->storage->get_capability('RIGHTS');
if (is_array($capa)) {
$rights = strtolower($capa[0]);
}
else {
$rights = 'cd';
}
return $this->supported = str_split('lrswi' . $rights . 'pa');
}
/**
* Username realm detection.
*
* @return string Username realm (domain)
*/
private function get_realm()
{
// When user enters a username without domain part, realm
// allows to add it to the username (and display correct username in the table)
if (isset($_SESSION['acl_username_realm'])) {
return $_SESSION['acl_username_realm'];
}
// find realm in username of logged user (?)
list($name, $domain) = explode('@', $_SESSION['username']);
// Use (always existent) ACL entry on the INBOX for the user to determine
// whether or not the user ID in ACL entries need to be qualified and how
// they would need to be qualified.
if (empty($domain)) {
$acl = $this->rc->storage->get_acl('INBOX');
if (is_array($acl)) {
$regexp = '/^' . preg_quote($_SESSION['username'], '/') . '@(.*)$/';
foreach (array_keys($acl) as $name) {
if (preg_match($regexp, $name, $matches)) {
$domain = $matches[1];
break;
}
}
}
}
return $_SESSION['acl_username_realm'] = $domain;
}
/**
* Initializes autocomplete LDAP backend
*/
private function init_ldap()
{
if ($this->ldap)
return $this->ldap->ready;
// get LDAP config
$config = $this->rc->config->get('acl_users_source');
if (empty($config)) {
return false;
}
// not an array, use configured ldap_public source
if (!is_array($config)) {
$ldap_config = (array) $this->rc->config->get('ldap_public');
$config = $ldap_config[$config];
}
$uid_field = $this->rc->config->get('acl_users_field', 'mail');
$filter = $this->rc->config->get('acl_users_filter');
if (empty($uid_field) || empty($config)) {
return false;
}
// get name attribute
if (!empty($config['fieldmap'])) {
$name_field = $config['fieldmap']['name'];
}
// ... no fieldmap, use the old method
if (empty($name_field)) {
$name_field = $config['name_field'];
}
// add UID field to fieldmap, so it will be returned in a record with name
$config['fieldmap'] = array(
'name' => $name_field,
'uid' => $uid_field,
);
// search in UID and name fields
$config['search_fields'] = array_values($config['fieldmap']);
$config['required_fields'] = array($uid_field);
// set search filter
if ($filter)
$config['filter'] = $filter;
// disable vlv
$config['vlv'] = false;
// Initialize LDAP connection
$this->ldap = new rcube_ldap($config,
$this->rc->config->get('ldap_debug'),
$this->rc->config->mail_domain($_SESSION['imap_host']));
return $this->ldap->ready;
}
/**
* Modify user login according to 'login_lc' setting
*/
protected function mod_login($user)
{
$login_lc = $this->rc->config->get('login_lc');
if ($login_lc === true || $login_lc == 2) {
$user = mb_strtolower($user);
}
// lowercase domain name
else if ($login_lc && strpos($user, '@')) {
list($local, $domain) = explode('@', $user);
$user = $local . '@' . mb_strtolower($domain);
}
return $user;
}
}
diff --git a/plugins/acl/config.inc.php.dist b/plugins/acl/config.inc.php.dist
index f957a233a..d0e1a0932 100644
--- a/plugins/acl/config.inc.php.dist
+++ b/plugins/acl/config.inc.php.dist
@@ -1,19 +1,25 @@
<?php
// Default look of access rights table
// In advanced mode all access rights are displayed separately
// In simple mode access rights are grouped into four groups: read, write, delete, full
$rcmail_config['acl_advanced_mode'] = false;
// LDAP addressbook that would be searched for user names autocomplete.
// That should be an array refering to the $rcmail_config['ldap_public'] array key
// or complete addressbook configuration array.
$rcmail_config['acl_users_source'] = '';
// The LDAP attribute which will be used as ACL user identifier
$rcmail_config['acl_users_field'] = 'mail';
// The LDAP search filter will be &'d with search queries
$rcmail_config['acl_users_filter'] = '';
+// Include the following 'special' access control subjects in the ACL dialog;
+// Defaults to array('anyone', 'anonymous') (not when set to an empty array)
+// Example: array('anyone') to exclude 'anonymous'.
+// Set to an empty array to exclude all special aci subjects.
+$rcmail_config['acl_specials'] = array('anyone', 'anonymous');
+
?>
diff --git a/plugins/attachment_reminder/attachment_reminder.php b/plugins/attachment_reminder/attachment_reminder.php
index a215ff57c..82ad7a60e 100755
--- a/plugins/attachment_reminder/attachment_reminder.php
+++ b/plugins/attachment_reminder/attachment_reminder.php
@@ -1,82 +1,84 @@
<?php
/**
* Attachement Reminder
*
* A plugin that reminds a user to attach the files
*
* @version @package_version@
* @author Thomas Yu - Sian, Liu
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2013 Thomas Yu - Sian, Liu
* Copyright (C) 2013, Kolab Systems AG
*
* 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 3 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, see <http://www.gnu.org/licenses/>
*/
class attachment_reminder extends rcube_plugin
{
public $task = 'mail|settings';
public $noajax = true;
function init()
{
$rcmail = rcube::get_instance();
if ($rcmail->task == 'mail' && $rcmail->action == 'compose') {
- $this->include_script('attachment_reminder.js');
- $this->add_texts('localization/', array('keywords', 'forgotattachment'));
- $rcmail->output->add_label('addattachment', 'send');
+ if ($rcmail->config->get('attachment_reminder')) {
+ $this->include_script('attachment_reminder.js');
+ $this->add_texts('localization/', array('keywords', 'forgotattachment'));
+ $rcmail->output->add_label('addattachment', 'send');
+ }
}
if ($rcmail->task == 'settings') {
$dont_override = $rcmail->config->get('dont_override', array());
if (!in_array('attachment_reminder', $dont_override)) {
$this->add_hook('preferences_list', array($this, 'prefs_list'));
$this->add_hook('preferences_save', array($this, 'prefs_save'));
}
}
}
function prefs_list($args)
{
if ($args['section'] == 'compose') {
$this->add_texts('localization/');
$reminder = rcube::get_instance()->config->get('attachment_reminder');
$field_id = 'rcmfd_attachment_reminder';
$checkbox = new html_checkbox(array('name' => '_attachment_reminder', 'id' => $field_id, 'value' => 1));
$args['blocks']['main']['options']['attachment_reminder'] = array(
'title' => html::label($field_id, rcube::Q($this->gettext('reminderoption'))),
'content' => $checkbox->show($reminder ? 1 : 0),
);
}
return $args;
}
function prefs_save($args)
{
if ($args['section'] == 'compose') {
$dont_override = rcube::get_instance()->config->get('dont_override', array());
if (!in_array('attachment_reminder', $dont_override)) {
$args['prefs']['attachment_reminder'] = !empty($_POST['_attachment_reminder']);
}
}
return $args;
}
}
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index 39d804d1f..eff0425c8 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -1,1946 +1,1946 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcmail.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2012, The Roundcube Dev Team |
| Copyright (C) 2011-2012, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Application class providing core functions and holding |
| instances of all 'global' objects like db- and imap-connections |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Application class of Roundcube Webmail
* implemented as singleton
*
* @package Core
*/
class rcmail extends rcube
{
/**
* Main tasks.
*
* @var array
*/
static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
/**
* Current task.
*
* @var string
*/
public $task;
/**
* Current action.
*
* @var string
*/
public $action = '';
public $comm_path = './';
private $address_books = array();
private $action_map = array();
const ERROR_STORAGE = -2;
const ERROR_INVALID_REQUEST = 1;
const ERROR_INVALID_HOST = 2;
const ERROR_COOKIES_DISABLED = 3;
/**
* This implements the 'singleton' design pattern
*
* @return rcmail The one and only instance
*/
static function get_instance()
{
if (!self::$instance || !is_a(self::$instance, 'rcmail')) {
self::$instance = new rcmail();
self::$instance->startup(); // init AFTER object was linked with self::$instance
}
return self::$instance;
}
/**
* Initial startup function
* to register session, create database and imap connections
*/
protected function startup()
{
$this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS);
// start session
$this->session_init();
// create user object
$this->set_user(new rcube_user($_SESSION['user_id']));
// set task and action properties
$this->set_task(rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC));
$this->action = asciiwords(rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC));
// reset some session parameters when changing task
if ($this->task != 'utils') {
// we reset list page when switching to another task
// but only to the main task interface - empty action (#1489076)
// this will prevent from unintentional page reset on cross-task requests
if ($this->session && $_SESSION['task'] != $this->task && empty($this->action))
$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']));
// load plugins
$this->plugins->init($this, $this->task);
$this->plugins->load_plugins((array)$this->config->get('plugins', array()), array('filesystem_attachments', 'jqueryui'));
}
/**
* Setter for application task
*
* @param string Task to set
*/
public function set_task($task)
{
$task = asciiwords($task, true);
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;
// overwrite config with user preferences
$this->config->set_user_prefs((array)$this->user->get_prefs());
}
$lang = $this->language_prop($this->config->get('language', $_SESSION['language']));
$_SESSION['language'] = $this->user->language = $lang;
// set localization
setlocale(LC_ALL, $lang . '.utf8', $lang . '.UTF-8', 'en_US.utf8', 'en_US.UTF-8');
// workaround for http://bugs.php.net/bug.php?id=18556
if (in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) {
setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8');
}
}
/**
* Return instance of the internal address book class
*
* @param string Address book identifier (-1 for default addressbook)
* @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');
// 'sql' is the alias for '0' used by autocomplete
if ($id == 'sql')
$id = '0';
else if ($id == -1) {
$id = $this->config->get('default_addressbook');
$default = true;
}
// use existing instance
if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) {
$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['storage_host']));
}
else if ($id === '0') {
$contacts = new rcube_contacts($this->db, $this->get_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'];
}
}
// when user requested default writeable addressbook
// we need to check if default is writeable, if not we
// will return first writeable book (if any exist)
if ($contacts && $default && $contacts->readonly && $writeable) {
$contacts = null;
}
// Get first addressbook from the list if configured default doesn't exist
// This can happen when user deleted the addressbook (e.g. Kolab folder)
if (!$contacts && (!$id || $default)) {
$source = reset($this->get_address_sources($writeable, !$default));
if (!empty($source)) {
$contacts = $this->get_address_book($source['id']);
if ($contacts) {
$id = $source['id'];
}
}
}
if (!$contacts) {
// there's no default, just return
if ($default) {
return null;
}
self::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
$this->address_books[$id] = $contacts;
if ($writeable && $contacts->readonly) {
return null;
}
// set configured sort order
if ($sort_col = $this->config->get('addressbook_sort_col')) {
$contacts->set_sort_order($sort_col);
}
return $contacts;
}
/**
* Return address books list
*
* @param boolean True if the address book needs to be writeable
* @param boolean True if the address book needs to be not hidden
*
* @return array Address books array
*/
public function get_address_sources($writeable = false, $skip_hidden = false)
{
$abook_type = (string) $this->config->get('address_book_type');
$ldap_config = (array) $this->config->get('ldap_public');
$autocomplete = (array) $this->config->get('autocomplete_addressbooks');
$list = array();
// We are using the DB address book or a plugin address book
if (!empty($abook_type) && strtolower($abook_type) != 'ldap') {
if (!isset($this->address_books['0']))
$this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id());
$list['0'] = array(
'id' => '0',
'name' => $this->gettext('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 (!empty($ldap_config)) {
foreach ($ldap_config as $id => $prop) {
// handle misconfiguration
if (empty($prop) || !is_array($prop)) {
continue;
}
$list[$id] = array(
'id' => $id,
'name' => html::quote($prop['name']),
- 'groups' => is_array($prop['groups']),
+ 'groups' => !empty($prop['groups']) || !empty($prop['group_filters']),
'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]);
}
// remove from list if hidden as requested
else if ($skip_hidden && $item['hidden']) {
unset($list[$idx]);
}
}
return $list;
}
/**
* Init output object for GUI and add common scripts.
* This will instantiate a rcmail_output_html 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_output Reference to HTML output object
*/
public function load_gui($framed = false)
{
// init output page
if (!($this->output instanceof rcmail_output_html))
$this->output = new rcmail_output_html($this->task, $framed);
// set refresh interval
$this->output->set_env('refresh_interval', $this->config->get('refresh_interval', 0));
$this->output->set_env('session_lifetime', $this->config->get('session_lifetime', 0) * 60);
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(RCUBE_CHARSET);
// add some basic labels to client
$this->output->add_label('loading', 'servererror', 'requesttimedout', 'refreshing');
return $this->output;
}
/**
* Create an output object for JSON responses
*
* @return rcube_output Reference to JSON output object
*/
public function json_init()
{
if (!($this->output instanceof rcmail_output_json))
$this->output = new rcmail_output_json($this->task);
return $this->output;
}
/**
* Create session object and start the session.
*/
public function session_init()
{
parent::session_init();
// set initial session vars
if (!$_SESSION['user_id'])
$_SESSION['temp'] = true;
// restore skin selection after logout
if ($_SESSION['temp'] && !empty($_SESSION['skin']))
$this->config->set('skin', $_SESSION['skin']);
}
/**
* Perfom login to the mail server and to the webmail service.
* This will also create a new user entry if auto_create_user is configured.
*
* @param string Mail storage (IMAP) user name
* @param string Mail storage (IMAP) password
* @param string Mail storage (IMAP) host
* @param bool Enables cookie check
*
* @return boolean True on success, False on failure
*/
function login($username, $pass, $host = null, $cookiecheck = false)
{
$this->login_error = null;
if (empty($username)) {
return false;
}
if ($cookiecheck && empty($_COOKIE)) {
$this->login_error = self::ERROR_COOKIES_DISABLED;
return false;
}
$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) {
$host = null;
}
}
else if (!empty($config['default_host']) && $host != rcube_utils::parse_host($config['default_host'])) {
$host = null;
}
if (!$host) {
$this->login_error = self::ERROR_INVALID_HOST;
return false;
}
// parse $host URL
$a_host = parse_url($host);
if ($a_host['host']) {
$host = $a_host['host'];
$ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if (!empty($a_host['port']))
$port = $a_host['port'];
else if ($ssl && $ssl != 'tls' && (!$config['default_port'] || $config['default_port'] == 143))
$port = 993;
}
if (!$port) {
$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_utils::parse_host($config['username_domain'][$host], $host);
else if (is_string($config['username_domain']))
$username .= '@'.rcube_utils::parse_host($config['username_domain'], $host);
}
if (!isset($config['login_lc'])) {
$config['login_lc'] = 2; // default
}
// Convert username to lowercase. If storage backend
// is case-insensitive we need to store always the same username (#1487113)
if ($config['login_lc']) {
if ($config['login_lc'] == 2 || $config['login_lc'] === true) {
$username = mb_strtolower($username);
}
else if (strpos($username, '@')) {
// lowercase domain name
list($local, $domain) = explode('@', $username);
$username = $local . '@' . mb_strtolower($domain);
}
}
// 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_utils::idn_to_ascii($host);
$username = rcube_utils::idn_to_ascii($username);
// user already registered -> overwrite username
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
}
$storage = $this->get_storage();
// try to log in
if (!$storage->connect($host, $username, $pass, $port, $ssl)) {
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 {
self::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 {
self::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_storage_prop();
// 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']))) {
$storage->create_default_folders();
}
// set session vars
$_SESSION['user_id'] = $user->ID;
$_SESSION['username'] = $user->data['username'];
$_SESSION['storage_host'] = $host;
$_SESSION['storage_port'] = $port;
$_SESSION['storage_ssl'] = $ssl;
$_SESSION['password'] = $this->encrypt($pass);
$_SESSION['login_time'] = time();
if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_')
$_SESSION['timezone'] = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_GPC);
// force reloading complete list of subscribed mailboxes
$storage->clear_cache('mailboxes', true);
return true;
}
return false;
}
/**
* Returns error code of last login operation
*
* @return int Error code
*/
public function login_error()
{
if ($this->login_error) {
return $this->login_error;
}
if ($this->storage && $this->storage->get_error_code() < -1) {
return self::ERROR_STORAGE;
}
}
/**
* 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 = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
$post_user = rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST);
list(, $domain) = explode('@', $post_user);
// 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
else if (!empty($domain)) {
foreach ($default_host as $storage_host => $mail_domains) {
if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
$host = $storage_host;
break;
}
else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
$host = is_numeric($storage_host) ? $mail_domains : $storage_host;
break;
}
}
}
// take the first entry if $host is still not set
if (empty($host)) {
list($key, $val) = each($default_host);
$host = is_numeric($key) ? $val : $key;
}
}
else if (empty($default_host)) {
$host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
}
else
$host = rcube_utils::parse_host($default_host);
return $host;
}
/**
* 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, 'skin' => $this->config->get('skin'));
$this->user->reset();
}
/**
* Do server side actions on logout
*/
public function logout_actions()
{
$config = $this->config->all();
$storage = $this->get_storage();
if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
$storage->clear_folder($config['trash_mbox']);
}
if ($config['logout_expunge']) {
$storage->expunge_folder('INBOX');
}
// Try to save unsaved user preferences
if (!empty($_SESSION['preferences'])) {
$this->user->save_prefs(unserialize($_SESSION['preferences']));
}
}
/**
* 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->get_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_utils::INPUT_POST)
{
$token = rcube_utils::get_input_value('_token', $mode);
$sess_id = $_COOKIE[ini_get('session.name')];
return !empty($sess_id) && $token == $this->get_request_token();
}
/**
* 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)) {
if (strpos($p, 'http') === 0)
return $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 !== '' && $val !== null) {
$par = $key[0] == '_' ? $key : '_'.$key;
$url .= $delm.urlencode($par).'='.urlencode($val);
$delm = '&';
}
}
return $url;
}
/**
* Function to be executed in script shutdown
*/
public function shutdown()
{
parent::shutdown();
foreach ($this->address_books as $book) {
if (is_object($book) && is_a($book, 'rcube_addressbook'))
$book->close();
}
// write performance stats to logs/console
if ($this->config->get('devel_mode')) {
if (function_exists('memory_get_usage'))
$mem = $this->show_bytes(memory_get_usage());
if (function_exists('memory_get_peak_usage'))
$mem .= '/'.$this->show_bytes(memory_get_peak_usage());
$log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
if (defined('RCMAIL_START'))
self::print_timer(RCMAIL_START, $log);
else
self::console($log);
}
}
/**
* 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->storage->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->storage->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_folders'])) {
foreach ($prefs['default_folders'] as $idx => $name) {
if ($name != 'INBOX' && !preg_match($regexp, $name)) {
$prefs['default_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_storage_prop();
}
/**
* Overwrite action variable
*
* @param string New action value
*/
public function overwrite_action($action)
{
$this->action = $action;
$this->output->set_env('action', $action);
}
/**
* Returns RFC2822 formatted current date in user's timezone
*
* @return string Date
*/
public function user_date()
{
// get user's timezone
try {
$tz = new DateTimeZone($this->config->get('timezone'));
$date = new DateTime('now', $tz);
}
catch (Exception $e) {
$date = new DateTime();
}
return $date->format('r');
}
/**
* Write login data (name, ID, IP address) to the 'userlogins' log file.
*/
public function log_login()
{
if (!$this->config->get('log_logins')) {
return;
}
$user_name = $this->get_user_name();
$user_id = $this->get_user_id();
if (!$user_id) {
return;
}
self::write_log('userlogins',
sprintf('Successful login for %s (ID: %d) from %s in session %s',
$user_name, $user_id, rcube_utils::remote_ip(), session_id()));
}
/**
* 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
*/
public function table_output($attrib, $table_data, $a_show_cols, $id_col)
{
$table = new html_table($attrib);
// add table header
if (!$attrib['noheader']) {
foreach ($a_show_cols as $col) {
$table->add_header($col, $this->Q($this->gettext($col)));
}
}
if (!is_array($table_data)) {
$db = $this->get_dbh();
while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
$table->add_row(array('id' => 'rcmrow' . rcube_utils::html_identifier($sql_arr[$id_col])));
// format each col
foreach ($a_show_cols as $col) {
$table->add($col, $this->Q($sql_arr[$col]));
}
}
}
else {
foreach ($table_data as $row_data) {
$class = !empty($row_data['class']) ? $row_data['class'] : '';
$rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);
$table->add_row(array('id' => $rowid, 'class' => $class));
// format each col
foreach ($a_show_cols as $col) {
$table->add($col, $this->Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
}
}
}
return $table->show($attrib);
}
/**
* Convert the given date to a human readable form
* This uses the date formatting properties from config
*
* @param mixed Date representation (string, timestamp or DateTime object)
* @param string Date format to use
* @param bool Enables date convertion according to user timezone
*
* @return string Formatted date string
*/
public function format_date($date, $format = null, $convert = true)
{
if (is_object($date) && is_a($date, 'DateTime')) {
$timestamp = $date->format('U');
}
else {
if (!empty($date)) {
$timestamp = rcube_utils::strtotime($date);
}
if (empty($timestamp)) {
return '';
}
try {
$date = new DateTime("@".$timestamp);
}
catch (Exception $e) {
return '';
}
}
if ($convert) {
try {
// convert to the right timezone
$stz = date_default_timezone_get();
$tz = new DateTimeZone($this->config->get('timezone'));
$date->setTimezone($tz);
date_default_timezone_set($tz->getName());
$timestamp = $date->format('U');
}
catch (Exception $e) {
}
}
// define date format depending on current time
if (!$format) {
$now = 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']);
$pretty_date = $this->config->get('prettydate');
if ($pretty_date && $timestamp > $today_limit && $timestamp < $now) {
$format = $this->config->get('date_today', $this->config->get('time_format', 'H:i'));
$today = true;
}
else if ($pretty_date && $timestamp > $week_limit && $timestamp < $now) {
$format = $this->config->get('date_short', 'D H:i');
}
else {
$format = $this->config->get('date_long', 'Y-m-d H:i');
}
}
// strftime() format
if (preg_match('/%[a-z]+/i', $format)) {
$format = strftime($format, $timestamp);
if ($stz) {
date_default_timezone_set($stz);
}
return $today ? ($this->gettext('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 .= $this->gettext(strtolower(date('D', $timestamp)));
}
// weekday long
else if ($format[$i] == 'l') {
$out .= $this->gettext(strtolower(date('l', $timestamp)));
}
// month name (short)
else if ($format[$i] == 'M') {
$out .= $this->gettext(strtolower(date('M', $timestamp)));
}
// month name (long)
else if ($format[$i] == 'F') {
$out .= $this->gettext('long'.strtolower(date('M', $timestamp)));
}
else if ($format[$i] == 'x') {
$out .= strftime('%x %X', $timestamp);
}
else {
$out .= date($format[$i], $timestamp);
}
}
if ($today) {
$label = $this->gettext('today');
// replcae $ character with "Today" label (#1486120)
if (strpos($out, '$') !== false) {
$out = preg_replace('/\$/', $label, $out, 1);
}
else {
$out = $label . ' ' . $out;
}
}
if ($stz) {
date_default_timezone_set($stz);
}
return $out;
}
/**
* Return folders list in HTML
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function folder_list($attrib)
{
static $a_mailboxes;
$attrib += array('maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)');
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
// 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 current folder
$mbox_name = $storage->get_folder();
// build the folders tree
if (empty($a_mailboxes)) {
// get mailbox list
$a_folders = $storage->list_folders_subscribed(
'', $attrib['folder_name'], $attrib['folder_filter']);
$delimiter = $storage->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,
'type' => $type,
'attribs' => $attrib,
));
$a_mailboxes = $hook['list'];
$attrib = $hook['attribs'];
if ($type == 'select') {
$attrib['is_escaped'] = true;
$select = new html_select($attrib);
// add no-selection option
if ($attrib['noselection']) {
$select->add(html::quote($rcmail->gettext($attrib['noselection'])), '');
}
$rcmail->render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
$out = $select->show($attrib['default']);
}
else {
$js_mailboxlist = array();
$out = html::tag('ul', $attrib, $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
$rcmail->output->include_script('treelist.js');
$rcmail->output->add_gui_object('mailboxlist', $attrib['id']);
$rcmail->output->set_env('mailboxes', $js_mailboxlist);
$rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']);
$rcmail->output->set_env('collapsed_folders', (string)$rcmail->config->get('collapsed_folders'));
}
return $out;
}
/**
* Return folders list as html_select object
*
* @param array $p Named parameters
*
* @return html_select HTML drop-down object
*/
public function folder_selector($p = array())
{
$p += array('maxlength' => 100, 'realnames' => false, 'is_escaped' => true);
$a_mailboxes = array();
$storage = $this->get_storage();
if (empty($p['folder_name'])) {
$p['folder_name'] = '*';
}
if ($p['unsubscribed']) {
$list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
}
else {
$list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
}
$delimiter = $storage->get_hierarchy_delimiter();
foreach ($list as $folder) {
if (empty($p['exceptions']) || !in_array($folder, $p['exceptions'])) {
$this->build_folder_tree($a_mailboxes, $folder, $delimiter);
}
}
$select = new html_select($p);
if ($p['noselection']) {
$select->add(html::quote($p['noselection']), '');
}
$this->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
return $select;
}
/**
* Create a hierarchical array of the mailbox list
*/
public function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
{
// Handle namespace prefix
$prefix = '';
if (!$path) {
$n_folder = $folder;
$folder = $this->storage->mod_folder($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])) {
$arrFolders[$currentFolder] = array(
'id' => $path,
'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
'virtual' => $virtual,
'folders' => array());
}
else {
$arrFolders[$currentFolder]['virtual'] = $virtual;
}
if (strlen($subFolders)) {
$this->build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
}
}
/**
* Return html for a structured list &lt;ul&gt; for the mailbox tree
*/
public function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
{
$maxlength = intval($attrib['maxlength']);
$realnames = (bool)$attrib['realnames'];
$msgcounts = $this->storage->get_cache('messagecount');
$collapsed = $this->config->get('collapsed_folders');
$realnames = $this->config->get('show_real_foldernames');
$out = '';
foreach ($arrFolders as $folder) {
$title = null;
$folder_class = $this->folder_classname($folder['id']);
$is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
$unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
if ($folder_class && !$realnames) {
$foldername = $this->gettext($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 = rcube_utils::html_identifier($folder['id'], true);
$classes = array('mailbox');
// set special class for Sent, Drafts, Trash and Junk
if ($folder_class) {
$classes[] = $folder_class;
}
if ($folder['id'] == $mbox_name) {
$classes[] = 'selected';
}
if ($folder['virtual']) {
$classes[] = 'virtual';
}
else if ($unread) {
$classes[] = 'unread';
}
$js_name = $this->JQ($folder['id']);
$html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : '');
$link_attrib = $folder['virtual'] ? array() : array(
'href' => $this->url(array('_mbox' => $folder['id'])),
'onclick' => sprintf("return %s.command('list','%s',this)", rcmail_output::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));
if (!empty($folder['folders'])) {
$out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
}
$jslist[$folder['id']] = array(
'id' => $folder['id'],
'name' => $foldername,
'virtual' => $folder['virtual']
);
if (!empty($folder['folders'])) {
$out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
$this->render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
}
$out .= "</li>\n";
}
return $out;
}
/**
* Return html for a flat list <select> for the mailbox tree
*/
public function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
{
$out = '';
foreach ($arrFolders as $folder) {
// skip exceptions (and its subfolders)
if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
continue;
}
// skip folders in which it isn't possible to create subfolders
if (!empty($opts['skip_noinferiors'])) {
$attrs = $this->storage->folder_attributes($folder['id']);
if ($attrs && in_array('\\Noinferiors', $attrs)) {
continue;
}
}
if (!$realnames && ($folder_class = $this->folder_classname($folder['id']))) {
$foldername = $this->gettext($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) . html::quote($foldername), $folder['id']);
if (!empty($folder['folders'])) {
$out .= $this->render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
$select, $realnames, $nestLevel+1, $opts);
}
}
return $out;
}
/**
* Return internal name for the given folder if it matches the configured special folders
*/
public function folder_classname($folder_id)
{
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 === $this->config->get($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 $name Folder name
* @param bool $with_path Enable path localization
*
* @return string Localized folder name in UTF-8 encoding
*/
public function localize_foldername($name, $with_path = true)
{
$realnames = $this->config->get('show_real_foldernames');
// try to localize path of the folder
if ($with_path && !$realnames) {
$storage = $this->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$path = explode($delimiter, $name);
$count = count($path);
if ($count > 1) {
for ($i = 0; $i < $count; $i++) {
$folder = implode($delimiter, array_slice($path, 0, -$i));
if ($folder_class = $this->folder_classname($folder)) {
$name = implode($delimiter, array_slice($path, $count - $i));
return $this->gettext($folder_class) . $delimiter . rcube_charset::convert($name, 'UTF7-IMAP');
}
}
}
}
if (!$realnames && ($folder_class = $this->folder_classname($name))) {
return $this->gettext($folder_class);
}
return rcube_charset::convert($name, 'UTF7-IMAP');
}
public function localize_folderpath($path)
{
$protect_folders = $this->config->get('protect_default_folders');
$default_folders = (array) $this->config->get('default_folders');
$delimiter = $this->storage->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[] = $this->localize_foldername($directory);
}
else {
$result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
}
}
return implode($delimiter, $result);
}
public static function quota_display($attrib)
{
$rcmail = rcmail::get_instance();
if (!$attrib['id']) {
$attrib['id'] = 'rcmquotadisplay';
}
$_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
$rcmail->output->add_gui_object('quotadisplay', $attrib['id']);
$quota = $rcmail->quota_content($attrib);
$rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
return html::span($attrib, '');
}
public function quota_content($attrib = null)
{
$quota = $this->storage->get_quota();
$quota = $this->plugins->exec_hook('quota', $quota);
$quota_result = (array) $quota;
$quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
if ($quota['total'] > 0) {
if (!isset($quota['percent'])) {
$quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
}
$title = sprintf('%s / %s (%.0f%%)',
$this->show_bytes($quota['used'] * 1024), $this->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 {
$unlimited = $this->config->get('quota_zero_as_unlimited');
$quota_result['title'] = $this->gettext($unlimited ? 'unlimited' : 'unknown');
$quota_result['percent'] = 0;
}
return $quota_result;
}
/**
* Outputs error message according to server error/response codes
*
* @param string $fallback Fallback message label
* @param array $fallback_args Fallback message label arguments
* @param string $suffix Message label suffix
*/
public function display_server_error($fallback = null, $fallback_args = null, $suffix = '')
{
$err_code = $this->storage->get_error_code();
$res_code = $this->storage->get_response_code();
$args = array();
if ($res_code == rcube_storage::NOPERM) {
$error = 'errornoperm';
}
else if ($res_code == rcube_storage::READONLY) {
$error = 'errorreadonly';
}
else if ($res_code == rcube_storage::OVERQUOTA) {
$error = 'errorroverquota';
}
else if ($err_code && ($err_str = $this->storage->get_error_str())) {
// try to detect access rights problem and display appropriate message
if (stripos($err_str, 'Permission denied') !== false) {
$error = 'errornoperm';
}
// try to detect full mailbox problem and display appropriate message
// there can be e.g. "Quota exceeded" or "quotum would exceed"
else if (stripos($err_str, 'quot') !== false && stripos($err_str, 'exceed') !== false) {
$error = 'erroroverquota';
}
else {
$error = 'servererrormsg';
$args = array('msg' => $err_str);
}
}
else if ($err_code < 0) {
$error = 'storageerror';
}
else if ($fallback) {
$error = $fallback;
$args = $fallback_args;
}
if ($error) {
if ($suffix && $this->text_exists($error . $suffix)) {
$error .= $suffix;
}
$this->output->show_message($error, 'error', $args);
}
}
/**
* Output HTML editor scripts
*
* @param string $mode Editor mode
*/
public function html_editor($mode = '')
{
$hook = $this->plugins->exec_hook('html_editor', array('mode' => $mode));
if ($hook['abort']) {
return;
}
$lang = strtolower($_SESSION['language']);
// TinyMCE uses two-letter lang codes, with exception of Chinese
if (strpos($lang, 'zh_') === 0) {
$lang = str_replace('_', '-', $lang);
}
else {
$lang = substr($lang, 0, 2);
}
if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js')) {
$lang = 'en';
}
$script = json_encode(array(
'mode' => $mode,
'lang' => $lang,
'skin_path' => $this->output->get_skin_path(),
'spellcheck' => intval($this->config->get('enable_spellcheck')),
'spelldict' => intval($this->config->get('spellcheck_dictionary'))
));
$this->output->include_script('tiny_mce/tiny_mce.js');
$this->output->include_script('editor.js');
$this->output->add_script("rcmail_editor_init($script)", 'docready');
}
/**
* Replaces TinyMCE's emoticon images with plain-text representation
*
* @param string $html HTML content
*
* @return string HTML content
*/
public static function 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);
}
/**
* File upload progress handler.
*/
public function upload_progress()
{
$prefix = ini_get('apc.rfc1867_prefix');
$params = array(
'action' => $this->action,
'name' => rcube_utils::get_input_value('_progress', rcube_utils::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'] = $this->gettext(array('name' => 'uploadprogress', 'vars' => array(
'percent' => $params['percent'] . '%',
'current' => $this->show_bytes($params['current']),
'total' => $this->show_bytes($params['total'])
)));
$this->output->command('upload_progress_update', $params);
$this->output->send();
}
/**
* Initializes file uploading interface.
*/
public function upload_init()
{
// Enable upload progress bar
if (($seconds = $this->config->get('upload_progress')) && ini_get('apc.rfc1867')) {
if ($field_name = ini_get('apc.rfc1867_name')) {
$this->output->set_env('upload_progress_name', $field_name);
$this->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;
}
$this->output->set_env('max_filesize', $max_filesize);
$max_filesize = self::show_bytes($max_filesize);
$this->output->set_env('filesizeerror', $this->gettext(array(
'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
return $max_filesize;
}
/**
* Initializes client-side autocompletion.
*/
public function autocomplete_init()
{
static $init;
if ($init) {
return;
}
$init = 1;
if (($threads = (int)$this->config->get('autocomplete_threads')) > 0) {
$book_types = (array) $this->config->get('autocomplete_addressbooks', 'sql');
if (count($book_types) > 1) {
$this->output->set_env('autocomplete_threads', $threads);
$this->output->set_env('autocomplete_sources', $book_types);
}
}
$this->output->set_env('autocomplete_max', (int)$this->config->get('autocomplete_max', 15));
$this->output->set_env('autocomplete_min_length', $this->config->get('autocomplete_min_length'));
$this->output->add_label('autocompletechars', 'autocompletemore');
}
/**
* Returns supported font-family specifications
*
* @param string $font Font name
*
* @param string|array Font-family specification array or string (if $font is used)
*/
public static function font_defs($font = null)
{
$fonts = array(
'Andale Mono' => '"Andale Mono",Times,monospace',
'Arial' => 'Arial,Helvetica,sans-serif',
'Arial Black' => '"Arial Black","Avant Garde",sans-serif',
'Book Antiqua' => '"Book Antiqua",Palatino,serif',
'Courier New' => '"Courier New",Courier,monospace',
'Georgia' => 'Georgia,Palatino,serif',
'Helvetica' => 'Helvetica,Arial,sans-serif',
'Impact' => 'Impact,Chicago,sans-serif',
'Tahoma' => 'Tahoma,Arial,Helvetica,sans-serif',
'Terminal' => 'Terminal,Monaco,monospace',
'Times New Roman' => '"Times New Roman",Times,serif',
'Trebuchet MS' => '"Trebuchet MS",Geneva,sans-serif',
'Verdana' => 'Verdana,Geneva,sans-serif',
);
if ($font) {
return $fonts[$font];
}
return $fonts;
}
/**
* Create a human readable string for a number of bytes
*
* @param int Number of bytes
*
* @return string Byte string
*/
public function show_bytes($bytes)
{
if ($bytes >= 1073741824) {
$gb = $bytes/1073741824;
$str = sprintf($gb>=10 ? "%d " : "%.1f ", $gb) . $this->gettext('GB');
}
else if ($bytes >= 1048576) {
$mb = $bytes/1048576;
$str = sprintf($mb>=10 ? "%d " : "%.1f ", $mb) . $this->gettext('MB');
}
else if ($bytes >= 1024) {
$str = sprintf("%d ", round($bytes/1024)) . $this->gettext('KB');
}
else {
$str = sprintf('%d ', $bytes) . $this->gettext('B');
}
return $str;
}
/**
* Returns real size (calculated) of the message part
*
* @param rcube_message_part Message part
*
* @return string Part size (and unit)
*/
public function message_part_size($part)
{
if (isset($part->d_parameters['size'])) {
$size = $this->show_bytes((int)$part->d_parameters['size']);
}
else {
$size = $part->size;
if ($part->encoding == 'base64') {
$size = $size / 1.33;
}
$size = '~' . $this->show_bytes($size);
}
return $size;
}
/************************************************************************
********* Deprecated methods (to be removed) *********
***********************************************************************/
public static function setcookie($name, $value, $exp = 0)
{
rcube_utils::setcookie($name, $value, $exp);
}
public function imap_connect()
{
return $this->storage_connect();
}
public function imap_init()
{
return $this->storage_init();
}
/**
* Connect to the mail storage server with stored session data
*
* @return bool True on success, False on error
*/
public function storage_connect()
{
$storage = $this->get_storage();
if ($_SESSION['storage_host'] && !$storage->is_connected()) {
$host = $_SESSION['storage_host'];
$user = $_SESSION['username'];
$port = $_SESSION['storage_port'];
$ssl = $_SESSION['storage_ssl'];
$pass = $this->decrypt($_SESSION['password']);
if (!$storage->connect($host, $user, $pass, $port, $ssl)) {
if (is_object($this->output)) {
$this->output->show_message('storageerror', 'error');
}
}
else {
$this->set_storage_prop();
}
}
return $storage->is_connected();
}
}
diff --git a/program/js/app.js b/program/js/app.js
index cb08ce29d..41be99c0f 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -1,6833 +1,6907 @@
/*
+-----------------------------------------------------------------------+
| Roundcube Webmail Client Script |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2013, The Roundcube Dev Team |
| Copyright (C) 2011-2013, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
+-----------------------------------------------------------------------+
| Authors: Thomas Bruederli <roundcube@gmail.com> |
| Aleksander 'A.L.E.C' Machniak <alec@alec.pl> |
| Charles McNulty <charles@charlesmcnulty.com> |
+-----------------------------------------------------------------------+
| Requires: jquery.js, common.js, list.js |
+-----------------------------------------------------------------------+
*/
function rcube_webmail()
{
this.labels = {};
this.buttons = {};
this.buttons_sel = {};
this.gui_objects = {};
this.gui_containers = {};
this.commands = {};
this.command_handlers = {};
this.onloads = [];
this.messages = {};
this.group2expand = {};
// webmail client settings
this.dblclick_time = 500;
this.message_time = 4000;
this.identifier_expr = new RegExp('[^0-9a-z\-_]', 'gi');
// environment defaults
this.env = {
request_timeout: 180, // seconds
draft_autosave: 0, // seconds
comm_path: './',
blankpage: 'program/resources/blank.gif',
recipients_separator: ',',
recipients_delimiter: ', ',
popup_width: 1150,
popup_width_small: 900
};
// create protected reference to myself
this.ref = 'rcmail';
var ref = this;
// set jQuery ajax options
$.ajaxSetup({
cache: false,
timeout: this.env.request_timeout * 1000,
error: function(request, status, err){ ref.http_error(request, status, err); },
beforeSend: function(xmlhttp){ xmlhttp.setRequestHeader('X-Roundcube-Request', ref.env.request_token); }
});
// unload fix
$(window).bind('beforeunload', function() { rcmail.unload = true; });
// set environment variable(s)
this.set_env = function(p, value)
{
if (p != null && typeof p === 'object' && !value)
for (var n in p)
this.env[n] = p[n];
else
this.env[p] = value;
};
// add a localized label to the client environment
this.add_label = function(p, value)
{
if (typeof p == 'string')
this.labels[p] = value;
else if (typeof p == 'object')
$.extend(this.labels, p);
};
// add a button to the button list
this.register_button = function(command, id, type, act, sel, over)
{
var button_prop = {id:id, type:type};
if (act) button_prop.act = act;
if (sel) button_prop.sel = sel;
if (over) button_prop.over = over;
if (!this.buttons[command])
this.buttons[command] = [];
this.buttons[command].push(button_prop);
if (this.loaded)
init_button(command, button_prop);
};
// register a specific gui object
this.gui_object = function(name, id)
{
this.gui_objects[name] = this.loaded ? rcube_find_object(id) : id;
};
// register a container object
this.gui_container = function(name, id)
{
this.gui_containers[name] = id;
};
// add a GUI element (html node) to a specified container
this.add_element = function(elm, container)
{
if (this.gui_containers[container] && this.gui_containers[container].jquery)
this.gui_containers[container].append(elm);
};
// register an external handler for a certain command
this.register_command = function(command, callback, enable)
{
this.command_handlers[command] = callback;
if (enable)
this.enable_command(command, true);
};
// execute the given script on load
this.add_onload = function(f)
{
this.onloads.push(f);
};
// initialize webmail client
this.init = function()
{
var n, p = this;
this.task = this.env.task;
// check browser
if (!bw.dom || !bw.xmlhttp_test() || (bw.mz && bw.vendver < 1.9)) {
this.goto_url('error', '_code=0x199');
return;
}
// find all registered gui containers
for (n in this.gui_containers)
this.gui_containers[n] = $('#'+this.gui_containers[n]);
// find all registered gui objects
for (n in this.gui_objects)
this.gui_objects[n] = rcube_find_object(this.gui_objects[n]);
// clickjacking protection
if (this.env.x_frame_options) {
try {
// bust frame if not allowed
if (this.env.x_frame_options == 'deny' && top.location.href != self.location.href)
top.location.href = self.location.href;
else if (top.location.hostname != self.location.hostname)
throw 1;
} catch (e) {
// possible clickjacking attack: disable all form elements
$('form').each(function(){ ref.lock_form(this, true); });
this.display_message("Blocked: possible clickjacking attack!", 'error');
return;
}
}
// init registered buttons
this.init_buttons();
// tell parent window that this frame is loaded
if (this.is_framed()) {
parent.rcmail.set_busy(false, null, parent.rcmail.env.frame_lock);
parent.rcmail.env.frame_lock = null;
}
// enable general commands
this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref',
'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-save', true);
if (this.env.permaurl)
this.enable_command('permaurl', 'extwin', true);
switch (this.task) {
case 'mail':
// enable mail commands
this.enable_command('list', 'checkmail', 'add-contact', 'search', 'reset-search', 'collapse-folder', 'import-messages', true);
if (this.gui_objects.messagelist) {
this.message_list = new rcube_list_widget(this.gui_objects.messagelist, {
multiselect:true, multiexpand:true, draggable:true, keyboard:true,
column_movable:this.env.col_movable, dblclick_time:this.dblclick_time
});
this.message_list.row_init = function(o){ p.init_message_row(o); };
this.message_list.addEventListener('dblclick', function(o){ p.msglist_dbl_click(o); });
this.message_list.addEventListener('click', function(o){ p.msglist_click(o); });
this.message_list.addEventListener('keypress', function(o){ p.msglist_keypress(o); });
this.message_list.addEventListener('select', function(o){ p.msglist_select(o); });
this.message_list.addEventListener('dragstart', function(o){ p.drag_start(o); });
this.message_list.addEventListener('dragmove', function(e){ p.drag_move(e); });
this.message_list.addEventListener('dragend', function(e){ p.drag_end(e); });
this.message_list.addEventListener('expandcollapse', function(e){ p.msglist_expand(e); });
this.message_list.addEventListener('column_replace', function(e){ p.msglist_set_coltypes(e); });
this.message_list.addEventListener('listupdate', function(e){ p.triggerEvent('listupdate', e); });
document.onmouseup = function(e){ return p.doc_mouse_up(e); };
this.gui_objects.messagelist.parentNode.onmousedown = function(e){ return p.click_on_list(e); };
this.message_list.init();
this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
// load messages
this.command('list');
}
if (this.gui_objects.qsearchbox) {
if (this.env.search_text != null)
this.gui_objects.qsearchbox.value = this.env.search_text;
$(this.gui_objects.qsearchbox).focusin(function() { rcmail.message_list && rcmail.message_list.blur(); });
}
this.set_button_titles();
this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list',
'moveto', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource',
'print', 'load-attachment', 'download-attachment', 'show-headers', 'hide-headers', 'download',
'forward', 'forward-inline', 'forward-attachment', 'change-format'];
if (this.env.action == 'show' || this.env.action == 'preview') {
this.enable_command(this.env.message_commands, this.env.uid);
this.enable_command('reply-list', this.env.list_post);
if (this.env.action == 'show') {
this.http_request('pagenav', {_uid: this.env.uid, _mbox: this.env.mailbox, _search: this.env.search_request},
this.display_message('', 'loading'));
}
if (this.env.blockedobjects) {
if (this.gui_objects.remoteobjectsmsg)
this.gui_objects.remoteobjectsmsg.style.display = 'block';
this.enable_command('load-images', 'always-load', true);
}
// make preview/message frame visible
if (this.env.action == 'preview' && this.is_framed()) {
this.enable_command('compose', 'add-contact', false);
parent.rcmail.show_contentframe(true);
}
}
else if (this.env.action == 'compose') {
- this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'search', 'reset-search', 'extwin'];
+ this.env.address_group_stack = [];
+ this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin'];
if (this.env.drafts_mailbox)
this.env.compose_commands.push('savedraft')
this.enable_command(this.env.compose_commands, 'identities', true);
// add more commands (not enabled)
$.merge(this.env.compose_commands, ['add-recipient', 'firstpage', 'previouspage', 'nextpage', 'lastpage']);
if (this.env.spellcheck) {
this.env.spellcheck.spelling_state_observer = function(s) { ref.spellcheck_state(); };
this.env.compose_commands.push('spellcheck')
this.enable_command('spellcheck', true);
}
document.onmouseup = function(e){ return p.doc_mouse_up(e); };
// init message compose form
this.init_messageform();
}
// show printing dialog
else if (this.env.action == 'print' && this.env.uid) {
if (bw.safari)
setTimeout('window.print()', 10);
else
window.print();
}
// get unread count for each mailbox
if (this.gui_objects.mailboxlist) {
this.env.unread_counts = {};
this.gui_objects.folderlist = this.gui_objects.mailboxlist;
this.http_request('getunread');
}
// init address book widget
if (this.gui_objects.contactslist) {
this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
{ multiselect:true, draggable:false, keyboard:false });
this.contact_list.addEventListener('select', function(o){ ref.compose_recipient_select(o); });
this.contact_list.addEventListener('dblclick', function(o){ ref.compose_add_recipient('to'); });
this.contact_list.init();
}
if (this.gui_objects.addressbookslist) {
this.gui_objects.folderlist = this.gui_objects.addressbookslist;
this.enable_command('list-adresses', true);
}
// ask user to send MDN
if (this.env.mdn_request && this.env.uid) {
var postact = 'sendmdn',
postdata = {_uid: this.env.uid, _mbox: this.env.mailbox};
if (!confirm(this.get_label('mdnrequest'))) {
postdata._flag = 'mdnsent';
postact = 'mark';
}
this.http_post(postact, postdata);
}
// detect browser capabilities
if (!this.is_framed() && !this.env.extwin)
this.browser_capabilities_check();
break;
case 'addressbook':
+ this.env.address_group_stack = [];
+
if (this.gui_objects.folderlist)
this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
this.enable_command('add', 'import', this.env.writable_source);
- this.enable_command('list', 'listgroup', 'listsearch', 'advanced-search', true);
+ this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'advanced-search', true);
if (this.gui_objects.contactslist) {
this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
{multiselect:true, draggable:this.gui_objects.folderlist?true:false, keyboard:true});
this.contact_list.row_init = function(row){ p.triggerEvent('insertrow', { cid:row.uid, row:row }); };
this.contact_list.addEventListener('keypress', function(o){ p.contactlist_keypress(o); });
this.contact_list.addEventListener('select', function(o){ p.contactlist_select(o); });
this.contact_list.addEventListener('dragstart', function(o){ p.drag_start(o); });
this.contact_list.addEventListener('dragmove', function(e){ p.drag_move(e); });
this.contact_list.addEventListener('dragend', function(e){ p.drag_end(e); });
this.contact_list.init();
if (this.env.cid)
this.contact_list.highlight_row(this.env.cid);
this.gui_objects.contactslist.parentNode.onmousedown = function(e){ return p.click_on_list(e); };
document.onmouseup = function(e){ return p.doc_mouse_up(e); };
if (this.gui_objects.qsearchbox)
$(this.gui_objects.qsearchbox).focusin(function() { rcmail.contact_list.blur(); });
this.update_group_commands();
this.command('list');
}
this.set_page_buttons();
if (this.env.cid) {
this.enable_command('show', 'edit', true);
// register handlers for group assignment via checkboxes
if (this.gui_objects.editform) {
$('input.groupmember').change(function() {
ref.group_member_change(this.checked ? 'add' : 'del', ref.env.cid, ref.env.source, this.value);
});
}
}
if (this.gui_objects.editform) {
this.enable_command('save', true);
if (this.env.action == 'add' || this.env.action == 'edit' || this.env.action == 'search')
this.init_contact_form();
}
if (this.gui_objects.qsearchbox)
this.enable_command('search', 'reset-search', 'moveto', true);
break;
case 'settings':
this.enable_command('preferences', 'identities', 'save', 'folders', true);
if (this.env.action == 'identities') {
this.enable_command('add', this.env.identities_level < 2);
}
else if (this.env.action == 'edit-identity' || this.env.action == 'add-identity') {
this.enable_command('save', 'edit', 'toggle-editor', true);
this.enable_command('delete', this.env.identities_level < 2);
if (this.env.action == 'add-identity')
$("input[type='text']").first().select();
}
else if (this.env.action == 'folders') {
this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', true);
}
else if (this.env.action == 'edit-folder' && this.gui_objects.editform) {
this.enable_command('save', 'folder-size', true);
parent.rcmail.env.messagecount = this.env.messagecount;
parent.rcmail.enable_command('purge', this.env.messagecount);
$("input[type='text']").first().select();
}
if (this.gui_objects.identitieslist) {
this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist, {multiselect:false, draggable:false, keyboard:false});
this.identity_list.addEventListener('select', function(o){ p.identity_select(o); });
this.identity_list.init();
this.identity_list.focus();
if (this.env.iid)
this.identity_list.highlight_row(this.env.iid);
}
else if (this.gui_objects.sectionslist) {
this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:false});
this.sections_list.addEventListener('select', function(o){ p.section_select(o); });
this.sections_list.init();
this.sections_list.focus();
}
else if (this.gui_objects.subscriptionlist)
this.init_subscription_list();
break;
case 'login':
var input_user = $('#rcmloginuser');
input_user.bind('keyup', function(e){ return rcmail.login_user_keyup(e); });
if (input_user.val() == '')
input_user.focus();
else
$('#rcmloginpwd').focus();
// detect client timezone
if (window.jstz && !bw.ie6) {
var timezone = jstz.determine();
if (timezone.name())
$('#rcmlogintz').val(timezone.name());
}
else {
$('#rcmlogintz').val(new Date().getStdTimezoneOffset() / -60);
}
// display 'loading' message on form submit, lock submit button
$('form').submit(function () {
$('input[type=submit]', this).prop('disabled', true);
rcmail.clear_messages();
rcmail.display_message('', 'loading');
});
this.enable_command('login', true);
break;
}
// unset contentframe variable if preview_pane is enabled
if (this.env.contentframe && !$('#' + this.env.contentframe).is(':visible'))
this.env.contentframe = null;
// prevent from form submit with Enter key in file input fields
if (bw.ie)
$('input[type=file]').keydown(function(e) { if (e.keyCode == '13') e.preventDefault(); });
// flag object as complete
this.loaded = true;
// show message
if (this.pending_message)
this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]);
// map implicit containers
if (this.gui_objects.folderlist) {
this.gui_containers.foldertray = $(this.gui_objects.folderlist);
// init treelist widget
if (window.rcube_treelist_widget) {
this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
id_prefix: 'rcmli',
id_encode: this.html_identifier_encode,
id_decode: this.html_identifier_decode,
check_droptarget: function(node){ return !node.virtual && ref.check_droptarget(node.id) }
});
this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) });
this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) });
this.treelist.addEventListener('select', function(node){ ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) });
}
}
// activate html5 file drop feature (if browser supports it and if configured)
if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
$(document.body).bind('dragover dragleave drop', function(e){ return ref.document_drag_hover(e, e.type == 'dragover'); });
$(this.gui_objects.filedrop).addClass('droptarget')
.bind('dragover dragleave', function(e){ return ref.file_drag_hover(e, e.type == 'dragover'); })
.get(0).addEventListener('drop', function(e){ return ref.file_dropped(e); }, false);
}
// trigger init event hook
this.triggerEvent('init', { task:this.task, action:this.env.action });
// execute all foreign onload scripts
// @deprecated
for (var i in this.onloads) {
if (typeof this.onloads[i] === 'string')
eval(this.onloads[i]);
else if (typeof this.onloads[i] === 'function')
this.onloads[i]();
}
// start keep-alive and refresh intervals
this.start_refresh();
this.start_keepalive();
};
this.log = function(msg)
{
if (window.console && console.log)
console.log(msg);
};
/*********************************************************/
/********* client command interface *********/
/*********************************************************/
// execute a specific command on the web client
this.command = function(command, props, obj, event)
{
var ret, uid, cid, url, flag;
if (obj && obj.blur)
obj.blur();
if (this.busy)
return false;
// let the browser handle this click (shift/ctrl usually opens the link in a new window/tab)
if ((obj && obj.href && String(obj.href).indexOf('#') < 0) && rcube_event.get_modifier(event)) {
return true;
}
// command not supported or allowed
if (!this.commands[command]) {
// pass command to parent window
if (this.is_framed())
parent.rcmail.command(command, props);
return false;
}
// check input before leaving compose step
if (this.task == 'mail' && this.env.action == 'compose' && $.inArray(command, this.env.compose_commands)<0) {
if (this.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning')))
return false;
}
// process external commands
if (typeof this.command_handlers[command] === 'function') {
ret = this.command_handlers[command](props, obj);
return ret !== undefined ? ret : (obj ? false : true);
}
else if (typeof this.command_handlers[command] === 'string') {
ret = window[this.command_handlers[command]](props, obj);
return ret !== undefined ? ret : (obj ? false : true);
}
// trigger plugin hooks
this.triggerEvent('actionbefore', {props:props, action:command});
ret = this.triggerEvent('before'+command, props);
if (ret !== undefined) {
// abort if one of the handlers returned false
if (ret === false)
return false;
else
props = ret;
}
ret = undefined;
// process internal command
switch (command) {
case 'login':
if (this.gui_objects.loginform)
this.gui_objects.loginform.submit();
break;
// commands to switch task
case 'mail':
case 'addressbook':
case 'settings':
case 'logout':
this.switch_task(command);
break;
case 'about':
this.redirect('?_task=settings&_action=about', false);
break;
case 'permaurl':
if (obj && obj.href && obj.target)
return true;
else if (this.env.permaurl)
parent.location.href = this.env.permaurl;
break;
case 'extwin':
if (this.env.action == 'compose') {
var form = this.gui_objects.messageform,
win = this.open_window('');
$("input[name='_action']", form).val('compose');
form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
form.target = win.name;
form.submit();
}
else {
this.open_window(this.env.permaurl, true);
}
break;
case 'change-format':
url = this.env.permaurl + '&_format=' + props;
if (this.env.action == 'preview')
url = url.replace(/_action=show/, '_action=preview') + '&_framed=1';
if (this.env.extwin)
url += '&_extwin=1';
location.href = url;
break;
case 'menu-open':
if (props && props.menu == 'attachmentmenu') {
var mimetype = this.env.attachments[props.id];
this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0);
}
case 'menu-save':
this.triggerEvent(command, {props:props});
return false;
case 'open':
if (uid = this.get_single_uid()) {
obj.href = this.url('show', {_mbox: this.env.mailbox, _uid: uid});
return true;
}
break;
case 'close':
if (this.env.extwin)
window.close();
break;
case 'list':
if (props && props != '')
this.reset_qsearch();
if (this.env.action == 'compose' && this.env.extwin)
window.close();
else if (this.task == 'mail') {
this.list_mailbox(props);
this.set_button_titles();
}
else if (this.task == 'addressbook')
this.list_contacts(props);
break;
case 'sort':
var sort_order = this.env.sort_order,
sort_col = !this.env.disabled_sort_col ? props : this.env.sort_col;
if (!this.env.disabled_sort_order)
sort_order = this.env.sort_col == sort_col && sort_order == 'ASC' ? 'DESC' : 'ASC';
// set table header and update env
this.set_list_sorting(sort_col, sort_order);
// reload message list
this.list_mailbox('', '', sort_col+'_'+sort_order);
break;
case 'nextpage':
this.list_page('next');
break;
case 'lastpage':
this.list_page('last');
break;
case 'previouspage':
this.list_page('prev');
break;
case 'firstpage':
this.list_page('first');
break;
case 'expunge':
if (this.env.exists)
this.expunge_mailbox(this.env.mailbox);
break;
case 'purge':
case 'empty-mailbox':
if (this.env.exists)
this.purge_mailbox(this.env.mailbox);
break;
// common commands used in multiple tasks
case 'show':
if (this.task == 'mail') {
uid = this.get_single_uid();
if (uid && (!this.env.uid || uid != this.env.uid)) {
if (this.env.mailbox == this.env.drafts_mailbox)
this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
else
this.show_message(uid);
}
}
else if (this.task == 'addressbook') {
cid = props ? props : this.get_single_cid();
if (cid && !(this.env.action == 'show' && cid == this.env.cid))
this.load_contact(cid, 'show');
}
break;
case 'add':
if (this.task == 'addressbook')
this.load_contact(0, 'add');
else if (this.task == 'settings') {
this.identity_list.clear_selection();
this.load_identity(0, 'add-identity');
}
break;
case 'edit':
if (this.task == 'addressbook' && (cid = this.get_single_cid()))
this.load_contact(cid, 'edit');
else if (this.task == 'settings' && props)
this.load_identity(props, 'edit-identity');
else if (this.task == 'mail' && (cid = this.get_single_uid())) {
url = { _mbox: this.env.mailbox };
url[this.env.mailbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = cid;
this.open_compose_step(url);
}
break;
case 'save':
var input, form = this.gui_objects.editform;
if (form) {
// adv. search
if (this.env.action == 'search') {
}
// user prefs
else if ((input = $("input[name='_pagesize']", form)) && input.length && isNaN(parseInt(input.val()))) {
alert(this.get_label('nopagesizewarning'));
input.focus();
break;
}
// contacts/identities
else {
// reload form
if (props == 'reload') {
form.action += '?_reload=1';
}
else if (this.task == 'settings' && (this.env.identities_level % 2) == 0 &&
(input = $("input[name='_email']", form)) && input.length && !rcube_check_email(input.val())
) {
alert(this.get_label('noemailwarning'));
input.focus();
break;
}
// clear empty input fields
$('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; });
}
// add selected source (on the list)
if (parent.rcmail && parent.rcmail.env.source)
form.action = this.add_url(form.action, '_orig_source', parent.rcmail.env.source);
form.submit();
}
break;
case 'delete':
// mail task
if (this.task == 'mail')
this.delete_messages(event);
// addressbook task
else if (this.task == 'addressbook')
this.delete_contacts();
// user settings task
else if (this.task == 'settings')
this.delete_identity();
break;
// mail task commands
case 'move':
case 'moveto':
if (this.task == 'mail')
this.move_messages(props);
else if (this.task == 'addressbook')
this.copy_contact(null, props);
break;
case 'copy':
if (this.task == 'mail')
this.copy_messages(props);
break;
case 'mark':
if (props)
this.mark_message(props);
break;
case 'toggle_status':
if (props && !props._row)
break;
flag = 'read';
if (props._row.uid) {
uid = props._row.uid;
// toggle read/unread
if (this.message_list.rows[uid].deleted)
flag = 'undelete';
else if (!this.message_list.rows[uid].unread)
flag = 'unread';
}
this.mark_message(flag, uid);
break;
case 'toggle_flag':
if (props && !props._row)
break;
flag = 'flagged';
if (props._row.uid) {
uid = props._row.uid;
// toggle flagged/unflagged
if (this.message_list.rows[uid].flagged)
flag = 'unflagged';
}
this.mark_message(flag, uid);
break;
case 'always-load':
if (this.env.uid && this.env.sender) {
this.add_contact(this.env.sender);
setTimeout(function(){ ref.command('load-images'); }, 300);
break;
}
case 'load-images':
if (this.env.uid)
this.show_message(this.env.uid, true, this.env.action=='preview');
break;
case 'load-attachment':
case 'open-attachment':
case 'download-attachment':
var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props,
mimetype = this.env.attachments[props];
// open attachment in frame if it's of a supported mimetype
if (command != 'download-attachment' && mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0) {
if (this.open_window(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1', true, true))
break;
}
this.goto_url('get', qstring+'&_download=1', false);
break;
case 'select-all':
this.select_all_mode = props ? false : true;
this.dummy_select = true; // prevent msg opening if there's only one msg on the list
if (props == 'invert')
this.message_list.invert_selection();
else
this.message_list.select_all(props == 'page' ? '' : props);
this.dummy_select = null;
break;
case 'select-none':
this.select_all_mode = false;
this.message_list.clear_selection();
break;
case 'expand-all':
this.env.autoexpand_threads = 1;
this.message_list.expand_all();
break;
case 'expand-unread':
this.env.autoexpand_threads = 2;
this.message_list.collapse_all();
this.expand_unread();
break;
case 'collapse-all':
this.env.autoexpand_threads = 0;
this.message_list.collapse_all();
break;
case 'nextmessage':
if (this.env.next_uid)
this.show_message(this.env.next_uid, false, this.env.action == 'preview');
break;
case 'lastmessage':
if (this.env.last_uid)
this.show_message(this.env.last_uid);
break;
case 'previousmessage':
if (this.env.prev_uid)
this.show_message(this.env.prev_uid, false, this.env.action == 'preview');
break;
case 'firstmessage':
if (this.env.first_uid)
this.show_message(this.env.first_uid);
break;
case 'compose':
url = {};
if (this.task == 'mail') {
url._mbox = this.env.mailbox;
if (props)
url._to = props;
// also send search request so we can go back to search result after message is sent
if (this.env.search_request)
url._search = this.env.search_request;
}
// modify url if we're in addressbook
else if (this.task == 'addressbook') {
// switch to mail compose step directly
if (props && props.indexOf('@') > 0) {
url._to = props;
}
else {
var a_cids = [];
// use contact id passed as command parameter
if (props)
a_cids.push(props);
// get selected contacts
else if (this.contact_list)
a_cids = this.contact_list.get_selection();
if (a_cids.length)
this.http_post('mailto', { _cid: a_cids.join(','), _source: this.env.source }, true);
else if (this.env.group)
this.http_post('mailto', { _gid: this.env.group, _source: this.env.source }, true);
break;
}
}
else if (props)
url._to = props;
this.open_compose_step(url);
break;
case 'spellcheck':
if (this.spellcheck_state()) {
this.stop_spellchecking();
}
else {
if (window.tinyMCE && tinyMCE.get(this.env.composebody)) {
tinyMCE.execCommand('mceSpellCheck', true);
}
else if (this.env.spellcheck && this.env.spellcheck.spellCheck) {
this.env.spellcheck.spellCheck();
}
}
this.spellcheck_state();
break;
case 'savedraft':
// Reset the auto-save timer
clearTimeout(this.save_timer);
// compose form did not change (and draft wasn't saved already)
if (this.env.draft_id && this.cmp_hash == this.compose_field_hash()) {
this.auto_save_start();
break;
}
this.submit_messageform(true);
break;
case 'send':
if (!props.nocheck && !this.check_compose_input(command))
break;
// Reset the auto-save timer
clearTimeout(this.save_timer);
this.submit_messageform();
break;
case 'send-attachment':
// Reset the auto-save timer
clearTimeout(this.save_timer);
this.upload_file(props || this.gui_objects.uploadform, 'upload');
break;
case 'insert-sig':
this.change_identity($("[name='_from']")[0], true);
break;
case 'list-adresses':
this.list_contacts(props);
this.enable_command('add-recipient', false);
break;
case 'add-recipient':
this.compose_add_recipient(props);
break;
case 'reply-all':
case 'reply-list':
case 'reply':
if (uid = this.get_single_uid()) {
url = {_reply_uid: uid, _mbox: this.env.mailbox};
if (command == 'reply-all')
// do reply-list, when list is detected and popup menu wasn't used
url._all = (!props && this.commands['reply-list'] ? 'list' : 'all');
else if (command == 'reply-list')
url._all = 'list';
this.open_compose_step(url);
}
break;
case 'forward-attachment':
case 'forward-inline':
case 'forward':
var uids = this.env.uid ? [this.env.uid] : (this.message_list ? this.message_list.get_selection() : []);
if (uids.length) {
url = { _forward_uid: this.uids_to_list(uids), _mbox: this.env.mailbox };
if (command == 'forward-attachment' || (!props && this.env.forward_attachment) || uids.length > 1)
url._attachment = 1;
this.open_compose_step(url);
}
break;
case 'print':
if (uid = this.get_single_uid()) {
ref.printwin = this.open_window(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)+(this.env.safemode ? '&_safe=1' : ''), true, true);
if (this.printwin) {
if (this.env.action != 'show')
this.mark_message('read', uid);
}
}
break;
case 'viewsource':
if (uid = this.get_single_uid())
this.open_window(this.env.comm_path+'&_action=viewsource&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox), true, true);
break;
case 'download':
if (uid = this.get_single_uid())
this.goto_url('viewsource', { _uid: uid, _mbox: this.env.mailbox, _save: 1 });
break;
// quicksearch
case 'search':
if (!props && this.gui_objects.qsearchbox)
props = this.gui_objects.qsearchbox.value;
if (props) {
this.qsearch(props);
break;
}
// reset quicksearch
case 'reset-search':
var n, s = this.env.search_request || this.env.qsearch;
this.reset_qsearch();
this.select_all_mode = false;
if (s && this.env.action == 'compose') {
if (this.contact_list)
this.list_contacts_clear();
}
else if (s && this.env.mailbox) {
this.list_mailbox(this.env.mailbox, 1);
}
else if (s && this.task == 'addressbook') {
if (this.env.source == '') {
for (n in this.env.address_sources) break;
this.env.source = n;
this.env.group = '';
}
this.list_contacts(this.env.source, this.env.group, 1);
}
break;
+ case 'pushgroup':
+ // add group ID to stack
+ this.env.address_group_stack.push(props.id);
+ if (obj && event)
+ rcube_event.cancel(event);
+
case 'listgroup':
this.reset_qsearch();
this.list_contacts(props.source, props.id);
break;
+ case 'popgroup':
+ if (this.env.address_group_stack.length > 1) {
+ this.env.address_group_stack.pop();
+ this.reset_qsearch();
+ this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1]);
+ }
+ break;
+
case 'import-messages':
var form = props || this.gui_objects.importform;
$('input[name="_unlock"]', form).val(this.set_busy(true, 'importwait'));
this.upload_file(form, 'import');
break;
case 'import':
if (this.env.action == 'import' && this.gui_objects.importform) {
var file = document.getElementById('rcmimportfile');
if (file && !file.value) {
alert(this.get_label('selectimportfile'));
break;
}
this.gui_objects.importform.submit();
this.set_busy(true, 'importwait');
this.lock_form(this.gui_objects.importform, true);
}
else
this.goto_url('import', (this.env.source ? '_target='+urlencode(this.env.source)+'&' : ''));
break;
case 'export':
if (this.contact_list.rowcount > 0) {
this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request });
}
break;
case 'export-selected':
if (this.contact_list.rowcount > 0) {
this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') });
}
break;
case 'upload-photo':
this.upload_contact_photo(props || this.gui_objects.uploadform);
break;
case 'delete-photo':
this.replace_contact_photo('-del-');
break;
// user settings commands
case 'preferences':
case 'identities':
case 'folders':
this.goto_url('settings/' + command);
break;
case 'undo':
this.http_request('undo', '', this.display_message('', 'loading'));
break;
// unified command call (command name == function name)
default:
var func = command.replace(/-/g, '_');
if (this[func] && typeof this[func] === 'function') {
ret = this[func](props, obj);
}
break;
}
if (this.triggerEvent('after'+command, props) === false)
ret = false;
this.triggerEvent('actionafter', {props:props, action:command});
return ret === false ? false : obj ? false : true;
};
// set command(s) enabled or disabled
this.enable_command = function()
{
var i, n, args = Array.prototype.slice.call(arguments),
enable = args.pop(), cmd;
for (n=0; n<args.length; n++) {
cmd = args[n];
// argument of type array
if (typeof cmd === 'string') {
this.commands[cmd] = enable;
this.set_button(cmd, (enable ? 'act' : 'pas'));
this.triggerEvent('enable-command', {command: cmd, status: enable});
}
// push array elements into commands array
else {
for (i in cmd)
args.push(cmd[i]);
}
}
};
// lock/unlock interface
this.set_busy = function(a, message, id)
{
if (a && message) {
var msg = this.get_label(message);
if (msg == message)
msg = 'Loading...';
id = this.display_message(msg, 'loading');
}
else if (!a && id) {
this.hide_message(id);
}
this.busy = a;
//document.body.style.cursor = a ? 'wait' : 'default';
if (this.gui_objects.editform)
this.lock_form(this.gui_objects.editform, a);
return id;
};
// return a localized string
this.get_label = function(name, domain)
{
if (domain && this.labels[domain+'.'+name])
return this.labels[domain+'.'+name];
else if (this.labels[name])
return this.labels[name];
else
return name;
};
// alias for convenience reasons
this.gettext = this.get_label;
// switch to another application task
this.switch_task = function(task)
{
if (this.task===task && task!='mail')
return;
var url = this.get_task_url(task);
if (task=='mail')
url += '&_mbox=INBOX';
this.redirect(url);
};
this.get_task_url = function(task, url)
{
if (!url)
url = this.env.comm_path;
return url.replace(/_task=[a-z0-9_-]+/i, '_task='+task);
};
this.reload = function(delay)
{
if (this.is_framed())
parent.rcmail.reload(delay);
else if (delay)
setTimeout(function(){ rcmail.reload(); }, delay);
else if (window.location)
location.href = this.env.comm_path + (this.env.action ? '&_action='+this.env.action : '');
};
// Add variable to GET string, replace old value if exists
this.add_url = function(url, name, value)
{
value = urlencode(value);
if (/(\?.*)$/.test(url)) {
var urldata = RegExp.$1,
datax = RegExp('((\\?|&)'+RegExp.escape(name)+'=[^&]*)');
if (datax.test(urldata)) {
urldata = urldata.replace(datax, RegExp.$2 + name + '=' + value);
}
else
urldata += '&' + name + '=' + value
return url.replace(/(\?.*)$/, urldata);
}
return url + '?' + name + '=' + value;
};
this.is_framed = function()
{
return (this.env.framed && parent.rcmail && parent.rcmail != this && parent.rcmail.command);
};
this.save_pref = function(prop)
{
var request = {'_name': prop.name, '_value': prop.value};
if (prop.session)
request['_session'] = prop.session;
if (prop.env)
this.env[prop.env] = prop.value;
this.http_post('save-pref', request);
};
this.html_identifier = function(str, encode)
{
return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_');
};
this.html_identifier_encode = function(str)
{
return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
};
this.html_identifier_decode = function(str)
{
str = String(str).replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
return Base64.decode(str);
};
/*********************************************************/
/********* event handling methods *********/
/*********************************************************/
this.drag_menu = function(e, target)
{
var modkey = rcube_event.get_modifier(e),
menu = this.gui_objects.message_dragmenu;
if (menu && modkey == SHIFT_KEY && this.commands['copy']) {
var pos = rcube_event.get_mouse_pos(e);
this.env.drag_target = target;
$(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'}).show();
return true;
}
return false;
};
this.drag_menu_action = function(action)
{
var menu = this.gui_objects.message_dragmenu;
if (menu) {
$(menu).hide();
}
this.command(action, this.env.drag_target);
this.env.drag_target = null;
};
this.drag_start = function(list)
{
var model = this.task == 'mail' ? this.env.mailboxes : this.env.contactfolders;
this.drag_active = true;
if (this.preview_timer)
clearTimeout(this.preview_timer);
if (this.preview_read_timer)
clearTimeout(this.preview_read_timer);
// prepare treelist widget for dragging interactions
if (this.treelist)
this.treelist.drag_start();
};
this.drag_end = function(e)
{
this.drag_active = false;
this.env.last_folder_target = null;
if (this.treelist)
this.treelist.drag_end();
};
this.drag_move = function(e)
{
if (this.gui_objects.folderlist) {
var drag_target, oldclass,
layerclass = 'draglayernormal',
mouse = rcube_event.get_mouse_pos(e);
if (this.contact_list && this.contact_list.draglayer)
oldclass = this.contact_list.draglayer.attr('class');
// mouse intersects a valid drop target on the treelist
if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) {
this.env.last_folder_target = drag_target;
layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal');
}
else {
// Clear target, otherwise drag end will trigger move into last valid droptarget
this.env.last_folder_target = null;
}
if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
this.contact_list.draglayer.attr('class', layerclass);
}
};
this.collapse_folder = function(name)
{
if (this.treelist)
this.treelist.toggle(name);
};
this.folder_collapsed = function(node)
{
var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders';
if (node.collapsed) {
this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
// select the folder if one of its childs is currently selected
// don't select if it's virtual (#1488346)
if (this.env.mailbox && this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !node.virtual)
this.command('list', name);
}
else {
var reg = new RegExp('&'+urlencode(node.id)+'&');
this.env[prefname] = this.env[prefname].replace(reg, '');
}
if (!this.drag_active) {
this.command('save-pref', { name: prefname, value: this.env[prefname] });
if (this.env.unread_counts)
this.set_unread_count_display(node.id, false);
}
};
this.doc_mouse_up = function(e)
{
var model, list, id;
// ignore event if jquery UI dialog is open
if ($(rcube_event.get_target(e)).closest('.ui-dialog, .ui-widget-overlay').length)
return;
if (list = this.message_list)
model = this.env.mailboxes;
else if (list = this.contact_list)
model = this.env.contactfolders;
else if (this.ksearch_value)
this.ksearch_blur();
if (list && !rcube_mouse_is_over(e, list.list.parentNode))
list.blur();
// handle mouse release when dragging
if (this.drag_active && model && this.env.last_folder_target) {
var target = model[this.env.last_folder_target];
this.env.last_folder_target = null;
list.draglayer.hide();
this.drag_end(e);
if (!this.drag_menu(e, target))
this.command('moveto', target);
}
// reset 'pressed' buttons
if (this.buttons_sel) {
for (id in this.buttons_sel)
if (typeof id !== 'function')
this.button_out(this.buttons_sel[id], id);
this.buttons_sel = {};
}
};
this.click_on_list = function(e)
{
if (this.gui_objects.qsearchbox)
this.gui_objects.qsearchbox.blur();
if (this.message_list)
this.message_list.focus();
else if (this.contact_list)
this.contact_list.focus();
return true;
};
this.msglist_select = function(list)
{
if (this.preview_timer)
clearTimeout(this.preview_timer);
if (this.preview_read_timer)
clearTimeout(this.preview_read_timer);
var selected = list.get_single_selection();
this.enable_command(this.env.message_commands, selected != null);
if (selected) {
// Hide certain command buttons when Drafts folder is selected
if (this.env.mailbox == this.env.drafts_mailbox)
this.enable_command('reply', 'reply-all', 'reply-list', 'forward', 'forward-attachment', 'forward-inline', false);
// Disable reply-list when List-Post header is not set
else {
var msg = this.env.messages[selected];
if (!msg.ml)
this.enable_command('reply-list', false);
}
}
// Multi-message commands
this.enable_command('delete', 'moveto', 'copy', 'mark', 'forward', 'forward-attachment', list.selection.length > 0);
// reset all-pages-selection
if (selected || (list.selection.length && list.selection.length != list.rowcount))
this.select_all_mode = false;
// start timer for message preview (wait for double click)
if (selected && this.env.contentframe && !list.multi_selecting && !this.dummy_select)
this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time);
else if (this.env.contentframe)
this.show_contentframe(false);
};
// This allow as to re-select selected message and display it in preview frame
this.msglist_click = function(list)
{
if (list.multi_selecting || !this.env.contentframe)
return;
if (list.get_single_selection())
return;
var win = this.get_frame_window(this.env.contentframe);
if (win && win.location.href.indexOf(this.env.blankpage) >= 0) {
if (this.preview_timer)
clearTimeout(this.preview_timer);
if (this.preview_read_timer)
clearTimeout(this.preview_read_timer);
this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time);
}
};
this.msglist_dbl_click = function(list)
{
if (this.preview_timer)
clearTimeout(this.preview_timer);
if (this.preview_read_timer)
clearTimeout(this.preview_read_timer);
var uid = list.get_single_selection();
if (uid && this.env.mailbox == this.env.drafts_mailbox)
this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
else if (uid)
this.show_message(uid, false, false);
};
this.msglist_keypress = function(list)
{
if (list.modkey == CONTROL_KEY)
return;
if (list.key_pressed == list.ENTER_KEY)
this.command('show');
else if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY)
this.command('delete');
else if (list.key_pressed == 33)
this.command('previouspage');
else if (list.key_pressed == 34)
this.command('nextpage');
};
this.msglist_get_preview = function()
{
var uid = this.get_single_uid();
if (uid && this.env.contentframe && !this.drag_active)
this.show_message(uid, false, true);
else if (this.env.contentframe)
this.show_contentframe(false);
};
this.msglist_expand = function(row)
{
if (this.env.messages[row.uid])
this.env.messages[row.uid].expanded = row.expanded;
$(row.obj)[row.expanded?'addClass':'removeClass']('expanded');
};
this.msglist_set_coltypes = function(list)
{
var i, found, name, cols = list.thead.rows[0].cells;
this.env.coltypes = [];
for (i=0; i<cols.length; i++)
if (cols[i].id && cols[i].id.match(/^rcm/)) {
name = cols[i].id.replace(/^rcm/, '');
this.env.coltypes.push(name);
}
if ((found = $.inArray('flag', this.env.coltypes)) >= 0)
this.env.flagged_col = found;
if ((found = $.inArray('subject', this.env.coltypes)) >= 0)
this.env.subject_col = found;
this.command('save-pref', { name: 'list_cols', value: this.env.coltypes, session: 'list_attrib/columns' });
};
this.check_droptarget = function(id)
{
if (this.task == 'mail')
return (this.env.mailboxes[id] && this.env.mailboxes[id].id != this.env.mailbox && !this.env.mailboxes[id].virtual) ? 1 : 0;
if (this.task == 'settings')
return id != this.env.mailbox ? 1 : 0;
if (this.task == 'addressbook') {
if (id != this.env.source && this.env.contactfolders[id]) {
// droptarget is a group - contact add to group action
if (this.env.contactfolders[id].type == 'group') {
var target_abook = this.env.contactfolders[id].source;
if (this.env.contactfolders[id].id != this.env.group && !this.env.contactfolders[target_abook].readonly) {
// search result may contain contacts from many sources
return (this.env.selection_sources.length > 1 || $.inArray(target_abook, this.env.selection_sources) == -1) ? 2 : 1;
}
}
// droptarget is a (writable) addressbook - contact copy action
else if (!this.env.contactfolders[id].readonly) {
// search result may contain contacts from many sources
return (this.env.selection_sources.length > 1 || $.inArray(id, this.env.selection_sources) == -1) ? 2 : 0;
}
}
}
return 0;
};
// open popup window
this.open_window = function(url, small, toolbar)
{
var wname = 'rcmextwin' + new Date().getTime();
url += (url.match(/\?/) ? '&' : '?') + '_extwin=1';
if (this.env.standard_windows)
extwin = window.open(url, wname);
else {
var win = this.is_framed() ? parent.window : window,
page = $(win),
page_width = page.width(),
page_height = bw.mz ? $('body', win).height() : page.height(),
w = Math.min(small ? this.env.popup_width_small : this.env.popup_width, page_width),
h = page_height, // always use same height
l = (win.screenLeft || win.screenX) + 20,
t = (win.screenTop || win.screenY) + 20,
extwin = window.open(url, wname,
'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,location=no,scrollbars=yes'
+(toolbar ? ',toolbar=yes,menubar=yes,status=yes' : ',toolbar=no,menubar=no,status=no'));
}
// write loading... message to empty windows
if (!url && extwin.document) {
extwin.document.write('<html><body>' + this.get_label('loading') + '</body></html>');
}
// focus window, delayed to bring to front
window.setTimeout(function() { extwin.focus(); }, 10);
return extwin;
};
/*********************************************************/
/********* (message) list functionality *********/
/*********************************************************/
this.init_message_row = function(row)
{
var expando, self = this, uid = row.uid,
status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.uid;
if (uid && this.env.messages[uid])
$.extend(row, this.env.messages[uid]);
// set eventhandler to status icon
if (row.icon = document.getElementById(status_icon)) {
row.icon._row = row.obj;
row.icon.onmousedown = function(e) { self.command('toggle_status', this); rcube_event.cancel(e); };
}
// save message icon position too
if (this.env.status_col != null)
row.msgicon = document.getElementById('msgicn'+row.uid);
else
row.msgicon = row.icon;
// set eventhandler to flag icon, if icon found
if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.uid))) {
row.flagicon._row = row.obj;
row.flagicon.onmousedown = function(e) { self.command('toggle_flag', this); rcube_event.cancel(e); };
}
if (!row.depth && row.has_children && (expando = document.getElementById('rcmexpando'+row.uid))) {
row.expando = expando;
expando.onmousedown = function(e) { return self.expand_message_row(e, uid); };
}
this.triggerEvent('insertrow', { uid:uid, row:row });
};
// create a table row in the message list
this.add_message_row = function(uid, cols, flags, attop)
{
if (!this.gui_objects.messagelist || !this.message_list)
return false;
// Prevent from adding messages from different folder (#1487752)
if (flags.mbox != this.env.mailbox && !flags.skip_mbox_check)
return false;
if (!this.env.messages[uid])
this.env.messages[uid] = {};
// merge flags over local message object
$.extend(this.env.messages[uid], {
deleted: flags.deleted?1:0,
replied: flags.answered?1:0,
unread: !flags.seen?1:0,
forwarded: flags.forwarded?1:0,
flagged: flags.flagged?1:0,
has_children: flags.has_children?1:0,
depth: flags.depth?flags.depth:0,
unread_children: flags.unread_children?flags.unread_children:0,
parent_uid: flags.parent_uid?flags.parent_uid:0,
selected: this.select_all_mode || this.message_list.in_selection(uid),
ml: flags.ml?1:0,
ctype: flags.ctype,
// flags from plugins
flags: flags.extra_flags
});
var c, n, col, html, css_class,
tree = '', expando = '',
list = this.message_list,
rows = list.rows,
message = this.env.messages[uid],
row_class = 'message'
+ (!flags.seen ? ' unread' : '')
+ (flags.deleted ? ' deleted' : '')
+ (flags.flagged ? ' flagged' : '')
+ (flags.unread_children && flags.seen && !this.env.autoexpand_threads ? ' unroot' : '')
+ (message.selected ? ' selected' : ''),
row = { cols:[], style:{}, id:'rcmrow'+uid };
// message status icons
css_class = 'msgicon';
if (this.env.status_col === null) {
css_class += ' status';
if (flags.deleted)
css_class += ' deleted';
else if (!flags.seen)
css_class += ' unread';
else if (flags.unread_children > 0)
css_class += ' unreadchildren';
}
if (flags.answered)
css_class += ' replied';
if (flags.forwarded)
css_class += ' forwarded';
// update selection
if (message.selected && !list.in_selection(uid))
list.selection.push(uid);
// threads
if (this.env.threading) {
if (message.depth) {
// This assumes that div width is hardcoded to 15px,
tree += '<span id="rcmtab' + uid + '" class="branch" style="width:' + (message.depth * 15) + 'px;">&nbsp;&nbsp;</span>';
if ((rows[message.parent_uid] && rows[message.parent_uid].expanded === false)
|| ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) &&
(!rows[message.parent_uid] || !rows[message.parent_uid].expanded))
) {
row.style.display = 'none';
message.expanded = false;
}
else
message.expanded = true;
row_class += ' thread expanded';
}
else if (message.has_children) {
if (message.expanded === undefined && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
message.expanded = true;
}
expando = '<div id="rcmexpando' + uid + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
row_class += ' thread' + (message.expanded? ' expanded' : '');
}
}
tree += '<span id="msgicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
row.className = row_class;
// build subject link
if (!bw.ie && cols.subject) {
var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show';
var uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid';
cols.subject = '<a href="./?_task=mail&_action='+action+'&_mbox='+urlencode(flags.mbox)+'&'+uid_param+'='+uid+'"'+
' onclick="return rcube_event.cancel(event)" onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')">'+cols.subject+'</a>';
}
// add each submitted col
for (n in this.env.coltypes) {
c = this.env.coltypes[n];
col = { className: String(c).toLowerCase() };
if (c == 'flag') {
css_class = (flags.flagged ? 'flagged' : 'unflagged');
html = '<span id="flagicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
}
else if (c == 'attachment') {
if (/application\/|multipart\/(m|signed)/.test(flags.ctype))
html = '<span class="attachment">&nbsp;</span>';
else if (/multipart\/report/.test(flags.ctype))
html = '<span class="report">&nbsp;</span>';
else
html = '&nbsp;';
}
else if (c == 'status') {
if (flags.deleted)
css_class = 'deleted';
else if (!flags.seen)
css_class = 'unread';
else if (flags.unread_children > 0)
css_class = 'unreadchildren';
else
css_class = 'msgicon';
html = '<span id="statusicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
}
else if (c == 'threads')
html = expando;
else if (c == 'subject') {
if (bw.ie) {
col.onmouseover = function() { rcube_webmail.long_subject_title_ie(this, message.depth+1); };
if (bw.ie8)
tree = '<span></span>' + tree; // #1487821
}
html = tree + cols[c];
}
else if (c == 'priority') {
if (flags.prio > 0 && flags.prio < 6)
html = '<span class="prio'+flags.prio+'">&nbsp;</span>';
else
html = '&nbsp;';
}
else
html = cols[c];
col.innerHTML = html;
row.cols.push(col);
}
list.insert_row(row, attop);
// remove 'old' row
if (attop && this.env.pagesize && list.rowcount > this.env.pagesize) {
var uid = list.get_last_row();
list.remove_row(uid);
list.clear_selection(uid);
}
};
this.set_list_sorting = function(sort_col, sort_order)
{
// set table header class
$('#rcm'+this.env.sort_col).removeClass('sorted'+(this.env.sort_order.toUpperCase()));
if (sort_col)
$('#rcm'+sort_col).addClass('sorted'+sort_order);
this.env.sort_col = sort_col;
this.env.sort_order = sort_order;
};
this.set_list_options = function(cols, sort_col, sort_order, threads)
{
var update, post_data = {};
if (sort_col === undefined)
sort_col = this.env.sort_col;
if (!sort_order)
sort_order = this.env.sort_order;
if (this.env.sort_col != sort_col || this.env.sort_order != sort_order) {
update = 1;
this.set_list_sorting(sort_col, sort_order);
}
if (this.env.threading != threads) {
update = 1;
post_data._threads = threads;
}
if (cols && cols.length) {
// make sure new columns are added at the end of the list
var i, idx, name, newcols = [], oldcols = this.env.coltypes;
for (i=0; i<oldcols.length; i++) {
name = oldcols[i];
idx = $.inArray(name, cols);
if (idx != -1) {
newcols.push(name);
delete cols[idx];
}
}
for (i=0; i<cols.length; i++)
if (cols[i])
newcols.push(cols[i]);
if (newcols.join() != oldcols.join()) {
update = 1;
post_data._cols = newcols.join(',');
}
}
if (update)
this.list_mailbox('', '', sort_col+'_'+sort_order, post_data);
};
// when user double-clicks on a row
this.show_message = function(id, safe, preview)
{
if (!id)
return;
var win, target = window,
action = preview ? 'preview': 'show',
url = '&_action='+action+'&_uid='+id+'&_mbox='+urlencode(this.env.mailbox);
if (preview && (win = this.get_frame_window(this.env.contentframe))) {
target = win;
url += '&_framed=1';
}
if (safe)
url += '&_safe=1';
// also send search request to get the right messages
if (this.env.search_request)
url += '&_search='+this.env.search_request;
// add browser capabilities, so we can properly handle attachments
url += '&_caps='+urlencode(this.browser_capabilities());
if (this.env.extwin)
url += '&_extwin=1';
if (preview && String(target.location.href).indexOf(url) >= 0) {
this.show_contentframe(true);
}
else {
if (!preview && this.env.message_extwin && !this.env.extwin)
this.open_window(this.env.comm_path+url, true);
else
this.location_href(this.env.comm_path+url, target, true);
// mark as read and change mbox unread counter
if (preview && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read >= 0) {
this.preview_read_timer = setTimeout(function() {
ref.set_message(id, 'unread', false);
ref.update_thread_root(id, 'read');
if (ref.env.unread_counts[ref.env.mailbox]) {
ref.env.unread_counts[ref.env.mailbox] -= 1;
ref.set_unread_count(ref.env.mailbox, ref.env.unread_counts[ref.env.mailbox], ref.env.mailbox == 'INBOX');
}
if (ref.env.preview_pane_mark_read > 0)
ref.http_post('mark', {_uid: id, _flag: 'read', _quiet: 1});
}, this.env.preview_pane_mark_read * 1000);
}
}
};
this.show_contentframe = function(show)
{
var frame, win, name = this.env.contentframe;
if (name && (frame = this.get_frame_element(name))) {
if (!show && (win = this.get_frame_window(name))) {
if (win.location && win.location.href.indexOf(this.env.blankpage)<0)
win.location.href = this.env.blankpage;
}
else if (!bw.safari && !bw.konq)
$(frame)[show ? 'show' : 'hide']();
}
if (!show && this.busy)
this.set_busy(false, null, this.env.frame_lock);
};
this.get_frame_element = function(id)
{
var frame;
if (id && (frame = document.getElementById(id)))
return frame;
};
this.get_frame_window = function(id)
{
var frame = this.get_frame_element(id);
if (frame && frame.name && window.frames)
return window.frames[frame.name];
};
this.lock_frame = function()
{
if (!this.env.frame_lock)
(this.is_framed() ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
};
// list a specific page
this.list_page = function(page)
{
if (page == 'next')
page = this.env.current_page+1;
else if (page == 'last')
page = this.env.pagecount;
else if (page == 'prev' && this.env.current_page > 1)
page = this.env.current_page-1;
else if (page == 'first' && this.env.current_page > 1)
page = 1;
if (page > 0 && page <= this.env.pagecount) {
this.env.current_page = page;
if (this.task == 'addressbook' || this.contact_list)
this.list_contacts(this.env.source, this.env.group, page);
else if (this.task == 'mail')
this.list_mailbox(this.env.mailbox, page);
}
};
// sends request to check for recent messages
this.checkmail = function()
{
var lock = this.set_busy(true, 'checkingmail'),
params = this.check_recent_params();
this.http_request('check-recent', params, lock);
};
// list messages of a specific mailbox using filter
this.filter_mailbox = function(filter)
{
var lock = this.set_busy(true, 'searching');
this.clear_message_list();
// reset vars
this.env.current_page = 1;
this.http_request('search', this.search_params(false, filter), lock);
};
// list messages of a specific mailbox
this.list_mailbox = function(mbox, page, sort, url)
{
var win, target = window;
if (typeof url != 'object')
url = {};
if (!mbox)
mbox = this.env.mailbox ? this.env.mailbox : 'INBOX';
// add sort to url if set
if (sort)
url._sort = sort;
// also send search request to get the right messages
if (this.env.search_request)
url._search = this.env.search_request;
// set page=1 if changeing to another mailbox
if (this.env.mailbox != mbox) {
page = 1;
this.env.current_page = page;
this.select_all_mode = false;
}
// unselect selected messages and clear the list and message data
this.clear_message_list();
if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort))
url._refresh = 1;
this.select_folder(mbox, '', true);
this.unmark_folder(mbox, 'recent', '', true);
this.env.mailbox = mbox;
// load message list remotely
if (this.gui_objects.messagelist) {
this.list_mailbox_remote(mbox, page, url);
return;
}
if (win = this.get_frame_window(this.env.contentframe)) {
target = win;
url._framed = 1;
}
// load message list to target frame/window
if (mbox) {
this.set_busy(true, 'loading');
url._mbox = mbox;
if (page)
url._page = page;
this.location_href(url, target);
}
};
this.clear_message_list = function()
{
this.env.messages = {};
this.last_selected = 0;
this.show_contentframe(false);
if (this.message_list)
this.message_list.clear(true);
};
// send remote request to load message list
this.list_mailbox_remote = function(mbox, page, post_data)
{
// clear message list first
this.message_list.clear();
var lock = this.set_busy(true, 'loading');
if (typeof post_data != 'object')
post_data = {};
post_data._mbox = mbox;
if (page)
post_data._page = page;
this.http_request('list', post_data, lock);
};
// removes messages that doesn't exists from list selection array
this.update_selection = function()
{
var selected = this.message_list.selection,
rows = this.message_list.rows,
i, selection = [];
for (i in selected)
if (rows[selected[i]])
selection.push(selected[i]);
this.message_list.selection = selection;
}
// expand all threads with unread children
this.expand_unread = function()
{
var r, tbody = this.gui_objects.messagelist.tBodies[0],
new_row = tbody.firstChild;
while (new_row) {
if (new_row.nodeType == 1 && (r = this.message_list.rows[new_row.uid]) && r.unread_children) {
this.message_list.expand_all(r);
this.set_unread_children(r.uid);
}
new_row = new_row.nextSibling;
}
return false;
};
// thread expanding/collapsing handler
this.expand_message_row = function(e, uid)
{
var row = this.message_list.rows[uid];
// handle unread_children mark
row.expanded = !row.expanded;
this.set_unread_children(uid);
row.expanded = !row.expanded;
this.message_list.expand_row(e, uid);
};
// message list expanding
this.expand_threads = function()
{
if (!this.env.threading || !this.env.autoexpand_threads || !this.message_list)
return;
switch (this.env.autoexpand_threads) {
case 2: this.expand_unread(); break;
case 1: this.message_list.expand_all(); break;
}
};
// Initializes threads indicators/expanders after list update
this.init_threads = function(roots, mbox)
{
// #1487752
if (mbox && mbox != this.env.mailbox)
return false;
for (var n=0, len=roots.length; n<len; n++)
this.add_tree_icons(roots[n]);
this.expand_threads();
};
// adds threads tree icons to the list (or specified thread)
this.add_tree_icons = function(root)
{
var i, l, r, n, len, pos, tmp = [], uid = [],
row, rows = this.message_list.rows;
if (root)
row = rows[root] ? rows[root].obj : null;
else
row = this.message_list.tbody.firstChild;
while (row) {
if (row.nodeType == 1 && (r = rows[row.uid])) {
if (r.depth) {
for (i=tmp.length-1; i>=0; i--) {
len = tmp[i].length;
if (len > r.depth) {
pos = len - r.depth;
if (!(tmp[i][pos] & 2))
tmp[i][pos] = tmp[i][pos] ? tmp[i][pos]+2 : 2;
}
else if (len == r.depth) {
if (!(tmp[i][0] & 2))
tmp[i][0] += 2;
}
if (r.depth > len)
break;
}
tmp.push(new Array(r.depth));
tmp[tmp.length-1][0] = 1;
uid.push(r.uid);
}
else {
if (tmp.length) {
for (i in tmp) {
this.set_tree_icons(uid[i], tmp[i]);
}
tmp = [];
uid = [];
}
if (root && row != rows[root].obj)
break;
}
}
row = row.nextSibling;
}
if (tmp.length) {
for (i in tmp) {
this.set_tree_icons(uid[i], tmp[i]);
}
}
};
// adds tree icons to specified message row
this.set_tree_icons = function(uid, tree)
{
var i, divs = [], html = '', len = tree.length;
for (i=0; i<len; i++) {
if (tree[i] > 2)
divs.push({'class': 'l3', width: 15});
else if (tree[i] > 1)
divs.push({'class': 'l2', width: 15});
else if (tree[i] > 0)
divs.push({'class': 'l1', width: 15});
// separator div
else if (divs.length && !divs[divs.length-1]['class'])
divs[divs.length-1].width += 15;
else
divs.push({'class': null, width: 15});
}
for (i=divs.length-1; i>=0; i--) {
if (divs[i]['class'])
html += '<div class="tree '+divs[i]['class']+'" />';
else
html += '<div style="width:'+divs[i].width+'px" />';
}
if (html)
$('#rcmtab'+uid).html(html);
};
// update parent in a thread
this.update_thread_root = function(uid, flag)
{
if (!this.env.threading)
return;
var root = this.message_list.find_root(uid);
if (uid == root)
return;
var p = this.message_list.rows[root];
if (flag == 'read' && p.unread_children) {
p.unread_children--;
}
else if (flag == 'unread' && p.has_children) {
// unread_children may be undefined
p.unread_children = p.unread_children ? p.unread_children + 1 : 1;
}
else {
return;
}
this.set_message_icon(root);
this.set_unread_children(root);
};
// update thread indicators for all messages in a thread below the specified message
// return number of removed/added root level messages
this.update_thread = function (uid)
{
if (!this.env.threading)
return 0;
var r, parent, count = 0,
rows = this.message_list.rows,
row = rows[uid],
depth = rows[uid].depth,
roots = [];
if (!row.depth) // root message: decrease roots count
count--;
else if (row.unread) {
// update unread_children for thread root
parent = this.message_list.find_root(uid);
rows[parent].unread_children--;
this.set_unread_children(parent);
}
parent = row.parent_uid;
// childrens
row = row.obj.nextSibling;
while (row) {
if (row.nodeType == 1 && (r = rows[row.uid])) {
if (!r.depth || r.depth <= depth)
break;
r.depth--; // move left
// reset width and clear the content of a tab, icons will be added later
$('#rcmtab'+r.uid).width(r.depth * 15).html('');
if (!r.depth) { // a new root
count++; // increase roots count
r.parent_uid = 0;
if (r.has_children) {
// replace 'leaf' with 'collapsed'
$('#rcmrow'+r.uid+' '+'.leaf:first')
.attr('id', 'rcmexpando' + r.uid)
.attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
.bind('mousedown', {uid:r.uid, p:this},
function(e) { return e.data.p.expand_message_row(e, e.data.uid); });
r.unread_children = 0;
roots.push(r);
}
// show if it was hidden
if (r.obj.style.display == 'none')
$(r.obj).show();
}
else {
if (r.depth == depth)
r.parent_uid = parent;
if (r.unread && roots.length)
roots[roots.length-1].unread_children++;
}
}
row = row.nextSibling;
}
// update unread_children for roots
for (var i=0; i<roots.length; i++)
this.set_unread_children(roots[i].uid);
return count;
};
this.delete_excessive_thread_rows = function()
{
var rows = this.message_list.rows,
tbody = this.message_list.tbody,
row = tbody.firstChild,
cnt = this.env.pagesize + 1;
while (row) {
if (row.nodeType == 1 && (r = rows[row.uid])) {
if (!r.depth && cnt)
cnt--;
if (!cnt)
this.message_list.remove_row(row.uid);
}
row = row.nextSibling;
}
};
// set message icon
this.set_message_icon = function(uid)
{
var css_class,
row = this.message_list.rows[uid];
if (!row)
return false;
if (row.icon) {
css_class = 'msgicon';
if (row.deleted)
css_class += ' deleted';
else if (row.unread)
css_class += ' unread';
else if (row.unread_children)
css_class += ' unreadchildren';
if (row.msgicon == row.icon) {
if (row.replied)
css_class += ' replied';
if (row.forwarded)
css_class += ' forwarded';
css_class += ' status';
}
row.icon.className = css_class;
}
if (row.msgicon && row.msgicon != row.icon) {
css_class = 'msgicon';
if (!row.unread && row.unread_children)
css_class += ' unreadchildren';
if (row.replied)
css_class += ' replied';
if (row.forwarded)
css_class += ' forwarded';
row.msgicon.className = css_class;
}
if (row.flagicon) {
css_class = (row.flagged ? 'flagged' : 'unflagged');
row.flagicon.className = css_class;
}
};
// set message status
this.set_message_status = function(uid, flag, status)
{
var row = this.message_list.rows[uid];
if (!row)
return false;
if (flag == 'unread')
row.unread = status;
else if(flag == 'deleted')
row.deleted = status;
else if (flag == 'replied')
row.replied = status;
else if (flag == 'forwarded')
row.forwarded = status;
else if (flag == 'flagged')
row.flagged = status;
};
// set message row status, class and icon
this.set_message = function(uid, flag, status)
{
var row = this.message_list && this.message_list.rows[uid];
if (!row)
return false;
if (flag)
this.set_message_status(uid, flag, status);
var rowobj = $(row.obj);
if (row.unread && !rowobj.hasClass('unread'))
rowobj.addClass('unread');
else if (!row.unread && rowobj.hasClass('unread'))
rowobj.removeClass('unread');
if (row.deleted && !rowobj.hasClass('deleted'))
rowobj.addClass('deleted');
else if (!row.deleted && rowobj.hasClass('deleted'))
rowobj.removeClass('deleted');
if (row.flagged && !rowobj.hasClass('flagged'))
rowobj.addClass('flagged');
else if (!row.flagged && rowobj.hasClass('flagged'))
rowobj.removeClass('flagged');
this.set_unread_children(uid);
this.set_message_icon(uid);
};
// sets unroot (unread_children) class of parent row
this.set_unread_children = function(uid)
{
var row = this.message_list.rows[uid];
if (row.parent_uid)
return;
if (!row.unread && row.unread_children && !row.expanded)
$(row.obj).addClass('unroot');
else
$(row.obj).removeClass('unroot');
};
// copy selected messages to the specified mailbox
this.copy_messages = function(mbox)
{
if (mbox && typeof mbox === 'object')
mbox = mbox.id;
// exit if current or no mailbox specified
if (!mbox || mbox == this.env.mailbox)
return;
var post_data = this.selection_post_data({_target_mbox: mbox});
// exit if selection is empty
if (!post_data._uid)
return;
// send request to server
this.http_post('copy', post_data, this.display_message(this.get_label('copyingmessage'), 'loading'));
};
// move selected messages to the specified mailbox
this.move_messages = function(mbox)
{
if (mbox && typeof mbox === 'object')
mbox = mbox.id;
// exit if current or no mailbox specified
if (!mbox || mbox == this.env.mailbox)
return;
var lock = false, post_data = this.selection_post_data({_target_mbox: mbox});
// exit if selection is empty
if (!post_data._uid)
return;
// show wait message
if (this.env.action == 'show')
lock = this.set_busy(true, 'movingmessage');
else
this.show_contentframe(false);
// Hide message command buttons until a message is selected
this.enable_command(this.env.message_commands, false);
this._with_selected_messages('moveto', post_data, lock);
};
// delete selected messages from the current mailbox
this.delete_messages = function(event)
{
var uid, i, len, trash = this.env.trash_mailbox,
list = this.message_list,
selection = list ? list.get_selection() : [];
// exit if no mailbox specified or if selection is empty
if (!this.env.uid && !selection.length)
return;
// also select childs of collapsed rows
for (i=0, len=selection.length; i<len; i++) {
uid = selection[i];
if (list.rows[uid].has_children && !list.rows[uid].expanded)
list.select_children(uid);
}
// if config is set to flag for deletion
if (this.env.flag_for_deletion) {
this.mark_message('delete');
return false;
}
// if there isn't a defined trash mailbox or we are in it
else if (!trash || this.env.mailbox == trash)
this.permanently_remove_messages();
// we're in Junk folder and delete_junk is enabled
else if (this.env.delete_junk && this.env.junk_mailbox && this.env.mailbox == this.env.junk_mailbox)
this.permanently_remove_messages();
// if there is a trash mailbox defined and we're not currently in it
else {
// if shift was pressed delete it immediately
if ((list && list.modkey == SHIFT_KEY) || (event && rcube_event.get_modifier(event) == SHIFT_KEY)) {
if (confirm(this.get_label('deletemessagesconfirm')))
this.permanently_remove_messages();
}
else
this.move_messages(trash);
}
return true;
};
// delete the selected messages permanently
this.permanently_remove_messages = function()
{
var post_data = this.selection_post_data();
// exit if selection is empty
if (!post_data._uid)
return;
this.show_contentframe(false);
this._with_selected_messages('delete', post_data);
};
// Send a specifc moveto/delete request with UIDs of all selected messages
// @private
this._with_selected_messages = function(action, post_data, lock)
{
var count = 0, msg;
// update the list (remove rows, clear selection)
if (this.message_list) {
var n, id, root, roots = [],
selection = this.message_list.get_selection();
for (n=0, len=selection.length; n<len; n++) {
id = selection[n];
if (this.env.threading) {
count += this.update_thread(id);
root = this.message_list.find_root(id);
if (root != id && $.inArray(root, roots) < 0) {
roots.push(root);
}
}
this.message_list.remove_row(id, (this.env.display_next && n == selection.length-1));
}
// make sure there are no selected rows
if (!this.env.display_next)
this.message_list.clear_selection();
// update thread tree icons
for (n=0, len=roots.length; n<len; n++) {
this.add_tree_icons(roots[n]);
}
}
if (this.env.display_next && this.env.next_uid)
post_data._next_uid = this.env.next_uid;
if (count < 0)
post_data._count = (count*-1);
// remove threads from the end of the list
else if (count > 0)
this.delete_excessive_thread_rows();
if (!lock) {
msg = action == 'moveto' ? 'movingmessage' : 'deletingmessage';
lock = this.display_message(this.get_label(msg), 'loading');
}
// send request to server
this.http_post(action, post_data, lock);
};
// build post data for message delete/move/copy/flag requests
this.selection_post_data = function(data)
{
if (typeof(data) != 'object')
data = {};
data._mbox = this.env.mailbox;
if (!data._uid) {
var uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
data._uid = this.uids_to_list(uids);
}
if (this.env.action)
data._from = this.env.action;
// also send search request to get the right messages
if (this.env.search_request)
data._search = this.env.search_request;
return data;
};
// set a specific flag to one or more messages
this.mark_message = function(flag, uid)
{
var a_uids = [], r_uids = [], len, n, id,
list = this.message_list;
if (uid)
a_uids[0] = uid;
else if (this.env.uid)
a_uids[0] = this.env.uid;
else if (list)
a_uids = list.get_selection();
if (!list)
r_uids = a_uids;
else {
list.focus();
for (n=0, len=a_uids.length; n<len; n++) {
id = a_uids[n];
if ((flag == 'read' && list.rows[id].unread)
|| (flag == 'unread' && !list.rows[id].unread)
|| (flag == 'delete' && !list.rows[id].deleted)
|| (flag == 'undelete' && list.rows[id].deleted)
|| (flag == 'flagged' && !list.rows[id].flagged)
|| (flag == 'unflagged' && list.rows[id].flagged))
{
r_uids.push(id);
}
}
}
// nothing to do
if (!r_uids.length && !this.select_all_mode)
return;
switch (flag) {
case 'read':
case 'unread':
this.toggle_read_status(flag, r_uids);
break;
case 'delete':
case 'undelete':
this.toggle_delete_status(r_uids);
break;
case 'flagged':
case 'unflagged':
this.toggle_flagged_status(flag, a_uids);
break;
}
};
// set class to read/unread
this.toggle_read_status = function(flag, a_uids)
{
var i, len = a_uids.length,
post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
lock = this.display_message(this.get_label('markingmessage'), 'loading');
// mark all message rows as read/unread
for (i=0; i<len; i++)
this.set_message(a_uids[i], 'unread', (flag == 'unread' ? true : false));
this.http_post('mark', post_data, lock);
for (i=0; i<len; i++)
this.update_thread_root(a_uids[i], flag);
};
// set image to flagged or unflagged
this.toggle_flagged_status = function(flag, a_uids)
{
var i, len = a_uids.length,
post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
lock = this.display_message(this.get_label('markingmessage'), 'loading');
// mark all message rows as flagged/unflagged
for (i=0; i<len; i++)
this.set_message(a_uids[i], 'flagged', (flag == 'flagged' ? true : false));
this.http_post('mark', post_data, lock);
};
// mark all message rows as deleted/undeleted
this.toggle_delete_status = function(a_uids)
{
var len = a_uids.length,
i, uid, all_deleted = true,
rows = this.message_list ? this.message_list.rows : [];
if (len == 1) {
if (!rows.length || (rows[a_uids[0]] && !rows[a_uids[0]].deleted))
this.flag_as_deleted(a_uids);
else
this.flag_as_undeleted(a_uids);
return true;
}
for (i=0; i<len; i++) {
uid = a_uids[i];
if (rows[uid] && !rows[uid].deleted) {
all_deleted = false;
break;
}
}
if (all_deleted)
this.flag_as_undeleted(a_uids);
else
this.flag_as_deleted(a_uids);
return true;
};
this.flag_as_undeleted = function(a_uids)
{
var i, len = a_uids.length,
post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'undelete'}),
lock = this.display_message(this.get_label('markingmessage'), 'loading');
for (i=0; i<len; i++)
this.set_message(a_uids[i], 'deleted', false);
this.http_post('mark', post_data, lock);
};
this.flag_as_deleted = function(a_uids)
{
var r_uids = [],
post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'delete'}),
lock = this.display_message(this.get_label('markingmessage'), 'loading'),
rows = this.message_list ? this.message_list.rows : [],
count = 0;
for (var i=0, len=a_uids.length; i<len; i++) {
uid = a_uids[i];
if (rows[uid]) {
if (rows[uid].unread)
r_uids[r_uids.length] = uid;
if (this.env.skip_deleted) {
count += this.update_thread(uid);
this.message_list.remove_row(uid, (this.env.display_next && i == this.message_list.selection.length-1));
}
else
this.set_message(uid, 'deleted', true);
}
}
// make sure there are no selected rows
if (this.env.skip_deleted && this.message_list) {
if(!this.env.display_next)
this.message_list.clear_selection();
if (count < 0)
post_data._count = (count*-1);
else if (count > 0)
// remove threads from the end of the list
this.delete_excessive_thread_rows();
}
// ??
if (r_uids.length)
post_data._ruid = this.uids_to_list(r_uids);
if (this.env.skip_deleted && this.env.display_next && this.env.next_uid)
post_data._next_uid = this.env.next_uid;
this.http_post('mark', post_data, lock);
};
// flag as read without mark request (called from backend)
// argument should be a coma-separated list of uids
this.flag_deleted_as_read = function(uids)
{
var icn_src, uid, i, len,
rows = this.message_list ? this.message_list.rows : [];
uids = String(uids).split(',');
for (i=0, len=uids.length; i<len; i++) {
uid = uids[i];
if (rows[uid])
this.set_message(uid, 'unread', false);
}
};
// Converts array of message UIDs to comma-separated list for use in URL
// with select_all mode checking
this.uids_to_list = function(uids)
{
return this.select_all_mode ? '*' : uids.join(',');
};
// Sets title of the delete button
this.set_button_titles = function()
{
var label = 'deletemessage';
if (!this.env.flag_for_deletion
&& this.env.trash_mailbox && this.env.mailbox != this.env.trash_mailbox
&& (!this.env.delete_junk || !this.env.junk_mailbox || this.env.mailbox != this.env.junk_mailbox)
)
label = 'movemessagetotrash';
this.set_alttext('delete', label);
};
/*********************************************************/
/********* mailbox folders methods *********/
/*********************************************************/
this.expunge_mailbox = function(mbox)
{
var lock, post_data = {_mbox: mbox};
// lock interface if it's the active mailbox
if (mbox == this.env.mailbox) {
lock = this.set_busy(true, 'loading');
post_data._reload = 1;
if (this.env.search_request)
post_data._search = this.env.search_request;
}
// send request to server
this.http_post('expunge', post_data, lock);
};
this.purge_mailbox = function(mbox)
{
var lock, post_data = {_mbox: mbox};
if (!confirm(this.get_label('purgefolderconfirm')))
return false;
// lock interface if it's the active mailbox
if (mbox == this.env.mailbox) {
lock = this.set_busy(true, 'loading');
post_data._reload = 1;
}
// send request to server
this.http_post('purge', post_data, lock);
};
// test if purge command is allowed
this.purge_mailbox_test = function()
{
return (this.env.exists && (this.env.mailbox == this.env.trash_mailbox || this.env.mailbox == this.env.junk_mailbox
|| this.env.mailbox.match('^' + RegExp.escape(this.env.trash_mailbox) + RegExp.escape(this.env.delimiter))
|| this.env.mailbox.match('^' + RegExp.escape(this.env.junk_mailbox) + RegExp.escape(this.env.delimiter))));
};
/*********************************************************/
/********* login form methods *********/
/*********************************************************/
// handler for keyboard events on the _user field
this.login_user_keyup = function(e)
{
var key = rcube_event.get_keycode(e);
var passwd = $('#rcmloginpwd');
// enter
if (key == 13 && passwd.length && !passwd.val()) {
passwd.focus();
return rcube_event.cancel(e);
}
return true;
};
/*********************************************************/
/********* message compose methods *********/
/*********************************************************/
this.open_compose_step = function(p)
{
var url = this.url('mail/compose', p);
// open new compose window
if (this.env.compose_extwin && !this.env.extwin) {
this.open_window(url);
}
else {
this.redirect(url);
if (this.env.extwin)
window.resizeTo(Math.max(this.env.popup_width, $(window).width()), $(window).height() + 24);
}
};
// init message compose form: set focus and eventhandlers
this.init_messageform = function()
{
if (!this.gui_objects.messageform)
return false;
var input_from = $("[name='_from']"),
input_to = $("[name='_to']"),
input_subject = $("input[name='_subject']"),
input_message = $("[name='_message']").get(0),
html_mode = $("input[name='_is_html']").val() == '1',
ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
ac_props, opener_rc = this.opener();
// close compose step in opener
if (opener_rc && opener_rc.env.action == 'compose') {
setTimeout(function(){ opener.history.back(); }, 100);
this.env.opened_extwin = true;
}
// configure parallel autocompletion
if (this.env.autocomplete_threads > 0) {
ac_props = {
threads: this.env.autocomplete_threads,
sources: this.env.autocomplete_sources
};
}
// init live search events
this.init_address_input_events(input_to, ac_props);
for (var i in ac_fields) {
this.init_address_input_events($("[name='_"+ac_fields[i]+"']"), ac_props);
}
if (!html_mode) {
this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
// add signature according to selected identity
// if we have HTML editor, signature is added in callback
if (input_from.prop('type') == 'select-one' && !this.env.opened_extwin) {
this.change_identity(input_from[0]);
}
}
if (input_to.val() == '')
input_to.focus();
else if (input_subject.val() == '')
input_subject.focus();
else if (input_message)
input_message.focus();
this.env.compose_focus_elem = document.activeElement;
// get summary of all field values
this.compose_field_hash(true);
// start the auto-save timer
this.auto_save_start();
};
this.init_address_input_events = function(obj, props)
{
this.env.recipients_delimiter = this.env.recipients_separator + ' ';
obj[bw.ie || bw.safari || bw.chrome ? 'keydown' : 'keypress'](function(e) { return ref.ksearch_keydown(e, this, props); })
.attr('autocomplete', 'off');
};
this.submit_messageform = function(draft)
{
var form = this.gui_objects.messageform;
if (!form)
return;
// all checks passed, send message
var msgid = this.set_busy(true, draft ? 'savingmessage' : 'sendingmessage'),
lang = this.spellcheck_lang(),
files = [];
// send files list
$('li', this.gui_objects.attachmentlist).each(function() { files.push(this.id.replace(/^rcmfile/, '')); });
$('input[name="_attachments"]', form).val(files.join());
form.target = 'savetarget';
form._draft.value = draft ? '1' : '';
form.action = this.add_url(form.action, '_unlock', msgid);
form.action = this.add_url(form.action, '_lang', lang);
// register timer to notify about connection timeout
this.submit_timer = setTimeout(function(){
ref.set_busy(false, null, msgid);
ref.display_message(ref.get_label('requesttimedout'), 'error');
}, this.env.request_timeout * 1000);
form.submit();
};
this.compose_recipient_select = function(list)
{
- this.enable_command('add-recipient', list.selection.length > 0);
+ var id, n, recipients = 0;
+ for (n=0; n < list.selection.length; n++) {
+ id = list.selection[n];
+ if (this.env.contactdata[id])
+ recipients++;
+ }
+ this.enable_command('add-recipient', recipients);
};
this.compose_add_recipient = function(field)
{
var recipients = [], input = $('#_'+field), delim = this.env.recipients_delimiter;
if (this.contact_list && this.contact_list.selection.length) {
for (var id, n=0; n < this.contact_list.selection.length; n++) {
id = this.contact_list.selection[n];
if (id && this.env.contactdata[id]) {
recipients.push(this.env.contactdata[id]);
// group is added, expand it
if (id.charAt(0) == 'E' && this.env.contactdata[id].indexOf('@') < 0 && input.length) {
var gid = id.substr(1);
this.group2expand[gid] = { name:this.env.contactdata[id], input:input.get(0) };
this.http_request('group-expand', {_source: this.env.source, _gid: gid}, false);
}
}
}
}
if (recipients.length && input.length) {
var oldval = input.val(), rx = new RegExp(RegExp.escape(delim) + '\\s*$');
if (oldval && !rx.test(oldval))
oldval += delim + ' ';
input.val(oldval + recipients.join(delim + ' ') + delim + ' ');
this.triggerEvent('add-recipient', { field:field, recipients:recipients });
}
};
// checks the input fields before sending a message
this.check_compose_input = function(cmd)
{
// check input fields
var ed, input_to = $("[name='_to']"),
input_cc = $("[name='_cc']"),
input_bcc = $("[name='_bcc']"),
input_from = $("[name='_from']"),
input_subject = $("[name='_subject']"),
input_message = $("[name='_message']");
// check sender (if have no identities)
if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
alert(this.get_label('nosenderwarning'));
input_from.focus();
return false;
}
// check for empty recipient
var recipients = input_to.val() ? input_to.val() : (input_cc.val() ? input_cc.val() : input_bcc.val());
if (!rcube_check_email(recipients.replace(/^\s+/, '').replace(/[\s,;]+$/, ''), true)) {
alert(this.get_label('norecipientwarning'));
input_to.focus();
return false;
}
// check if all files has been uploaded
for (var key in this.env.attachments) {
if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) {
alert(this.get_label('notuploadedwarning'));
return false;
}
}
// display localized warning for missing subject
if (input_subject.val() == '') {
var myprompt = $('<div class="prompt">').html('<div class="message">' + this.get_label('nosubjectwarning') + '</div>').appendTo(document.body);
var prompt_value = $('<input>').attr('type', 'text').attr('size', 30).appendTo(myprompt).val(this.get_label('nosubject'));
var buttons = {};
buttons[this.get_label('cancel')] = function(){
input_subject.focus();
$(this).dialog('close');
};
buttons[this.get_label('sendmessage')] = function(){
input_subject.val(prompt_value.val());
$(this).dialog('close');
ref.command(cmd, { nocheck:true }); // repeat command which triggered this
};
myprompt.dialog({
modal: true,
resizable: false,
buttons: buttons,
close: function(event, ui) { $(this).remove() }
});
prompt_value.select();
return false;
}
// Apply spellcheck changes if spell checker is active
this.stop_spellchecking();
if (window.tinyMCE)
ed = tinyMCE.get(this.env.composebody);
// check for empty body
if (!ed && input_message.val() == '' && !confirm(this.get_label('nobodywarning'))) {
input_message.focus();
return false;
}
else if (ed) {
if (!ed.getContent() && !confirm(this.get_label('nobodywarning'))) {
ed.focus();
return false;
}
// move body from html editor to textarea (just to be sure, #1485860)
tinyMCE.triggerSave();
}
return true;
};
this.toggle_editor = function(props)
{
this.stop_spellchecking();
if (props.mode == 'html') {
this.plain2html($('#'+props.id).val(), props.id);
tinyMCE.execCommand('mceAddControl', false, props.id);
if (this.env.default_font)
setTimeout(function() {
$(tinyMCE.get(props.id).getBody()).css('font-family', rcmail.env.default_font);
}, 500);
}
else {
var thisMCE = tinyMCE.get(props.id), existingHtml;
if (existingHtml = thisMCE.getContent()) {
if (!confirm(this.get_label('editorwarning'))) {
return false;
}
this.html2plain(existingHtml, props.id);
}
tinyMCE.execCommand('mceRemoveControl', false, props.id);
}
return true;
};
this.stop_spellchecking = function()
{
var ed;
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody))) {
if (ed.plugins && ed.plugins.spellchecker && ed.plugins.spellchecker.active)
ed.execCommand('mceSpellCheck');
}
else if (ed = this.env.spellcheck) {
if (ed.state && ed.state != 'ready' && ed.state != 'no_error_found')
$(ed.spell_span).trigger('click');
}
this.spellcheck_state();
};
this.spellcheck_state = function()
{
var ed, active;
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)) && ed.plugins && ed.plugins.spellchecker)
active = ed.plugins.spellchecker.active;
else if ((ed = this.env.spellcheck) && ed.state)
active = ed.state != 'ready' && ed.state != 'no_error_found';
if (rcmail.buttons.spellcheck)
$('#'+rcmail.buttons.spellcheck[0].id)[active ? 'addClass' : 'removeClass']('selected');
return active;
};
// get selected language
this.spellcheck_lang = function()
{
var ed;
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)) && ed.plugins && ed.plugins.spellchecker)
return ed.plugins.spellchecker.selectedLang;
else if (this.env.spellcheck)
return GOOGIE_CUR_LANG;
};
this.spellcheck_lang_set = function(lang)
{
var ed;
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)) && ed.plugins)
ed.plugins.spellchecker.selectedLang = lang;
else if (this.env.spellcheck)
this.env.spellcheck.setCurrentLanguage(lang);
};
// resume spellchecking, highlight provided mispellings without new ajax request
this.spellcheck_resume = function(ishtml, data)
{
if (ishtml) {
var ed = tinyMCE.get(this.env.composebody);
sp = ed.plugins.spellchecker;
sp.active = 1;
sp._markWords(data);
ed.nodeChanged();
}
else {
var sp = this.env.spellcheck;
sp.prepare(false, true);
sp.processData(data);
}
this.spellcheck_state();
}
this.set_draft_id = function(id)
{
var rc;
if (!this.env.draft_id && id && (rc = this.opener())) {
// refresh the drafts folder in opener window
if (rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == this.env.drafts_mailbox)
rc.command('checkmail');
}
this.env.draft_id = id;
$("input[name='_draft_saveid']").val(id);
};
this.auto_save_start = function()
{
if (this.env.draft_autosave)
this.save_timer = setTimeout(function(){ ref.command("savedraft"); }, this.env.draft_autosave * 1000);
// Unlock interface now that saving is complete
this.busy = false;
};
this.compose_field_hash = function(save)
{
// check input fields
var ed, i, val, str = '', hash_fields = ['to', 'cc', 'bcc', 'subject'];
for (i=0; i<hash_fields.length; i++)
if (val = $('[name="_' + hash_fields[i] + '"]').val())
str += val + ':';
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)))
str += ed.getContent();
else
str += $("[name='_message']").val();
if (this.env.attachments)
for (var upload_id in this.env.attachments)
str += upload_id;
if (save)
this.cmp_hash = str;
return str;
};
this.change_identity = function(obj, show_sig)
{
if (!obj || !obj.options)
return false;
if (!show_sig)
show_sig = this.env.show_sig;
var i, rx, cursor_pos, p = -1,
id = obj.options[obj.selectedIndex].value,
input_message = $("[name='_message']"),
message = input_message.val(),
is_html = ($("input[name='_is_html']").val() == '1'),
sig = this.env.identity,
delim = this.env.recipients_delimiter,
headers = ['replyto', 'bcc'];
// update reply-to/bcc fields with addresses defined in identities
for (i in headers) {
var key = headers[i],
old_val = sig && this.env.identities[sig] ? this.env.identities[sig][key] : '',
new_val = id && this.env.identities[id] ? this.env.identities[id][key] : '',
input = $('[name="_'+key+'"]'), input_val = input.val();
// remove old address(es)
if (old_val && input_val) {
rx = new RegExp('\\s*' + RegExp.escape(old_val) + '\\s*');
input_val = input_val.replace(rx, '');
}
// cleanup
rx = new RegExp(RegExp.escape(delim) + '\\s*' + RegExp(delim), 'g');
input_val = input_val.replace(rx, delim)
rx = new RegExp('^\\s*' + RegExp.escape(delim) + '\\s*$');
input_val = input_val.replace(rx, '')
// add new address(es)
if (new_val) {
rx = new RegExp(RegExp.escape(delim) + '\\s*$');
if (input_val && !rx.test(input_val))
input_val += delim + ' ';
input_val += new_val + delim + ' ';
}
if (old_val || new_val)
input.val(input_val).change();
}
// enable manual signature insert
if (this.env.signatures && this.env.signatures[id]) {
this.enable_command('insert-sig', true);
this.env.compose_commands.push('insert-sig');
}
else
this.enable_command('insert-sig', false);
if (!is_html) {
// remove the 'old' signature
if (show_sig && sig && this.env.signatures && this.env.signatures[sig]) {
sig = this.env.signatures[sig].text;
sig = sig.replace(/\r\n/g, '\n');
p = this.env.top_posting ? message.indexOf(sig) : message.lastIndexOf(sig);
if (p >= 0)
message = message.substring(0, p) + message.substring(p+sig.length, message.length);
}
// add the new signature string
if (show_sig && this.env.signatures && this.env.signatures[id]) {
sig = this.env.signatures[id].text;
sig = sig.replace(/\r\n/g, '\n');
if (this.env.top_posting) {
if (p >= 0) { // in place of removed signature
message = message.substring(0, p) + sig + message.substring(p, message.length);
cursor_pos = p - 1;
}
else if (!message) { // empty message
cursor_pos = 0;
message = '\n\n' + sig;
}
else if (pos = this.get_caret_pos(input_message.get(0))) { // at cursor position
message = message.substring(0, pos) + '\n' + sig + '\n\n' + message.substring(pos, message.length);
cursor_pos = pos;
}
else { // on top
cursor_pos = 0;
message = '\n\n' + sig + '\n\n' + message.replace(/^[\r\n]+/, '');
}
}
else {
message = message.replace(/[\r\n]+$/, '');
cursor_pos = !this.env.top_posting && message.length ? message.length+1 : 0;
message += '\n\n' + sig;
}
}
else
cursor_pos = this.env.top_posting ? 0 : message.length;
input_message.val(message);
// move cursor before the signature
this.set_caret_pos(input_message.get(0), cursor_pos);
}
else if (show_sig && this.env.signatures) { // html
var editor = tinyMCE.get(this.env.composebody),
sigElem = editor.dom.get('_rc_sig');
// Append the signature as a div within the body
if (!sigElem) {
var body = editor.getBody(),
doc = editor.getDoc();
sigElem = doc.createElement('div');
sigElem.setAttribute('id', '_rc_sig');
if (this.env.top_posting) {
// if no existing sig and top posting then insert at caret pos
editor.getWin().focus(); // correct focus in IE & Chrome
var node = editor.selection.getNode();
if (node.nodeName == 'BODY') {
// no real focus, insert at start
body.insertBefore(sigElem, body.firstChild);
body.insertBefore(doc.createElement('br'), body.firstChild);
}
else {
body.insertBefore(sigElem, node.nextSibling);
body.insertBefore(doc.createElement('br'), node.nextSibling);
}
}
else {
if (bw.ie) // add empty line before signature on IE
body.appendChild(doc.createElement('br'));
body.appendChild(sigElem);
}
}
if (this.env.signatures[id])
sigElem.innerHTML = this.env.signatures[id].html;
}
this.env.identity = id;
return true;
};
// upload (attachment) file
this.upload_file = function(form, action)
{
if (!form)
return false;
// count files and size on capable browser
var size = 0, numfiles = 0;
$('input[type=file]', form).each(function(i, field) {
var files = field.files ? field.files.length : (field.value ? 1 : 0);
// check file size
if (field.files) {
for (var i=0; i < files; i++)
size += field.files[i].size;
}
numfiles += files;
});
// create hidden iframe and post upload form
if (numfiles) {
if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) {
this.display_message(this.env.filesizeerror, 'error');
return;
}
var frame_name = this.async_upload_form(form, action || 'upload', function(e) {
var d, content = '';
try {
if (this.contentDocument) {
d = this.contentDocument;
} else if (this.contentWindow) {
d = this.contentWindow.document;
}
content = d.childNodes[0].innerHTML;
} catch (err) {}
if (!content.match(/add2attachment/) && (!bw.opera || (rcmail.env.uploadframe && rcmail.env.uploadframe == e.data.ts))) {
if (!content.match(/display_message/))
rcmail.display_message(rcmail.get_label('fileuploaderror'), 'error');
rcmail.remove_from_attachment_list(e.data.ts);
}
// Opera hack: handle double onload
if (bw.opera)
rcmail.env.uploadframe = e.data.ts;
});
// display upload indicator and cancel button
var content = '<span>' + this.get_label('uploading' + (numfiles > 1 ? 'many' : '')) + '</span>',
ts = frame_name.replace(/^rcmupload/, '');
this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', frame:frame_name, complete:false });
// upload progress support
if (this.env.upload_progress_time) {
this.upload_progress_start('upload', ts);
}
}
// set reference to the form object
this.gui_objects.attachmentform = form;
return true;
};
// add file name to attachment list
// called from upload page
this.add2attachment_list = function(name, att, upload_id)
{
if (!this.gui_objects.attachmentlist)
return false;
if (!att.complete && ref.env.loadingicon)
att.html = '<img src="'+ref.env.loadingicon+'" alt="" class="uploading" />' + att.html;
if (!att.complete && att.frame)
att.html = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">'
+ (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="" />' : this.get_label('cancel')) + '</a>' + att.html;
var indicator, li = $('<li>').attr('id', name).addClass(att.classname).html(att.html);
// replace indicator's li
if (upload_id && (indicator = document.getElementById(upload_id))) {
li.replaceAll(indicator);
}
else { // add new li
li.appendTo(this.gui_objects.attachmentlist);
}
if (upload_id && this.env.attachments[upload_id])
delete this.env.attachments[upload_id];
this.env.attachments[name] = att;
return true;
};
this.remove_from_attachment_list = function(name)
{
delete this.env.attachments[name];
$('#'+name).remove();
};
this.remove_attachment = function(name)
{
if (name && this.env.attachments[name])
this.http_post('remove-attachment', { _id:this.env.compose_id, _file:name });
return true;
};
this.cancel_attachment_upload = function(name, frame_name)
{
if (!name || !frame_name)
return false;
this.remove_from_attachment_list(name);
$("iframe[name='"+frame_name+"']").remove();
return false;
};
this.upload_progress_start = function(action, name)
{
setTimeout(function() { rcmail.http_request(action, {_progress: name}); },
this.env.upload_progress_time * 1000);
};
this.upload_progress_update = function(param)
{
var elem = $('#'+param.name + '> span');
if (!elem.length || !param.text)
return;
elem.text(param.text);
if (!param.done)
this.upload_progress_start(param.action, param.name);
};
// send remote request to add a new contact
this.add_contact = function(value)
{
if (value)
this.http_post('addcontact', {_address: value});
return true;
};
// send remote request to search mail or contacts
this.qsearch = function(value)
{
if (value != '') {
var r, lock = this.set_busy(true, 'searching'),
url = this.search_params(value);
if (this.message_list)
this.clear_message_list();
else if (this.contact_list)
this.list_contacts_clear();
if (this.env.source)
url._source = this.env.source;
if (this.env.group)
url._gid = this.env.group;
// reset vars
this.env.current_page = 1;
var action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';
r = this.http_request(action, url, lock);
this.env.qsearch = {lock: lock, request: r};
}
};
// build URL params for search
this.search_params = function(search, filter)
{
var n, url = {}, mods_arr = [],
mods = this.env.search_mods,
mbox = this.env.mailbox;
if (!filter && this.gui_objects.search_filter)
filter = this.gui_objects.search_filter.value;
if (!search && this.gui_objects.qsearchbox)
search = this.gui_objects.qsearchbox.value;
if (filter)
url._filter = filter;
if (search) {
url._q = search;
if (mods && this.message_list)
mods = mods[mbox] ? mods[mbox] : mods['*'];
if (mods) {
for (n in mods)
mods_arr.push(n);
url._headers = mods_arr.join(',');
}
}
if (mbox)
url._mbox = mbox;
return url;
};
// reset quick-search form
this.reset_qsearch = function()
{
if (this.gui_objects.qsearchbox)
this.gui_objects.qsearchbox.value = '';
if (this.env.qsearch)
this.abort_request(this.env.qsearch);
this.env.qsearch = null;
this.env.search_request = null;
this.env.search_id = null;
};
this.sent_successfully = function(type, msg, target)
{
this.display_message(msg, type);
if (this.env.extwin) {
var rc = this.opener();
this.lock_form(this.gui_objects.messageform);
if (rc) {
rc.display_message(msg, type);
// refresh the folder where sent message was saved
if (target && rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == target)
rc.command('checkmail');
}
setTimeout(function(){ window.close() }, 1000);
}
else {
// before redirect we need to wait some time for Chrome (#1486177)
setTimeout(function(){ ref.list_mailbox(); }, 500);
}
};
/*********************************************************/
/********* keyboard live-search methods *********/
/*********************************************************/
// handler for keyboard events on address-fields
this.ksearch_keydown = function(e, obj, props)
{
if (this.ksearch_timer)
clearTimeout(this.ksearch_timer);
var highlight,
key = rcube_event.get_keycode(e),
mod = rcube_event.get_modifier(e);
switch (key) {
case 38: // arrow up
case 40: // arrow down
if (!this.ksearch_visible())
break;
var dir = key==38 ? 1 : 0;
highlight = document.getElementById('rcmksearchSelected');
if (!highlight)
highlight = this.ksearch_pane.__ul.firstChild;
if (highlight)
this.ksearch_select(dir ? highlight.previousSibling : highlight.nextSibling);
return rcube_event.cancel(e);
case 9: // tab
if (mod == SHIFT_KEY || !this.ksearch_visible()) {
this.ksearch_hide();
return;
}
case 13: // enter
if (!this.ksearch_visible())
return false;
// insert selected address and hide ksearch pane
this.insert_recipient(this.ksearch_selected);
this.ksearch_hide();
return rcube_event.cancel(e);
case 27: // escape
this.ksearch_hide();
return;
case 37: // left
case 39: // right
if (mod != SHIFT_KEY)
return;
}
// start timer
this.ksearch_timer = setTimeout(function(){ ref.ksearch_get_results(props); }, 200);
this.ksearch_input = obj;
return true;
};
this.ksearch_visible = function()
{
return (this.ksearch_selected !== null && this.ksearch_selected !== undefined && this.ksearch_value);
};
this.ksearch_select = function(node)
{
var current = $('#rcmksearchSelected');
if (current[0] && node) {
current.removeAttr('id').removeClass('selected');
}
if (node) {
$(node).attr('id', 'rcmksearchSelected').addClass('selected');
this.ksearch_selected = node._rcm_id;
}
};
this.insert_recipient = function(id)
{
if (id === null || !this.env.contacts[id] || !this.ksearch_input)
return;
// get cursor pos
var inp_value = this.ksearch_input.value,
cpos = this.get_caret_pos(this.ksearch_input),
p = inp_value.lastIndexOf(this.ksearch_value, cpos),
trigger = false,
insert = '',
// replace search string with full address
pre = inp_value.substring(0, p),
end = inp_value.substring(p+this.ksearch_value.length, inp_value.length);
this.ksearch_destroy();
// insert all members of a group
if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].id) {
insert += this.env.contacts[id].name + this.env.recipients_delimiter;
this.group2expand[this.env.contacts[id].id] = $.extend({ input: this.ksearch_input }, this.env.contacts[id]);
this.http_request('mail/group-expand', {_source: this.env.contacts[id].source, _gid: this.env.contacts[id].id}, false);
}
else if (typeof this.env.contacts[id] === 'string') {
insert = this.env.contacts[id] + this.env.recipients_delimiter;
trigger = true;
}
this.ksearch_input.value = pre + insert + end;
// set caret to insert pos
cpos = p+insert.length;
if (this.ksearch_input.setSelectionRange)
this.ksearch_input.setSelectionRange(cpos, cpos);
if (trigger)
this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert });
};
this.replace_group_recipients = function(id, recipients)
{
if (this.group2expand[id]) {
this.group2expand[id].input.value = this.group2expand[id].input.value.replace(this.group2expand[id].name, recipients);
this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients });
this.group2expand[id] = null;
}
};
// address search processor
this.ksearch_get_results = function(props)
{
var inp_value = this.ksearch_input ? this.ksearch_input.value : null;
if (inp_value === null)
return;
if (this.ksearch_pane && this.ksearch_pane.is(":visible"))
this.ksearch_pane.hide();
// get string from current cursor pos to last comma
var cpos = this.get_caret_pos(this.ksearch_input),
p = inp_value.lastIndexOf(this.env.recipients_separator, cpos-1),
q = inp_value.substring(p+1, cpos),
min = this.env.autocomplete_min_length,
ac = this.ksearch_data;
// trim query string
q = $.trim(q);
// Don't (re-)search if the last results are still active
if (q == this.ksearch_value)
return;
this.ksearch_destroy();
if (q.length && q.length < min) {
if (!this.ksearch_info) {
this.ksearch_info = this.display_message(
this.get_label('autocompletechars').replace('$min', min));
}
return;
}
var old_value = this.ksearch_value;
this.ksearch_value = q;
// ...string is empty
if (!q.length)
return;
// ...new search value contains old one and previous search was not finished or its result was empty
if (old_value && old_value.length && q.indexOf(old_value) == 0 && (!ac || ac.num <= 0) && this.env.contacts && !this.env.contacts.length)
return;
var i, lock, source, xhr, reqid = new Date().getTime(),
post_data = {_search: q, _id: reqid},
threads = props && props.threads ? props.threads : 1,
sources = props && props.sources ? props.sources : [],
action = props && props.action ? props.action : 'mail/autocomplete';
this.ksearch_data = {id: reqid, sources: sources.slice(), action: action,
locks: [], requests: [], num: sources.length};
for (i=0; i<threads; i++) {
source = this.ksearch_data.sources.shift();
if (threads > 1 && source === undefined)
break;
post_data._source = source ? source : '';
lock = this.display_message(this.get_label('searching'), 'loading');
xhr = this.http_post(action, post_data, lock);
this.ksearch_data.locks.push(lock);
this.ksearch_data.requests.push(xhr);
}
};
this.ksearch_query_results = function(results, search, reqid)
{
// search stopped in meantime?
if (!this.ksearch_value)
return;
// ignore this outdated search response
if (this.ksearch_input && search != this.ksearch_value)
return;
// display search results
var i, len, ul, li, text, init,
value = this.ksearch_value,
data = this.ksearch_data,
maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
// create results pane if not present
if (!this.ksearch_pane) {
ul = $('<ul>');
this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane')
.css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
this.ksearch_pane.__ul = ul[0];
}
ul = this.ksearch_pane.__ul;
// remove all search results or add to existing list if parallel search
if (reqid && this.ksearch_pane.data('reqid') == reqid) {
maxlen -= ul.childNodes.length;
}
else {
this.ksearch_pane.data('reqid', reqid);
init = 1;
// reset content
ul.innerHTML = '';
this.env.contacts = [];
// move the results pane right under the input box
var pos = $(this.ksearch_input).offset();
this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px', display: 'none'});
}
// add each result line to list
if (results && (len = results.length)) {
for (i=0; i < len && maxlen > 0; i++) {
text = typeof results[i] === 'object' ? results[i].name : results[i];
li = document.createElement('LI');
li.innerHTML = text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/##([^%]+)%%/g, '<b>$1</b>');
li.onmouseover = function(){ ref.ksearch_select(this); };
li.onmouseup = function(){ ref.ksearch_click(this) };
li._rcm_id = this.env.contacts.length + i;
ul.appendChild(li);
maxlen -= 1;
}
}
if (ul.childNodes.length) {
this.ksearch_pane.show();
// select the first
if (!this.env.contacts.length) {
$('li:first', ul).attr('id', 'rcmksearchSelected').addClass('selected');
this.ksearch_selected = 0;
}
}
if (len)
this.env.contacts = this.env.contacts.concat(results);
// run next parallel search
if (data.id == reqid) {
data.num--;
if (maxlen > 0 && data.sources.length) {
var lock, xhr, source = data.sources.shift(), post_data;
if (source) {
post_data = {_search: value, _id: reqid, _source: source};
lock = this.display_message(this.get_label('searching'), 'loading');
xhr = this.http_post(data.action, post_data, lock);
this.ksearch_data.locks.push(lock);
this.ksearch_data.requests.push(xhr);
}
}
else if (!maxlen) {
if (!this.ksearch_msg)
this.ksearch_msg = this.display_message(this.get_label('autocompletemore'));
// abort pending searches
this.ksearch_abort();
}
}
};
this.ksearch_click = function(node)
{
if (this.ksearch_input)
this.ksearch_input.focus();
this.insert_recipient(node._rcm_id);
this.ksearch_hide();
};
this.ksearch_blur = function()
{
if (this.ksearch_timer)
clearTimeout(this.ksearch_timer);
this.ksearch_input = null;
this.ksearch_hide();
};
this.ksearch_hide = function()
{
this.ksearch_selected = null;
this.ksearch_value = '';
if (this.ksearch_pane)
this.ksearch_pane.hide();
this.ksearch_destroy();
};
// Clears autocomplete data/requests
this.ksearch_destroy = function()
{
this.ksearch_abort();
if (this.ksearch_info)
this.hide_message(this.ksearch_info);
if (this.ksearch_msg)
this.hide_message(this.ksearch_msg);
this.ksearch_data = null;
this.ksearch_info = null;
this.ksearch_msg = null;
}
// Aborts pending autocomplete requests
this.ksearch_abort = function()
{
var i, len, ac = this.ksearch_data;
if (!ac)
return;
for (i=0, len=ac.locks.length; i<len; i++)
this.abort_request({request: ac.requests[i], lock: ac.locks[i]});
};
/*********************************************************/
/********* address book methods *********/
/*********************************************************/
this.contactlist_keypress = function(list)
{
if (list.key_pressed == list.DELETE_KEY)
this.command('delete');
};
this.contactlist_select = function(list)
{
if (this.preview_timer)
clearTimeout(this.preview_timer);
- var n, id, sid, ref = this, writable = false,
+ var n, id, sid, contact, ref = this, writable = false,
source = this.env.source ? this.env.address_sources[this.env.source] : null;
// we don't have dblclick handler here, so use 200 instead of this.dblclick_time
if (id = list.get_single_selection())
this.preview_timer = setTimeout(function(){ ref.load_contact(id, 'show'); }, 200);
else if (this.env.contentframe)
this.show_contentframe(false);
if (list.selection.length) {
// no source = search result, we'll need to detect if any of
// selected contacts are in writable addressbook to enable edit/delete
// we'll also need to know sources used in selection for copy
// and group-addmember operations (drag&drop)
this.env.selection_sources = [];
- if (!source) {
- for (n in list.selection) {
+
+ if (source) {
+ this.env.selection_sources.push(this.env.source);
+ }
+
+ for (n in list.selection) {
+ contact = list.data[list.selection[n]];
+ if (!source) {
sid = String(list.selection[n]).replace(/^[^-]+-/, '');
if (sid && this.env.address_sources[sid]) {
- writable = writable || !this.env.address_sources[sid].readonly;
+ writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly);
this.env.selection_sources.push(sid);
}
}
- this.env.selection_sources = $.unique(this.env.selection_sources);
- }
- else {
- this.env.selection_sources.push(this.env.source);
- writable = !source.readonly;
+ else {
+ writable = writable || (!source.readonly && !contact.readonly);
+ }
}
+
+ this.env.selection_sources = $.unique(this.env.selection_sources);
}
// if a group is currently selected, and there is at least one contact selected
// thend we can enable the group-remove-selected command
- this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0);
+ this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0 && writable);
this.enable_command('compose', this.env.group || list.selection.length > 0);
this.enable_command('export-selected', list.selection.length > 0);
this.enable_command('edit', id && writable);
- this.enable_command('delete', list.selection.length && writable);
+ this.enable_command('delete', list.selection.length > 0 && writable);
return false;
};
this.list_contacts = function(src, group, page)
{
var win, folder, url = {},
target = window;
if (!src)
src = this.env.source;
if (page && this.current_page == page && src == this.env.source && group == this.env.group)
return false;
if (src != this.env.source) {
page = this.env.current_page = 1;
this.reset_qsearch();
}
else if (group != this.env.group)
page = this.env.current_page = 1;
if (this.env.search_id)
folder = 'S'+this.env.search_id;
else if (!this.env.search_request)
folder = group ? 'G'+src+group : src;
- this.select_folder(folder, '', true);
-
this.env.source = src;
this.env.group = group;
+ // truncate groups listing stack
+ var index = $.inArray(this.env.group, this.env.address_group_stack);
+ if (index < 0)
+ this.env.address_group_stack = [];
+ else
+ this.env.address_group_stack = this.env.address_group_stack.slice(0,index);
+
+ // make sure the current group is on top of the stack
+ if (this.env.group) {
+ this.env.address_group_stack.push(this.env.group);
+
+ // mark the first group on the stack as selected in the directory list
+ folder = 'G'+src+this.env.address_group_stack[0];
+ }
+ else if (this.gui_objects.addresslist_title) {
+ $(this.gui_objects.addresslist_title).html(this.get_label('contacts'));
+ }
+
+ this.select_folder(folder, '', true);
+
// load contacts remotely
if (this.gui_objects.contactslist) {
this.list_contacts_remote(src, group, page);
return;
}
if (win = this.get_frame_window(this.env.contentframe)) {
target = win;
url._framed = 1;
}
if (group)
url._gid = group;
if (page)
url._page = page;
if (src)
url._source = src;
// also send search request to get the correct listing
if (this.env.search_request)
url._search = this.env.search_request;
this.set_busy(true, 'loading');
this.location_href(url, target);
};
// send remote request to load contacts list
this.list_contacts_remote = function(src, group, page)
{
// clear message list first
this.list_contacts_clear();
// send request to server
var url = {}, lock = this.set_busy(true, 'loading');
if (src)
url._source = src;
if (page)
url._page = page;
if (group)
url._gid = group;
this.env.source = src;
this.env.group = group;
// also send search request to get the right records
if (this.env.search_request)
url._search = this.env.search_request;
this.http_request(this.env.task == 'mail' ? 'list-contacts' : 'list', url, lock);
};
this.list_contacts_clear = function()
{
+ this.contact_list.data = {};
this.contact_list.clear(true);
this.show_contentframe(false);
this.enable_command('delete', false);
this.enable_command('compose', this.env.group ? true : false);
};
+ this.set_group_prop = function(prop)
+ {
+ if (this.gui_objects.addresslist_title) {
+ var boxtitle = $(this.gui_objects.addresslist_title).html(''); // clear contents
+
+ // add link to pop back to parent group
+ if (this.env.address_group_stack.length > 1) {
+ $('<a href="#list">...</a>')
+ .addClass('poplink')
+ .appendTo(boxtitle)
+ .click(function(e){ return ref.command('popgroup','',this); });
+ boxtitle.append('&nbsp;&raquo;&nbsp;');
+ }
+
+ boxtitle.append($('<span>'+prop.name+'</span>'));
+ }
+
+ this.triggerEvent('groupupdate', prop);
+ };
+
// load contact record
this.load_contact = function(cid, action, framed)
{
- var win, url = {}, target = window;
+ var win, url = {}, target = window,
+ rec = this.contact_list ? this.contact_list.data[cid] : null;
if (win = this.get_frame_window(this.env.contentframe)) {
url._framed = 1;
target = win;
this.show_contentframe(true);
// load dummy content, unselect selected row(s)
if (!cid)
this.contact_list.clear_selection();
- this.enable_command('delete', 'compose', 'export-selected', cid);
+
+ this.enable_command('compose', rec && rec.email);
+ this.enable_command('export-selected', rec && rec._type != 'group');
}
else if (framed)
return false;
if (action && (cid || action=='add') && !this.drag_active) {
if (this.env.group)
url._gid = this.env.group;
url._action = action;
url._source = this.env.source;
url._cid = cid;
this.location_href(url, target, true);
}
return true;
};
// add/delete member to/from the group
this.group_member_change = function(what, cid, source, gid)
{
what = what == 'add' ? 'add' : 'del';
var label = this.get_label(what == 'add' ? 'addingmember' : 'removingmember'),
lock = this.display_message(label, 'loading'),
post_data = {_cid: cid, _source: source, _gid: gid};
this.http_post('group-'+what+'members', post_data, lock);
};
// copy a contact to the specified target (group or directory)
this.copy_contact = function(cid, to)
{
var n, dest = to.type == 'group' ? to.source : to.id,
source = this.env.source,
group = this.env.group ? this.env.group : '';
if (!cid)
cid = this.contact_list.get_selection().join(',');
if (!cid || !this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
return;
// search result may contain contacts from many sources, but if there is only one...
if (source == '' && this.env.selection_sources.length == 1)
source = this.env.selection_sources[0];
// tagret is a group
if (to.type == 'group') {
if (dest == source)
this.group_member_change('add', cid, dest, to.id);
else {
var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group};
this.http_post('copy', post_data, lock);
}
}
// target is an addressbook
else if (to.id != source) {
var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group};
this.http_post('copy', post_data, lock);
}
};
this.delete_contacts = function()
{
var selection = this.contact_list.get_selection(),
undelete = this.env.source && this.env.address_sources[this.env.source].undelete;
// exit if no mailbox specified or if selection is empty
if (!(selection.length || this.env.cid) || (!undelete && !confirm(this.get_label('deletecontactconfirm'))))
return;
var id, n, a_cids = [],
post_data = {_source: this.env.source, _from: (this.env.action ? this.env.action : '')},
lock = this.display_message(this.get_label('contactdeleting'), 'loading');
if (this.env.cid)
a_cids.push(this.env.cid);
else {
for (n=0; n<selection.length; n++) {
id = selection[n];
a_cids.push(id);
this.contact_list.remove_row(id, (n == selection.length-1));
}
// hide content frame if we delete the currently displayed contact
if (selection.length == 1)
this.show_contentframe(false);
}
post_data._cid = a_cids.join(',');
if (this.env.group)
post_data._gid = this.env.group;
// also send search request to get the right records from the next page
if (this.env.search_request)
post_data._search = this.env.search_request;
// send request to server
this.http_post('delete', post_data, lock)
return true;
};
// update a contact record in the list
- this.update_contact_row = function(cid, cols_arr, newcid, source)
+ this.update_contact_row = function(cid, cols_arr, newcid, source, data)
{
var c, row, list = this.contact_list;
cid = this.html_identifier(cid);
// when in searching mode, concat cid with the source name
if (!list.rows[cid]) {
cid = cid+'-'+source;
if (newcid)
newcid = newcid+'-'+source;
}
list.update_row(cid, cols_arr, newcid, true);
+ list.data[cid] = data;
};
// add row to contacts list
- this.add_contact_row = function(cid, cols, classes)
+ this.add_contact_row = function(cid, cols, classes, data)
{
if (!this.gui_objects.contactslist)
return false;
var c, col, list = this.contact_list,
row = { cols:[] };
row.id = 'rcmrow'+this.html_identifier(cid);
row.className = 'contact ' + (classes || '');
if (list.in_selection(cid))
row.className += ' selected';
// add each submitted col
for (c in cols) {
col = {};
col.className = String(c).toLowerCase();
col.innerHTML = cols[c];
row.cols.push(col);
}
+ // store data in list member
+ list.data[cid] = data;
list.insert_row(row);
this.enable_command('export', list.rowcount > 0);
};
this.init_contact_form = function()
{
var ref = this, col;
if (this.env.coltypes) {
this.set_photo_actions($('#ff_photo').val());
for (col in this.env.coltypes)
this.init_edit_field(col, null);
}
$('.contactfieldgroup .row a.deletebutton').click(function() {
ref.delete_edit_field(this);
return false;
});
$('select.addfieldmenu').change(function(e) {
ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
this.selectedIndex = 0;
});
// enable date pickers on date fields
if ($.datepicker && this.env.date_format) {
$.datepicker.setDefaults({
dateFormat: this.env.date_format,
changeMonth: true,
changeYear: true,
yearRange: '-100:+10',
showOtherMonths: true,
selectOtherMonths: true,
onSelect: function(dateText) { $(this).focus().val(dateText) }
});
$('input.datepicker').datepicker();
}
$("input[type='text']:visible").first().focus();
// Submit search form on Enter
if (this.env.action == 'search')
$(this.gui_objects.editform).append($('<input type="submit">').hide())
.submit(function() { $('input.mainaction').click(); return false; });
};
this.group_create = function()
{
this.add_input_row('contactgroup');
};
this.group_rename = function()
{
if (!this.env.group || !this.gui_objects.folderlist)
return;
if (!this.name_input) {
this.enable_command('list', 'listgroup', false);
this.name_input = $('<input>').attr('type', 'text').val(this.env.contactgroups['G'+this.env.source+this.env.group].name);
this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
this.env.group_renaming = true;
var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true);
if (li && (link = li.firstChild)) {
$(link).hide().before(this.name_input);
}
}
this.name_input.select().focus();
};
this.group_delete = function()
{
if (this.env.group && confirm(this.get_label('deletegroupconfirm'))) {
var lock = this.set_busy(true, 'groupdeleting');
this.http_post('group-delete', {_source: this.env.source, _gid: this.env.group}, lock);
}
};
// callback from server upon group-delete command
this.remove_group_item = function(prop)
{
var key = 'G'+prop.source+prop.id;
if (this.treelist.remove(key)) {
delete this.env.contactfolders[key];
delete this.env.contactgroups[key];
}
this.list_contacts(prop.source, 0);
};
// @TODO: maybe it would be better to use popup instead of inserting input to the list?
this.add_input_row = function(type)
{
if (!this.gui_objects.folderlist)
return;
if (!this.name_input) {
this.name_input = $('<input>').attr('type', 'text').data('tt', type);
this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
this.name_input_li = $('<li>').addClass(type).append(this.name_input);
var ul, li;
// find list (UL) element
if (type == 'contactsearch')
ul = this.gui_objects.folderlist;
else
ul = $('ul.groups', this.get_folder_li(this.env.source,'',true));
// append to the list
li = $('li:last', ul);
if (li.length)
this.name_input_li.insertAfter(li);
else {
this.name_input_li.appendTo(ul);
ul.show(); // make sure the list is visible
}
}
this.name_input.select().focus();
};
//remove selected contacts from current active group
this.group_remove_selected = function()
{
ref.http_post('group-delmembers', {_cid: this.contact_list.selection,
_source: this.env.source, _gid: this.env.group});
};
//callback after deleting contact(s) from current group
this.remove_group_contacts = function(props)
{
if('undefined' != typeof this.env.group && (this.env.group === props.gid)){
var n, selection = this.contact_list.get_selection();
for (n=0; n<selection.length; n++) {
id = selection[n];
this.contact_list.remove_row(id, (n == selection.length-1));
}
}
}
// handler for keyboard events on the input field
this.add_input_keydown = function(e)
{
var key = rcube_event.get_keycode(e),
input = $(e.target), itype = input.data('tt');
// enter
if (key == 13) {
var newname = input.val();
if (newname) {
var lock = this.set_busy(true, 'loading');
if (itype == 'contactsearch')
this.http_post('search-create', {_search: this.env.search_request, _name: newname}, lock);
else if (this.env.group_renaming)
this.http_post('group-rename', {_source: this.env.source, _gid: this.env.group, _name: newname}, lock);
else
this.http_post('group-create', {_source: this.env.source, _name: newname}, lock);
}
return false;
}
// escape
else if (key == 27)
this.reset_add_input();
return true;
};
this.reset_add_input = function()
{
if (this.name_input) {
var li = this.name_input.parent();
if (this.env.group_renaming) {
li.children().last().show();
this.env.group_renaming = false;
}
else if ($('li', li.parent()).length == 1)
li.parent().hide();
this.name_input.remove();
if (this.name_input_li)
this.name_input_li.remove();
this.name_input = this.name_input_li = null;
}
this.enable_command('list', 'listgroup', true);
};
// callback for creating a new contact group
this.insert_contact_group = function(prop)
{
this.reset_add_input();
prop.type = 'group';
var key = 'G'+prop.source+prop.id,
link = $('<a>').attr('href', '#')
.attr('rel', prop.source+':'+prop.id)
.click(function() { return rcmail.command('listgroup', prop, this); })
.html(prop.name);
this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, true);
this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
};
// callback for renaming a contact group
this.update_contact_group = function(prop)
{
this.reset_add_input();
var key = 'G'+prop.source+prop.id,
newnode = {};
// group ID has changed, replace link node and identifiers
if (prop.newid) {
var newkey = 'G'+prop.source+prop.newid,
newprop = $.extend({}, prop);
this.env.contactfolders[newkey] = this.env.contactfolders[key];
this.env.contactfolders[newkey].id = prop.newid;
this.env.group = prop.newid;
delete this.env.contactfolders[key];
delete this.env.contactgroups[key];
newprop.id = prop.newid;
newprop.type = 'group';
newnode.id = newkey;
newnode.html = $('<a>').attr('href', '#')
.attr('rel', prop.source+':'+prop.newid)
.click(function() { return rcmail.command('listgroup', newprop, this); })
.html(prop.name);
}
// update displayed group name
else {
$(this.treelist.get_item(key)).children().first().html(prop.name);
this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
}
// update list node and re-sort it
this.treelist.update(key, newnode, true);
this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
};
this.update_group_commands = function()
{
var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null;
this.enable_command('group-create', (source && source.groups && !source.readonly));
this.enable_command('group-rename', 'group-delete', (source && source.groups && this.env.group && !source.readonly));
};
this.init_edit_field = function(col, elem)
{
var label = this.env.coltypes[col].label;
if (!elem)
elem = $('.ff_' + col);
if (label)
elem.placeholder(label);
};
this.insert_edit_field = function(col, section, menu)
{
// just make pre-defined input field visible
var elem = $('#ff_'+col);
if (elem.length) {
elem.show().focus();
$(menu).children('option[value="'+col+'"]').prop('disabled', true);
}
else {
var lastelem = $('.ff_'+col),
appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
if (!appendcontainer.length) {
var sect = $('#contactsection'+section),
lastgroup = $('.contactfieldgroup', sect).last();
appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col);
if (lastgroup.length)
appendcontainer.insertAfter(lastgroup);
else
sect.prepend(appendcontainer);
}
if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
var input, colprop = this.env.coltypes[col],
row = $('<div>').addClass('row'),
cell = $('<div>').addClass('contactfieldcontent data'),
label = $('<div>').addClass('contactfieldlabel label');
if (colprop.subtypes_select)
label.html(colprop.subtypes_select);
else
label.html(colprop.label);
var name_suffix = colprop.limit != 1 ? '[]' : '';
if (colprop.type == 'text' || colprop.type == 'date') {
input = $('<input>')
.addClass('ff_'+col)
.attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size})
.appendTo(cell);
this.init_edit_field(col, input);
if (colprop.type == 'date' && $.datepicker)
input.datepicker();
}
else if (colprop.type == 'textarea') {
input = $('<textarea>')
.addClass('ff_'+col)
.attr({ name: '_'+col+name_suffix, cols:colprop.size, rows:colprop.rows })
.appendTo(cell);
this.init_edit_field(col, input);
}
else if (colprop.type == 'composite') {
var childcol, cp, first, templ, cols = [], suffices = [];
// read template for composite field order
if ((templ = this.env[col+'_template'])) {
for (var j=0; j < templ.length; j++) {
cols.push(templ[j][1]);
suffices.push(templ[j][2]);
}
}
else { // list fields according to appearance in colprop
for (childcol in colprop.childs)
cols.push(childcol);
}
for (var i=0; i < cols.length; i++) {
childcol = cols[i];
cp = colprop.childs[childcol];
input = $('<input>')
.addClass('ff_'+childcol)
.attr({ type: 'text', name: '_'+childcol+name_suffix, size: cp.size })
.appendTo(cell);
cell.append(suffices[i] || " ");
this.init_edit_field(childcol, input);
if (!first) first = input;
}
input = first; // set focus to the first of this composite fields
}
else if (colprop.type == 'select') {
input = $('<select>')
.addClass('ff_'+col)
.attr('name', '_'+col+name_suffix)
.appendTo(cell);
var options = input.attr('options');
options[options.length] = new Option('---', '');
if (colprop.options)
$.each(colprop.options, function(i, val){ options[options.length] = new Option(val, i); });
}
if (input) {
var delbutton = $('<a href="#del"></a>')
.addClass('contactfieldbutton deletebutton')
.attr({title: this.get_label('delete'), rel: col})
.html(this.env.delbutton)
.click(function(){ ref.delete_edit_field(this); return false })
.appendTo(cell);
row.append(label).append(cell).appendTo(appendcontainer.show());
input.first().focus();
// disable option if limit reached
if (!colprop.count) colprop.count = 0;
if (++colprop.count == colprop.limit && colprop.limit)
$(menu).children('option[value="'+col+'"]').prop('disabled', true);
}
}
}
};
this.delete_edit_field = function(elem)
{
var col = $(elem).attr('rel'),
colprop = this.env.coltypes[col],
fieldset = $(elem).parents('fieldset.contactfieldgroup'),
addmenu = fieldset.parent().find('select.addfieldmenu');
// just clear input but don't hide the last field
if (--colprop.count <= 0 && colprop.visible)
$(elem).parent().children('input').val('').blur();
else {
$(elem).parents('div.row').remove();
// hide entire fieldset if no more rows
if (!fieldset.children('div.row').length)
fieldset.hide();
}
// enable option in add-field selector or insert it if necessary
if (addmenu.length) {
var option = addmenu.children('option[value="'+col+'"]');
if (option.length)
option.prop('disabled', false);
else
option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
addmenu.show();
}
};
this.upload_contact_photo = function(form)
{
if (form && form.elements._photo.value) {
this.async_upload_form(form, 'upload-photo', function(e) {
rcmail.set_busy(false, null, rcmail.file_upload_id);
});
// display upload indicator
this.file_upload_id = this.set_busy(true, 'uploading');
}
};
this.replace_contact_photo = function(id)
{
var img_src = id == '-del-' ? this.env.photo_placeholder :
this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + this.env.cid + '&_photo=' + id;
this.set_photo_actions(id);
$(this.gui_objects.contactphoto).children('img').attr('src', img_src);
};
this.photo_upload_end = function()
{
this.set_busy(false, null, this.file_upload_id);
delete this.file_upload_id;
};
this.set_photo_actions = function(id)
{
var n, buttons = this.buttons['upload-photo'];
for (n=0; buttons && n < buttons.length; n++)
$('a#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
$('#ff_photo').val(id);
this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
this.enable_command('delete-photo', this.env.coltypes.photo && id != '-del-');
};
// load advanced search page
this.advanced_search = function()
{
var win, url = {_form: 1, _action: 'search'}, target = window;
if (win = this.get_frame_window(this.env.contentframe)) {
url._framed = 1;
target = win;
this.contact_list.clear_selection();
}
this.location_href(url, target, true);
return true;
};
// unselect directory/group
this.unselect_directory = function()
{
this.select_folder('');
this.enable_command('search-delete', false);
};
// callback for creating a new saved search record
this.insert_saved_search = function(name, id)
{
this.reset_add_input();
var key = 'S'+id,
link = $('<a>').attr('href', '#')
.attr('rel', id)
.click(function() { return rcmail.command('listsearch', id, this); })
.html(name),
prop = { name:name, id:id };
this.treelist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
this.select_folder(key,'',true);
this.enable_command('search-delete', true);
this.env.search_id = id;
this.triggerEvent('abook_search_insert', prop);
};
// creates an input for saved search name
this.search_create = function()
{
this.add_input_row('contactsearch');
};
this.search_delete = function()
{
if (this.env.search_request) {
var lock = this.set_busy(true, 'savedsearchdeleting');
this.http_post('search-delete', {_sid: this.env.search_id}, lock);
}
};
// callback from server upon search-delete command
this.remove_search_item = function(id)
{
var li, key = 'S'+id;
if (this.treelist.remove(key)) {
this.triggerEvent('search_delete', { id:id, li:li });
}
this.env.search_id = null;
this.env.search_request = null;
this.list_contacts_clear();
this.reset_qsearch();
this.enable_command('search-delete', 'search-create', false);
};
this.listsearch = function(id)
{
var folder, lock = this.set_busy(true, 'searching');
if (this.contact_list) {
this.list_contacts_clear();
}
this.reset_qsearch();
this.select_folder('S'+id, '', true);
// reset vars
this.env.current_page = 1;
this.http_request('search', {_sid: id}, lock);
};
/*********************************************************/
/********* user settings methods *********/
/*********************************************************/
// preferences section select and load options frame
this.section_select = function(list)
{
var win, id = list.get_single_selection(), target = window,
url = {_action: 'edit-prefs', _section: id};
if (id) {
if (win = this.get_frame_window(this.env.contentframe)) {
url._framed = 1;
target = win;
}
this.location_href(url, target, true);
}
return true;
};
this.identity_select = function(list)
{
var id;
if (id = list.get_single_selection()) {
this.enable_command('delete', list.rowcount > 1 && this.env.identities_level < 2);
this.load_identity(id, 'edit-identity');
}
};
// load identity record
this.load_identity = function(id, action)
{
if (action == 'edit-identity' && (!id || id == this.env.iid))
return false;
var win, target = window,
url = {_action: action, _iid: id};
if (win = this.get_frame_window(this.env.contentframe)) {
url._framed = 1;
target = win;
}
if (action && (id || action == 'add-identity')) {
this.set_busy(true);
this.location_href(url, target);
}
return true;
};
this.delete_identity = function(id)
{
// exit if no identity is specified or if selection is empty
var selection = this.identity_list.get_selection();
if (!(selection.length || this.env.iid))
return;
if (!id)
id = this.env.iid ? this.env.iid : selection[0];
// submit request with appended token
if (confirm(this.get_label('deleteidentityconfirm')))
this.goto_url('delete-identity', { _iid: id, _token: this.env.request_token }, true);
return true;
};
this.update_identity_row = function(id, name, add)
{
var list = this.identity_list,
rid = this.html_identifier(id);
if (add) {
list.insert_row({ id:'rcmrow'+rid, cols:[ { className:'mail', innerHTML:name } ] });
list.select(rid);
}
else {
list.update_row(rid, [ name ]);
}
};
/*********************************************************/
/********* folder manager methods *********/
/*********************************************************/
this.init_subscription_list = function()
{
var p = this;
this.subscription_list = new rcube_list_widget(this.gui_objects.subscriptionlist,
{multiselect:false, draggable:true, keyboard:false, toggleselect:true});
this.subscription_list.addEventListener('select', function(o){ p.subscription_select(o); });
this.subscription_list.addEventListener('dragstart', function(o){ p.drag_active = true; });
this.subscription_list.addEventListener('dragend', function(o){ p.subscription_move_folder(o); });
this.subscription_list.row_init = function (row) {
row.obj.onmouseover = function() { p.focus_subscription(row.id); };
row.obj.onmouseout = function() { p.unfocus_subscription(row.id); };
};
this.subscription_list.init();
$('#mailboxroot')
.mouseover(function(){ p.focus_subscription(this.id); })
.mouseout(function(){ p.unfocus_subscription(this.id); })
};
this.focus_subscription = function(id)
{
var row, folder,
delim = RegExp.escape(this.env.delimiter),
reg = RegExp('['+delim+']?[^'+delim+']+$');
if (this.drag_active && this.env.mailbox && (row = document.getElementById(id)))
if (this.env.subscriptionrows[id] &&
(folder = this.env.subscriptionrows[id][0]) !== null
) {
if (this.check_droptarget(folder) &&
!this.env.subscriptionrows[this.get_folder_row_id(this.env.mailbox)][2] &&
(folder != this.env.mailbox.replace(reg, '')) &&
(!folder.match(new RegExp('^'+RegExp.escape(this.env.mailbox+this.env.delimiter))))
) {
this.env.dstfolder = folder;
$(row).addClass('droptarget');
}
}
};
this.unfocus_subscription = function(id)
{
var row = $('#'+id);
this.env.dstfolder = null;
if (this.env.subscriptionrows[id] && row[0])
row.removeClass('droptarget');
else
$(this.subscription_list.frame).removeClass('droptarget');
};
this.subscription_select = function(list)
{
var id, folder;
if (list && (id = list.get_single_selection()) &&
(folder = this.env.subscriptionrows['rcmrow'+id])
) {
this.env.mailbox = folder[0];
this.show_folder(folder[0]);
this.enable_command('delete-folder', !folder[2]);
}
else {
this.env.mailbox = null;
this.show_contentframe(false);
this.enable_command('delete-folder', 'purge', false);
}
};
this.subscription_move_folder = function(list)
{
var delim = RegExp.escape(this.env.delimiter),
reg = RegExp('['+delim+']?[^'+delim+']+$');
if (this.env.mailbox && this.env.dstfolder !== null && (this.env.dstfolder != this.env.mailbox) &&
(this.env.dstfolder != this.env.mailbox.replace(reg, ''))
) {
reg = new RegExp('[^'+delim+']*['+delim+']', 'g');
var basename = this.env.mailbox.replace(reg, ''),
newname = this.env.dstfolder === '' ? basename : this.env.dstfolder+this.env.delimiter+basename;
if (newname != this.env.mailbox) {
this.http_post('rename-folder', {_folder_oldname: this.env.mailbox, _folder_newname: newname}, this.set_busy(true, 'foldermoving'));
this.subscription_list.draglayer.hide();
}
}
this.drag_active = false;
this.unfocus_subscription(this.get_folder_row_id(this.env.dstfolder));
};
// tell server to create and subscribe a new mailbox
this.create_folder = function()
{
this.show_folder('', this.env.mailbox);
};
// delete a specific mailbox with all its messages
this.delete_folder = function(name)
{
var id = this.get_folder_row_id(name ? name : this.env.mailbox),
folder = this.env.subscriptionrows[id][0];
if (folder && confirm(this.get_label('deletefolderconfirm'))) {
var lock = this.set_busy(true, 'folderdeleting');
this.http_post('delete-folder', {_mbox: folder}, lock);
}
};
// Add folder row to the table and initialize it
this.add_folder_row = function (name, display_name, is_protected, subscribed, skip_init, class_name)
{
if (!this.gui_objects.subscriptionlist)
return false;
var row, n, i, tmp, tmp_name, folders, rowid, list = [], slist = [],
tbody = this.gui_objects.subscriptionlist.tBodies[0],
refrow = $('tr', tbody).get(1),
id = 'rcmrow'+((new Date).getTime());
if (!refrow) {
// Refresh page if we don't have a table row to clone
this.goto_url('folders');
return false;
}
// clone a table row if there are existing rows
row = $(refrow).clone(true);
// set ID, reset css class
row.attr('id', id);
row.attr('class', class_name);
// set folder name
row.find('td:first').html(display_name);
// update subscription checkbox
$('input[name="_subscribed[]"]', row).val(name)
.prop({checked: subscribed ? true : false, disabled: is_protected ? true : false});
// add to folder/row-ID map
this.env.subscriptionrows[id] = [name, display_name, 0];
// sort folders, to find a place where to insert the row
folders = [];
$.each(this.env.subscriptionrows, function(k,v){ folders.push(v) });
folders.sort(function(a,b){ return a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0) });
for (n in folders) {
// protected folder
if (folders[n][2]) {
tmp_name = folders[n][0] + this.env.delimiter;
// prefix namespace cannot have subfolders (#1488349)
if (tmp_name == this.env.prefix_ns)
continue;
slist.push(folders[n][0]);
tmp = tmp_name;
}
// protected folder's child
else if (tmp && folders[n][0].indexOf(tmp) == 0)
slist.push(folders[n][0]);
// other
else {
list.push(folders[n][0]);
tmp = null;
}
}
// check if subfolder of a protected folder
for (n=0; n<slist.length; n++) {
if (name.indexOf(slist[n]+this.env.delimiter) == 0)
rowid = this.get_folder_row_id(slist[n]);
}
// find folder position after sorting
for (n=0; !rowid && n<list.length; n++) {
if (n && list[n] == name)
rowid = this.get_folder_row_id(list[n-1]);
}
// add row to the table
if (rowid)
$('#'+rowid).after(row);
else
row.appendTo(tbody);
// update list widget
this.subscription_list.clear_selection();
if (!skip_init)
this.init_subscription_list();
row = row.get(0);
if (row.scrollIntoView)
row.scrollIntoView();
return row;
};
// replace an existing table row with a new folder line (with subfolders)
this.replace_folder_row = function(oldfolder, newfolder, display_name, is_protected, class_name)
{
if (!this.gui_objects.subscriptionlist)
return false;
var i, n, len, name, dispname, oldrow, tmprow, row, level,
tbody = this.gui_objects.subscriptionlist.tBodies[0],
folders = this.env.subscriptionrows,
id = this.get_folder_row_id(oldfolder),
regex = new RegExp('^'+RegExp.escape(oldfolder)),
subscribed = $('input[name="_subscribed[]"]', $('#'+id)).prop('checked'),
// find subfolders of renamed folder
list = this.get_subfolders(oldfolder);
// replace an existing table row
this._remove_folder_row(id);
row = $(this.add_folder_row(newfolder, display_name, is_protected, subscribed, true, class_name));
// detect tree depth change
if (len = list.length) {
level = (oldfolder.split(this.env.delimiter)).length - (newfolder.split(this.env.delimiter)).length;
}
// move subfolders to the new branch
for (n=0; n<len; n++) {
id = list[n];
name = this.env.subscriptionrows[id][0];
dispname = this.env.subscriptionrows[id][1];
oldrow = $('#'+id);
tmprow = oldrow.clone(true);
oldrow.remove();
row.after(tmprow);
row = tmprow;
// update folder index
name = name.replace(regex, newfolder);
$('input[name="_subscribed[]"]', row).val(name);
this.env.subscriptionrows[id][0] = name;
// update the name if level is changed
if (level != 0) {
if (level > 0) {
for (i=level; i>0; i--)
dispname = dispname.replace(/^&nbsp;&nbsp;&nbsp;&nbsp;/, '');
}
else {
for (i=level; i<0; i++)
dispname = '&nbsp;&nbsp;&nbsp;&nbsp;' + dispname;
}
row.find('td:first').html(dispname);
this.env.subscriptionrows[id][1] = dispname;
}
}
// update list widget
this.init_subscription_list();
};
// remove the table row of a specific mailbox from the table
this.remove_folder_row = function(folder, subs)
{
var n, len, list = [], id = this.get_folder_row_id(folder);
// get subfolders if any
if (subs)
list = this.get_subfolders(folder);
// remove old row
this._remove_folder_row(id);
// remove subfolders
for (n=0, len=list.length; n<len; n++)
this._remove_folder_row(list[n]);
};
this._remove_folder_row = function(id)
{
this.subscription_list.remove_row(id.replace(/^rcmrow/, ''));
$('#'+id).remove();
delete this.env.subscriptionrows[id];
}
this.get_subfolders = function(folder)
{
var name, list = [],
regex = new RegExp('^'+RegExp.escape(folder)+RegExp.escape(this.env.delimiter)),
row = $('#'+this.get_folder_row_id(folder)).get(0);
while (row = row.nextSibling) {
if (row.id) {
name = this.env.subscriptionrows[row.id][0];
if (regex.test(name)) {
list.push(row.id);
}
else
break;
}
}
return list;
}
this.subscribe = function(folder)
{
if (folder) {
var lock = this.display_message(this.get_label('foldersubscribing'), 'loading');
this.http_post('subscribe', {_mbox: folder}, lock);
}
};
this.unsubscribe = function(folder)
{
if (folder) {
var lock = this.display_message(this.get_label('folderunsubscribing'), 'loading');
this.http_post('unsubscribe', {_mbox: folder}, lock);
}
};
// helper method to find a specific mailbox row ID
this.get_folder_row_id = function(folder)
{
var id, folders = this.env.subscriptionrows;
for (id in folders)
if (folders[id] && folders[id][0] == folder)
break;
return id;
};
// when user select a folder in manager
this.show_folder = function(folder, path, force)
{
var win, target = window,
url = '&_action=edit-folder&_mbox='+urlencode(folder);
if (path)
url += '&_path='+urlencode(path);
if (win = this.get_frame_window(this.env.contentframe)) {
target = win;
url += '&_framed=1';
}
if (String(target.location.href).indexOf(url) >= 0 && !force)
this.show_contentframe(true);
else
this.location_href(this.env.comm_path+url, target, true);
};
// disables subscription checkbox (for protected folder)
this.disable_subscription = function(folder)
{
var id = this.get_folder_row_id(folder);
if (id)
$('input[name="_subscribed[]"]', $('#'+id)).prop('disabled', true);
};
this.folder_size = function(folder)
{
var lock = this.set_busy(true, 'loading');
this.http_post('folder-size', {_mbox: folder}, lock);
};
this.folder_size_update = function(size)
{
$('#folder-size').replaceWith(size);
};
/*********************************************************/
/********* GUI functionality *********/
/*********************************************************/
var init_button = function(cmd, prop)
{
var elm = document.getElementById(prop.id);
if (!elm)
return;
var preload = false;
if (prop.type == 'image') {
elm = elm.parentNode;
preload = true;
}
elm._command = cmd;
elm._id = prop.id;
if (prop.sel) {
elm.onmousedown = function(e){ return rcmail.button_sel(this._command, this._id); };
elm.onmouseup = function(e){ return rcmail.button_out(this._command, this._id); };
if (preload)
new Image().src = prop.sel;
}
if (prop.over) {
elm.onmouseover = function(e){ return rcmail.button_over(this._command, this._id); };
elm.onmouseout = function(e){ return rcmail.button_out(this._command, this._id); };
if (preload)
new Image().src = prop.over;
}
};
// set event handlers on registered buttons
this.init_buttons = function()
{
for (var cmd in this.buttons) {
if (typeof cmd !== 'string')
continue;
for (var i=0; i<this.buttons[cmd].length; i++) {
init_button(cmd, this.buttons[cmd][i]);
}
}
// set active task button
this.set_button(this.task, 'sel');
};
// set button to a specific state
this.set_button = function(command, state)
{
var n, button, obj, a_buttons = this.buttons[command],
len = a_buttons ? a_buttons.length : 0;
for (n=0; n<len; n++) {
button = a_buttons[n];
obj = document.getElementById(button.id);
if (!obj)
continue;
// get default/passive setting of the button
if (button.type == 'image' && !button.status) {
button.pas = obj._original_src ? obj._original_src : obj.src;
// respect PNG fix on IE browsers
if (obj.runtimeStyle && obj.runtimeStyle.filter && obj.runtimeStyle.filter.match(/src=['"]([^'"]+)['"]/))
button.pas = RegExp.$1;
}
else if (!button.status)
button.pas = String(obj.className);
// set image according to button state
if (button.type == 'image' && button[state]) {
button.status = state;
obj.src = button[state];
}
// set class name according to button state
else if (button[state] !== undefined) {
button.status = state;
obj.className = button[state];
}
// disable/enable input buttons
if (button.type == 'input') {
button.status = state;
obj.disabled = !state;
}
}
};
// display a specific alttext
this.set_alttext = function(command, label)
{
var n, button, obj, link, a_buttons = this.buttons[command],
len = a_buttons ? a_buttons.length : 0;
for (n=0; n<len; n++) {
button = a_buttons[n];
obj = document.getElementById(button.id);
if (button.type == 'image' && obj) {
obj.setAttribute('alt', this.get_label(label));
if ((link = obj.parentNode) && link.tagName.toLowerCase() == 'a')
link.setAttribute('title', this.get_label(label));
}
else if (obj)
obj.setAttribute('title', this.get_label(label));
}
};
// mouse over button
this.button_over = function(command, id)
{
var n, button, obj, a_buttons = this.buttons[command],
len = a_buttons ? a_buttons.length : 0;
for (n=0; n<len; n++) {
button = a_buttons[n];
if (button.id == id && button.status == 'act') {
obj = document.getElementById(button.id);
if (obj && button.over) {
if (button.type == 'image')
obj.src = button.over;
else
obj.className = button.over;
}
}
}
};
// mouse down on button
this.button_sel = function(command, id)
{
var n, button, obj, a_buttons = this.buttons[command],
len = a_buttons ? a_buttons.length : 0;
for (n=0; n<len; n++) {
button = a_buttons[n];
if (button.id == id && button.status == 'act') {
obj = document.getElementById(button.id);
if (obj && button.sel) {
if (button.type == 'image')
obj.src = button.sel;
else
obj.className = button.sel;
}
this.buttons_sel[id] = command;
}
}
};
// mouse out of button
this.button_out = function(command, id)
{
var n, button, obj, a_buttons = this.buttons[command],
len = a_buttons ? a_buttons.length : 0;
for (n=0; n<len; n++) {
button = a_buttons[n];
if (button.id == id && button.status == 'act') {
obj = document.getElementById(button.id);
if (obj && button.act) {
if (button.type == 'image')
obj.src = button.act;
else
obj.className = button.act;
}
}
}
};
// write to the document/window title
this.set_pagetitle = function(title)
{
if (title && document.title)
document.title = title;
};
// display a system message, list of types in common.css (below #message definition)
this.display_message = function(msg, type, timeout)
{
// pass command to parent window
if (this.is_framed())
return parent.rcmail.display_message(msg, type, timeout);
if (!this.gui_objects.message) {
// save message in order to display after page loaded
if (type != 'loading')
this.pending_message = [msg, type, timeout];
return 1;
}
type = type ? type : 'notice';
var ref = this,
key = this.html_identifier(msg),
date = new Date(),
id = type + date.getTime();
if (!timeout)
timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1);
if (type == 'loading') {
key = 'loading';
timeout = this.env.request_timeout * 1000;
if (!msg)
msg = this.get_label('loading');
}
// The same message is already displayed
if (this.messages[key]) {
// replace label
if (this.messages[key].obj)
this.messages[key].obj.html(msg);
// store label in stack
if (type == 'loading') {
this.messages[key].labels.push({'id': id, 'msg': msg});
}
// add element and set timeout
this.messages[key].elements.push(id);
setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
return id;
}
// create DOM object and display it
var obj = $('<div>').addClass(type).html(msg).data('key', key),
cont = $(this.gui_objects.message).append(obj).show();
this.messages[key] = {'obj': obj, 'elements': [id]};
if (type == 'loading') {
this.messages[key].labels = [{'id': id, 'msg': msg}];
}
else {
obj.click(function() { return ref.hide_message(obj); });
}
this.triggerEvent('message', { message:msg, type:type, timeout:timeout, object:obj });
if (timeout > 0)
setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
return id;
};
// make a message to disapear
this.hide_message = function(obj, fade)
{
// pass command to parent window
if (this.is_framed())
return parent.rcmail.hide_message(obj, fade);
if (!this.gui_objects.message)
return;
var k, n, i, o, m = this.messages;
// Hide message by object, don't use for 'loading'!
if (typeof obj === 'object') {
o = $(obj);
k = o.data('key');
this.hide_message_object(o, fade);
if (m[k])
delete m[k];
}
// Hide message by id
else {
for (k in m) {
for (n in m[k].elements) {
if (m[k] && m[k].elements[n] == obj) {
m[k].elements.splice(n, 1);
// hide DOM element if last instance is removed
if (!m[k].elements.length) {
this.hide_message_object(m[k].obj, fade);
delete m[k];
}
// set pending action label for 'loading' message
else if (k == 'loading') {
for (i in m[k].labels) {
if (m[k].labels[i].id == obj) {
delete m[k].labels[i];
}
else {
o = m[k].labels[i].msg;
m[k].obj.html(o);
}
}
}
}
}
}
}
};
// hide message object and remove from the DOM
this.hide_message_object = function(o, fade)
{
if (fade)
o.fadeOut(600, function() {$(this).remove(); });
else
o.hide().remove();
};
// remove all messages immediately
this.clear_messages = function()
{
// pass command to parent window
if (this.is_framed())
return parent.rcmail.clear_messages();
var k, n, m = this.messages;
for (k in m)
for (n in m[k].elements)
if (m[k].obj)
this.hide_message_object(m[k].obj);
this.messages = {};
};
// open a jquery UI dialog with the given content
this.show_popup_dialog = function(html, title, buttons)
{
// forward call to parent window
if (this.is_framed()) {
parent.rcmail.show_popup_dialog(html, title, buttons);
return;
}
var popup = $('<div class="popup">')
.html(html)
.dialog({
title: title,
buttons: buttons,
modal: true,
resizable: true,
width: 500,
close: function(event, ui) { $(this).remove() }
});
// resize and center popup
var win = $(window), w = win.width(), h = win.height(),
width = popup.width(), height = popup.height();
popup.dialog('option', {
height: Math.min(h - 40, height + 75 + (buttons ? 50 : 0)),
width: Math.min(w - 20, width + 20)
});
};
// enable/disable buttons for page shifting
this.set_page_buttons = function()
{
this.enable_command('nextpage', 'lastpage', (this.env.pagecount > this.env.current_page));
this.enable_command('previouspage', 'firstpage', (this.env.current_page > 1));
};
// mark a mailbox as selected and set environment variable
this.select_folder = function(name, prefix, encode)
{
if (this.treelist) {
this.treelist.select(name);
}
else if (this.gui_objects.folderlist) {
var current_li, target_li;
if ((current_li = $('li.selected', this.gui_objects.folderlist))) {
current_li.removeClass('selected').addClass('unfocused');
}
if ((target_li = this.get_folder_li(name, prefix, encode))) {
$(target_li).removeClass('unfocused').addClass('selected');
}
// trigger event hook
this.triggerEvent('selectfolder', { folder:name, prefix:prefix });
}
};
// adds a class to selected folder
this.mark_folder = function(name, class_name, prefix, encode)
{
$(this.get_folder_li(name, prefix, encode)).addClass(class_name);
};
// adds a class to selected folder
this.unmark_folder = function(name, class_name, prefix, encode)
{
$(this.get_folder_li(name, prefix, encode)).removeClass(class_name);
};
// helper method to find a folder list item
this.get_folder_li = function(name, prefix, encode)
{
if (!prefix)
prefix = 'rcmli';
if (this.gui_objects.folderlist) {
name = this.html_identifier(name, encode);
return document.getElementById(prefix+name);
}
return null;
};
// for reordering column array (Konqueror workaround)
// and for setting some message list global variables
this.set_message_coltypes = function(coltypes, repl, smart_col)
{
var list = this.message_list,
thead = list ? list.thead : null,
cell, col, n, len, th, tr;
this.env.coltypes = coltypes;
// replace old column headers
if (thead) {
if (repl) {
th = document.createElement('thead');
tr = document.createElement('tr');
for (c=0, len=repl.length; c < len; c++) {
cell = document.createElement('td');
cell.innerHTML = repl[c].html || '';
if (repl[c].id) cell.id = repl[c].id;
if (repl[c].className) cell.className = repl[c].className;
tr.appendChild(cell);
}
th.appendChild(tr);
thead.parentNode.replaceChild(th, thead);
list.thead = thead = th;
}
for (n=0, len=this.env.coltypes.length; n<len; n++) {
col = this.env.coltypes[n];
if ((cell = thead.rows[0].cells[n]) && (col == 'from' || col == 'to' || col == 'fromto')) {
cell.id = 'rcm'+col;
$('span,a', cell).text(this.get_label(col == 'fromto' ? smart_col : col));
// if we have links for sorting, it's a bit more complicated...
$('a', cell).click(function(){
return rcmail.command('sort', this.id.replace(/^rcm/, ''), this);
});
}
}
}
this.env.subject_col = null;
this.env.flagged_col = null;
this.env.status_col = null;
if ((n = $.inArray('subject', this.env.coltypes)) >= 0) {
this.env.subject_col = n;
if (list)
list.subject_col = n;
}
if ((n = $.inArray('flag', this.env.coltypes)) >= 0)
this.env.flagged_col = n;
if ((n = $.inArray('status', this.env.coltypes)) >= 0)
this.env.status_col = n;
if (list)
list.init_header();
};
// replace content of row count display
this.set_rowcount = function(text, mbox)
{
// #1487752
if (mbox && mbox != this.env.mailbox)
return false;
$(this.gui_objects.countdisplay).html(text);
// update page navigation buttons
this.set_page_buttons();
};
// replace content of mailboxname display
this.set_mailboxname = function(content)
{
if (this.gui_objects.mailboxname && content)
this.gui_objects.mailboxname.innerHTML = content;
};
// replace content of quota display
this.set_quota = function(content)
{
if (this.gui_objects.quotadisplay && content && content.type == 'text')
$(this.gui_objects.quotadisplay).html(content.percent+'%').attr('title', content.title);
this.triggerEvent('setquota', content);
this.env.quota_content = content;
};
// update the mailboxlist
this.set_unread_count = function(mbox, count, set_title, mark)
{
if (!this.gui_objects.mailboxlist)
return false;
this.env.unread_counts[mbox] = count;
this.set_unread_count_display(mbox, set_title);
if (mark)
this.mark_folder(mbox, mark, '', true);
else if (!count)
this.unmark_folder(mbox, 'recent', '', true);
};
// update the mailbox count display
this.set_unread_count_display = function(mbox, set_title)
{
var reg, link, text_obj, item, mycount, childcount, div;
if (item = this.get_folder_li(mbox, '', true)) {
mycount = this.env.unread_counts[mbox] ? this.env.unread_counts[mbox] : 0;
link = $(item).children('a').eq(0);
text_obj = link.children('span.unreadcount');
if (!text_obj.length && mycount)
text_obj = $('<span>').addClass('unreadcount').appendTo(link);
reg = /\s+\([0-9]+\)$/i;
childcount = 0;
if ((div = item.getElementsByTagName('div')[0]) &&
div.className.match(/collapsed/)) {
// add children's counters
for (var k in this.env.unread_counts)
if (k.indexOf(mbox + this.env.delimiter) == 0)
childcount += this.env.unread_counts[k];
}
if (mycount && text_obj.length)
text_obj.html(this.env.unreadwrap.replace(/%[sd]/, mycount));
else if (text_obj.length)
text_obj.remove();
// set parent's display
reg = new RegExp(RegExp.escape(this.env.delimiter) + '[^' + RegExp.escape(this.env.delimiter) + ']+$');
if (mbox.match(reg))
this.set_unread_count_display(mbox.replace(reg, ''), false);
// set the right classes
if ((mycount+childcount)>0)
$(item).addClass('unread');
else
$(item).removeClass('unread');
}
// set unread count to window title
reg = /^\([0-9]+\)\s+/i;
if (set_title && document.title) {
var new_title = '',
doc_title = String(document.title);
if (mycount && doc_title.match(reg))
new_title = doc_title.replace(reg, '('+mycount+') ');
else if (mycount)
new_title = '('+mycount+') '+doc_title;
else
new_title = doc_title.replace(reg, '');
this.set_pagetitle(new_title);
}
};
// display fetched raw headers
this.set_headers = function(content)
{
if (this.gui_objects.all_headers_row && this.gui_objects.all_headers_box && content)
$(this.gui_objects.all_headers_box).html(content).show();
};
// display all-headers row and fetch raw message headers
this.show_headers = function(props, elem)
{
if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box || !this.env.uid)
return;
$(elem).removeClass('show-headers').addClass('hide-headers');
$(this.gui_objects.all_headers_row).show();
elem.onclick = function() { rcmail.command('hide-headers', '', elem); };
// fetch headers only once
if (!this.gui_objects.all_headers_box.innerHTML) {
var lock = this.display_message(this.get_label('loading'), 'loading');
this.http_post('headers', {_uid: this.env.uid}, lock);
}
};
// hide all-headers row
this.hide_headers = function(props, elem)
{
if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box)
return;
$(elem).removeClass('hide-headers').addClass('show-headers');
$(this.gui_objects.all_headers_row).hide();
elem.onclick = function() { rcmail.command('show-headers', '', elem); };
};
/********************************************************/
/********* html to text conversion functions *********/
/********************************************************/
this.html2plain = function(htmlText, id)
{
var rcmail = this,
url = '?_task=utils&_action=html2text',
lock = this.set_busy(true, 'converting');
this.log('HTTP POST: ' + url);
$.ajax({ type: 'POST', url: url, data: htmlText, contentType: 'application/octet-stream',
error: function(o, status, err) { rcmail.http_error(o, status, err, lock); },
success: function(data) { rcmail.set_busy(false, null, lock); $('#'+id).val(data); rcmail.log(data); }
});
};
this.plain2html = function(plain, id)
{
var lock = this.set_busy(true, 'converting');
plain = plain.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
$('#'+id).val(plain ? '<pre>'+plain+'</pre>' : '');
this.set_busy(false, null, lock);
};
/********************************************************/
/********* remote request methods *********/
/********************************************************/
// compose a valid url with the given parameters
this.url = function(action, query)
{
var querystring = typeof query === 'string' ? '&' + query : '';
if (typeof action !== 'string')
query = action;
else if (!query || typeof query !== 'object')
query = {};
if (action)
query._action = action;
else
query._action = this.env.action;
var base = this.env.comm_path, k, param = {};
// overwrite task name
if (query._action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
query._action = RegExp.$2;
base = base.replace(/\_task=[a-z0-9_-]+/, '_task='+RegExp.$1);
}
// remove undefined values
for (k in query) {
if (query[k] !== undefined && query[k] !== null)
param[k] = query[k];
}
return base + '&' + $.param(param) + querystring;
};
this.redirect = function(url, lock)
{
if (lock || lock === null)
this.set_busy(true);
if (this.is_framed()) {
parent.rcmail.redirect(url, lock);
}
else {
if (this.env.extwin) {
if (typeof url == 'string')
url += (url.indexOf('?') < 0 ? '?' : '&') + '_extwin=1';
else
url._extwin = 1;
}
this.location_href(url, window);
}
};
this.goto_url = function(action, query, lock)
{
this.redirect(this.url(action, query));
};
this.location_href = function(url, target, frame)
{
if (frame)
this.lock_frame();
if (typeof url == 'object')
url = this.env.comm_path + '&' + $.param(url);
// simulate real link click to force IE to send referer header
if (bw.ie && target == window)
$('<a>').attr('href', url).appendTo(document.body).get(0).click();
else
target.location.href = url;
// reset keep-alive interval
this.start_keepalive();
};
// send a http request to the server
this.http_request = function(action, query, lock)
{
var url = this.url(action, query);
// trigger plugin hook
var result = this.triggerEvent('request'+action, query);
if (result !== undefined) {
// abort if one the handlers returned false
if (result === false)
return false;
else
query = result;
}
url += '&_remote=1';
// send request
this.log('HTTP GET: ' + url);
// reset keep-alive interval
this.start_keepalive();
return $.ajax({
type: 'GET', url: url, data: { _unlock:(lock?lock:0) }, dataType: 'json',
success: function(data){ ref.http_response(data); },
error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
});
};
// send a http POST request to the server
this.http_post = function(action, postdata, lock)
{
var url = this.url(action);
if (postdata && typeof postdata === 'object') {
postdata._remote = 1;
postdata._unlock = (lock ? lock : 0);
}
else
postdata += (postdata ? '&' : '') + '_remote=1' + (lock ? '&_unlock='+lock : '');
// trigger plugin hook
var result = this.triggerEvent('request'+action, postdata);
if (result !== undefined) {
// abort if one of the handlers returned false
if (result === false)
return false;
else
postdata = result;
}
// send request
this.log('HTTP POST: ' + url);
// reset keep-alive interval
this.start_keepalive();
return $.ajax({
type: 'POST', url: url, data: postdata, dataType: 'json',
success: function(data){ ref.http_response(data); },
error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
});
};
// aborts ajax request
this.abort_request = function(r)
{
if (r.request)
r.request.abort();
if (r.lock)
this.set_busy(false, null, r.lock);
};
// handle HTTP response
this.http_response = function(response)
{
if (!response)
return;
if (response.unlock)
this.set_busy(false);
this.triggerEvent('responsebefore', {response: response});
this.triggerEvent('responsebefore'+response.action, {response: response});
// set env vars
if (response.env)
this.set_env(response.env);
// we have labels to add
if (typeof response.texts === 'object') {
for (var name in response.texts)
if (typeof response.texts[name] === 'string')
this.add_label(name, response.texts[name]);
}
// if we get javascript code from server -> execute it
if (response.exec) {
this.log(response.exec);
eval(response.exec);
}
// execute callback functions of plugins
if (response.callbacks && response.callbacks.length) {
for (var i=0; i < response.callbacks.length; i++)
this.triggerEvent(response.callbacks[i][0], response.callbacks[i][1]);
}
// process the response data according to the sent action
switch (response.action) {
case 'delete':
if (this.task == 'addressbook') {
var sid, uid = this.contact_list.get_selection(), writable = false;
if (uid && this.contact_list.rows[uid]) {
// search results, get source ID from record ID
if (this.env.source == '') {
sid = String(uid).replace(/^[^-]+-/, '');
writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
}
else {
writable = !this.env.address_sources[this.env.source].readonly;
}
}
this.enable_command('compose', (uid && this.contact_list.rows[uid]));
this.enable_command('delete', 'edit', writable);
this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
this.enable_command('export-selected', false);
}
case 'moveto':
if (this.env.action == 'show') {
// re-enable commands on move/delete error
this.enable_command(this.env.message_commands, true);
if (!this.env.list_post)
this.enable_command('reply-list', false);
}
else if (this.task == 'addressbook') {
this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
}
case 'purge':
case 'expunge':
if (this.task == 'mail') {
if (!this.env.exists) {
// clear preview pane content
if (this.env.contentframe)
this.show_contentframe(false);
// disable commands useless when mailbox is empty
this.enable_command(this.env.message_commands, 'purge', 'expunge',
'select-all', 'select-none', 'expand-all', 'expand-unread', 'collapse-all', false);
}
if (this.message_list)
this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
}
break;
case 'refresh':
case 'check-recent':
case 'getunread':
case 'search':
this.env.qsearch = null;
case 'list':
if (this.task == 'mail') {
this.enable_command('show', 'select-all', 'select-none', this.env.messagecount > 0);
this.enable_command('expunge', this.env.exists);
this.enable_command('purge', this.purge_mailbox_test());
this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount);
if ((response.action == 'list' || response.action == 'search') && this.message_list) {
this.msglist_select(this.message_list);
this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
}
}
else if (this.task == 'addressbook') {
this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
if (response.action == 'list' || response.action == 'search') {
this.enable_command('search-create', this.env.source == '');
this.enable_command('search-delete', this.env.search_id);
this.update_group_commands();
this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
}
}
break;
}
if (response.unlock)
this.hide_message(response.unlock);
this.triggerEvent('responseafter', {response: response});
this.triggerEvent('responseafter'+response.action, {response: response});
// reset keep-alive interval
this.start_keepalive();
};
// handle HTTP request errors
this.http_error = function(request, status, err, lock, action)
{
var errmsg = request.statusText;
this.set_busy(false, null, lock);
request.abort();
// don't display error message on page unload (#1488547)
if (this.unload)
return;
if (request.status && errmsg)
this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
else if (status == 'timeout')
this.display_message(this.get_label('requesttimedout'), 'error');
else if (request.status == 0 && status != 'abort')
this.display_message(this.get_label('servererror') + ' (No connection)', 'error');
// redirect to url specified in location header if not empty
var location_url = request.getResponseHeader("Location");
if (location_url && this.env.action != 'compose') // don't redirect on compose screen, contents might get lost (#1488926)
this.redirect(location_url);
// 403 Forbidden response (CSRF prevention) - reload the page.
// In case there's a new valid session it will be used, otherwise
// login form will be presented (#1488960).
if (request.status == 403) {
(this.is_framed() ? parent : window).location.reload();
return;
}
// re-send keep-alive requests after 30 seconds
if (action == 'keep-alive')
setTimeout(function(){ ref.keep_alive(); ref.start_keepalive(); }, 30000);
};
// callback when an iframe finished loading
this.iframe_loaded = function(unlock)
{
this.set_busy(false, null, unlock);
if (this.submit_timer)
clearTimeout(this.submit_timer);
};
// post the given form to a hidden iframe
this.async_upload_form = function(form, action, onload)
{
var ts = new Date().getTime(),
frame_name = 'rcmupload'+ts;
// upload progress support
if (this.env.upload_progress_name) {
var fname = this.env.upload_progress_name,
field = $('input[name='+fname+']', form);
if (!field.length) {
field = $('<input>').attr({type: 'hidden', name: fname});
field.prependTo(form);
}
field.val(ts);
}
// have to do it this way for IE
// otherwise the form will be posted to a new window
if (document.all) {
var html = '<iframe name="'+frame_name+'" src="program/resources/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>';
document.body.insertAdjacentHTML('BeforeEnd', html);
}
else { // for standards-compilant browsers
var frame = document.createElement('iframe');
frame.name = frame_name;
frame.style.border = 'none';
frame.style.width = 0;
frame.style.height = 0;
frame.style.visibility = 'hidden';
document.body.appendChild(frame);
}
// handle upload errors, parsing iframe content in onload
$(frame_name).bind('load', {ts:ts}, onload);
$(form).attr({
target: frame_name,
action: this.url(action, { _id:this.env.compose_id||'', _uploadid:ts }),
method: 'POST'})
.attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data')
.submit();
return frame_name;
};
// html5 file-drop API
this.document_drag_hover = function(e, over)
{
e.preventDefault();
$(ref.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active');
};
this.file_drag_hover = function(e, over)
{
e.preventDefault();
e.stopPropagation();
$(ref.gui_objects.filedrop)[(over?'addClass':'removeClass')]('hover');
};
// handler when files are dropped to a designated area.
// compose a multipart form data and submit it to the server
this.file_dropped = function(e)
{
// abort event and reset UI
this.file_drag_hover(e, false);
// prepare multipart form data composition
var files = e.target.files || e.dataTransfer.files,
formdata = window.FormData ? new FormData() : null,
fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
boundary = '------multipartformboundary' + (new Date).getTime(),
dashdash = '--', crlf = '\r\n',
multipart = dashdash + boundary + crlf;
if (!files || !files.length)
return;
// inline function to submit the files to the server
var submit_data = function() {
var multiple = files.length > 1,
ts = new Date().getTime(),
content = '<span>' + (multiple ? ref.get_label('uploadingmany') : files[0].name) + '</span>';
// add to attachments list
if (!ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false }))
ref.file_upload_id = ref.set_busy(true, 'uploading');
// complete multipart content and post request
multipart += dashdash + boundary + dashdash + crlf;
$.ajax({
type: 'POST',
dataType: 'json',
url: ref.url(ref.env.filedrop.action||'upload', { _id:ref.env.compose_id||ref.env.cid||'', _uploadid:ts, _remote:1 }),
contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary,
processData: false,
timeout: 0, // disable default timeout set in ajaxSetup()
data: formdata || multipart,
headers: {'X-Roundcube-Request': ref.env.request_token},
beforeSend: function(xhr, s) { if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; },
success: function(data){ ref.http_response(data); },
error: function(o, status, err) { ref.http_error(o, status, err, null, 'attachment'); }
});
};
// get contents of all dropped files
var last = this.env.filedrop.single ? 0 : files.length - 1;
for (var j=0, i=0, f; j <= last && (f = files[i]); i++) {
if (!f.name) f.name = f.fileName;
if (!f.size) f.size = f.fileSize;
if (!f.type) f.type = 'application/octet-stream';
// file name contains non-ASCII characters, do UTF8-binary string conversion.
if (!formdata && /[^\x20-\x7E]/.test(f.name))
f.name_bin = unescape(encodeURIComponent(f.name));
// filter by file type if requested
if (this.env.filedrop.filter && !f.type.match(new RegExp(this.env.filedrop.filter))) {
// TODO: show message to user
continue;
}
// do it the easy way with FormData (FF 4+, Chrome 5+, Safari 5+)
if (formdata) {
formdata.append(fieldname, f);
if (j == last)
return submit_data();
}
// use FileReader supporetd by Firefox 3.6
else if (window.FileReader) {
var reader = new FileReader();
// closure to pass file properties to async callback function
reader.onload = (function(file, j) {
return function(e) {
multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf;
multipart += 'Content-Length: ' + file.size + crlf;
multipart += 'Content-Type: ' + file.type + crlf + crlf;
multipart += e.target.result + crlf;
multipart += dashdash + boundary + crlf;
if (j == last) // we're done, submit the data
return submit_data();
}
})(f,j);
reader.readAsBinaryString(f);
}
// Firefox 3
else if (f.getAsBinary) {
multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
multipart += '; filename="' + (f.name_bin || f.name) + '"' + crlf;
multipart += 'Content-Length: ' + f.size + crlf;
multipart += 'Content-Type: ' + f.type + crlf + crlf;
multipart += f.getAsBinary() + crlf;
multipart += dashdash + boundary +crlf;
if (j == last)
return submit_data();
}
j++;
}
};
// starts interval for keep-alive signal
this.start_keepalive = function()
{
if (!this.env.session_lifetime || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
return;
if (this._keepalive)
clearInterval(this._keepalive);
this._keepalive = setInterval(function(){ ref.keep_alive(); }, this.env.session_lifetime * 0.5 * 1000);
};
// starts interval for refresh signal
this.start_refresh = function()
{
if (!this.env.refresh_interval || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
return;
if (this._refresh)
clearInterval(this._refresh);
this._refresh = setInterval(function(){ ref.refresh(); }, this.env.refresh_interval * 1000);
};
// sends keep-alive signal
this.keep_alive = function()
{
if (!this.busy)
this.http_request('keep-alive');
};
// sends refresh signal
this.refresh = function()
{
if (this.busy) {
// try again after 10 seconds
setTimeout(function(){ ref.refresh(); ref.start_refresh(); }, 10000);
return;
}
var params = {}, lock = this.set_busy(true, 'refreshing');
if (this.task == 'mail' && this.gui_objects.mailboxlist)
params = this.check_recent_params();
// plugins should bind to 'requestrefresh' event to add own params
this.http_request('refresh', params, lock);
};
// returns check-recent request parameters
this.check_recent_params = function()
{
var params = {_mbox: this.env.mailbox};
if (this.gui_objects.mailboxlist)
params._folderlist = 1;
if (this.gui_objects.messagelist)
params._list = 1;
if (this.gui_objects.quotadisplay)
params._quota = 1;
if (this.env.search_request)
params._search = this.env.search_request;
return params;
};
/********************************************************/
/********* helper methods *********/
/********************************************************/
// get window.opener.rcmail if available
this.opener = function()
{
// catch Error: Permission denied to access property rcmail
try {
if (window.opener && !opener.closed && opener.rcmail)
return opener.rcmail;
}
catch (e) {}
};
// check if we're in show mode or if we have a unique selection
// and return the message uid
this.get_single_uid = function()
{
return this.env.uid ? this.env.uid : (this.message_list ? this.message_list.get_single_selection() : null);
};
// same as above but for contacts
this.get_single_cid = function()
{
return this.env.cid ? this.env.cid : (this.contact_list ? this.contact_list.get_single_selection() : null);
};
// gets cursor position
this.get_caret_pos = function(obj)
{
if (obj.selectionEnd !== undefined)
return obj.selectionEnd;
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange();
if (range.parentElement() != obj)
return 0;
var gm = range.duplicate();
if (obj.tagName == 'TEXTAREA')
gm.moveToElementText(obj);
else
gm.expand('textedit');
gm.setEndPoint('EndToStart', range);
var p = gm.text.length;
return p <= obj.value.length ? p : -1;
}
return obj.value.length;
};
// moves cursor to specified position
this.set_caret_pos = function(obj, pos)
{
if (obj.setSelectionRange)
obj.setSelectionRange(pos, pos);
else if (obj.createTextRange) {
var range = obj.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
};
// disable/enable all fields of a form
this.lock_form = function(form, lock)
{
if (!form || !form.elements)
return;
var n, len, elm;
if (lock)
this.disabled_form_elements = [];
for (n=0, len=form.elements.length; n<len; n++) {
elm = form.elements[n];
if (elm.type == 'hidden')
continue;
// remember which elem was disabled before lock
if (lock && elm.disabled)
this.disabled_form_elements.push(elm);
// check this.disabled_form_elements before inArray() as a workaround for FF5 bug
// http://bugs.jquery.com/ticket/9873
else if (lock || (this.disabled_form_elements && $.inArray(elm, this.disabled_form_elements)<0))
elm.disabled = lock;
}
};
this.mailto_handler_uri = function()
{
return location.href.split('?')[0] + '?_task=mail&_action=compose&_to=%s';
};
this.register_protocol_handler = function(name)
{
try {
window.navigator.registerProtocolHandler('mailto', this.mailto_handler_uri(), name);
}
catch(e) {};
};
this.check_protocol_handler = function(name, elem)
{
var nav = window.navigator;
if (!nav
|| (typeof nav.registerProtocolHandler != 'function')
|| ((typeof nav.isProtocolHandlerRegistered == 'function')
&& nav.isProtocolHandlerRegistered('mailto', this.mailto_handler_uri()) == 'registered')
)
$(elem).addClass('disabled');
else
$(elem).click(function() { rcmail.register_protocol_handler(name); return false; });
};
// Checks browser capabilities eg. PDF support, TIF support
this.browser_capabilities_check = function()
{
if (!this.env.browser_capabilities)
this.env.browser_capabilities = {};
if (this.env.browser_capabilities.pdf === undefined)
this.env.browser_capabilities.pdf = this.pdf_support_check();
if (this.env.browser_capabilities.flash === undefined)
this.env.browser_capabilities.flash = this.flash_support_check();
if (this.env.browser_capabilities.tif === undefined)
this.tif_support_check();
};
// Returns browser capabilities string
this.browser_capabilities = function()
{
if (!this.env.browser_capabilities)
return '';
var n, ret = [];
for (n in this.env.browser_capabilities)
ret.push(n + '=' + this.env.browser_capabilities[n]);
return ret.join();
};
this.tif_support_check = function()
{
var img = new Image();
img.onload = function() { rcmail.env.browser_capabilities.tif = 1; };
img.onerror = function() { rcmail.env.browser_capabilities.tif = 0; };
img.src = 'program/resources/blank.tif';
};
this.pdf_support_check = function()
{
var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {},
plugins = navigator.plugins,
len = plugins.length,
regex = /Adobe Reader|PDF|Acrobat/i;
if (plugin && plugin.enabledPlugin)
return 1;
if (window.ActiveXObject) {
try {
if (axObj = new ActiveXObject("AcroPDF.PDF"))
return 1;
}
catch (e) {}
try {
if (axObj = new ActiveXObject("PDF.PdfCtrl"))
return 1;
}
catch (e) {}
}
for (i=0; i<len; i++) {
plugin = plugins[i];
if (typeof plugin === 'String') {
if (regex.test(plugin))
return 1;
}
else if (plugin.name && regex.test(plugin.name))
return 1;
}
return 0;
};
this.flash_support_check = function()
{
var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
if (plugin && plugin.enabledPlugin)
return 1;
if (window.ActiveXObject) {
try {
if (axObj = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
return 1;
}
catch (e) {}
}
return 0;
};
// Cookie setter
this.set_cookie = function(name, value, expires)
{
setCookie(name, value, expires, this.env.cookie_path, this.env.cookie_domain, this.env.cookie_secure);
}
} // end object rcube_webmail
// some static methods
rcube_webmail.long_subject_title = function(elem, indent)
{
if (!elem.title) {
var $elem = $(elem);
if ($elem.width() + indent * 15 > $elem.parent().width())
elem.title = $elem.html();
}
};
rcube_webmail.long_subject_title_ie = function(elem, indent)
{
if (!elem.title) {
var $elem = $(elem),
txt = $.trim($elem.text()),
tmp = $('<span>').text(txt)
.css({'position': 'absolute', 'float': 'left', 'visibility': 'hidden',
'font-size': $elem.css('font-size'), 'font-weight': $elem.css('font-weight')})
.appendTo($('body')),
w = tmp.width();
tmp.remove();
if (w + indent * 15 > $elem.width())
elem.title = txt;
}
};
rcube_webmail.prototype.get_cookie = getCookie;
// copy event engine prototype
rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
rcube_webmail.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;
diff --git a/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php
index 18055f77d..913eacb05 100644
--- a/program/lib/Roundcube/rcube_config.php
+++ b/program/lib/Roundcube/rcube_config.php
@@ -1,454 +1,454 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class to read configuration settings |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Configuration class for Roundcube
*
* @package Framework
* @subpackage Core
*/
class rcube_config
{
const DEFAULT_SKIN = 'larry';
private $prop = array();
private $errors = array();
private $userprefs = array();
/**
* Renamed options
*
* @var array
*/
private $legacy_props = array(
// new name => old name
'default_folders' => 'default_imap_folders',
'mail_pagesize' => 'pagesize',
'addressbook_pagesize' => 'pagesize',
'reply_mode' => 'top_posting',
'refresh_interval' => 'keep_alive',
'min_refresh_interval' => 'min_keep_alive',
'messages_cache_ttl' => 'message_cache_lifetime',
'redundant_attachments_cache_ttl' => 'redundant_attachments_memcache_ttl',
);
/**
* Object constructor
*/
public function __construct()
{
$this->load();
// Defaults, that we do not require you to configure,
// but contain information that is used in various
// locations in the code:
$this->set('contactlist_fields', array('name', 'firstname', 'surname', 'email'));
}
/**
* Load config from local config file
*
* @todo Remove global $CONFIG
*/
private function load()
{
// load main config file
if (!$this->load_from_file(RCUBE_CONFIG_DIR . 'main.inc.php'))
$this->errors[] = 'main.inc.php was not found.';
// load database config
if (!$this->load_from_file(RCUBE_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'])) {
if (!empty($this->prop['skin_path'])) {
$this->prop['skin'] = str_replace('skins/', '', unslashify($this->prop['skin_path']));
}
else {
$this->prop['skin'] = self::DEFAULT_SKIN;
}
}
// larry is the new default skin :-)
if ($this->prop['skin'] == 'default')
$this->prop['skin'] = self::DEFAULT_SKIN;
// fix paths
$this->prop['log_dir'] = $this->prop['log_dir'] ? realpath(unslashify($this->prop['log_dir'])) : RCUBE_INSTALL_PATH . 'logs';
$this->prop['temp_dir'] = $this->prop['temp_dir'] ? realpath(unslashify($this->prop['temp_dir'])) : RCUBE_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], RCUBE_CHARSET, 'UTF7-IMAP');
if (!empty($this->prop['default_folders']))
foreach ($this->prop['default_folders'] as $n => $folder)
$this->prop['default_folders'][$n] = rcube_charset::convert($folder, RCUBE_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['_timezone_value'] = $this->client_timezone();
}
else if (is_numeric($this->prop['timezone']) && ($tz = timezone_name_from_abbr("", $this->prop['timezone'] * 3600, 0))) {
$this->prop['timezone'] = $tz;
}
else if (empty($this->prop['timezone'])) {
$this->prop['timezone'] = 'UTC';
}
// remove deprecated properties
unset($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(RCUBE_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->merge($rcmail_config);
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)
{
- if (isset($this->prop[$name])) {
+ if (array_key_exists($name, $this->prop)) {
$result = $this->prop[$name];
}
else {
$result = $def;
}
$rcube = rcube::get_instance();
if ($name == 'timezone' && isset($this->prop['_timezone_value'])) {
$result = $this->prop['_timezone_value'];
}
else if ($name == 'client_mimetypes') {
if ($result == null && $def == null)
$result = 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,image/bmp,image/tiff,application/x-javascript,application/pdf,application/x-shockwave-flash';
if ($result && is_string($result))
$result = explode(',', $result);
}
$plugin = $rcube->plugins->exec_hook('config_get', array(
'name' => $name, 'default' => $def, 'result' => $result));
return $plugin['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);
$this->fix_legacy_props();
}
/**
* 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 ($dont_override as $key) {
unset($prefs[$key]);
}
}
// convert user's timezone into the new format
if (is_numeric($prefs['timezone']) && ($tz = timezone_name_from_abbr('', $prefs['timezone'] * 3600, 0))) {
$prefs['timezone'] = $tz;
}
// larry is the new default skin :-)
if ($prefs['skin'] == 'default') {
$prefs['skin'] = self::DEFAULT_SKIN;
}
$this->userprefs = $prefs;
$this->prop = array_merge($this->prop, $prefs);
$this->fix_legacy_props();
// override timezone settings with client values
if ($this->prop['timezone'] == 'auto') {
$this->prop['_timezone_value'] = isset($_SESSION['timezone']) ? $this->client_timezone() : $this->prop['_timezone_value'];
}
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)
* @deprecated
*/
public function get_timezone()
{
if ($tz = $this->get('timezone')) {
try {
$tz = new DateTimeZone($tz);
return $tz->getOffset(new DateTime('now')) / 3600;
}
catch (Exception $e) {
}
}
return 0;
}
/**
* 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)) {
rcube::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) {
rcube::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
rcube::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_utils::parse_host($this->prop['mail_domain']);
}
if ($encode) {
$domain = rcube_utils::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);
}
/**
* Internal getter for client's (browser) timezone identifier
*/
private function client_timezone()
{
if (isset($_SESSION['timezone']) && is_numeric($_SESSION['timezone'])
&& ($ctz = timezone_name_from_abbr("", $_SESSION['timezone'] * 3600, 0))) {
return $ctz;
}
else if (!empty($_SESSION['timezone'])) {
try {
$tz = timezone_open($_SESSION['timezone']);
return $tz->getName();
}
catch (Exception $e) { /* gracefully ignore */ }
}
// fallback to server's timezone
return date_default_timezone_get();
}
/**
* Convert legacy options into new ones
*/
private function fix_legacy_props()
{
foreach ($this->legacy_props as $new => $old) {
if (isset($this->prop[$old])) {
if (!isset($this->prop[$new])) {
$this->prop[$new] = $this->prop[$old];
}
unset($this->prop[$old]);
}
}
}
}
diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php
index 39a48b456..54077c692 100644
--- a/program/lib/Roundcube/rcube_ldap.php
+++ b/program/lib/Roundcube/rcube_ldap.php
@@ -1,2376 +1,1953 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | Copyright (C) 2011-2013, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| 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> |
+-----------------------------------------------------------------------+
*/
/**
* Model class to access an LDAP address directory
*
* @package Framework
* @subpackage 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 $coltypes = array();
/** private properties */
- protected $conn;
+ protected $ldap;
protected $prop = array();
protected $fieldmap = array();
protected $sub_filter;
protected $filter = '';
protected $result = null;
protected $ldap_result = null;
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 $p LDAP connection properties
+ * @param array $p LDAP connection properties
* @param boolean $debug Enables debug mode
* @param string $mail_domain Current user mail domain name
*/
function __construct($p, $debug = false, $mail_domain = null)
{
$this->prop = $p;
+ $fetch_attributes = array('objectClass');
+
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';
+
+ // add group name attrib to the list of attributes to be fetched
+ $fetch_attributes[] = $this->prop['groups']['name_attr'];
+ }
+ if (is_array($p['group_filters']) && count($p['group_filters'])) {
+ $this->groups = true;
+
+ foreach ($p['group_filters'] as $group_filter) {
+ if ($group_filter['name_attr'])
+ $fetch_attributes[] = $group_filter['name_attr'];
+ }
}
// fieldmap property is given
if (is_array($p['fieldmap'])) {
foreach ($p['fieldmap'] as $rf => $lf)
$this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
}
else if (!empty($p)) {
// 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 $colv => $lfv) {
list($col, $type) = explode(':', $colv);
list($lf, $limit, $delim) = explode(':', $lfv);
if ($limit == '*') $limit = null;
else $limit = max(1, intval($limit));
if (!is_array($this->coltypes[$col])) {
$subtypes = $type ? array($type) : null;
$this->coltypes[$col] = array('limit' => $limit, 'subtypes' => $subtypes, 'attributes' => array($lf));
}
elseif ($type) {
$this->coltypes[$col]['subtypes'][] = $type;
$this->coltypes[$col]['attributes'][] = $lf;
$this->coltypes[$col]['limit'] += $limit;
}
if ($delim)
$this->coltypes[$col]['serialized'][$type] = $delim;
$this->fieldmap[$colv] = $lf;
}
// support for composite address
if ($this->coltypes['street'] && $this->coltypes['locality']) {
$this->coltypes['address'] = array(
'limit' => max(1, $this->coltypes['locality']['limit'] + $this->coltypes['address']['limit']),
'subtypes' => array_merge((array)$this->coltypes['address']['subtypes'], (array)$this->coltypes['locality']['subtypes']),
'childs' => array(),
) + (array)$this->coltypes['address'];
foreach (array('street','locality','zipcode','region','country') as $childcol) {
if ($this->coltypes[$childcol]) {
$this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
unset($this->coltypes[$childcol]); // remove address child col from global coltypes list
}
}
// at least one address type must be specified
if (empty($this->coltypes['address']['subtypes'])) {
$this->coltypes['address']['subtypes'] = array('home');
}
}
else if ($this->coltypes['address']) {
$this->coltypes['address'] += array('type' => 'textarea', 'childs' => null, 'size' => 40);
// 'serialized' means the UI has to present a composite address field
if ($this->coltypes['address']['serialized']) {
$childprop = array('type' => 'text');
$this->coltypes['address']['type'] = 'composite';
$this->coltypes['address']['childs'] = array('street' => $childprop, 'locality' => $childprop, 'zipcode' => $childprop, 'country' => $childprop);
}
}
// make sure 'required_fields' is an array
if (!is_array($this->prop['required_fields'])) {
$this->prop['required_fields'] = (array) $this->prop['required_fields'];
}
// make sure LDAP_rdn field is required
if (!empty($this->prop['LDAP_rdn']) && !in_array($this->prop['LDAP_rdn'], $this->prop['required_fields'])
&& !in_array($this->prop['LDAP_rdn'], array_keys((array)$this->prop['autovalues']))) {
$this->prop['required_fields'][] = $this->prop['LDAP_rdn'];
}
foreach ($this->prop['required_fields'] as $key => $val) {
$this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
}
// Build sub_fields filter
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
$this->sub_filter = '';
foreach ($this->prop['sub_fields'] as $class) {
if (!empty($class)) {
$class = is_array($class) ? array_pop($class) : $class;
$this->sub_filter .= '(objectClass=' . $class . ')';
}
}
if (count($this->prop['sub_fields']) > 1) {
$this->sub_filter = '(|' . $this->sub_filter . ')';
}
}
$this->sort_col = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
$this->debug = $debug;
$this->mail_domain = $mail_domain;
// initialize cache
- $rcube = rcube::get_instance();
- $cache_type = $rcube->config->get('ldap_cache', 'db');
- $cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m');
- $cache_name = 'LDAP.' . asciiwords($this->prop['name']);
+ $rcube = rcube::get_instance();
+ if ($cache_type = $rcube->config->get('ldap_cache', 'db')) {
+ $cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m');
+ $cache_name = 'LDAP.' . asciiwords($this->prop['name']);
+
+ $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
+ }
+
+ // determine which attributes to fetch
+ $this->prop['list_attributes'] = array_unique($fetch_attributes);
+ $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes);
+ foreach ($rcube->config->get('contactlist_fields') as $col) {
+ $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col));
+ }
- $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
+ // initialize ldap wrapper object
+ $this->ldap = new rcube_ldap_generic($this->prop);
+ $this->ldap->set_cache($this->cache);
+ $this->ldap->set_debug($this->debug);
$this->_connect();
}
/**
* Establish a connection to the LDAP server
*/
private function _connect()
{
$rcube = rcube::get_instance();
- if (!function_exists('ldap_connect'))
- rcube::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))
+ if ($this->ready)
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;
-
// try to connect + bind for every host configured
// with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
// see http://www.php.net/manual/en/function.ldap-connect.php
foreach ($this->prop['hosts'] as $host) {
- $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
- $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
-
- $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]");
-
- 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;
-
- if (!empty($this->prop['network_timeout']))
- ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->prop['network_timeout']);
-
- if (isset($this->prop['referrals']))
- ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
- }
- else {
- $this->_debug("S: NOT OK");
+ // skip host if connection failed
+ if (!$this->ldap->connect($host)) {
continue;
}
// See if the directory is writeable.
if ($this->prop['writable']) {
$this->readonly = 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;
+ $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 = $rcube->get_user_password();
}
// Get the pieces needed for variable replacement.
if ($fu = $rcube->get_user_email())
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);
+ // Search for the dn to use to authenticate
if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
- if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
- $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
- }
-
- // 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);
+ $search_bind_dn = strtr($this->prop['search_bind_dn'], $replaces);
+ $search_base_dn = strtr($this->prop['search_base_dn'], $replaces);
+ $search_filter = strtr($this->prop['search_filter'], $replaces);
- $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
+ $cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:"
+ .$this->prop['search_bind_pw']);
- $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];
- }
+ if ($this->cache && ($dn = $this->cache->get($cache_key))) {
+ $replaces['%dn'] = $dn;
}
else {
- $this->_debug("S: ".ldap_error($this->conn));
+ $ldap = $this->ldap;
+ if (!empty($search_bind_dn) && !empty($this->prop['search_bind_pw'])) {
+ // To protect from "Critical extension is unavailable" error
+ // we need to use a separate LDAP connection
+ if (!empty($this->prop['vlv'])) {
+ $ldap = new rcube_ldap_generic($this->prop);
+ $ldap->set_debug($this->debug);
+ $ldap->set_cache($this->cache);
+ if (!$ldap->connect($host)) {
+ continue;
+ }
+ }
+
+ if (!$ldap->bind($search_bind_dn, $this->prop['search_bind_pw'])) {
+ continue; // bind failed, try next host
+ }
+ }
+
+ $res = $ldap->search($search_base_dn, $search_filter, 'sub', array('uid'));
+ if ($res) {
+ $res->rewind();
+ $replaces['%dn'] = $res->get_dn();
+ }
+
+ if ($ldap != $this->ldap) {
+ $ldap->close();
+ }
}
// DN not found
if (empty($replaces['%dn'])) {
if (!empty($this->prop['search_dn_default']))
$replaces['%dn'] = $this->prop['search_dn_default'];
else {
rcube::raise_error(array(
'code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "DN not found using LDAP search."), true);
- return false;
+ continue;
}
}
+
+ if ($this->cache && !empty($replaces['%dn'])) {
+ $this->cache->set($cache_key, $replaces['%dn']);
+ }
}
// 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);
+ $this->ready = $this->ldap->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);
+ $this->ready = $this->ldap->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
}
else {
- $this->ready = $this->sasl_bind($bind_user, $bind_pass);
+ $this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);
}
}
// connection established, we're done here
if ($this->ready) {
break;
}
} // end foreach hosts
- if (!is_resource($this->conn)) {
+ if (!is_resource($this->ldap->conn)) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
+ 'message' => "Could not connect to any LDAP server, last tried $host"), true);
return false;
}
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
+ * Close connection to LDAP server
*/
- public function sasl_bind($authc, $pass, $authz=null)
+ function close()
{
- if (!$this->conn) {
- return false;
- }
-
- if (!function_exists('ldap_sasl_bind')) {
- rcube::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;
+ if ($this->ldap) {
+ $this->ldap->close();
}
-
- $this->_debug("S: ".ldap_error($this->conn));
-
- rcube::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
+ * Returns address book name
*
- * @return boolean True on success, False on error
+ * @return string Address book name
*/
- public function bind($dn, $pass)
+ function get_name()
{
- 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));
-
- rcube::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;
+ return $this->prop['name'];
}
/**
- * Close connection to LDAP server
+ * Set internal list page
+ *
+ * @param number Page number to list
*/
- function close()
+ function set_page($page)
{
- if ($this->conn)
- {
- $this->_debug("C: Close");
- ldap_unbind($this->conn);
- $this->conn = null;
- }
+ $this->list_page = (int)$page;
+ $this->ldap->set_vlv_page($this->list_page, $this->page_size);
}
-
/**
- * Returns address book name
+ * Set internal page size
*
- * @return string Address book name
+ * @param number Number of records to display on one page
*/
- function get_name()
+ function set_pagesize($size)
{
- return $this->prop['name'];
+ $this->page_size = (int)$size;
+ $this->ldap->set_vlv_page($this->list_page, $this->page_size);
}
/**
* Set internal sort settings
*
* @param string $sort_col Sort column
* @param string $sort_order Sort order
*/
function set_sort_order($sort_col, $sort_order = null)
{
if ($this->coltypes[$sort_col]['attributes'])
$this->sort_col = $this->coltypes[$sort_col]['attributes'][0];
}
/**
* 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)
- {
+ 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'])
- {
+ 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']);
+ else {
+ $prop = $this->group_id ? $this->group_data : $this->prop;
+
+ // use global search filter
+ if (!empty($this->filter))
+ $prop['filter'] = $this->filter;
// exec LDAP search if no result resource is stored
- if ($this->conn && !$this->ldap_result)
- $this->_exec_search();
+ if ($this->ready && !$this->ldap_result)
+ $this->ldap_result = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop);
// count contacts for this user
$this->result = $this->count();
// we have a search result resource
- if ($this->ldap_result && $this->result->count > 0)
- {
+ 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);
+ if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active)
+ $this->ldap_result->sort($this->sort_col);
// get all entries from the ldap server
- $entries = ldap_get_entries($this->conn, $this->ldap_result);
+ $entries = $this->ldap_result->entries();
}
} // end else
// start and end of the page
- $start_row = $this->vlv_active ? 0 : $this->result->first;
+ $start_row = $this->ldap->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));
+ $entries = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
+ if ($entries === false) {
return $group_members;
}
-
- $entries = @ldap_get_entries($this->conn, $result);
}
- for ($i=0; $i < $entries['count']; $i++)
- {
+ for ($i=0; $i < $entries['count']; $i++) {
$entry = $entries[$i];
if (empty($entry['objectclass']))
continue;
- foreach ((array)$entry['objectclass'] as $objectclass)
- {
+ foreach ((array)$entry['objectclass'] as $objectclass) {
switch (strtolower($objectclass)) {
case "group":
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 = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
$attrib[] = 'member';
$attrib[] = 'uniqueMember';
$attrib[] = 'memberURL';
- for ($i=0; $i < $entry[$attr]['count']; $i++)
- {
+ $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
+
+ for ($i=0; $i < $entry[$attr]['count']; $i++) {
if (empty($entry[$attr][$i]))
continue;
- $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 = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib);
+ if ($members == false) {
$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++)
- {
+ 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]);
- }
- 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];
+ $attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
+ if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
+ $entries = $result->entries();
+ for ($j = 0; $j < $entries['count']; $j++) {
+ if (self::is_group_entry($entries[$j]) && ($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 int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* @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, $mode=0, $select=true, $nocount=false, $required=array())
{
$mode = intval($mode);
// special treatment for ID-based search
- if ($fields == 'ID' || $fields == $this->primary_key)
- {
+ 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))
- {
+ foreach ($ids as $id) {
+ if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
}
return $result;
}
// use VLV pseudo-search for autocompletion
$rcube = rcube::get_instance();
$list_fields = $rcube->config->get('contactlist_fields');
- if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $list_fields))
- {
- // 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, $this->page_size, (int)$this->prop['timelimit']);
-
+ if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {
$this->result = new rcube_result_set(0);
- if (!$this->ldap_result) {
- $this->_debug("S: ".ldap_error($this->conn));
+ $search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : '';
+ $ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'],
+ array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */));
+ if ($ldap_data === false) {
return $this->result;
}
- $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
-
// get all entries of this page and post-filter those that really match the query
- $search = mb_strtolower($value);
- $entries = ldap_get_entries($this->conn, $this->ldap_result);
-
- for ($i = 0; $i < $entries['count']; $i++) {
- $rec = $this->_ldap2result($entries[$i]);
+ $search = mb_strtolower($value);
+ foreach ($ldap_data as $i => $entry) {
+ $rec = $this->_ldap2result($entry);
foreach ($fields as $f) {
foreach ((array)$rec[$f] as $val) {
if ($this->compare_search_value($f, $val, $search, $mode)) {
$this->result->add($rec);
$this->result->count++;
break 2;
}
}
}
}
return $this->result;
}
// use AND operator for advanced searches
$filter = is_array($value) ? '(&' : '(|';
// set wildcards
$wp = $ws = '';
if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
$ws = '*';
if (!$mode) {
$wp = '*';
}
}
- if ($fields == '*')
- {
+ if ($fields == '*') {
// search_fields are required for fulltext search
- if (empty($this->prop['search_fields']))
- {
+ 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']))
- {
+ if (is_array($this->prop['search_fields'])) {
foreach ($this->prop['search_fields'] as $field) {
- $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
+ $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)";
}
}
}
- else
- {
+ else {
foreach ((array)$fields as $idx => $field) {
$val = is_array($value) ? $value[$idx] : $value;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$filter .= '(|';
foreach ($attrs as $f)
- $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
+ $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
if (count($attrs) > 1)
$filter .= ')';
}
}
}
$filter .= ')';
// add required (non empty) fields filter
$req_filter = '';
foreach ((array)$required as $field) {
+ if (in_array($field, (array)$fields)) // required field is already in search filter
+ continue;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$req_filter .= '(|';
foreach ($attrs as $f)
$req_filter .= "($f=*)";
if (count($attrs) > 1)
$req_filter .= ')';
}
}
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);
+ if ($this->ldap_result) {
+ $count = $this->ldap_result->count();
}
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'];
+ // We have a connection but no result set, attempt to get one.
+ else if ($this->ready) {
+ $prop = $this->group_id ? $this->group_data : $this->prop;
+
+ if (!empty($this->filter)) { // Use global search filter
+ $prop['filter'] = $this->filter;
}
- $count = (int) $this->_exec_search(true);
+ $count = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], array('dn'), $prop, true);
}
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 = $this->result = null;
- if ($this->conn && $dn)
- {
+ if ($this->ready && $dn) {
$dn = self::dn_decode($dn);
- $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
-
- if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) {
- $this->_debug("S: OK");
-
- $entry = ldap_first_entry($this->conn, $ldap_result);
-
- if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) {
- $rec = array_change_key_case($rec, CASE_LOWER);
- }
- }
- else {
- $this->_debug("S: ".ldap_error($this->conn));
+ if ($rec = $this->ldap->get_entry($dn)) {
+ $rec = array_change_key_case($rec, CASE_LOWER);
}
// Use ldap_list to get subentries like country (c) attribute (#1488123)
if (!empty($rec) && $this->sub_filter) {
- if ($entries = $this->ldap_list($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
+ if ($entries = $this->ldap->list_entries($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
foreach ($entries as $entry) {
$lrec = array_change_key_case($entry, CASE_LOWER);
$rec = array_merge($lrec, $rec);
}
}
}
if (!empty($rec)) {
// Add in the dn for the entry.
$rec['dn'] = $dn;
$res = $this->_ldap2result($rec);
- $this->result = new rcube_result_set();
+ $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)
{
// validate e-mail addresses
if (!parent::validate($save_data, $autofix)) {
return 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]) || $ldap_data[$fld] === '') {
$missing[$fld] = 1;
}
}
if ($missing) {
// try to complete record automatically
if ($autofix) {
$sn_field = $this->fieldmap['surname'];
$fn_field = $this->fieldmap['firstname'];
$mail_field = $this->fieldmap['email'];
// try to extract surname and firstname from displayname
$name_parts = preg_split('/[\s,.]+/', $save_data['name']);
if ($sn_field && $missing[$sn_field]) {
$save_data['surname'] = array_pop($name_parts);
unset($missing[$sn_field]);
}
if ($fn_field && $missing[$fn_field]) {
$save_data['firstname'] = array_shift($name_parts);
unset($missing[$fn_field]);
}
// try to fix missing e-mail, very often on import
// from vCard we have email:other only defined
if ($mail_field && $missing[$mail_field]) {
$emails = $this->get_col_values('email', $save_data, true);
if (!empty($emails) && ($email = array_shift($emails))) {
$save_data['email'] = $email;
unset($missing[$mail_field]);
}
}
}
// TODO: generate message saying which fields are missing
if (!empty($missing)) {
$this->set_error(self::ERROR_VALIDATE, 'formincomplete');
return false;
}
}
return true;
}
/**
* 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'];
// add automatically generated attributes
$this->add_autovalues($newentry);
// 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;
+ $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
// Remove attributes that need to be added separately (child objects)
$xfields = array();
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
foreach (array_keys($this->prop['sub_fields']) as $xf) {
if (!empty($newentry[$xf])) {
$xfields[$xf] = $newentry[$xf];
unset($newentry[$xf]);
}
}
}
- if (!$this->ldap_add($dn, $newentry)) {
+ if (!$this->ldap->add($dn, $newentry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
foreach ($xfields as $xidx => $xf) {
- $xdn = $xidx.'='.$this->_quote_string($xf).','.$dn;
+ $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;
$xf = array(
$xidx => $xf,
'objectClass' => (array) $this->prop['sub_fields'][$xidx],
);
- $this->ldap_add($xdn, $xf);
+ $this->ldap->add($xdn, $xf);
}
$dn = self::dn_encode($dn);
// add new contact to the selected group
if ($this->group_id)
$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);
$newdata = array();
$replacedata = array();
$deletedata = array();
$subdata = array();
$subdeldata = array();
$subnewdata = array();
$ldap_data = $this->_map_data($save_cols);
$old_data = $record['_raw_attrib'];
// special handling of photo col
if ($photo_fld = $this->fieldmap['photo']) {
// undefined means keep old photo
if (!array_key_exists('photo', $save_cols)) {
$ldap_data[$photo_fld] = $record['photo'];
}
}
foreach ($this->fieldmap as $fld) {
if ($fld) {
$val = $ldap_data[$fld];
$old = $old_data[$fld];
// remove empty array values
if (is_array($val))
$val = array_filter($val);
// $this->_map_data() result and _raw_attrib use different format
// make sure comparing array with one element with a string works as expected
if (is_array($old) && count($old) == 1 && !is_array($val)) {
$old = array_pop($old);
}
if (is_array($val) && count($val) == 1 && !is_array($old)) {
$val = array_pop($val);
}
// Subentries must be handled separately
if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) {
if ($old != $val) {
if ($old !== null) {
$subdeldata[$fld] = $old;
}
if ($val) {
$subnewdata[$fld] = $val;
}
}
else if ($old !== null) {
$subdata[$fld] = $old;
}
continue;
}
// The field does exist compare it to the ldap record.
if ($old != $val) {
// Changed, but find out how.
if ($old === null) {
// Field was not set prior, need to add it.
$newdata[$fld] = $val;
}
else if ($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.
// #1488420: Workaround "ldap_mod_del(): Modify: Inappropriate matching in..."
// jpegPhoto attribute require an array() here. It looks to me that it works for other attribs too
$deletedata[$fld] = array();
//$deletedata[$fld] = $old_data[$fld];
}
}
else {
// The data was modified, save it out.
$replacedata[$fld] = $val;
}
} // end if
} // end if
} // end foreach
// console($old_data, $ldap_data, '----', $newdata, $replacedata, $deletedata, '----', $subdata, $subnewdata, $subdeldata);
$dn = self::dn_decode($id);
// Update the entry as required.
if (!empty($deletedata)) {
// Delete the fields.
- if (!$this->ldap_mod_del($dn, $deletedata)) {
+ if (!$this->ldap->mod_del($dn, $deletedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
} // 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)
+ .rcube_ldap_generic::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);
+ .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
unset($replacedata[$this->prop['LDAP_rdn']]);
}
}
// Replace the fields.
if (!empty($replacedata)) {
- if (!$this->ldap_mod_replace($dn, $replacedata)) {
+ if (!$this->ldap->mod_replace($dn, $replacedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
}
} // end if
// RDN change, we need to remove all sub-entries
if (!empty($newrdn)) {
$subdeldata = array_merge($subdeldata, $subdata);
$subnewdata = array_merge($subnewdata, $subdata);
}
// remove sub-entries
if (!empty($subdeldata)) {
foreach ($subdeldata as $fld => $val) {
- $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
- if (!$this->ldap_delete($subdn)) {
+ $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
+ if (!$this->ldap->delete($subdn)) {
return false;
}
}
}
if (!empty($newdata)) {
// Add the fields.
- if (!$this->ldap_mod_add($dn, $newdata)) {
+ if (!$this->ldap->mod_add($dn, $newdata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
} // end if
// Handle RDN change
if (!empty($newrdn)) {
- if (!$this->ldap_rename($dn, $newrdn, null, true)) {
+ if (!$this->ldap->rename($dn, $newrdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$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)
+ foreach ($group_ids as $group_id => $group_prop)
{
$this->remove_from_group($group_id, $dn);
$this->add_to_group($group_id, $newdn);
}
}
$dn = self::dn_decode($newdn);
}
// add sub-entries
if (!empty($subnewdata)) {
foreach ($subnewdata as $fld => $val) {
- $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
+ $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
$xf = array(
$fld => $val,
'objectClass' => (array) $this->prop['sub_fields'][$fld],
);
- $this->ldap_add($subdn, $xf);
+ $this->ldap->add($subdn, $xf);
}
}
return $newdn ? $newdn : 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);
// Need to delete all sub-entries first
if ($this->sub_filter) {
- if ($entries = $this->ldap_list($dn, $this->sub_filter)) {
+ if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {
foreach ($entries as $entry) {
- if (!$this->ldap_delete($entry['dn'])) {
+ if (!$this->ldap->delete($entry['dn'])) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
}
}
}
// Delete the record.
- if (!$this->ldap_delete($dn)) {
+ if (!$this->ldap->delete($dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
// 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) {
+ foreach ($group_ids as $group_id => $group_prop) {
$this->remove_from_group($group_id, $dn);
}
}
} // end foreach
return count($ids);
}
/**
* Remove all contact records
*/
function delete_all()
{
- //searching for contact entries
- $dn_list = $this->ldap_list($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
+ // searching for contact entries
+ $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
if (!empty($dn_list)) {
foreach ($dn_list as $idx => $entry) {
$dn_list[$idx] = self::dn_encode($entry['dn']);
}
$this->delete($dn_list);
}
}
/**
* Generate missing attributes as configured
*
* @param array LDAP record attributes
*/
protected function add_autovalues(&$attrs)
{
if (empty($this->prop['autovalues'])) {
return;
}
$attrvals = array();
foreach ($attrs as $k => $v) {
$attrvals['{'.$k.'}'] = is_array($v) ? $v[0] : $v;
}
foreach ((array)$this->prop['autovalues'] as $lf => $templ) {
if (empty($attrs[$lf])) {
if (strpos($templ, '(') !== false) {
// replace {attr} placeholders with (escaped!) attribute values to be safely eval'd
$code = preg_replace('/\{\w+\}/', '', strtr($templ, array_map('addslashes', $attrvals)));
$fn = create_function('', "return ($code);");
if (!$fn) {
rcube::raise_error(array(
'code' => 505, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Expression parse error on: ($code)"), true, false);
continue;
}
$attrs[$lf] = $fn();
}
else {
// replace {attr} placeholders with concrete attribute values
$attrs[$lf] = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals));
}
}
}
}
- /**
- * 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')) {
- if (ldap_parse_result($this->conn, $this->ldap_result,
- $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
- && $serverctrls // can be null e.g. in case of adm. limit error
- ) {
- 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");
- }
- else {
- $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
- }
- }
-
- $entries_count = ldap_count_entries($this->conn, $this->ldap_result);
- $this->_debug("S: $entries_count record(s)");
-
- return $count ? $entries_count : 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();
+ $out = array('_type' => 'person');
+ $fieldmap = $this->fieldmap;
if ($rec['dn'])
$out[$this->primary_key] = self::dn_encode($rec['dn']);
- foreach ($this->fieldmap as $rf => $lf)
+ // determine record type
+ if (self::is_group_entry($rec)) {
+ $out['_type'] = 'group';
+ $out['readonly'] = true;
+ $fieldmap['name'] = $this->group_data['name_attr'] ? $this->group_data['name_attr'] : $this->prop['groups']['name_attr'];
+ }
+
+ foreach ($fieldmap as $rf => $lf)
{
for ($i=0; $i < $rec[$lf]['count']; $i++) {
if (!($value = $rec[$lf][$i]))
continue;
list($col, $subtype) = explode(':', $rf);
$out['_raw_attrib'][$lf][$i] = $value;
if ($col == 'email' && $this->mail_domain && !strpos($value, '@'))
$out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
else if (in_array($col, array('street','zipcode','locality','country','region')))
$out['address'.($subtype?':':'').$subtype][$i][$col] = $value;
else if ($col == 'address' && strpos($value, '$') !== false) // address data is represented as string separated with $
list($out[$rf][$i]['street'], $out[$rf][$i]['locality'], $out[$rf][$i]['zipcode'], $out[$rf][$i]['country']) = explode('$', $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['_raw_attrib'][$lf] = $out[$rf][0];
}
}
return $out;
}
/**
* Return LDAP attribute(s) for the given field
*/
private function _map_field($field)
{
return (array)$this->coltypes[$field]['attributes'];
}
/**
* Convert a record data set into LDAP field attributes
*/
private function _map_data($save_cols)
{
// flatten composite fields first
foreach ($this->coltypes as $col => $colprop) {
if (is_array($colprop['childs']) && ($values = $this->get_col_values($col, $save_cols, false))) {
foreach ($values as $subtype => $childs) {
$subtype = $subtype ? ':'.$subtype : '';
foreach ($childs as $i => $child_values) {
foreach ((array)$child_values as $childcol => $value) {
$save_cols[$childcol.$subtype][$i] = $value;
}
}
}
}
// if addresses are to be saved as serialized string, do so
if (is_array($colprop['serialized'])) {
foreach ($colprop['serialized'] as $subtype => $delim) {
$key = $col.':'.$subtype;
- foreach ((array)$save_cols[$key] as $i => $val)
- $save_cols[$key][$i] = join($delim, array($val['street'], $val['locality'], $val['zipcode'], $val['country']));
+ foreach ((array)$save_cols[$key] as $i => $val) {
+ $values = array($val['street'], $val['locality'], $val['zipcode'], $val['country']);
+ $save_cols[$key][$i] = count(array_filter($values)) ? join($delim, $values) : null;
+ }
}
}
}
$ldap_data = array();
foreach ($this->fieldmap as $rf => $fld) {
$val = $save_cols[$rf];
// check for value in base field (eg.g email instead of email:foo)
list($col, $subtype) = explode(':', $rf);
if (!$val && !empty($save_cols[$col])) {
$val = $save_cols[$col];
unset($save_cols[$col]); // only use this value once
}
else if (!$val && !$subtype) { // extract values from subtype cols
$val = $this->get_col_values($col, $save_cols, true);
}
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($namev)
{
// list of known attribute aliases
static $aliases = array(
'gn' => 'givenname',
'rfc822mailbox' => 'email',
'userid' => 'uid',
'emailaddress' => 'email',
'pkcs9email' => 'email',
);
list($name, $limit) = explode(':', $namev, 2);
$suffix = $limit ? ':'.$limit : '';
return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
}
+ /**
+ * Determines whether the given LDAP entry is a group record
+ */
+ private static function is_group_entry($entry)
+ {
+ return array_intersect(
+ array('group', 'groupofnames', 'kolabgroupofnames', 'groupofuniquenames','kolabgroupofuniquenames','groupofurls'),
+ array_map('strtolower', (array)$entry['objectclass'])
+ );
+ }
/**
* Prints debug info to the log
*/
private function _debug($str)
{
if ($this->debug) {
rcube::write_log('ldap', $str);
}
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if LDAP commands should be logged
- * @access public
*/
function set_debug($dbg = true)
{
$this->debug = $dbg;
- }
-
- /**
- * 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);
+ if ($this->ldap) {
+ $this->ldap->set_debug($dbg);
+ }
}
/**
* 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();
-
+ if ($group_id) {
$this->group_id = $group_id;
- $this->group_data = $group_cache[$group_id];
+ $this->group_data = $this->get_group_entry($group_id);
}
- else
- {
+ 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
* @param int Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
if (!$this->groups)
return array();
// use cached list for searching
- $this->cache->expunge();
- if (!$search || ($group_cache = $this->cache->get('groups')) === null)
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
$group_cache = $this->_fetch_groups();
+ }
$groups = array();
if ($search) {
foreach ($group_cache as $group) {
if ($this->compare_search_value('name', $group['name'], $search, $mode)) {
$groups[] = $group;
}
}
}
else
$groups = $group_cache;
return array_values($groups);
}
/**
* Fetch groups from server
*/
private function _fetch_groups($vlv_page = 0)
{
+ // special case: list groups from 'group_filters' config
+ if (!empty($this->prop['group_filters'])) {
+ $groups = array();
+
+ // list regular groups configuration as special filter
+ if (!empty($this->prop['groups']['filter'])) {
+ $id = '__groups__';
+ $groups[$id] = array('ID' => $id, 'name' => rcube_label('groups'), 'virtual' => true) + $this->prop['groups'];
+ }
+
+ foreach ($this->prop['group_filters'] as $id => $prop) {
+ $groups[$id] = $prop + array('ID' => $id, 'name' => ucfirst($id), 'virtual' => true, 'base_dn' => $this->base_dn);
+ }
+
+ return $groups;
+ }
+
$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]");
+ $ldap = $this->ldap;
// 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);
+
+ $ldap = clone $this->ldap;
+ $ldap->set_config($this->prop['groups']);
+ $ldap->set_vlv_page($vlv_page+1, $page_size);
}
- $function = $this->_scope2func($this->prop['groups']['scope']);
- $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));
+ $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
+ $ldap_data = $ldap->search($base_dn, $filter, $this->prop['groups']['scope'], $attrs, $this->prop['groups']);
+ if ($ldap_data === false) {
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);
+ $group_count = $ldap_data->count();
+ foreach ($ldap_data as $entry) {
+ if (!$entry['dn']) // DN is mandatory
+ $entry['dn'] = $ldap_data->get_dn();
+
+ $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
+ $group_id = self::dn_encode($entry['dn']);
$groups[$group_id]['ID'] = $group_id;
- $groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
+ $groups[$group_id]['dn'] = $entry['dn'];
$groups[$group_id]['name'] = $group_name;
- $groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']);
+ $groups[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
// 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];
+ for ($j=0; $entry[$email_attr] && $j < $entry[$email_attr]['count']; $j++) {
+ if (strpos($entry[$email_attr][$j], '@') > 0)
+ $groups[$group_id]['email'][] = $entry[$email_attr][$j];
}
- $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]);
+ $group_sortnames[] = mb_strtolower($entry[$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)
- {
+ while ($this->prop['groups']['vlv'] && $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);
+ if ($this->cache) {
+ $this->cache->set('groups', $groups);
+ }
return $groups;
}
+ /**
+ * Fetch a group entry from LDAP and save in local cache
+ */
+ private function get_group_entry($group_id)
+ {
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
+ $group_cache = $this->_fetch_groups();
+ }
+
+ // add group record to cache if it isn't yet there
+ if (!isset($group_cache[$group_id])) {
+ $name_attr = $this->prop['groups']['name_attr'];
+ $dn = self::dn_decode($group_id);
+
+ if ($list = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr,$this->fieldmap['email']))) {
+ $entry = $list[0];
+ $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
+ $group_cache[$group_id]['ID'] = $group_id;
+ $group_cache[$group_id]['dn'] = $dn;
+ $group_cache[$group_id]['name'] = $group_name;
+ $group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
+ }
+ else {
+ $group_cache[$group_id] = false;
+ }
+
+ if ($this->cache) {
+ $this->cache->set('groups', $group_cache);
+ }
+ }
+
+ return $group_cache[$group_id];
+ }
+
/**
* 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];
+ $group_data = $this->get_group_entry($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);
+ $new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn;
+ $new_gid = self::dn_encode($new_dn);
$member_attr = $this->get_group_member_attr();
$name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
$new_entry = array(
'objectClass' => $this->prop['groups']['object_classes'],
$name_attr => $group_name,
$member_attr => '',
);
- if (!$this->ldap_add($new_dn, $new_entry)) {
+ if (!$this->ldap->add($new_dn, $new_entry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ $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)
+ if (!$this->cache || ($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";
+ $del_dn = $group_cache[$group_id]['dn'];
- if (!$this->ldap_delete($del_dn)) {
+ if (!$this->ldap->delete($del_dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ unset($group_cache[$group_id]);
+ $this->cache->set('groups', $group_cache);
+ }
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)
+ if (!$this->cache || ($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);
+ $old_dn = $group_cache[$group_id]['dn'];
+ $new_rdn = "cn=" . rcube_ldap_generic::quote_string($new_name, true);
+ $new_gid = self::dn_encode($new_rdn . ',' . $this->groups_base_dn);
- if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) {
+ if (!$this->ldap->rename($old_dn, $new_rdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ $this->cache->remove('groups');
+ }
return $new_name;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array|string 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)
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
$group_cache = $this->_fetch_groups();
+ }
if (!is_array($contact_ids))
$contact_ids = explode(',', $contact_ids);
- $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";
+ $group_dn = $group_cache[$group_id]['dn'];
$new_attrs = array();
foreach ($contact_ids as $id)
$new_attrs[$member_attr][] = self::dn_decode($id);
- if (!$this->ldap_mod_add($group_dn, $new_attrs)) {
+ if (!$this->ldap->mod_add($group_dn, $new_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ $this->cache->remove('groups');
+ }
return count($new_attrs[$member_attr]);
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array|string 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)
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
$group_cache = $this->_fetch_groups();
+ }
if (!is_array($contact_ids))
$contact_ids = explode(',', $contact_ids);
- $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();
+ $group_dn = $group_cache[$group_id]['dn'];
+ $del_attrs = array();
foreach ($contact_ids as $id)
$del_attrs[$member_attr][] = self::dn_decode($id);
- if (!$this->ldap_mod_del($group_dn, $del_attrs)) {
+ if (!$this->ldap->mod_del($group_dn, $del_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ $this->cache->remove('groups');
+ }
return count($del_attrs[$member_attr]);
}
/**
* 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'] ? $this->prop['groups']['name_attr'] : 'cn';
$member_attr = $this->get_group_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));
+ $ldap_data = $this->ldap->search($base_dn, $filter, 'sub', array('dn', $name_attr));
+ if ($res === false) {
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;
+ foreach ($ldap_data as $entry) {
+ if (!$entry['dn'])
+ $entry['dn'] = $ldap_data->get_dn();
+ $group_name = $entry[$name_attr][0];
+ $group_id = self::dn_encode($entry['dn']);
+ $groups[$group_id] = array('ID' => $group_id, 'name' => $group_name, 'dn' => $entry['dn']);
}
return $groups;
}
/**
* Detects group member attribute name
*/
private function get_group_member_attr($object_classes = array())
{
if (empty($object_classes)) {
$object_classes = $this->prop['groups']['object_classes'];
}
if (!empty($object_classes)) {
foreach ((array)$object_classes as $oc) {
switch (strtolower($oc)) {
case 'group':
case 'groupofnames':
case 'kolabgroupofnames':
$member_attr = 'member';
break;
case 'groupofuniquenames':
case 'kolabgroupofuniquenames':
$member_attr = 'uniqueMember';
break;
}
}
}
if (!empty($member_attr)) {
return $member_attr;
}
if (!empty($this->prop['groups']['member_attr'])) {
return $this->prop['groups']['member_attr'];
}
return 'member';
}
- /**
- * 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);
}
- /**
- * Wrapper for ldap_add()
- */
- protected function ldap_add($dn, $entry)
- {
- $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
-
- $res = ldap_add($this->conn, $dn, $entry);
- if ($res === false) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_delete()
- */
- protected function ldap_delete($dn)
- {
- $this->_debug("C: Delete [dn: $dn]");
-
- $res = ldap_delete($this->conn, $dn);
- if ($res === false) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_mod_replace()
- */
- protected function ldap_mod_replace($dn, $entry)
- {
- $this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true));
-
- if (!ldap_mod_replace($this->conn, $dn, $entry)) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_mod_add()
- */
- protected function ldap_mod_add($dn, $entry)
- {
- $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
-
- if (!ldap_mod_add($this->conn, $dn, $entry)) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_mod_del()
- */
- protected function ldap_mod_del($dn, $entry)
- {
- $this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true));
-
- if (!ldap_mod_del($this->conn, $dn, $entry)) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_rename()
- */
- protected function ldap_rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
- {
- $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
-
- if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_list()
- */
- protected function ldap_list($dn, $filter, $attrs = array(''))
- {
- $list = array();
- $this->_debug("C: List [dn: $dn] [{$filter}]");
-
- if ($result = ldap_list($this->conn, $dn, $filter, $attrs)) {
- $list = ldap_get_entries($this->conn, $result);
-
- if ($list === false) {
- $this->_debug("S: ".ldap_error($this->conn));
- return array();
- }
-
- $count = $list['count'];
- unset($list['count']);
-
- $this->_debug("S: $count record(s)");
- }
- else {
- $this->_debug("S: ".ldap_error($this->conn));
- }
-
- return $list;
- }
-
}
diff --git a/program/lib/Roundcube/rcube_ldap_generic.php b/program/lib/Roundcube/rcube_ldap_generic.php
new file mode 100644
index 000000000..88378dc22
--- /dev/null
+++ b/program/lib/Roundcube/rcube_ldap_generic.php
@@ -0,0 +1,1049 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | Roundcube/rcube_ldap_generic.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | Copyright (C) 2012-2013, Kolab Systems AG |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Provide basic functionality for accessing LDAP directories |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ | Aleksander Machniak <machniak@kolabsys.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/*
+ LDAP connection properties
+ --------------------------
+
+ $prop = array(
+ 'host' => '<ldap-server-address>',
+ // or
+ 'hosts' => array('directory.verisign.com'),
+ 'port' => 389,
+ 'use_tls' => true|false,
+ 'ldap_version' => 3, // using LDAPv3
+ 'auth_method' => '', // SASL authentication method (for proxy auth), e.g. DIGEST-MD5
+ 'attributes' => array('dn'), // List of attributes to read from the server
+ 'vlv' => false, // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
+ 'config_root_dn' => 'cn=config', // Root DN to read config (e.g. vlv indexes) from
+ '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.
+ 'network_timeout' => 10, // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
+ 'referrals' => true|false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
+ );
+*/
+
+/**
+ * Model class to access an LDAP directories
+ *
+ * @package Framework
+ * @subpackage LDAP
+ */
+class rcube_ldap_generic
+{
+ const UPDATE_MOD_ADD = 1;
+ const UPDATE_MOD_DELETE = 2;
+ const UPDATE_MOD_REPLACE = 4;
+ const UPDATE_MOD_FULL = 7;
+
+ public $conn;
+ public $vlv_active = false;
+
+ /** private properties */
+ protected $cache = null;
+ protected $config = array();
+ protected $attributes = array('dn');
+ protected $entries = null;
+ protected $result = null;
+ protected $debug = false;
+ protected $list_page = 1;
+ protected $page_size = 10;
+ protected $vlv_config = null;
+
+
+ /**
+ * Object constructor
+ *
+ * @param array $p LDAP connection properties
+ */
+ function __construct($p)
+ {
+ $this->config = $p;
+
+ if (is_array($p['attributes']))
+ $this->attributes = $p['attributes'];
+
+ if (!is_array($p['hosts']) && !empty($p['host']))
+ $this->config['hosts'] = array($p['host']);
+ }
+
+ /**
+ * Activate/deactivate debug mode
+ *
+ * @param boolean $dbg True if LDAP commands should be logged
+ */
+ public function set_debug($dbg = true)
+ {
+ $this->debug = $dbg;
+ }
+
+ /**
+ * Set connection options
+ *
+ * @param mixed $opt Option name as string or hash array with multiple options
+ * @param mixed $val Option value
+ */
+ public function set_config($opt, $val = null)
+ {
+ if (is_array($opt))
+ $this->config = array_merge($this->config, $opt);
+ else
+ $this->config[$opt] = $value;
+ }
+
+ /**
+ * Enable caching by passing an instance of rcube_cache to be used by this object
+ *
+ * @param object rcube_cache Instance or False to disable caching
+ */
+ public function set_cache($cache_engine)
+ {
+ $this->cache = $cache_engine;
+ }
+
+ /**
+ * Set properties for VLV-based paging
+ *
+ * @param number $page Page number to list (starting at 1)
+ * @param number $size Number of entries to display on one page
+ */
+ public function set_vlv_page($page, $size = 10)
+ {
+ $this->list_page = $page;
+ $this->page_size = $size;
+ }
+
+ /**
+ * Establish a connection to the LDAP server
+ */
+ public function connect($host = null)
+ {
+ if (!function_exists('ldap_connect')) {
+ rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "No ldap support in this installation of PHP"),
+ true);
+ return false;
+ }
+
+ if (is_resource($this->conn) && $this->config['host'] == $host)
+ return true;
+
+ if (empty($this->config['ldap_version']))
+ $this->config['ldap_version'] = 3;
+
+ // iterate over hosts if none specified
+ if (!$host) {
+ if (!is_array($this->config['hosts']))
+ $this->config['hosts'] = array($this->config['hosts']);
+
+ foreach ($this->config['hosts'] as $host) {
+ if ($this->connect($host)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // open connection to the given $host
+ $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
+ $hostname = $host . ($this->config['port'] ? ':'.$this->config['port'] : '');
+
+ $this->_debug("C: Connect to $hostname [{$this->config['name']}]");
+
+ if ($lc = @ldap_connect($host, $this->config['port'])) {
+ if ($this->config['use_tls'] === true)
+ if (!ldap_start_tls($lc))
+ continue;
+
+ $this->_debug("S: OK");
+
+ ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->config['ldap_version']);
+ $this->config['host'] = $host;
+ $this->conn = $lc;
+
+ if (!empty($this->config['network_timeout']))
+ ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->config['network_timeout']);
+
+ if (isset($this->config['referrals']))
+ ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->config['referrals']);
+ }
+ else {
+ $this->_debug("S: NOT OK");
+ }
+
+ if (!is_resource($this->conn)) {
+ rcube::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;
+ }
+
+ return true;
+ }
+
+ /**
+ * 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')) {
+ rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Unable to bind: ldap_sasl_bind() not exists"),
+ true);
+ return false;
+ }
+
+ if (!empty($authz)) {
+ $authz = 'u:' . $authz;
+ }
+
+ if (!empty($this->config['auth_method'])) {
+ $method = $this->config['auth_method'];
+ }
+ else {
+ $method = 'DIGEST-MD5';
+ }
+
+ $this->_debug("C: SASL 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));
+
+ rcube::raise_error(array(
+ 'code' => ldap_errno($this->conn), 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "SASL Bind failed for authcid=$authc ".ldap_error($this->conn)),
+ true);
+ return false;
+ }
+
+ /**
+ * Bind connection with DN and password
+ *
+ * @param string $dn Bind DN
+ * @param string $pass 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 [pass: $pass]");
+
+ if (@ldap_bind($this->conn, $dn, $pass)) {
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ $this->_debug("S: ".ldap_error($this->conn));
+
+ rcube::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
+ */
+ public function close()
+ {
+ if ($this->conn) {
+ $this->_debug("C: Close");
+ ldap_unbind($this->conn);
+ $this->conn = null;
+ }
+ }
+
+ /**
+ * Return the last result set
+ *
+ * @return object rcube_ldap_result Result object
+ */
+ function get_result()
+ {
+ return $this->result;
+ }
+
+ /**
+ * Get a specific LDAP entry, identified by its DN
+ *
+ * @param string $dn Record identifier
+ * @return array Hash array
+ */
+ function get_entry($dn)
+ {
+ $rec = null;
+
+ if ($this->conn && $dn) {
+ $this->_debug("C: Read $dn [(objectclass=*)]");
+
+ if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', $this->attributes)) {
+ $this->_debug("S: OK");
+
+ if ($entry = ldap_first_entry($this->conn, $ldap_result)) {
+ $rec = ldap_get_attributes($this->conn, $entry);
+ }
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ }
+
+ if (!empty($rec)) {
+ $rec['dn'] = $dn; // Add in the dn for the entry.
+ }
+ }
+
+ return $rec;
+ }
+
+ /**
+ * Execute the LDAP search based on the stored credentials
+ *
+ * @param string $base_dn The base DN to query
+ * @param string $filter The LDAP filter for search
+ * @param string $scope The LDAP scope (list|sub|base)
+ * @param array $attrs List of entry attributes to read
+ * @param array $prop Hash array with query configuration properties:
+ * - sort: array of sort attributes (has to be in sync with the VLV index)
+ * - search: search string used for VLV controls
+ * @param boolean $count_only Set to true if only entry count is requested
+ *
+ * @return mixed rcube_ldap_result object or number of entries (if count_only=true) or false on error
+ */
+ public function search($base_dn, $filter = '', $scope = 'sub', $attrs = array('dn'), $prop = array(), $count_only = false)
+ {
+ if (!$this->conn) {
+ return false;
+ }
+
+ if (empty($filter)) {
+ $filter = '(objectclass=*)';
+ }
+
+ $this->_debug("C: Search $base_dn for $filter");
+
+ $function = self::scope2func($scope, $ns_function);
+
+ // find available VLV index for this query
+ if (!$count_only && ($vlv_sort = $this->_find_vlv($base_dn, $filter, $scope, $prop['sort']))) {
+ // when using VLV, we get the total count by...
+ // ...either reading numSubOrdinates attribute
+ if (($sub_filter = $this->config['numsub_filter']) &&
+ ($result_count = @$ns_function($this->conn, $base_dn, $sub_filter, array('numSubOrdinates'), 0, 0, 0))
+ ) {
+ $counts = ldap_get_entries($this->conn, $result_count);
+ for ($vlv_count = $j = 0; $j < $counts['count']; $j++)
+ $vlv_count += $counts[$j]['numsubordinates'][0];
+ $this->_debug("D: total numsubordinates = " . $vlv_count);
+ }
+ // ...or by fetching all records dn and count them
+ else if (!function_exists('ldap_parse_virtuallist_control')) {
+ $vlv_count = $this->search($base_dn, $filter, $scope, array('dn'), $prop, true);
+ }
+
+ $this->vlv_active = $this->_vlv_set_controls($vlv_sort, $this->list_page, $this->page_size, $prop['search']);
+ }
+ else {
+ $this->vlv_active = false;
+ }
+
+ // only fetch dn for count (should keep the payload low)
+ if ($ldap_result = @$function($this->conn, $base_dn, $filter,
+ $attrs, 0, (int)$this->config['sizelimit'], (int)$this->config['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')) {
+ if (ldap_parse_result($this->conn, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) {
+ ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $vlv_count, $vresult);
+ $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$vlv_count");
+ }
+ else {
+ $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
+ }
+ }
+ else if ($this->debug) {
+ $this->_debug("S: ".ldap_count_entries($this->conn, $ldap_result)." record(s) found");
+ }
+
+ $this->result = new rcube_ldap_result($this->conn, $ldap_result, $base_dn, $filter, $vlv_count);
+
+ return $count_only ? $this->result->count() : $this->result;
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ }
+
+ return false;
+ }
+
+ /**
+ * Modify an LDAP entry on the server
+ *
+ * @param string $dn Entry DN
+ * @param array $params Hash array of entry attributes
+ * @param int $mode Update mode (UPDATE_MOD_ADD | UPDATE_MOD_DELETE | UPDATE_MOD_REPLACE)
+ */
+ public function modify($dn, $parms, $mode = 255)
+ {
+ // TODO: implement this
+
+ return false;
+ }
+
+ /**
+ * Wrapper for ldap_add()
+ *
+ * @see ldap_add()
+ */
+ public function add($dn, $entry)
+ {
+ $this->_debug("C: Add $dn: ".print_r($entry, true));
+
+ $res = ldap_add($this->conn, $dn, $entry);
+ if ($res === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_delete()
+ *
+ * @see ldap_delete()
+ */
+ public function delete($dn)
+ {
+ $this->_debug("C: Delete $dn");
+
+ $res = ldap_delete($this->conn, $dn);
+ if ($res === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_mod_replace()
+ *
+ * @see ldap_mod_replace()
+ */
+ public function mod_replace($dn, $entry)
+ {
+ $this->_debug("C: Replace $dn: ".print_r($entry, true));
+
+ if (!ldap_mod_replace($this->conn, $dn, $entry)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_mod_add()
+ *
+ * @see ldap_mod_add()
+ */
+ public function mod_add($dn, $entry)
+ {
+ $this->_debug("C: Add $dn: ".print_r($entry, true));
+
+ if (!ldap_mod_add($this->conn, $dn, $entry)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_mod_del()
+ *
+ * @see ldap_mod_del()
+ */
+ public function mod_del($dn, $entry)
+ {
+ $this->_debug("C: Delete $dn: ".print_r($entry, true));
+
+ if (!ldap_mod_del($this->conn, $dn, $entry)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_rename()
+ *
+ * @see ldap_rename()
+ */
+ public function rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
+ {
+ $this->_debug("C: Rename $dn to $newrdn");
+
+ if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_list() + ldap_get_entries()
+ *
+ * @see ldap_list()
+ * @see ldap_get_entries()
+ */
+ public function list_entries($dn, $filter, $attributes = array('dn'))
+ {
+ $list = array();
+ $this->_debug("C: List $dn [{$filter}]");
+
+ if ($result = ldap_list($this->conn, $dn, $filter, $attributes)) {
+ $list = ldap_get_entries($this->conn, $result);
+
+ if ($list === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return array();
+ }
+
+ $count = $list['count'];
+ unset($list['count']);
+
+ $this->_debug("S: $count record(s)");
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ }
+
+ return $list;
+ }
+
+ /**
+ * Wrapper for ldap_read() + ldap_get_entries()
+ *
+ * @see ldap_read()
+ * @see ldap_get_entries()
+ */
+ public function read_entries($dn, $filter, $attributes = null)
+ {
+ $this->_debug("C: Read $dn [{$filter}]");
+
+ if ($this->conn && $dn) {
+ if (!$attributes)
+ $attributes = $this->attributes;
+
+ $result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']);
+ if ($result === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return ldap_get_entries($this->conn, $result);
+ }
+
+ return false;
+ }
+
+ /**
+ * Choose the right PHP function according to scope property
+ *
+ * @param string $scope The LDAP scope (sub|base|list)
+ * @param string $ns_function Function to be used for numSubOrdinates queries
+ * @return string PHP function to be used to query directory
+ */
+ public static 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;
+ }
+
+ /**
+ * Convert the given scope integer value to a string representation
+ */
+ public static function scopeint2str($scope)
+ {
+ switch ($scope) {
+ case 2: return 'sub';
+ case 1: return 'one';
+ case 0: return 'base';
+ default: $this->_debug("Scope $scope is not a valid scope integer");
+ }
+
+ return '';
+ }
+
+ /**
+ * Escapes the given value according to RFC 2254 so that it can be safely used in LDAP filters.
+ *
+ * @param string $val Value to quote
+ * @return string The escaped value
+ */
+ public static function escape_value($val)
+ {
+ return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29',
+ '\\'=>'\5c', '/'=>'\2f'));
+ }
+
+ /**
+ * Escapes a DN value according to RFC 2253
+ *
+ * @param string $dn DN value o quote
+ * @return string The escaped value
+ */
+ public static function escape_dn($dn)
+ {
+ return strtr($str, array(','=>'\2c', '='=>'\3d', '+'=>'\2b',
+ '<'=>'\3c', '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c',
+ '"'=>'\22', '#'=>'\23'));
+ }
+
+ /**
+ * Normalize a LDAP result by converting entry attributes arrays into single values
+ *
+ * @param array $result LDAP result set fetched with ldap_get_entries()
+ * @return array Hash array with normalized entries, indexed by their DNs
+ */
+ public static function normalize_result($result)
+ {
+ if (!is_array($result)) {
+ return array();
+ }
+
+ $entries = array();
+ for ($i = 0; $i < $result['count']; $i++) {
+ $key = $result[$i]['dn'] ? $result[$i]['dn'] : $i;
+ $entries[$key] = self::normalize_entry($result[$i]);
+ }
+
+ return $entries;
+ }
+
+ /**
+ * Turn an LDAP entry into a regular PHP array with attributes as keys.
+ *
+ * @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
+ * @return array Hash array with attributes as keys
+ */
+ public static function normalize_entry($entry)
+ {
+ $rec = array();
+ for ($i=0; $i < $entry['count']; $i++) {
+ $attr = $entry[$i];
+ if ($entry[$attr]['count'] == 1) {
+ switch ($attr) {
+ case 'objectclass':
+ $rec[$attr] = array(strtolower($entry[$attr][0]));
+ break;
+ default:
+ $rec[$attr] = $entry[$attr][0];
+ break;
+ }
+ }
+ else {
+ for ($j=0; $j < $entry[$attr]['count']; $j++) {
+ $rec[$attr][$j] = $entry[$attr][$j];
+ }
+ }
+ }
+
+ return $rec;
+ }
+
+ /**
+ * Set server controls for Virtual List View (paginated listing)
+ */
+ private function _vlv_set_controls($sort, $list_page, $page_size, $search = null)
+ {
+ $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => self::_sort_ber_encode((array)$sort));
+ $vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => self::_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
+
+ $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; $search)");
+
+ 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;
+ }
+
+ /**
+ * Returns unified attribute name (resolving aliases)
+ */
+ private static function _attr_name($namev)
+ {
+ // list of known attribute aliases
+ static $aliases = array(
+ 'gn' => 'givenname',
+ 'rfc822mailbox' => 'email',
+ 'userid' => 'uid',
+ 'emailaddress' => 'email',
+ 'pkcs9email' => 'email',
+ );
+
+ list($name, $limit) = explode(':', $namev, 2);
+ $suffix = $limit ? ':'.$limit : '';
+
+ return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
+ }
+
+ /**
+ * Quotes attribute value string
+ *
+ * @param string $str Attribute value
+ * @param bool $dn True if the attribute is a DN
+ *
+ * @return string Quoted string
+ */
+ public 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);
+ }
+
+ /**
+ * Prints debug info to the log
+ */
+ private function _debug($str)
+ {
+ if ($this->debug && class_exists('rcube')) {
+ rcube::write_log('ldap', $str);
+ }
+ }
+
+
+ /***************** Virtual List View (VLV) related utility functions **************** */
+
+ /**
+ * Return the search string value to be used in VLV controls
+ */
+ private function _vlv_search($sort, $search)
+ {
+ foreach ($search as $attr => $value) {
+ if (!in_array(strtolower($attr), $sort)) {
+ $this->_debug("d: Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")");
+ return null;
+ } else {
+ return $value;
+ }
+ }
+ }
+
+ /**
+ * Find a VLV index matching the given query attributes
+ *
+ * @return string Sort attribute or False if no match
+ */
+ private function _find_vlv($base_dn, $filter, $scope, $sort_attrs = null)
+ {
+ if (!$this->config['vlv'] || $scope == 'base') {
+ return false;
+ }
+
+ // get vlv config
+ $vlv_config = $this->_read_vlv_config();
+
+ if ($vlv = $vlv_config[$base_dn]) {
+ $this->_debug("D: Found a VLV for $base_dn");
+
+ if ($vlv['filter'] == strtolower($filter) || stripos($filter, '(&'.$vlv['filter'].'(') === 0) {
+ $this->_debug("D: Filter matches");
+ if ($vlv['scope'] == $scope) {
+ // Not passing any sort attributes means you don't care
+ if (empty($sort_attrs) || in_array($sort_attrs, $vlv['sort'])) {
+ return $vlv['sort'][0];
+ }
+ }
+ else {
+ $this->_debug("D: Scope does not match");
+ }
+ }
+ else {
+ $this->_debug("D: Filter does not match");
+ }
+ }
+ else {
+ $this->_debug("D: No VLV for $base_dn");
+ }
+
+ return false;
+ }
+
+ /**
+ * Return VLV indexes and searches including necessary configuration
+ * details.
+ */
+ private function _read_vlv_config()
+ {
+ if (empty($this->config['vlv']) || empty($this->config['config_root_dn'])) {
+ return array();
+ }
+ // return hard-coded VLV config
+ else if (is_array($this->config['vlv'])) {
+ return $this->config['vlv'];
+ }
+
+ // return cached result
+ if (is_array($this->vlv_config)) {
+ return $this->vlv_config;
+ }
+
+ if ($this->cache && ($cached_config = $this->cache->get('vlvconfig'))) {
+ $this->vlv_config = $cached_config;
+ return $this->vlv_config;
+ }
+
+ $this->vlv_config = array();
+
+ $ldap_result = ldap_search($this->conn, $this->config['config_root_dn'], '(objectclass=vlvsearch)', array('*'), 0, 0, 0);
+ $vlv_searches = new rcube_ldap_result($this->conn, $ldap_result, $this->config['config_root_dn'], '(objectclass=vlvsearch)');
+
+ if ($vlv_searches->count() < 1) {
+ $this->_debug("D: Empty result from search for '(objectclass=vlvsearch)' on '$config_root_dn'");
+ return array();
+ }
+
+ foreach ($vlv_searches->entries(true) as $vlv_search_dn => $vlv_search_attrs) {
+ // Multiple indexes may exist
+ $ldap_result = ldap_search($this->conn, $vlv_search_dn, '(objectclass=vlvindex)', array('*'), 0, 0, 0);
+ $vlv_indexes = new rcube_ldap_result($this->conn, $ldap_result, $vlv_search_dn, '(objectclass=vlvindex)');
+
+ // Reset this one for each VLV search.
+ $_vlv_sort = array();
+ foreach ($vlv_indexes->entries(true) as $vlv_index_dn => $vlv_index_attrs) {
+ $_vlv_sort[] = explode(' ', $vlv_index_attrs['vlvsort']);
+ }
+
+ $this->vlv_config[$vlv_search_attrs['vlvbase']] = array(
+ 'scope' => self::scopeint2str($vlv_search_attrs['vlvscope']),
+ 'filter' => strtolower($vlv_search_attrs['vlvfilter']),
+ 'sort' => $_vlv_sort,
+ );
+ }
+
+ // cache this
+ if ($this->cache)
+ $this->cache->set('vlvconfig', $this->vlv_config);
+
+ $this->_debug("D: Refreshed VLV config: " . var_export($this->vlv_config, true));
+
+ return $this->vlv_config;
+ }
+
+ /**
+ * 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 static 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
+
+ with 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 static 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;
+ }
+
+}
diff --git a/program/lib/Roundcube/rcube_ldap_result.php b/program/lib/Roundcube/rcube_ldap_result.php
new file mode 100644
index 000000000..efc3331bc
--- /dev/null
+++ b/program/lib/Roundcube/rcube_ldap_result.php
@@ -0,0 +1,130 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | Roundcube/rcube_ldap_result.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | Copyright (C) 2013, Kolab Systems AG |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Model class that represents an LDAP search result |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/**
+ * Model class representing an LDAP search result
+ *
+ * @package Framework
+ * @subpackage LDAP
+ */
+class rcube_ldap_result implements Iterator
+{
+ public $conn;
+ public $ldap;
+ public $base_dn;
+ public $filter;
+
+ private $count = null;
+ private $current = null;
+ private $iteratorkey = 0;
+
+ /**
+ * Default constructor
+ *
+ * @param resource $conn LDAP link identifier
+ * @param resource $ldap LDAP result entry identifier
+ * @param string $base_dn Base DN used to get this result
+ * @param string $filter Filter query used to get this result
+ * @param integer $count Record count value (pre-calculated)
+ */
+ function __construct($conn, $ldap, $base_dn, $filter, $count = null)
+ {
+ $this->conn = $conn;
+ $this->ldap = $ldap;
+ $this->base_dn = $base_dn;
+ $this->filter = $filter;
+ $this->count = $count;
+ }
+
+ /**
+ * Wrapper for ldap_sort()
+ */
+ public function sort($attr)
+ {
+ return ldap_sort($this->conn, $this->ldap, $attr);
+ }
+
+ /**
+ * Get entries count
+ */
+ public function count()
+ {
+ if (!isset($this->count))
+ $this->count = ldap_count_entries($this->conn, $this->ldap);
+
+ return $this->count;
+ }
+
+ /**
+ * Wrapper for ldap_get_entries()
+ *
+ * @param boolean $normalize Optionally normalize the entries to a list of hash arrays
+ * @return array List of LDAP entries
+ */
+ public function entries($normalize = false)
+ {
+ $entries = ldap_get_entries($this->conn, $this->ldap);
+ return $normalize ? rcube_ldap_generic::normalize_result($entries) : $entries;
+ }
+
+ /**
+ * Wrapper for ldap_get_dn() using the current entry pointer
+ */
+ public function get_dn()
+ {
+ return $this->current ? ldap_get_dn($this->conn, $this->current) : null;
+ }
+
+
+ /*** Implements the PHP 5 Iterator interface to make foreach work ***/
+
+ function current()
+ {
+ $attrib = ldap_get_attributes($this->conn, $this->current);
+ $attrib['dn'] = ldap_get_dn($this->conn, $this->current);
+ return $attrib;
+ }
+
+ function key()
+ {
+ return $this->iteratorkey;
+ }
+
+ function rewind()
+ {
+ $this->iteratorkey = 0;
+ $this->current = ldap_first_entry($this->conn, $this->ldap);
+ }
+
+ function next()
+ {
+ $this->iteratorkey++;
+ $this->current = ldap_next_entry($this->conn, $this->current);
+ }
+
+ function valid()
+ {
+ return (bool)$this->current;
+ }
+
+}
diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php
index 90acb2110..a71305c4b 100644
--- a/program/lib/Roundcube/rcube_vcard.php
+++ b/program/lib/Roundcube/rcube_vcard.php
@@ -1,872 +1,878 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Logical representation of a vcard address record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Logical representation of a vcard-based address record
* Provides functions to parse and export vCard data format
*
* @package Framework
* @subpackage Addressbook
*/
class rcube_vcard
{
private static $values_decoded = false;
private $raw = array(
'FN' => array(),
'N' => array(array('','','','','')),
);
private static $fieldmap = array(
'phone' => 'TEL',
'birthday' => 'BDAY',
'website' => 'URL',
'notes' => 'NOTE',
'email' => 'EMAIL',
'address' => 'ADR',
'jobtitle' => 'TITLE',
'department' => 'X-DEPARTMENT',
'gender' => 'X-GENDER',
'maidenname' => 'X-MAIDENNAME',
'anniversary' => 'X-ANNIVERSARY',
'assistant' => 'X-ASSISTANT',
'manager' => 'X-MANAGER',
'spouse' => 'X-SPOUSE',
'edit' => 'X-AB-EDIT',
);
private $typemap = array(
'IPHONE' => 'mobile',
'CELL' => 'mobile',
'WORK,FAX' => 'workfax',
);
private $phonetypemap = array(
'HOME1' => 'HOME',
'BUSINESS1' => 'WORK',
'BUSINESS2' => 'WORK2',
'BUSINESSFAX' => 'WORK,FAX',
'MOBILE' => 'CELL',
);
private $addresstypemap = array(
'BUSINESS' => 'WORK',
);
private $immap = array(
'X-JABBER' => 'jabber',
'X-ICQ' => 'icq',
'X-MSN' => 'msn',
'X-AIM' => 'aim',
'X-YAHOO' => 'yahoo',
'X-SKYPE' => 'skype',
'X-SKYPE-USERNAME' => 'skype',
);
public $business = false;
public $displayname;
public $surname;
public $firstname;
public $middlename;
public $nickname;
public $organization;
public $email = array();
public static $eol = "\r\n";
/**
* Constructor
*/
public function __construct($vcard = null, $charset = RCUBE_CHARSET, $detect = false, $fieldmap = array())
{
if (!empty($fieldmap)) {
$this->extend_fieldmap($fieldmap);
}
if (!empty($vcard)) {
$this->load($vcard, $charset, $detect);
}
}
/**
* Load record from (internal, unfolded) vcard 3.0 format
*
* @param string vCard string to parse
* @param string Charset of string values
* @param boolean True if loading a 'foreign' vcard and extra heuristics for charset detection is required
*/
public function load($vcard, $charset = RCUBE_CHARSET, $detect = false)
{
self::$values_decoded = false;
$this->raw = self::vcard_decode($vcard);
// resolve charset parameters
if ($charset == null) {
$this->raw = self::charset_convert($this->raw);
}
// vcard has encoded values and charset should be detected
else if ($detect && self::$values_decoded
&& ($detected_charset = self::detect_encoding(self::vcard_encode($this->raw)))
&& $detected_charset != RCUBE_CHARSET
) {
$this->raw = self::charset_convert($this->raw, $detected_charset);
}
// consider FN empty if the same as the primary e-mail address
if ($this->raw['FN'][0][0] == $this->raw['EMAIL'][0][0]) {
$this->raw['FN'][0][0] = '';
}
// find well-known address fields
$this->displayname = $this->raw['FN'][0][0];
$this->surname = $this->raw['N'][0][0];
$this->firstname = $this->raw['N'][0][1];
$this->middlename = $this->raw['N'][0][2];
$this->nickname = $this->raw['NICKNAME'][0][0];
$this->organization = $this->raw['ORG'][0][0];
$this->business = ($this->raw['X-ABSHOWAS'][0][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization));
foreach ((array)$this->raw['EMAIL'] as $i => $raw_email) {
$this->email[$i] = is_array($raw_email) ? $raw_email[0] : $raw_email;
}
// make the pref e-mail address the first entry in $this->email
$pref_index = $this->get_type_index('EMAIL', 'pref');
if ($pref_index > 0) {
$tmp = $this->email[0];
$this->email[0] = $this->email[$pref_index];
$this->email[$pref_index] = $tmp;
}
}
/**
* Return vCard data as associative array to be unsed in Roundcube address books
*
* @return array Hash array with key-value pairs
*/
public function get_assoc()
{
$out = array('name' => $this->displayname);
$typemap = $this->typemap;
// copy name fields to output array
foreach (array('firstname','surname','middlename','nickname','organization') as $col) {
if (strlen($this->$col)) {
$out[$col] = $this->$col;
}
}
if ($this->raw['N'][0][3])
$out['prefix'] = $this->raw['N'][0][3];
if ($this->raw['N'][0][4])
$out['suffix'] = $this->raw['N'][0][4];
// convert from raw vcard data into associative data for Roundcube
foreach (array_flip(self::$fieldmap) as $tag => $col) {
foreach ((array)$this->raw[$tag] as $i => $raw) {
if (is_array($raw)) {
$k = -1;
$key = $col;
$subtype = '';
if (!empty($raw['type'])) {
$combined = join(',', self::array_filter((array)$raw['type'], 'internet,pref', true));
$combined = strtoupper($combined);
if ($typemap[$combined]) {
$subtype = $typemap[$combined];
}
else if ($typemap[$raw['type'][++$k]]) {
$subtype = $typemap[$raw['type'][$k]];
}
else {
$subtype = strtolower($raw['type'][$k]);
}
while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref')) {
$subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]);
}
}
// read vcard 2.1 subtype
if (!$subtype) {
foreach ($raw as $k => $v) {
if (!is_numeric($k) && $v === true && ($k = strtolower($k))
&& !in_array($k, array('pref','internet','voice','base64'))
) {
$k_uc = strtoupper($k);
$subtype = $typemap[$k_uc] ? $typemap[$k_uc] : $k;
break;
}
}
}
// force subtype if none set
if (!$subtype && preg_match('/^(email|phone|address|website)/', $key)) {
$subtype = 'other';
}
if ($subtype) {
$key .= ':' . $subtype;
}
// split ADR values into assoc array
if ($tag == 'ADR') {
list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw;
$out[$key][] = $value;
}
else {
$out[$key][] = $raw[0];
}
}
else {
$out[$col][] = $raw;
}
}
}
// handle special IM fields as used by Apple
foreach ($this->immap as $tag => $type) {
foreach ((array)$this->raw[$tag] as $i => $raw) {
$out['im:'.$type][] = $raw[0];
}
}
// copy photo data
if ($this->raw['PHOTO']) {
$out['photo'] = $this->raw['PHOTO'][0][0];
}
return $out;
}
/**
* Convert the data structure into a vcard 3.0 string
*/
public function export($folded = true)
{
$vcard = self::vcard_encode($this->raw);
return $folded ? self::rfc2425_fold($vcard) : $vcard;
}
/**
* Clear the given fields in the loaded vcard data
*
* @param array List of field names to be reset
*/
public function reset($fields = null)
{
if (!$fields) {
$fields = array_merge(array_values(self::$fieldmap), array_keys($this->immap),
array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY'));
}
foreach ($fields as $f) {
unset($this->raw[$f]);
}
if (!$this->raw['N']) {
$this->raw['N'] = array(array('','','','',''));
}
if (!$this->raw['FN']) {
$this->raw['FN'] = array();
}
$this->email = array();
}
/**
* Setter for address record fields
*
* @param string Field name
* @param string Field value
* @param string Type/section name
*/
public function set($field, $value, $type = 'HOME')
{
$field = strtolower($field);
$type_uc = strtoupper($type);
switch ($field) {
case 'name':
case 'displayname':
$this->raw['FN'][0][0] = $this->displayname = $value;
break;
case 'surname':
$this->raw['N'][0][0] = $this->surname = $value;
break;
case 'firstname':
$this->raw['N'][0][1] = $this->firstname = $value;
break;
case 'middlename':
$this->raw['N'][0][2] = $this->middlename = $value;
break;
case 'prefix':
$this->raw['N'][0][3] = $value;
break;
case 'suffix':
$this->raw['N'][0][4] = $value;
break;
case 'nickname':
$this->raw['NICKNAME'][0][0] = $this->nickname = $value;
break;
case 'organization':
$this->raw['ORG'][0][0] = $this->organization = $value;
break;
case 'photo':
if (strpos($value, 'http:') === 0) {
// TODO: fetch file from URL and save it locally?
$this->raw['PHOTO'][0] = array(0 => $value, 'url' => true);
}
else {
$this->raw['PHOTO'][0] = array(0 => $value, 'base64' => (bool) preg_match('![^a-z0-9/=+-]!i', $value));
}
break;
case 'email':
$this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type_uc)));
$this->email[] = $value;
break;
case 'im':
// save IM subtypes into extension fields
$typemap = array_flip($this->immap);
if ($field = $typemap[strtolower($type)]) {
$this->raw[$field][] = array(0 => $value);
}
break;
case 'birthday':
case 'anniversary':
if (($val = rcube_utils::strtotime($value)) && ($fn = self::$fieldmap[$field])) {
$this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));
}
break;
case 'address':
if ($this->addresstypemap[$type_uc]) {
$type = $this->addresstypemap[$type_uc];
}
$value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']);
// fall through if not empty
if (!strlen(join('', $value))) {
break;
}
default:
if ($field == 'phone' && $this->phonetypemap[$type_uc]) {
$type = $this->phonetypemap[$type_uc];
}
if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) {
$index = count($this->raw[$tag]);
$this->raw[$tag][$index] = (array)$value;
if ($type) {
$typemap = array_flip($this->typemap);
$this->raw[$tag][$index]['type'] = explode(',', ($typemap[$type_uc] ? $typemap[$type_uc] : $type));
}
}
break;
}
}
/**
* Setter for individual vcard properties
*
* @param string VCard tag name
* @param array Value-set of this vcard property
* @param boolean Set to true if the value-set should be appended instead of replacing any existing value-set
*/
public function set_raw($tag, $value, $append = false)
{
$index = $append ? count($this->raw[$tag]) : 0;
$this->raw[$tag][$index] = (array)$value;
}
/**
* Find index with the '$type' attribute
*
* @param string Field name
* @return int Field index having $type set
*/
private function get_type_index($field, $type = 'pref')
{
$result = 0;
if ($this->raw[$field]) {
foreach ($this->raw[$field] as $i => $data) {
if (is_array($data['type']) && in_array_nocase('pref', $data['type'])) {
$result = $i;
}
}
}
return $result;
}
/**
* Convert a whole vcard (array) to UTF-8.
* If $force_charset is null, each member value that has a charset parameter will be converted
*/
private static function charset_convert($card, $force_charset = null)
{
foreach ($card as $key => $node) {
foreach ($node as $i => $subnode) {
if (is_array($subnode) && (($charset = $force_charset) || ($subnode['charset'] && ($charset = $subnode['charset'][0])))) {
foreach ($subnode as $j => $value) {
if (is_numeric($j) && is_string($value)) {
$card[$key][$i][$j] = rcube_charset::convert($value, $charset);
}
}
unset($card[$key][$i]['charset']);
}
}
}
return $card;
}
/**
* Extends fieldmap definition
*/
public function extend_fieldmap($map)
{
if (is_array($map)) {
self::$fieldmap = array_merge($map, self::$fieldmap);
}
}
/**
* Factory method to import a vcard file
*
* @param string vCard file content
*
* @return array List of rcube_vcard objects
*/
public static function import($data)
{
$out = array();
// check if charsets are specified (usually vcard version < 3.0 but this is not reliable)
if (preg_match('/charset=/i', substr($data, 0, 2048))) {
$charset = null;
}
// detect charset and convert to utf-8
else if (($charset = self::detect_encoding($data)) && $charset != RCUBE_CHARSET) {
$data = rcube_charset::convert($data, $charset);
$data = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $data); // also remove BOM
$charset = RCUBE_CHARSET;
}
$vcard_block = '';
$in_vcard_block = false;
foreach (preg_split("/[\r\n]+/", $data) as $line) {
if ($in_vcard_block && !empty($line)) {
$vcard_block .= $line . "\n";
}
$line = trim($line);
if (preg_match('/^END:VCARD$/i', $line)) {
// parse vcard
$obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true, self::$fieldmap);
// FN and N is required by vCard format (RFC 2426)
// on import we can be less restrictive, let's addressbook decide
if (!empty($obj->displayname) || !empty($obj->surname) || !empty($obj->firstname) || !empty($obj->email)) {
$out[] = $obj;
}
$in_vcard_block = false;
}
else if (preg_match('/^BEGIN:VCARD$/i', $line)) {
$vcard_block = $line . "\n";
$in_vcard_block = true;
}
}
return $out;
}
/**
* Normalize vcard data for better parsing
*
* @param string vCard block
*
* @return string Cleaned vcard block
*/
public static function cleanup($vcard)
{
// Convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
$vcard = preg_replace(
'/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
'\2;type=\5\3:\4',
$vcard);
// convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility
$vcard = preg_replace_callback(
'/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
array('self', 'x_abrelatednames_callback'),
$vcard);
// Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines
$vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard);
// convert X-WAB-GENDER to X-GENDER
if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) {
$value = $matches[1] == '2' ? 'male' : 'female';
$vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard);
}
// if N doesn't have any semicolons, add some
$vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard);
return $vcard;
}
private static function x_abrelatednames_callback($matches)
{
return 'X-' . strtoupper($matches[5]) . $matches[3] . ':'. $matches[4];
}
private static function rfc2425_fold_callback($matches)
{
// chunk_split string and avoid lines breaking multibyte characters
$c = 71;
$out .= substr($matches[1], 0, $c);
for ($n = $c; $c < strlen($matches[1]); $c++) {
// break if length > 75 or mutlibyte character starts after position 71
if ($n > 75 || ($n > 71 && ord($matches[1][$c]) >> 6 == 3)) {
$out .= "\r\n ";
$n = 0;
}
$out .= $matches[1][$c];
$n++;
}
return $out;
}
public static function rfc2425_fold($val)
{
return preg_replace_callback('/([^\n]{72,})/', array('self', 'rfc2425_fold_callback'), $val);
}
/**
* Decodes a vcard block (vcard 3.0 format, unfolded)
* into an array structure
*
* @param string vCard block to parse
*
* @return array Raw data structure
*/
private static function vcard_decode($vcard)
{
// Perform RFC2425 line unfolding and split lines
$vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard);
$lines = explode("\n", $vcard);
$data = array();
for ($i=0; $i < count($lines); $i++) {
if (!preg_match('/^([^:]+):(.+)$/', $lines[$i], $line))
continue;
if (preg_match('/^(BEGIN|END)$/i', $line[1]))
continue;
// convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:"
if ($data['VERSION'][0] == "2.1"
&& preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2)
&& !preg_match('/^TYPE=/i', $regs2[2])
) {
$line[1] = $regs2[1];
foreach (explode(';', $regs2[2]) as $prop) {
$line[1] .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop);
}
}
if (preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) {
$entry = array();
$field = strtoupper($regs2[1][0]);
$enc = null;
foreach($regs2[1] as $attrid => $attr) {
if ((list($key, $value) = explode('=', $attr)) && $value) {
$value = trim($value);
if ($key == 'ENCODING') {
$value = strtoupper($value);
// add next line(s) to value string if QP line end detected
if ($value == 'QUOTED-PRINTABLE') {
while (preg_match('/=$/', $lines[$i])) {
$line[2] .= "\n" . $lines[++$i];
}
}
$enc = $value;
}
else {
$lc_key = strtolower($key);
$entry[$lc_key] = array_merge((array)$entry[$lc_key], (array)self::vcard_unquote($value, ','));
}
}
else if ($attrid > 0) {
$entry[strtolower($key)] = true; // true means attr without =value
}
}
// decode value
if ($enc || !empty($entry['base64'])) {
// save encoding type (#1488432)
if ($enc == 'B') {
$entry['encoding'] = 'B';
// should we use vCard 3.0 instead?
// $entry['base64'] = true;
}
$line[2] = self::decode_value($line[2], $enc ? $enc : 'base64');
}
if ($enc != 'B' && empty($entry['base64'])) {
$line[2] = self::vcard_unquote($line[2]);
}
$entry = array_merge($entry, (array) $line[2]);
$data[$field][] = $entry;
}
}
unset($data['VERSION']);
return $data;
}
/**
* Decode a given string with the encoding rule from ENCODING attributes
*
* @param string String to decode
* @param string Encoding type (quoted-printable and base64 supported)
*
* @return string Decoded 8bit value
*/
private static function decode_value($value, $encoding)
{
switch (strtolower($encoding)) {
case 'quoted-printable':
self::$values_decoded = true;
return quoted_printable_decode($value);
case 'base64':
case 'b':
self::$values_decoded = true;
return base64_decode($value);
default:
return $value;
}
}
/**
* Encodes an entry for storage in our database (vcard 3.0 format, unfolded)
*
* @param array Raw data structure to encode
*
* @return string vCard encoded string
*/
static function vcard_encode($data)
{
foreach ((array)$data as $type => $entries) {
// valid N has 5 properties
while ($type == "N" && is_array($entries[0]) && count($entries[0]) < 5) {
$entries[0][] = "";
}
// make sure FN is not empty (required by RFC2426)
if ($type == "FN" && empty($entries)) {
$entries[0] = $data['EMAIL'][0][0];
}
foreach ((array)$entries as $entry) {
$attr = '';
if (is_array($entry)) {
$value = array();
foreach ($entry as $attrname => $attrvalues) {
if (is_int($attrname)) {
if (!empty($entry['base64']) || $entry['encoding'] == 'B') {
$attrvalues = base64_encode($attrvalues);
}
$value[] = $attrvalues;
}
else if (is_bool($attrvalues)) {
- // true means just tag, not tag=value, as in PHOTO;BASE64:...
+ // true means just a tag, not tag=value, as in PHOTO;BASE64:...
if ($attrvalues) {
- $attr .= strtoupper(";$attrname");
+ // vCard v3 uses ENCODING=B (#1489183)
+ if ($attrname == 'base64') {
+ $attr .= ";ENCODING=B";
+ }
+ else {
+ $attr .= strtoupper(";$attrname");
+ }
}
}
else {
foreach((array)$attrvalues as $attrvalue) {
$attr .= strtoupper(";$attrname=") . self::vcard_quote($attrvalue, ',');
}
}
}
}
else {
$value = $entry;
}
// skip empty entries
if (self::is_empty($value)) {
continue;
}
$vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . self::$eol;
}
}
return 'BEGIN:VCARD' . self::$eol . 'VERSION:3.0' . self::$eol . $vcard . 'END:VCARD';
}
/**
* Join indexed data array to a vcard quoted string
*
* @param array Field data
* @param string Separator
*
* @return string Joined and quoted string
*/
private static function vcard_quote($s, $sep = ';')
{
if (is_array($s)) {
foreach($s as $part) {
$r[] = self::vcard_quote($part, $sep);
}
return(implode($sep, (array)$r));
}
return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;'));
}
/**
* Split quoted string
*
* @param string vCard string to split
* @param string Separator char/string
*
* @return array List with splited values
*/
private static function vcard_unquote($s, $sep = ';')
{
// break string into parts separated by $sep
if (!empty($sep)) {
// Handle properly backslash escaping (#1488896)
$rep1 = array("\\\\" => "\010", "\\$sep" => "\007");
$rep2 = array("\007" => "\\$sep", "\010" => "\\\\");
if (count($parts = explode($sep, strtr($s, $rep1))) > 1) {
foreach ($parts as $s) {
$result[] = self::vcard_unquote(strtr($s, $rep2));
}
return $result;
}
$s = strtr($s, $rep2);
}
// some implementations (GMail) use non-standard backslash before colon (#1489085)
// we will handle properly any backslashed character - removing dummy backslahes
// return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';'));
$s = str_replace("\r", '', $s);
$pos = 0;
while (($pos = strpos($s, '\\', $pos)) !== false) {
$next = substr($s, $pos + 1, 1);
if ($next == 'n' || $next == 'N') {
$s = substr_replace($s, "\n", $pos, 2);
}
else {
$s = substr_replace($s, '', $pos, 1);
}
$pos += 1;
}
return $s;
}
/**
* Check if vCard entry is empty: empty string or an array with
* all entries empty.
*
* @param mixed $value Attribute value (string or array)
*
* @return bool True if the value is empty, False otherwise
*/
private static function is_empty($value)
{
foreach ((array)$value as $v) {
if (((string)$v) !== '') {
return false;
}
}
return true;
}
/**
* Extract array values by a filter
*
* @param array Array to filter
* @param keys Array or comma separated list of values to keep
* @param boolean Invert key selection: remove the listed values
*
* @return array The filtered array
*/
private static function array_filter($arr, $values, $inverse = false)
{
if (!is_array($values)) {
$values = explode(',', $values);
}
$result = array();
$keep = array_flip((array)$values);
foreach ($arr as $key => $val) {
if ($inverse != isset($keep[strtolower($val)])) {
$result[$key] = $val;
}
}
return $result;
}
/**
* Returns UNICODE type based on BOM (Byte Order Mark)
*
* @param string Input string to test
*
* @return string Detected encoding
*/
private static function detect_encoding($string)
{
$fallback = rcube::get_instance()->config->get('default_charset', 'ISO-8859-1'); // fallback to Latin-1
return rcube_charset::detect($string, $fallback);
}
}
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index c5e6cae4c..046f2f488 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -1,541 +1,542 @@
<?php
/*
+-----------------------------------------------------------------------+
| localization/<lang>/labels.inc |
| |
| Localization file of the Roundcube Webmail client |
| Copyright (C) 2005-2013, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/labels/
*/
$labels = array();
// login page
$labels['welcome'] = 'Welcome to $product';
$labels['username'] = 'Username';
$labels['password'] = 'Password';
$labels['server'] = 'Server';
$labels['login'] = 'Login';
// taskbar
$labels['logout'] = 'Logout';
$labels['mail'] = 'Mail';
$labels['settings'] = 'Settings';
$labels['addressbook'] = 'Address Book';
// mailbox names
$labels['inbox'] = 'Inbox';
$labels['drafts'] = 'Drafts';
$labels['sent'] = 'Sent';
$labels['trash'] = 'Trash';
$labels['junk'] = 'Junk';
$labels['show_real_foldernames'] = 'Show real names for special folders';
// message listing
$labels['subject'] = 'Subject';
$labels['from'] = 'From';
$labels['sender'] = 'Sender';
$labels['to'] = 'To';
$labels['cc'] = 'Cc';
$labels['bcc'] = 'Bcc';
$labels['replyto'] = 'Reply-To';
$labels['followupto'] = 'Followup-To';
$labels['date'] = 'Date';
$labels['size'] = 'Size';
$labels['priority'] = 'Priority';
$labels['organization'] = 'Organization';
$labels['readstatus'] = 'Read status';
$labels['listoptions'] = 'List options...';
$labels['mailboxlist'] = 'Folders';
$labels['messagesfromto'] = 'Messages $from to $to of $count';
$labels['threadsfromto'] = 'Threads $from to $to of $count';
$labels['messagenrof'] = 'Message $nr of $count';
$labels['fromtoshort'] = '$from – $to of $count';
$labels['copy'] = 'Copy';
$labels['move'] = 'Move';
$labels['moveto'] = 'Move to...';
$labels['download'] = 'Download';
$labels['open'] = 'Open';
$labels['showattachment'] = 'Show';
$labels['showanyway'] = 'Show it anyway';
$labels['filename'] = 'File name';
$labels['filesize'] = 'File size';
$labels['addtoaddressbook'] = 'Add to address book';
// weekdays short
$labels['sun'] = 'Sun';
$labels['mon'] = 'Mon';
$labels['tue'] = 'Tue';
$labels['wed'] = 'Wed';
$labels['thu'] = 'Thu';
$labels['fri'] = 'Fri';
$labels['sat'] = 'Sat';
// weekdays long
$labels['sunday'] = 'Sunday';
$labels['monday'] = 'Monday';
$labels['tuesday'] = 'Tuesday';
$labels['wednesday'] = 'Wednesday';
$labels['thursday'] = 'Thursday';
$labels['friday'] = 'Friday';
$labels['saturday'] = 'Saturday';
// months short
$labels['jan'] = 'Jan';
$labels['feb'] = 'Feb';
$labels['mar'] = 'Mar';
$labels['apr'] = 'Apr';
$labels['may'] = 'May';
$labels['jun'] = 'Jun';
$labels['jul'] = 'Jul';
$labels['aug'] = 'Aug';
$labels['sep'] = 'Sep';
$labels['oct'] = 'Oct';
$labels['nov'] = 'Nov';
$labels['dec'] = 'Dec';
// months long
$labels['longjan'] = 'January';
$labels['longfeb'] = 'February';
$labels['longmar'] = 'March';
$labels['longapr'] = 'April';
$labels['longmay'] = 'May';
$labels['longjun'] = 'June';
$labels['longjul'] = 'July';
$labels['longaug'] = 'August';
$labels['longsep'] = 'September';
$labels['longoct'] = 'October';
$labels['longnov'] = 'November';
$labels['longdec'] = 'December';
$labels['today'] = 'Today';
// toolbar buttons
$labels['refresh'] = 'Refresh';
$labels['checkmail'] = 'Check for new messages';
$labels['compose'] = 'Compose';
$labels['writenewmessage'] = 'Create a new message';
$labels['reply'] = 'Reply';
$labels['replytomessage'] = 'Reply to sender';
$labels['replytoallmessage'] = 'Reply to list or to sender and all recipients';
$labels['replyall'] = 'Reply all';
$labels['replylist'] = 'Reply list';
$labels['forward'] = 'Forward';
$labels['forwardinline'] = 'Forward inline';
$labels['forwardattachment'] = 'Forward as attachment';
$labels['forwardmessage'] = 'Forward the message';
$labels['deletemessage'] = 'Delete message';
$labels['movemessagetotrash'] = 'Move message to trash';
$labels['printmessage'] = 'Print this message';
$labels['previousmessage'] = 'Show previous message';
$labels['firstmessage'] = 'Show first message';
$labels['nextmessage'] = 'Show next message';
$labels['lastmessage'] = 'Show last message';
$labels['backtolist'] = 'Back to message list';
$labels['viewsource'] = 'Show source';
$labels['mark'] = 'Mark';
$labels['markmessages'] = 'Mark messages';
$labels['markread'] = 'As read';
$labels['markunread'] = 'As unread';
$labels['markflagged'] = 'As flagged';
$labels['markunflagged'] = 'As unflagged';
$labels['moreactions'] = 'More actions...';
$labels['more'] = 'More';
$labels['back'] = 'Back';
$labels['options'] = 'Options';
$labels['select'] = 'Select';
$labels['all'] = 'All';
$labels['none'] = 'None';
$labels['currpage'] = 'Current page';
$labels['unread'] = 'Unread';
$labels['flagged'] = 'Flagged';
$labels['unanswered'] = 'Unanswered';
$labels['withattachment'] = 'With attachment';
$labels['deleted'] = 'Deleted';
$labels['undeleted'] = 'Not deleted';
$labels['invert'] = 'Invert';
$labels['filter'] = 'Filter';
$labels['list'] = 'List';
$labels['threads'] = 'Threads';
$labels['expand-all'] = 'Expand All';
$labels['expand-unread'] = 'Expand Unread';
$labels['collapse-all'] = 'Collapse All';
$labels['threaded'] = 'Threaded';
$labels['autoexpand_threads'] = 'Expand message threads';
$labels['do_expand'] = 'all threads';
$labels['expand_only_unread'] = 'only with unread messages';
$labels['fromto'] = 'From/To';
$labels['flag'] = 'Flag';
$labels['attachment'] = 'Attachment';
$labels['nonesort'] = 'None';
$labels['sentdate'] = 'Sent date';
$labels['arrival'] = 'Arrival date';
$labels['asc'] = 'ascending';
$labels['desc'] = 'descending';
$labels['listcolumns'] = 'List columns';
$labels['listsorting'] = 'Sorting column';
$labels['listorder'] = 'Sorting order';
$labels['listmode'] = 'List view mode';
$labels['folderactions'] = 'Folder actions...';
$labels['compact'] = 'Compact';
$labels['empty'] = 'Empty';
$labels['importmessages'] = 'Import messages';
$labels['quota'] = 'Disk usage';
$labels['unknown'] = 'unknown';
$labels['unlimited'] = 'unlimited';
$labels['quicksearch'] = 'Quick search';
$labels['resetsearch'] = 'Reset search';
$labels['searchmod'] = 'Search modifiers';
$labels['msgtext'] = 'Entire message';
$labels['body'] = 'Body';
$labels['openinextwin'] = 'Open in new window';
$labels['emlsave'] = 'Download (.eml)';
$labels['changeformattext'] = 'Display in plain text format';
$labels['changeformathtml'] = 'Display in HTML format';
// message compose
$labels['editasnew'] = 'Edit as new';
$labels['send'] = 'Send';
$labels['sendmessage'] = 'Send message';
$labels['savemessage'] = 'Save as draft';
$labels['addattachment'] = 'Attach a file';
$labels['charset'] = 'Charset';
$labels['editortype'] = 'Editor type';
$labels['returnreceipt'] = 'Return receipt';
$labels['dsn'] = 'Delivery status notification';
$labels['mailreplyintro'] = 'On $date, $sender wrote:';
$labels['originalmessage'] = 'Original Message';
$labels['editidents'] = 'Edit identities';
$labels['spellcheck'] = 'Spell';
$labels['checkspelling'] = 'Check spelling';
$labels['resumeediting'] = 'Resume editing';
$labels['revertto'] = 'Revert to';
$labels['attach'] = 'Attach';
$labels['attachments'] = 'Attachments';
$labels['upload'] = 'Upload';
$labels['uploadprogress'] = '$percent ($current from $total)';
$labels['close'] = 'Close';
$labels['messageoptions'] = 'Message options...';
$labels['low'] = 'Low';
$labels['lowest'] = 'Lowest';
$labels['normal'] = 'Normal';
$labels['high'] = 'High';
$labels['highest'] = 'Highest';
$labels['nosubject'] = '(no subject)';
$labels['showimages'] = 'Display images';
$labels['alwaysshow'] = 'Always show images from $sender';
$labels['isdraft'] = 'This is a draft message.';
$labels['andnmore'] = '$nr more...';
$labels['togglemoreheaders'] = 'Show more message headers';
$labels['togglefullheaders'] = 'Toggle raw message headers';
$labels['htmltoggle'] = 'HTML';
$labels['plaintoggle'] = 'Plain text';
$labels['savesentmessagein'] = 'Save sent message in';
$labels['dontsave'] = 'don\'t save';
$labels['maxuploadsize'] = 'Maximum allowed file size is $size';
$labels['addcc'] = 'Add Cc';
$labels['addbcc'] = 'Add Bcc';
$labels['addreplyto'] = 'Add Reply-To';
$labels['addfollowupto'] = 'Add Followup-To';
// mdn
$labels['mdnrequest'] = 'The sender of this message has asked to be notified when you read this message. Do you wish to notify the sender?';
$labels['receiptread'] = 'Return Receipt (read)';
$labels['yourmessage'] = 'This is a Return Receipt for your message';
$labels['receiptnote'] = 'Note: This receipt only acknowledges that the message was displayed on the recipient\'s computer. There is no guarantee that the recipient has read or understood the message contents.';
// address boook
$labels['name'] = 'Display Name';
$labels['firstname'] = 'First Name';
$labels['surname'] = 'Last Name';
$labels['middlename'] = 'Middle Name';
$labels['nameprefix'] = 'Prefix';
$labels['namesuffix'] = 'Suffix';
$labels['nickname'] = 'Nickname';
$labels['jobtitle'] = 'Job Title';
$labels['department'] = 'Department';
$labels['gender'] = 'Gender';
$labels['maidenname'] = 'Maiden Name';
$labels['email'] = 'Email';
$labels['phone'] = 'Phone';
$labels['address'] = 'Address';
$labels['street'] = 'Street';
$labels['locality'] = 'City';
$labels['zipcode'] = 'ZIP Code';
$labels['region'] = 'State/Province';
$labels['country'] = 'Country';
$labels['birthday'] = 'Birthday';
$labels['anniversary'] = 'Anniversary';
$labels['website'] = 'Website';
$labels['instantmessenger'] = 'IM';
$labels['notes'] = 'Notes';
$labels['male'] = 'male';
$labels['female'] = 'female';
$labels['manager'] = 'Manager';
$labels['assistant'] = 'Assistant';
$labels['spouse'] = 'Spouse';
$labels['allfields'] = 'All fields';
$labels['search'] = 'Search';
$labels['advsearch'] = 'Advanced Search';
$labels['advanced'] = 'Advanced';
$labels['other'] = 'Other';
$labels['typehome'] = 'Home';
$labels['typework'] = 'Work';
$labels['typeother'] = 'Other';
$labels['typemobile'] = 'Mobile';
$labels['typemain'] = 'Main';
$labels['typehomefax'] = 'Home Fax';
$labels['typeworkfax'] = 'Work Fax';
$labels['typecar'] = 'Car';
$labels['typepager'] = 'Pager';
$labels['typevideo'] = 'Video';
$labels['typeassistant'] = 'Assistant';
$labels['typehomepage'] = 'Home Page';
$labels['typeblog'] = 'Blog';
$labels['typeprofile'] = 'Profile';
$labels['addfield'] = 'Add field...';
$labels['addcontact'] = 'Add new contact';
$labels['editcontact'] = 'Edit contact';
$labels['contacts'] = 'Contacts';
$labels['contactproperties'] = 'Contact properties';
$labels['personalinfo'] = 'Personal information';
$labels['edit'] = 'Edit';
$labels['cancel'] = 'Cancel';
$labels['save'] = 'Save';
$labels['delete'] = 'Delete';
$labels['rename'] = 'Rename';
$labels['addphoto'] = 'Add';
$labels['replacephoto'] = 'Replace';
$labels['uploadphoto'] = 'Upload photo';
$labels['newcontact'] = 'Create new contact card';
$labels['deletecontact'] = 'Delete selected contacts';
$labels['composeto'] = 'Compose mail to';
$labels['contactsfromto'] = 'Contacts $from to $to of $count';
$labels['print'] = 'Print';
$labels['export'] = 'Export';
$labels['exportall'] = 'Export all';
$labels['exportsel'] = 'Export selected';
$labels['exportvcards'] = 'Export contacts in vCard format';
$labels['newcontactgroup'] = 'Create new contact group';
$labels['grouprename'] = 'Rename group';
$labels['groupdelete'] = 'Delete group';
$labels['groupremoveselected'] = 'Remove selected contacts from group';
$labels['previouspage'] = 'Show previous page';
$labels['firstpage'] = 'Show first page';
$labels['nextpage'] = 'Show next page';
$labels['lastpage'] = 'Show last page';
$labels['group'] = 'Group';
$labels['groups'] = 'Groups';
+$labels['listgroup'] = 'List group members';
$labels['personaladrbook'] = 'Personal Addresses';
$labels['searchsave'] = 'Save search';
$labels['searchdelete'] = 'Delete search';
$labels['import'] = 'Import';
$labels['importcontacts'] = 'Import contacts';
$labels['importfromfile'] = 'Import from file:';
$labels['importtarget'] = 'Add new contacts to address book:';
$labels['importreplace'] = 'Replace the entire address book';
$labels['importdesc'] = 'You can upload contacts from an existing address book.<br/>We currently support importing addresses from the <a href="http://en.wikipedia.org/wiki/VCard">vCard</a> or CSV (comma-separated) data format.';
$labels['done'] = 'Done';
// settings
$labels['settingsfor'] = 'Settings for';
$labels['about'] = 'About';
$labels['preferences'] = 'Preferences';
$labels['userpreferences'] = 'User preferences';
$labels['editpreferences'] = 'Edit user preferences';
$labels['identities'] = 'Identities';
$labels['manageidentities'] = 'Manage identities for this account';
$labels['newidentity'] = 'New identity';
$labels['newitem'] = 'New item';
$labels['edititem'] = 'Edit item';
$labels['preferhtml'] = 'Display HTML';
$labels['defaultcharset'] = 'Default Character Set';
$labels['htmlmessage'] = 'HTML Message';
$labels['messagepart'] = 'Part';
$labels['digitalsig'] = 'Digital Signature';
$labels['dateformat'] = 'Date format';
$labels['timeformat'] = 'Time format';
$labels['prettydate'] = 'Pretty dates';
$labels['setdefault'] = 'Set default';
$labels['autodetect'] = 'Auto';
$labels['language'] = 'Language';
$labels['timezone'] = 'Time zone';
$labels['pagesize'] = 'Rows per page';
$labels['signature'] = 'Signature';
$labels['dstactive'] = 'Daylight saving time';
$labels['showinextwin'] = 'Open message in a new window';
$labels['composeextwin'] = 'Compose in a new window';
$labels['htmleditor'] = 'Compose HTML messages';
$labels['htmlonreply'] = 'on reply to HTML message';
$labels['htmlonreplyandforward'] = 'on forward or reply to HTML message';
$labels['htmlsignature'] = 'HTML signature';
$labels['showemail'] = 'Show email address with display name';
$labels['previewpane'] = 'Show preview pane';
$labels['skin'] = 'Interface skin';
$labels['logoutclear'] = 'Clear Trash on logout';
$labels['logoutcompact'] = 'Compact Inbox on logout';
$labels['uisettings'] = 'User Interface';
$labels['serversettings'] = 'Server Settings';
$labels['mailboxview'] = 'Mailbox View';
$labels['mdnrequests'] = 'On request for return receipt';
$labels['askuser'] = 'ask me';
$labels['autosend'] = 'send receipt';
$labels['autosendknown'] = 'send receipt to my contacts, otherwise ask me';
$labels['autosendknownignore'] = 'send receipt to my contacts, otherwise ignore';
$labels['ignore'] = 'ignore';
$labels['readwhendeleted'] = 'Mark the message as read on delete';
$labels['flagfordeletion'] = 'Flag the message for deletion instead of delete';
$labels['skipdeleted'] = 'Do not show deleted messages';
$labels['deletealways'] = 'If moving messages to Trash fails, delete them';
$labels['deletejunk'] = 'Directly delete messages in Junk';
$labels['showremoteimages'] = 'Display remote inline images';
$labels['fromknownsenders'] = 'from known senders';
$labels['always'] = 'always';
$labels['showinlineimages'] = 'Display attached images below the message';
$labels['autosavedraft'] = 'Automatically save draft';
$labels['everynminutes'] = 'every $n minute(s)';
$labels['refreshinterval'] = 'Refresh (check for new messages, etc.)';
$labels['never'] = 'never';
$labels['immediately'] = 'immediately';
$labels['messagesdisplaying'] = 'Displaying Messages';
$labels['messagescomposition'] = 'Composing Messages';
$labels['mimeparamfolding'] = 'Attachment names';
$labels['2231folding'] = 'Full RFC 2231 (Thunderbird)';
$labels['miscfolding'] = 'RFC 2047/2231 (MS Outlook)';
$labels['2047folding'] = 'Full RFC 2047 (other)';
$labels['force7bit'] = 'Use MIME encoding for 8-bit characters';
$labels['advancedoptions'] = 'Advanced options';
$labels['focusonnewmessage'] = 'Focus browser window on new message';
$labels['checkallfolders'] = 'Check all folders for new messages';
$labels['displaynext'] = 'After message delete/move display the next message';
$labels['defaultfont'] = 'Default font of HTML message';
$labels['mainoptions'] = 'Main Options';
$labels['browseroptions'] = 'Browser Options';
$labels['section'] = 'Section';
$labels['maintenance'] = 'Maintenance';
$labels['newmessage'] = 'New Message';
$labels['signatureoptions'] = 'Signature Options';
$labels['whenreplying'] = 'When replying';
$labels['replyempty'] = 'do not quote the original message';
$labels['replytopposting'] = 'start new message above the quote';
$labels['replybottomposting'] = 'start new message below the quote';
$labels['replyremovesignature'] = 'When replying remove original signature from message';
$labels['autoaddsignature'] = 'Automatically add signature';
$labels['newmessageonly'] = 'new message only';
$labels['replyandforwardonly'] = 'replies and forwards only';
$labels['insertsignature'] = 'Insert signature';
$labels['previewpanemarkread'] = 'Mark previewed messages as read';
$labels['afternseconds'] = 'after $n seconds';
$labels['reqmdn'] = 'Always request a return receipt';
$labels['reqdsn'] = 'Always request a delivery status notification';
$labels['replysamefolder'] = 'Place replies in the folder of the message being replied to';
$labels['defaultabook'] = 'Default address book';
$labels['autocompletesingle'] = 'Skip alternative email addresses in autocompletion';
$labels['listnamedisplay'] = 'List contacts as';
$labels['spellcheckbeforesend'] = 'Check spelling before sending a message';
$labels['spellcheckoptions'] = 'Spellcheck Options';
$labels['spellcheckignoresyms'] = 'Ignore words with symbols';
$labels['spellcheckignorenums'] = 'Ignore words with numbers';
$labels['spellcheckignorecaps'] = 'Ignore words with all letters capitalized';
$labels['addtodict'] = 'Add to dictionary';
$labels['mailtoprotohandler'] = 'Register protocol handler for mailto: links';
$labels['standardwindows'] = 'Handle popups as standard windows';
$labels['forwardmode'] = 'Messages forwarding';
$labels['inline'] = 'inline';
$labels['asattachment'] = 'as attachment';
$labels['folder'] = 'Folder';
$labels['folders'] = 'Folders';
$labels['foldername'] = 'Folder name';
$labels['subscribed'] = 'Subscribed';
$labels['messagecount'] = 'Messages';
$labels['create'] = 'Create';
$labels['createfolder'] = 'Create new folder';
$labels['managefolders'] = 'Manage folders';
$labels['specialfolders'] = 'Special Folders';
$labels['properties'] = 'Properties';
$labels['folderproperties'] = 'Folder properties';
$labels['parentfolder'] = 'Parent folder';
$labels['location'] = 'Location';
$labels['info'] = 'Information';
$labels['getfoldersize'] = 'Click to get folder size';
$labels['changesubscription'] = 'Click to change subscription';
$labels['foldertype'] = 'Folder Type';
$labels['personalfolder'] = 'Private Folder';
$labels['otherfolder'] = 'Other User\'s Folder';
$labels['sharedfolder'] = 'Public Folder';
$labels['sortby'] = 'Sort by';
$labels['sortasc'] = 'Sort ascending';
$labels['sortdesc'] = 'Sort descending';
$labels['undo'] = 'Undo';
$labels['installedplugins'] = 'Installed plugins';
$labels['plugin'] = 'Plugin';
$labels['version'] = 'Version';
$labels['source'] = 'Source';
$labels['license'] = 'License';
$labels['support'] = 'Get support';
// units
$labels['B'] = 'B';
$labels['KB'] = 'KB';
$labels['MB'] = 'MB';
$labels['GB'] = 'GB';
// character sets
$labels['unicode'] = 'Unicode';
$labels['english'] = 'English';
$labels['westerneuropean'] = 'Western European';
$labels['easterneuropean'] = 'Eastern European';
$labels['southeasterneuropean'] = 'South-Eastern European';
$labels['baltic'] = 'Baltic';
$labels['cyrillic'] = 'Cyrillic';
$labels['arabic'] = 'Arabic';
$labels['greek'] = 'Greek';
$labels['hebrew'] = 'Hebrew';
$labels['turkish'] = 'Turkish';
$labels['nordic'] = 'Nordic';
$labels['thai'] = 'Thai';
$labels['celtic'] = 'Celtic';
$labels['vietnamese'] = 'Vietnamese';
$labels['japanese'] = 'Japanese';
$labels['korean'] = 'Korean';
$labels['chinese'] = 'Chinese';
?>
diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc
index 3db2409e8..c7f7fb479 100644
--- a/program/steps/addressbook/func.inc
+++ b/program/steps/addressbook/func.inc
@@ -1,810 +1,856 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/addressbook/func.inc |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide addressbook functionality and GUI objects |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$SEARCH_MODS_DEFAULT = array('name'=>1, 'firstname'=>1, 'surname'=>1, 'email'=>1, '*'=>1);
// general definition of contact coltypes
$CONTACT_COLTYPES = array(
'name' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('name'), 'category' => 'main'),
'firstname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('firstname'), 'category' => 'main'),
'surname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('surname'), 'category' => 'main'),
'email' => array('type' => 'text', 'size' => 40, 'maxlength' => 254, 'label' => rcube_label('email'), 'subtypes' => array('home','work','other'), 'category' => 'main'),
'middlename' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('middlename'), 'category' => 'main'),
'prefix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => rcube_label('nameprefix'), 'category' => 'main'),
'suffix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => rcube_label('namesuffix'), 'category' => 'main'),
'nickname' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('nickname'), 'category' => 'main'),
'jobtitle' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('jobtitle'), 'category' => 'main'),
'organization' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('organization'), 'category' => 'main'),
'department' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('department'), 'category' => 'main'),
'gender' => array('type' => 'select', 'limit' => 1, 'label' => rcube_label('gender'), 'options' => array('male' => rcube_label('male'), 'female' => rcube_label('female')), 'category' => 'personal'),
'maidenname' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('maidenname'), 'category' => 'personal'),
'phone' => array('type' => 'text', 'size' => 40, 'maxlength' => 20, 'label' => rcube_label('phone'), 'subtypes' => array('home','home2','work','work2','mobile','main','homefax','workfax','car','pager','video','assistant','other'), 'category' => 'main'),
'address' => array('type' => 'composite', 'label' => rcube_label('address'), 'subtypes' => array('home','work','other'), 'childs' => array(
'street' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => rcube_label('street'), 'category' => 'main'),
'locality' => array('type' => 'text', 'size' => 28, 'maxlength' => 50, 'label' => rcube_label('locality'), 'category' => 'main'),
'zipcode' => array('type' => 'text', 'size' => 8, 'maxlength' => 15, 'label' => rcube_label('zipcode'), 'category' => 'main'),
'region' => array('type' => 'text', 'size' => 12, 'maxlength' => 50, 'label' => rcube_label('region'), 'category' => 'main'),
'country' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => rcube_label('country'), 'category' => 'main'),
), 'category' => 'main'),
'birthday' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => rcube_label('birthday'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'),
'anniversary' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => rcube_label('anniversary'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'),
'website' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => rcube_label('website'), 'subtypes' => array('homepage','work','blog','profile','other'), 'category' => 'main'),
'im' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => rcube_label('instantmessenger'), 'subtypes' => array('aim','icq','msn','yahoo','jabber','skype','other'), 'category' => 'main'),
'notes' => array('type' => 'textarea', 'size' => 40, 'rows' => 15, 'maxlength' => 500, 'label' => rcube_label('notes'), 'limit' => 1),
'photo' => array('type' => 'image', 'limit' => 1, 'category' => 'main'),
'assistant' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('assistant'), 'category' => 'personal'),
'manager' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('manager'), 'category' => 'personal'),
'spouse' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('spouse'), 'category' => 'personal'),
// TODO: define fields for vcards like GEO, KEY
);
$PAGE_SIZE = $RCMAIL->config->get('addressbook_pagesize', $RCMAIL->config->get('pagesize', 50));
// Addressbook UI
if (!$RCMAIL->action && !$OUTPUT->ajax_call) {
// add list of address sources to client env
$js_list = $RCMAIL->get_address_sources();
// count all/writeable sources
$writeable = 0;
$count = 0;
foreach ($js_list as $sid => $s) {
$count++;
if (!$s['readonly']) {
$writeable++;
}
// unset hidden sources
if ($s['hidden']) {
unset($js_list[$sid]);
}
}
$search_mods = $RCMAIL->config->get('addressbook_search_mods', $SEARCH_MODS_DEFAULT);
$OUTPUT->set_env('search_mods', $search_mods);
$OUTPUT->set_env('address_sources', $js_list);
$OUTPUT->set_env('writable_source', $writeable);
$OUTPUT->set_env('compose_extwin', $RCMAIL->config->get('compose_extwin',false));
$OUTPUT->set_pagetitle(rcube_label('addressbook'));
$_SESSION['addressbooks_count'] = $count;
$_SESSION['addressbooks_count_writeable'] = $writeable;
// select address book
$source = get_input_value('_source', RCUBE_INPUT_GPC);
// use first directory by default
if (!strlen($source) || !isset($js_list[$source])) {
$source = $RCMAIL->config->get('default_addressbook');
if (!strlen($source) || !isset($js_list[$source])) {
$source = strval(key($js_list));
}
}
$CONTACTS = rcmail_contact_source($source, true);
}
// remove undo information...
if ($undo = $_SESSION['contact_undo']) {
// ...after timeout
$undo_time = $RCMAIL->config->get('undo_timeout', 0);
if ($undo['ts'] < time() - $undo_time)
$RCMAIL->session->remove('contact_undo');
}
// instantiate a contacts object according to the given source
function rcmail_contact_source($source=null, $init_env=false, $writable=false)
{
global $RCMAIL, $OUTPUT, $CONTACT_COLTYPES, $PAGE_SIZE;
if (!strlen($source)) {
$source = get_input_value('_source', RCUBE_INPUT_GPC);
}
// Get object
$CONTACTS = $RCMAIL->get_address_book($source, $writable);
$CONTACTS->set_pagesize($PAGE_SIZE);
// set list properties and session vars
if (!empty($_GET['_page']))
$CONTACTS->set_page(($_SESSION['page'] = intval($_GET['_page'])));
else
$CONTACTS->set_page(isset($_SESSION['page']) ? $_SESSION['page'] : 1);
if (!empty($_REQUEST['_gid']))
$CONTACTS->set_group(get_input_value('_gid', RCUBE_INPUT_GPC));
if (!$init_env)
return $CONTACTS;
$OUTPUT->set_env('readonly', $CONTACTS->readonly);
$OUTPUT->set_env('source', $source);
// reduce/extend $CONTACT_COLTYPES with specification from the current $CONTACT object
if (is_array($CONTACTS->coltypes)) {
// remove cols not listed by the backend class
$contact_cols = $CONTACTS->coltypes[0] ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes;
$CONTACT_COLTYPES = array_intersect_key($CONTACT_COLTYPES, $contact_cols);
// add associative coltypes definition
if (!$CONTACTS->coltypes[0]) {
foreach ($CONTACTS->coltypes as $col => $colprop) {
if (is_array($colprop['childs'])) {
foreach ($colprop['childs'] as $childcol => $childprop)
$colprop['childs'][$childcol] = array_merge((array)$CONTACT_COLTYPES[$col]['childs'][$childcol], $childprop);
}
$CONTACT_COLTYPES[$col] = $CONTACT_COLTYPES[$col] ? array_merge($CONTACT_COLTYPES[$col], $colprop) : $colprop;
}
}
}
$OUTPUT->set_env('photocol', is_array($CONTACT_COLTYPES['photo']));
return $CONTACTS;
}
function rcmail_set_sourcename($abook)
{
global $OUTPUT;
// get address book name (for display)
if ($abook && $_SESSION['addressbooks_count'] > 1) {
$name = $abook->get_name();
if (!$name) {
$name = rcube_label('personaladrbook');
}
$OUTPUT->set_env('sourcename', html_entity_decode($name, ENT_COMPAT, 'UTF-8'));
}
}
function rcmail_directory_list($attrib)
{
global $RCMAIL, $OUTPUT;
if (!$attrib['id'])
$attrib['id'] = 'rcmdirectorylist';
$out = '';
$jsdata = array();
$line_templ = html::tag('li', array(
'id' => 'rcmli%s', 'class' => '%s', 'noclose' => true),
html::a(array('href' => '%s',
'rel' => '%s',
'onclick' => "return ".JS_OBJECT_NAME.".command('list','%s',this)"), '%s'));
$sources = (array) $OUTPUT->get_env('address_sources');
reset($sources);
// currently selected source
$current = get_input_value('_source', RCUBE_INPUT_GPC);
foreach ($sources as $j => $source) {
$id = strval(strlen($source['id']) ? $source['id'] : $j);
$js_id = JQ($id);
// set class name(s)
$class_name = 'addressbook';
if ($current === $id)
$class_name .= ' selected';
if ($source['readonly'])
$class_name .= ' readonly';
if ($source['class_name'])
$class_name .= ' ' . $source['class_name'];
$name = !empty($source['name']) ? $source['name'] : $id;
$out .= sprintf($line_templ,
rcube_utils::html_identifier($id, true),
$class_name,
Q(rcmail_url(null, array('_source' => $id))),
$source['id'],
$js_id, $name);
$groupdata = array('out' => $out, 'jsdata' => $jsdata, 'source' => $id);
if ($source['groups'])
$groupdata = rcmail_contact_groups($groupdata);
$jsdata = $groupdata['jsdata'];
$out = $groupdata['out'];
$out .= '</li>';
}
$line_templ = html::tag('li', array(
'id' => 'rcmli%s', 'class' => '%s'),
html::a(array('href' => '#', 'rel' => 'S%s',
'onclick' => "return ".JS_OBJECT_NAME.".command('listsearch', '%s', this)"), '%s'));
// Saved searches
$sources = $RCMAIL->user->list_searches(rcube_user::SEARCH_ADDRESSBOOK);
foreach ($sources as $j => $source) {
$id = $source['id'];
$js_id = JQ($id);
// set class name(s)
$class_name = 'contactsearch';
if ($current === $id)
$class_name .= ' selected';
if ($source['class_name'])
$class_name .= ' ' . $source['class_name'];
$out .= sprintf($line_templ,
rcube_utils::html_identifier('S'.$id, true),
$class_name,
$id,
$js_id, (!empty($source['name']) ? Q($source['name']) : Q($id)));
}
$OUTPUT->set_env('contactgroups', $jsdata);
$OUTPUT->set_env('collapsed_abooks', (string)$RCMAIL->config->get('collapsed_abooks',''));
$OUTPUT->add_gui_object('folderlist', $attrib['id']);
$OUTPUT->include_script('treelist.js');
// add some labels to client
$OUTPUT->add_label('deletegroupconfirm', 'groupdeleting', 'addingmember', 'removingmember');
return html::tag('ul', $attrib, $out, html::$common_attrib);
}
function rcmail_contact_groups($args)
{
global $RCMAIL;
$groups_html = '';
$groups = $RCMAIL->get_address_book($args['source'])->list_groups();
if (!empty($groups)) {
$line_templ = html::tag('li', array(
'id' => 'rcmli%s', 'class' => 'contactgroup'),
html::a(array('href' => '#',
'rel' => '%s:%s',
'onclick' => "return ".JS_OBJECT_NAME.".command('listgroup',{'source':'%s','id':'%s'},this)"), '%s'));
// append collapse/expand toggle and open a new <ul>
$is_collapsed = strpos($RCMAIL->config->get('collapsed_abooks',''), '&'.rawurlencode($args['source']).'&') !== false;
$args['out'] .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
foreach ($groups as $group) {
$groups_html .= sprintf($line_templ,
rcube_utils::html_identifier('G' . $args['source'] . $group['ID'], true),
$args['source'], $group['ID'],
$args['source'], $group['ID'], Q($group['name'])
);
$args['jsdata']['G'.$args['source'].$group['ID']] = array(
'source' => $args['source'], 'id' => $group['ID'],
'name' => $group['name'], 'type' => 'group');
}
}
$args['out'] .= html::tag('ul',
array('class' => 'groups', 'style' => ($is_collapsed || empty($groups) ? "display:none;" : null)),
$groups_html);
return $args;
}
// return the contacts list as HTML table
function rcmail_contacts_list($attrib)
{
global $CONTACTS, $OUTPUT;
// define list of cols to be displayed
- $a_show_cols = array('name');
+ $a_show_cols = array('name','action');
// add id to message list table if not specified
if (!strlen($attrib['id']))
$attrib['id'] = 'rcmAddressList';
// create XHTML table
$out = rcube_table_output($attrib, array(), $a_show_cols, $CONTACTS->primary_key);
// set client env
$OUTPUT->add_gui_object('contactslist', $attrib['id']);
$OUTPUT->set_env('current_page', (int)$CONTACTS->list_page);
$OUTPUT->include_script('list.js');
// add some labels to client
$OUTPUT->add_label('deletecontactconfirm', 'copyingcontact', 'contactdeleting');
return $out;
}
function rcmail_js_contacts_list($result, $prefix='')
{
global $OUTPUT;
if (empty($result) || $result->count == 0)
return;
// define list of cols to be displayed
- $a_show_cols = array('name');
+ $a_show_cols = array('name','action');
while ($row = $result->next()) {
+ $row['CID'] = $row['ID'];
+ $row['email'] = reset(rcube_addressbook::get_col_values('email', $row, true));
+
+ $source_id = $OUTPUT->get_env('source');
$a_row_cols = array();
- $classes = array('person'); // org records will follow some day
+ $classes = array($row['_type'] ? $row['_type'] : 'person');
// build contact ID with source ID
if (isset($row['sourceid'])) {
$row['ID'] = $row['ID'].'-'.$row['sourceid'];
+ $source_id = $row['sourceid'];
}
// format each col
foreach ($a_show_cols as $col) {
- $val = $col == 'name' ? rcube_addressbook::compose_list_name($row) : $row[$col];
- $a_row_cols[$col] = Q($val);
+ $val = '';
+ switch ($col) {
+ case 'name':
+ $val = Q(rcube_addressbook::compose_list_name($row));
+ break;
+
+ case 'action':
+ if ($row['_type'] == 'group') {
+ $val = html::a(array(
+ 'href' => '#list',
+ 'rel' => $row['ID'],
+ 'title' => rcube_label('listgroup'),
+ 'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", JS_OBJECT_NAME, $source_id, $row['CID']),
+ ), '&raquo;');
+ }
+ else
+ $val = '&nbsp;';
+ break;
+
+ default:
+ $val = Q($row[$col]);
+ break;
+ }
+
+ $a_row_cols[$col] = $val;
}
if ($row['readonly'])
$classes[] = 'readonly';
- $OUTPUT->command($prefix.'add_contact_row', $row['ID'], $a_row_cols, join(' ', $classes));
+ $OUTPUT->command($prefix.'add_contact_row', $row['ID'], $a_row_cols, join(' ', $classes), array_intersect_key($row, array('ID'=>1,'readonly'=>1,'_type'=>1,'email'=>1,'name'=>1)));
}
}
+function rcmail_contacts_list_title($attrib)
+{
+ global $OUTPUT;
+
+ $attrib += array('label' => 'contacts', 'id' => 'rcmabooklisttitle', 'tag' => 'span');
+ unset($attrib['name']);
+
+ $OUTPUT->add_gui_object('addresslist_title', $attrib['id']);
+ $OUTPUT->add_label('contacts');
+
+ return html::tag($attrib['tag'], $attrib, rcube_label($attrib['label']), html::$common_attrib);
+}
+
+
// similar function as /steps/settings/identities.inc::rcmail_identity_frame()
function rcmail_contact_frame($attrib)
{
global $OUTPUT;
if (!$attrib['id'])
$attrib['id'] = 'rcmcontactframe';
return $OUTPUT->frame($attrib, true);
}
function rcmail_rowcount_display($attrib)
{
global $OUTPUT;
if (!$attrib['id'])
$attrib['id'] = 'rcmcountdisplay';
$OUTPUT->add_gui_object('countdisplay', $attrib['id']);
if ($attrib['label'])
$_SESSION['contactcountdisplay'] = $attrib['label'];
return html::span($attrib, rcube_label('loading'));
}
function rcmail_get_rowcount_text($result=null)
{
global $CONTACTS, $PAGE_SIZE;
// read nr of contacts
if (!$result) {
$result = $CONTACTS->get_result();
}
if ($result->count == 0)
$out = rcube_label('nocontactsfound');
else
$out = rcube_label(array(
'name' => $_SESSION['contactcountdisplay'] ? $_SESSION['contactcountdisplay'] : 'contactsfromto',
'vars' => array(
'from' => $result->first + 1,
'to' => min($result->count, $result->first + $PAGE_SIZE),
'count' => $result->count)
));
return $out;
}
function rcmail_get_type_label($type)
{
$label = 'type'.$type;
if (rcube_label_exists($label, '*', $domain))
return rcube_label($label, $domain);
else if (preg_match('/\w+(\d+)$/', $label, $m)
&& ($label = preg_replace('/(\d+)$/', '', $label))
&& rcube_label_exists($label, '*', $domain))
return rcube_label($label, $domain) . ' ' . $m[1];
return ucfirst($type);
}
function rcmail_contact_form($form, $record, $attrib = null)
{
- global $RCMAIL, $CONFIG;
+ global $RCMAIL;
// Allow plugins to modify contact form content
$plugin = $RCMAIL->plugins->exec_hook('contact_form', array(
'form' => $form, 'record' => $record));
$form = $plugin['form'];
$record = $plugin['record'];
$edit_mode = $RCMAIL->action != 'show';
- $del_button = $attrib['deleteicon'] ? html::img(array('src' => $CONFIG['skin_path'] . $attrib['deleteicon'], 'alt' => rcube_label('delete'))) : rcube_label('delete');
+ $del_button = $attrib['deleteicon'] ? html::img(array('src' => $RCMAIL->output->get_skin_file($attrib['deleteicon']), 'alt' => rcube_label('delete'))) : rcube_label('delete');
unset($attrib['deleteicon']);
$out = '';
// get default coltypes
$coltypes = $GLOBALS['CONTACT_COLTYPES'];
$coltype_labels = array();
foreach ($coltypes as $col => $prop) {
if ($prop['subtypes']) {
$subtype_names = array_map('rcmail_get_type_label', $prop['subtypes']);
$select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype'));
$select_subtype->add($subtype_names, $prop['subtypes']);
$coltypes[$col]['subtypes_select'] = $select_subtype->show();
}
if ($prop['childs']) {
foreach ($prop['childs'] as $childcol => $cp)
$coltype_labels[$childcol] = array('label' => $cp['label']);
}
}
foreach ($form as $section => $fieldset) {
// skip empty sections
if (empty($fieldset['content']))
continue;
$select_add = new html_select(array('class' => 'addfieldmenu', 'rel' => $section));
$select_add->add(rcube_label('addfield'), '');
// render head section with name fields (not a regular list of rows)
if ($section == 'head') {
$content = '';
// unset display name if it is composed from name parts
if ($record['name'] == rcube_addressbook::compose_display_name(array('name' => '') + (array)$record))
unset($record['name']);
// group fields
$field_blocks = array(
'names' => array('prefix','firstname','middlename','surname','suffix'),
'displayname' => array('name'),
'nickname' => array('nickname'),
'organization' => array('organization'),
'department' => array('department'),
'jobtitle' => array('jobtitle'),
);
foreach ($field_blocks as $blockname => $colnames) {
$fields = '';
foreach ($colnames as $col) {
// skip cols unknown to the backend
if (!$coltypes[$col])
continue;
// only string values are expected here
if (is_array($record[$col]))
$record[$col] = join(' ', $record[$col]);
if ($RCMAIL->action == 'show') {
if (!empty($record[$col]))
$fields .= html::span('namefield ' . $col, Q($record[$col])) . " ";
}
else {
$colprop = (array)$fieldset['content'][$col] + (array)$coltypes[$col];
$colprop['id'] = 'ff_'.$col;
if (empty($record[$col]) && !$colprop['visible']) {
$colprop['style'] = 'display:none';
$select_add->add($colprop['label'], $col);
}
$fields .= rcmail_get_edit_field($col, $record[$col], $colprop, $colprop['type']);
}
}
$content .= html::div($blockname, $fields);
}
if ($edit_mode)
$content .= html::p('addfield', $select_add->show(null));
$out .= html::tag('fieldset', $attrib, (!empty($fieldset['name']) ? html::tag('legend', null, Q($fieldset['name'])) : '') . $content) ."\n";
continue;
}
$content = '';
if (is_array($fieldset['content'])) {
foreach ($fieldset['content'] as $col => $colprop) {
// remove subtype part of col name
list($field, $subtype) = explode(':', $col);
if (!$subtype) $subtype = 'home';
$fullkey = $col.':'.$subtype;
// skip cols unknown to the backend
if (!$coltypes[$field])
continue;
// merge colprop with global coltype configuration
$colprop += $coltypes[$field];
$label = isset($colprop['label']) ? $colprop['label'] : rcube_label($col);
// prepare subtype selector in edit mode
if ($edit_mode && is_array($colprop['subtypes'])) {
$subtype_names = array_map('rcmail_get_type_label', $colprop['subtypes']);
$select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype'));
$select_subtype->add($subtype_names, $colprop['subtypes']);
}
else
$select_subtype = null;
if (!empty($colprop['value'])) {
$values = (array)$colprop['value'];
}
else {
// iterate over possible subtypes and collect values with their subtype
if (is_array($colprop['subtypes'])) {
$values = $subtypes = array();
foreach (rcube_addressbook::get_col_values($field, $record) as $st => $vals) {
foreach((array)$vals as $value) {
$i = count($values);
$subtypes[$i] = $st;
$values[$i] = $value;
}
// TODO: add $st to $select_subtype if missing ?
}
}
else {
$values = $record[$fullkey] ? $record[$fullkey] : $record[$field];
$subtypes = null;
}
}
// hack: create empty values array to force this field to be displayed
if (empty($values) && $colprop['visible'])
$values[] = '';
if (!is_array($values)) {
// $values can be an object, don't use (array)$values syntax
$values = !empty($values) ? array($values) : array();
}
$rows = '';
foreach ($values as $i => $val) {
if ($subtypes[$i])
$subtype = $subtypes[$i];
// render composite field
if ($colprop['type'] == 'composite') {
$composite = array(); $j = 0;
$template = $RCMAIL->config->get($col . '_template', '{'.join('} {', array_keys($colprop['childs'])).'}');
foreach ($colprop['childs'] as $childcol => $cp) {
if (!empty($val) && is_array($val)) {
$childvalue = $val[$childcol] ? $val[$childcol] : $val[$j];
}
else {
$childvalue = '';
}
if ($edit_mode) {
if ($colprop['subtypes'] || $colprop['limit'] != 1) $cp['array'] = true;
$composite['{'.$childcol.'}'] = rcmail_get_edit_field($childcol, $childvalue, $cp, $cp['type']) . " ";
}
else {
$childval = $cp['render_func'] ? call_user_func($cp['render_func'], $childvalue, $childcol) : Q($childvalue);
$composite['{'.$childcol.'}'] = html::span('data ' . $childcol, $childval) . " ";
}
$j++;
}
$coltypes[$field] += (array)$colprop;
$coltypes[$field]['count']++;
$val = preg_replace('/\{\w+\}/', '', strtr($template, $composite));
}
else if ($edit_mode) {
// call callback to render/format value
if ($colprop['render_func'])
$val = call_user_func($colprop['render_func'], $val, $col);
$coltypes[$field] = (array)$colprop + $coltypes[$field];
if ($colprop['subtypes'] || $colprop['limit'] != 1)
$colprop['array'] = true;
// load jquery UI datepicker for date fields
if ($colprop['type'] == 'date') {
$colprop['class'] .= ($colprop['class'] ? ' ' : '') . 'datepicker';
if (!$colprop['render_func'])
$val = rcmail_format_date_col($val);
}
$val = rcmail_get_edit_field($col, $val, $colprop, $colprop['type']);
$coltypes[$field]['count']++;
}
else if ($colprop['render_func'])
$val = call_user_func($colprop['render_func'], $val, $col);
else if (is_array($colprop['options']) && isset($colprop['options'][$val]))
$val = $colprop['options'][$val];
else
$val = Q($val);
// use subtype as label
if ($colprop['subtypes'])
$label = rcmail_get_type_label($subtype);
// add delete button/link
if ($edit_mode && !($colprop['visible'] && $colprop['limit'] == 1))
$val .= html::a(array('href' => '#del', 'class' => 'contactfieldbutton deletebutton', 'title' => rcube_label('delete'), 'rel' => $col), $del_button);
// display row with label
if ($label) {
$rows .= html::div('row',
html::div('contactfieldlabel label', $select_subtype ? $select_subtype->show($subtype) : Q($label)) .
html::div('contactfieldcontent '.$colprop['type'], $val));
}
else // row without label
$rows .= html::div('row', html::div('contactfield', $val));
}
// add option to the add-field menu
if (!$colprop['limit'] || $coltypes[$field]['count'] < $colprop['limit']) {
$select_add->add($colprop['label'], $col);
$select_add->_count++;
}
// wrap rows in fieldgroup container
if ($rows) {
$content .= html::tag('fieldset', array('class' => 'contactfieldgroup ' . ($colprop['subtypes'] ? 'contactfieldgroupmulti ' : '') . 'contactcontroller' . $col, 'style' => ($rows ? null : 'display:none')),
($colprop['subtypes'] ? html::tag('legend', null, Q($colprop['label'])) : ' ') .
$rows);
}
}
if (!$content && (!$edit_mode || !$select_add->_count))
continue;
// also render add-field selector
if ($edit_mode)
$content .= html::p('addfield', $select_add->show(null, array('style' => $select_add->_count ? null : 'display:none')));
$content = html::div(array('id' => 'contactsection' . $section), $content);
}
else {
$content = $fieldset['content'];
}
if ($content)
$out .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $content) ."\n";
}
if ($edit_mode) {
$RCMAIL->output->set_env('coltypes', $coltypes + $coltype_labels);
$RCMAIL->output->set_env('delbutton', $del_button);
$RCMAIL->output->add_label('delete');
}
return $out;
}
function rcmail_contact_photo($attrib)
{
- global $SOURCE_ID, $CONTACTS, $CONTACT_COLTYPES, $RCMAIL, $CONFIG;
+ global $SOURCE_ID, $CONTACTS, $CONTACT_COLTYPES, $RCMAIL;
if ($result = $CONTACTS->get_result())
$record = $result->first();
- $photo_img = $attrib['placeholder'] ? $CONFIG['skin_path'] . $attrib['placeholder'] : 'program/resources/blank.gif';
+ $photo_img = $attrib['placeholder'] ? $RCMAIL->output->get_skin_file($attrib['placeholder']) : 'program/resources/blank.gif';
+ if ($record['_type'] == 'group' && $attrib['placeholdergroup'])
+ $photo_img = $RCMAIL->output->get_skin_file($attrib['placeholdergroup']);
+
$RCMAIL->output->set_env('photo_placeholder', $photo_img);
unset($attrib['placeholder']);
$plugin = $RCMAIL->plugins->exec_hook('contact_photo', array('record' => $record, 'data' => $record['photo']));
if ($plugin['url'])
$photo_img = $plugin['url'];
else if (preg_match('!^https?://!i', $record['photo']))
$photo_img = $record['photo'];
else if ($record['photo'])
$photo_img = $RCMAIL->url(array('_action' => 'photo', '_cid' => $record['ID'], '_source' => $SOURCE_ID));
else
$ff_value = '-del-'; // will disable delete-photo action
$img = html::img(array('src' => $photo_img, 'border' => 1, 'alt' => ''));
$content = html::div($attrib, $img);
if ($CONTACT_COLTYPES['photo'] && ($RCMAIL->action == 'edit' || $RCMAIL->action == 'add')) {
$RCMAIL->output->add_gui_object('contactphoto', $attrib['id']);
$hidden = new html_hiddenfield(array('name' => '_photo', 'id' => 'ff_photo', 'value' => $ff_value));
$content .= $hidden->show();
}
return $content;
}
function rcmail_format_date_col($val)
{
global $RCMAIL;
return format_date($val, $RCMAIL->config->get('date_format', 'Y-m-d'), false);
}
/**
* Returns contact ID(s) and source(s) from GET/POST data
*
* @return array List of contact IDs per-source
*/
function rcmail_get_cids($filter = null)
{
// contact ID (or comma-separated list of IDs) is provided in two
// forms. If _source is an empty string then the ID is a string
// containing contact ID and source name in form: <ID>-<SOURCE>
$cid = get_input_value('_cid', RCUBE_INPUT_GPC);
$source = (string) get_input_value('_source', RCUBE_INPUT_GPC);
if (is_array($cid)) {
return $cid;
}
if (!preg_match('/^[a-zA-Z0-9\+\/=_-]+(,[a-zA-Z0-9\+\/=_-]+)*$/', $cid)) {
return array();
}
$cid = explode(',', $cid);
$got_source = strlen($source);
$result = array();
// create per-source contact IDs array
foreach ($cid as $id) {
// extract source ID from contact ID (it's there in search mode)
// see #1488959 and #1488862 for reference
if (!$got_source) {
if ($sep = strrpos($id, '-')) {
$contact_id = substr($id, 0, $sep);
$source_id = (string) substr($id, $sep+1);
if (strlen($source_id)) {
$result[$source_id][] = $contact_id;
}
}
}
else {
if (substr($id, -($got_source+1)) === "-$source") {
$id = substr($id, 0, -($got_source+1));
}
$result[$source][] = $id;
}
}
return $filter !== null ? $result[$filter] : $result;
}
// register UI objects
$OUTPUT->add_handlers(array(
'directorylist' => 'rcmail_directory_list',
// 'groupslist' => 'rcmail_contact_groups',
'addresslist' => 'rcmail_contacts_list',
+ 'addresslisttitle' => 'rcmail_contacts_list_title',
'addressframe' => 'rcmail_contact_frame',
'recordscountdisplay' => 'rcmail_rowcount_display',
'searchform' => array($OUTPUT, 'search_form')
));
// register action aliases
$RCMAIL->register_action_map(array(
'add' => 'edit.inc',
'photo' => 'show.inc',
'group-create' => 'groups.inc',
'group-rename' => 'groups.inc',
'group-delete' => 'groups.inc',
'group-addmembers' => 'groups.inc',
'group-delmembers' => 'groups.inc',
'search-create' => 'search.inc',
'search-delete' => 'search.inc',
));
diff --git a/program/steps/addressbook/list.inc b/program/steps/addressbook/list.inc
index 1bb28658b..6f3a3e0f3 100644
--- a/program/steps/addressbook/list.inc
+++ b/program/steps/addressbook/list.inc
@@ -1,94 +1,99 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/addressbook/list.inc |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send contacts list to client (as remote response) |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$afields = $RCMAIL->config->get('contactlist_fields');
// Use search result
if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']]))
{
$search = (array)$_SESSION['search'][$_REQUEST['_search']];
$records = array();
if (!empty($_GET['_page']))
$page = intval($_GET['_page']);
else
$page = isset($_SESSION['page']) ? $_SESSION['page'] : 1;
$_SESSION['page'] = $page;
$sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name');
// Get records from all sources
foreach ($search as $s => $set) {
$source = $RCMAIL->get_address_book($s);
// reset page
$source->set_page(1);
$source->set_pagesize(9999);
$source->set_search_set($set);
// get records
$result = $source->list_records($afields);
while ($row = $result->next()) {
$row['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($row, $sort_col);
$records[$key] = $row;
}
unset($result);
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$first = ($page-1) * $PAGE_SIZE;
$result = new rcube_result_set($count, $first);
// we need only records for current page
if ($PAGE_SIZE < $count) {
$records = array_slice($records, $first, $PAGE_SIZE);
}
$result->records = array_values($records);
}
// List selected directory
else {
$CONTACTS = rcmail_contact_source(null, true);
// get contacts for this user
$result = $CONTACTS->list_records($afields);
if (!$result->count && $result->searchonly) {
$OUTPUT->show_message('contactsearchonly', 'notice');
$OUTPUT->command('command', 'advanced-search');
}
+
+ if ($CONTACTS->group_id) {
+ $OUTPUT->command('set_group_prop', array('ID' => $CONTACTS->group_id)
+ + array_intersect_key((array)$CONTACTS->get_group($CONTACTS->group_id), array('name'=>1,'email'=>1)));
+ }
}
// update message count display
$OUTPUT->set_env('pagecount', ceil($result->count / $PAGE_SIZE));
$OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result));
// create javascript list
rcmail_js_contacts_list($result);
// send response
$OUTPUT->send();
diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc
index 25bfbd48b..e7e5efc63 100644
--- a/program/steps/addressbook/save.inc
+++ b/program/steps/addressbook/save.inc
@@ -1,228 +1,228 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/addressbook/save.inc |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2011, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save a contact entry or to add a new one |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$CONTACTS = rcmail_contact_source(null, true, true);
$cid = get_input_value('_cid', RCUBE_INPUT_POST);
$return_action = empty($cid) ? 'add' : 'edit';
// Source changed, display the form again
if (!empty($_GET['_reload'])) {
rcmail_overwrite_action($return_action);
return;
}
// cannot edit record
if ($CONTACTS->readonly) {
$OUTPUT->show_message('contactreadonly', 'error');
rcmail_overwrite_action($return_action);
return;
}
// read POST values into hash array
$a_record = array();
foreach ($GLOBALS['CONTACT_COLTYPES'] as $col => $colprop) {
$fname = '_'.$col;
if ($colprop['composite'])
continue;
// gather form data of composite fields
if ($colprop['childs']) {
$values = array();
foreach ($colprop['childs'] as $childcol => $cp) {
$vals = get_input_value('_'.$childcol, RCUBE_INPUT_POST, true);
foreach ((array)$vals as $i => $val)
$values[$i][$childcol] = $val;
}
$subtypes = isset($_REQUEST['_subtype_' . $col]) ? (array)get_input_value('_subtype_' . $col, RCUBE_INPUT_POST) : array('');
foreach ($subtypes as $i => $subtype) {
$suffix = $subtype ? ':'.$subtype : '';
if ($values[$i])
$a_record[$col.$suffix][] = $values[$i];
}
}
// assign values and subtypes
else if (is_array($_POST[$fname])) {
$values = get_input_value($fname, RCUBE_INPUT_POST, true);
$subtypes = get_input_value('_subtype_' . $col, RCUBE_INPUT_POST);
foreach ($values as $i => $val) {
$subtype = $subtypes[$i] ? ':'.$subtypes[$i] : '';
$a_record[$col.$subtype][] = $val;
}
}
else if (isset($_POST[$fname])) {
$a_record[$col] = get_input_value($fname, RCUBE_INPUT_POST, true);
}
}
// Generate contact's display name (must be before validation)
if (empty($a_record['name'])) {
$a_record['name'] = rcube_addressbook::compose_display_name($a_record, true);
// Reset it if equals to email address (from compose_display_name())
if ($a_record['name'] == $a_record['email'][0])
$a_record['name'] = '';
}
// do input checks (delegated to $CONTACTS instance)
if (!$CONTACTS->validate($a_record)) {
$err = (array)$CONTACTS->get_error();
$OUTPUT->show_message($err['message'] ? Q($err['message']) : 'formincomplete', 'warning');
$GLOBALS['EDIT_RECORD'] = $a_record; // store submitted data to be used in edit form
rcmail_overwrite_action($return_action);
return;
}
// get raw photo data if changed
if (isset($a_record['photo'])) {
if ($a_record['photo'] == '-del-') {
$a_record['photo'] = '';
}
else if ($tempfile = $_SESSION['contacts']['files'][$a_record['photo']]) {
$tempfile = $RCMAIL->plugins->exec_hook('attachment_get', $tempfile);
if ($tempfile['status'])
$a_record['photo'] = $tempfile['data'] ? $tempfile['data'] : @file_get_contents($tempfile['path']);
}
else
unset($a_record['photo']);
// cleanup session data
$RCMAIL->plugins->exec_hook('attachments_cleanup', array('group' => 'contact'));
$RCMAIL->session->remove('contacts');
}
$source = get_input_value('_source', RCUBE_INPUT_GPC);
// update an existing contact
if (!empty($cid))
{
$plugin = $RCMAIL->plugins->exec_hook('contact_update',
array('id' => $cid, 'record' => $a_record, 'source' => $source));
$a_record = $plugin['record'];
if (!$plugin['abort'])
$result = $CONTACTS->update($cid, $a_record);
else
$result = $plugin['result'];
if ($result) {
// LDAP DN change
if (is_string($result) && strlen($result)>1) {
$newcid = $result;
// change cid in POST for 'show' action
$_POST['_cid'] = $newcid;
}
// define list of cols to be displayed
$a_js_cols = array();
$record = $CONTACTS->get_record($newcid ? $newcid : $cid, true);
$record['email'] = reset($CONTACTS->get_col_values('email', $record, true));
$record['name'] = rcube_addressbook::compose_list_name($record);
- foreach (array('name', 'email') as $col)
+ foreach (array('name') as $col)
$a_js_cols[] = Q((string)$record[$col]);
// update the changed col in list
- $OUTPUT->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source);
+ $OUTPUT->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source, $record);
// show confirmation
$OUTPUT->show_message('successfullysaved', 'confirmation', null, false);
rcmail_overwrite_action('show');
}
else {
// show error message
$err = $CONTACTS->get_error();
$OUTPUT->show_message($plugin['message'] ? $plugin['message'] : ($err['message'] ? $err['message'] : 'errorsaving'), 'error', null, false);
rcmail_overwrite_action('show');
}
}
// insert a new contact
else {
// Name of the addressbook already selected on the list
$orig_source = get_input_value('_orig_source', RCUBE_INPUT_GPC);
if (!strlen($source))
$source = $orig_source;
// show notice if existing contacts with same e-mail are found
foreach ($CONTACTS->get_col_values('email', $a_record, true) as $email) {
if ($email && ($res = $CONTACTS->search('email', $email, 1, false, true)) && $res->count) {
$OUTPUT->show_message('contactexists', 'notice', null, false);
break;
}
}
$plugin = $RCMAIL->plugins->exec_hook('contact_create', array(
'record' => $a_record, 'source' => $source));
$a_record = $plugin['record'];
// insert record and send response
if (!$plugin['abort'])
$insert_id = $CONTACTS->insert($a_record);
else
$insert_id = $plugin['result'];
if ($insert_id) {
$CONTACTS->reset();
// add new contact to the specified group
if ($CONTACTS->groups && $CONTACTS->group_id) {
$plugin = $RCMAIL->plugins->exec_hook('group_addmembers', array(
'group_id' => $CONTACTS->group_id, 'ids' => $insert_id, 'source' => $source));
$counts = $CONTACTS->count();
if (!$plugin['abort']) {
if (($maxnum = $RCMAIL->config->get('max_group_members', 0)) && ($counts->count + 1 > $maxnum))
$OUTPUT->show_message('maxgroupmembersreached', 'warning', array('max' => $maxnum));
$CONTACTS->add_to_group($plugin['group_id'], $plugin['ids']);
}
}
else
$counts = $CONTACTS->count();
if ((string)$source === (string)$orig_source) {
// add contact row or jump to the page where it should appear
$CONTACTS->reset();
$result = $CONTACTS->search($CONTACTS->primary_key, $insert_id);
rcmail_js_contacts_list($result, 'parent.');
$OUTPUT->command('parent.contact_list.select', html_identifier($insert_id));
// update record count display
$CONTACTS->reset();
$OUTPUT->command('parent.set_rowcount', rcmail_get_rowcount_text($counts));
}
else {
// re-set iframe
$OUTPUT->command('parent.show_contentframe');
}
// show confirmation
$OUTPUT->show_message('successfullysaved', 'confirmation', null, false);
$OUTPUT->send('iframe');
}
else {
// show error message
$err = $CONTACTS->get_error();
$OUTPUT->show_message($plugin['message'] ? $plugin['message'] : ($err['message'] ? $err['message'] : 'errorsaving'), 'error', null, false);
rcmail_overwrite_action('add');
}
}
diff --git a/program/steps/addressbook/show.inc b/program/steps/addressbook/show.inc
index 1a97c65b1..63abc8c4b 100644
--- a/program/steps/addressbook/show.inc
+++ b/program/steps/addressbook/show.inc
@@ -1,246 +1,249 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/addressbook/show.inc |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show contact details |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// Get contact ID and source ID from request
$cids = rcmail_get_cids();
$source = key($cids);
$cid = $cids ? array_shift($cids[$source]) : null;
// Initialize addressbook source
$CONTACTS = rcmail_contact_source($source, true);
$SOURCE_ID = $source;
// read contact record
if ($cid && ($record = $CONTACTS->get_record($cid, true))) {
$OUTPUT->set_env('readonly', $CONTACTS->readonly || $record['readonly']);
$OUTPUT->set_env('cid', $record['ID']);
$OUTPUT->set_env('compose_extwin', $RCMAIL->config->get('compose_extwin',false));
}
// get address book name (for display)
rcmail_set_sourcename($CONTACTS);
// return raw photo of the given contact
if ($RCMAIL->action == 'photo') {
// search for contact first
if (!$record && ($email = get_input_value('_email', RCUBE_INPUT_GPC))) {
foreach ($RCMAIL->get_address_sources() as $s) {
$abook = $RCMAIL->get_address_book($s['id']);
$result = $abook->search(array('email'), $email, 1, true, true, 'photo');
while ($result && ($record = $result->iterate())) {
if ($record['photo'])
break 2;
}
}
}
// read the referenced file
if (($file_id = get_input_value('_photo', RCUBE_INPUT_GPC)) && ($tempfile = $_SESSION['contacts']['files'][$file_id])) {
$tempfile = $RCMAIL->plugins->exec_hook('attachment_display', $tempfile);
if ($tempfile['status']) {
if ($tempfile['data'])
$data = $tempfile['data'];
else if ($tempfile['path'])
$data = file_get_contents($tempfile['path']);
}
}
else if ($record['photo']) {
$data = is_array($record['photo']) ? $record['photo'][0] : $record['photo'];
if (!preg_match('![^a-z0-9/=+-]!i', $data))
$data = base64_decode($data, true);
}
// let plugins do fancy things with contact photos
$plugin = $RCMAIL->plugins->exec_hook('contact_photo', array('record' => $record, 'email' => $email, 'data' => $data));
// redirect to url provided by a plugin
if ($plugin['url'])
$RCMAIL->output->redirect($plugin['url']);
else
$data = $plugin['data'];
// deliver alt image
if (!$data && ($alt_img = get_input_value('_alt', RCUBE_INPUT_GPC)) && is_file($alt_img))
$data = file_get_contents($alt_img);
// cache for one day if requested by email
if (!$cid && $email)
$RCMAIL->output->future_expire_header(86400);
header('Content-Type: ' . rc_image_content_type($data));
echo $data ? $data : file_get_contents('program/resources/blank.gif');
exit;
}
function rcmail_contact_head($attrib)
{
global $CONTACTS, $RCMAIL;
// check if we have a valid result
if (!(($result = $CONTACTS->get_result()) && ($record = $result->first()))) {
$RCMAIL->output->show_message('contactnotfound');
return false;
}
$form = array(
'head' => array( // section 'head' is magic!
'content' => array(
'prefix' => array('type' => 'text'),
'firstname' => array('type' => 'text'),
'middlename' => array('type' => 'text'),
'surname' => array('type' => 'text'),
'suffix' => array('type' => 'text'),
),
),
);
unset($attrib['name']);
return rcmail_contact_form($form, $record, $attrib);
}
function rcmail_contact_details($attrib)
{
global $CONTACTS, $RCMAIL, $CONTACT_COLTYPES;
// check if we have a valid result
if (!(($result = $CONTACTS->get_result()) && ($record = $result->first()))) {
//$RCMAIL->output->show_message('contactnotfound');
return false;
}
$i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
$form = array(
'contact' => array(
'name' => rcube_label('properties'),
'content' => array(
'email' => array('size' => $i_size, 'render_func' => 'rcmail_render_email_value'),
'phone' => array('size' => $i_size),
'address' => array(),
'website' => array('size' => $i_size, 'render_func' => 'rcmail_render_url_value'),
'im' => array('size' => $i_size),
),
),
'personal' => array(
'name' => rcube_label('personalinfo'),
'content' => array(
'gender' => array('size' => $i_size),
'maidenname' => array('size' => $i_size),
'birthday' => array('size' => $i_size),
'anniversary' => array('size' => $i_size),
'manager' => array('size' => $i_size),
'assistant' => array('size' => $i_size),
'spouse' => array('size' => $i_size),
),
),
);
if (isset($CONTACT_COLTYPES['notes'])) {
$form['notes'] = array(
'name' => rcube_label('notes'),
'content' => array(
'notes' => array('type' => 'textarea', 'label' => false),
),
);
}
if ($CONTACTS->groups) {
$form['groups'] = array(
'name' => rcube_label('groups'),
'content' => rcmail_contact_record_groups($record['ID']),
);
}
return rcmail_contact_form($form, $record);
}
function rcmail_render_email_value($email)
{
return html::a(array(
'href' => 'mailto:' . $email,
'onclick' => sprintf("return %s.command('compose','%s',this)", JS_OBJECT_NAME, JQ($email)),
'title' => rcube_label('composeto'),
'class' => 'email',
), Q($email));
}
function rcmail_render_url_value($url)
{
$prefix = preg_match('!^(http|ftp)s?://!', $url) ? '' : 'http://';
return html::a(array(
'href' => $prefix . $url,
'target' => '_blank',
'class' => 'url',
), Q($url));
}
function rcmail_contact_record_groups($contact_id)
{
global $RCMAIL, $CONTACTS, $GROUPS;
$GROUPS = $CONTACTS->list_groups();
if (empty($GROUPS)) {
return '';
}
$table = new html_table(array('cols' => 2, 'cellspacing' => 0, 'border' => 0));
$members = $CONTACTS->get_record_groups($contact_id);
$checkbox = new html_checkbox(array('name' => '_gid[]',
'class' => 'groupmember', 'disabled' => $CONTACTS->readonly));
- foreach ($GROUPS as $group) {
+ foreach (array_merge($GROUPS, $members) as $group) {
$gid = $group['ID'];
+ if ($seen[$gid]++)
+ continue;
+
$table->add(null, $checkbox->show($members[$gid] ? $gid : null,
array('value' => $gid, 'id' => 'ff_gid' . $gid)));
$table->add(null, html::label('ff_gid' . $gid, Q($group['name'])));
}
$hiddenfields = new html_hiddenfield(array('name' => '_source', 'value' => get_input_value('_source', RCUBE_INPUT_GPC)));
$hiddenfields->add(array('name' => '_cid', 'value' => $contact_id));
$form_start = $RCMAIL->output->request_form(array(
'name' => "form", 'method' => "post",
'task' => $RCMAIL->task, 'action' => 'save',
'request' => 'save.'.intval($contact_id),
'noclose' => true), $hiddenfields->show());
$form_end = '</form>';
$RCMAIL->output->add_gui_object('editform', 'form');
$RCMAIL->output->add_label('addingmember', 'removingmember');
return $form_start . html::tag('fieldset', 'contactfieldgroup contactgroups', $table->show()) . $form_end;
}
$OUTPUT->add_handlers(array(
'contacthead' => 'rcmail_contact_head',
'contactdetails' => 'rcmail_contact_details',
'contactphoto' => 'rcmail_contact_photo',
));
$OUTPUT->send('contact');
diff --git a/program/steps/mail/list_contacts.inc b/program/steps/mail/list_contacts.inc
index 7e3b349cd..a48109fed 100644
--- a/program/steps/mail/list_contacts.inc
+++ b/program/steps/mail/list_contacts.inc
@@ -1,136 +1,157 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/mail/list_contacts.inc |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2012-2013, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send contacts list to client (as remote response) |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$afields = $RCMAIL->config->get('contactlist_fields');
$sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name');
$page_size = $RCMAIL->config->get('addressbook_pagesize', $RCMAIL->config->get('pagesize', 50));
$page = max(1, intval($_GET['_page']));
// Use search result
if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']])) {
$search = (array)$_SESSION['search'][$_REQUEST['_search']];
// get records from all sources
foreach ($search as $s => $set) {
$CONTACTS = $RCMAIL->get_address_book($s);
// reset page
$CONTACTS->set_page(1);
$CONTACTS->set_pagesize(9999);
$CONTACTS->set_search_set($set);
// get records
$result = $CONTACTS->list_records($afields);
while ($row = $result->next()) {
$row['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($row, $sort_col);
$records[$key] = $row;
}
unset($result);
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$first = ($page-1) * $page_size;
$result = new rcube_result_set($count, $first);
// we need only records for current page
if ($page_size < $count) {
$records = array_slice($records, $first, $page_size);
}
$result->records = array_values($records);
}
// list contacts from selected source
else {
$source = get_input_value('_source', RCUBE_INPUT_GPC);
$CONTACTS = $RCMAIL->get_address_book($source);
if ($CONTACTS && $CONTACTS->ready) {
// set list properties
$CONTACTS->set_pagesize($page_size);
$CONTACTS->set_page($page);
+ if ($group_id = get_input_value('_gid', RCUBE_INPUT_GPC)) {
+ $CONTACTS->set_group($group_id);
+ }
// list groups of this source (on page one)
- if ($CONTACTS->groups && $CONTACTS->list_page == 1) {
+ else if ($CONTACTS->groups && $CONTACTS->list_page == 1) {
foreach ($CONTACTS->list_groups() as $group) {
$CONTACTS->reset();
$CONTACTS->set_group($group['ID']);
$group_prop = $CONTACTS->get_group($group['ID']);
// group (distribution list) with email address(es)
if ($group_prop['email']) {
foreach ((array)$group_prop['email'] as $email) {
$row_id = 'G'.$group['ID'];
$jsresult[$row_id] = format_email_recipient($email, $group['name']);
$OUTPUT->command('add_contact_row', $row_id, array(
'contactgroup' => html::span(array('title' => $email), Q($group['name']))), 'group');
}
}
+ // make virtual groups clickable to list their members
+ else if ($group_prop['virtual']) {
+ $row_id = 'G'.$group['ID'];
+ $OUTPUT->command('add_contact_row', $row_id, array(
+ 'contactgroup' => html::a(array(
+ 'href' => '#list',
+ 'rel' => $row['ID'],
+ 'title' => rcube_label('listgroup'),
+ 'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", JS_OBJECT_NAME, $source, $group['ID']),
+ ), Q($group['name']) . '&nbsp;' . html::span('action', '&raquo;'))),
+ 'group',
+ array('ID' => $group['ID'], 'name' => $group['name'], 'virtual' => true));
+ }
// show group with count
else if (($result = $CONTACTS->count()) && $result->count) {
$row_id = 'E'.$group['ID'];
$jsresult[$row_id] = $group['name'];
$OUTPUT->command('add_contact_row', $row_id, array(
'contactgroup' => Q($group['name'] . ' (' . intval($result->count) . ')')), 'group');
}
}
+
+ $CONTACTS->reset();
+ $CONTACTS->set_group(0);
}
// get contacts for this user
- $CONTACTS->set_group(0);
$result = $CONTACTS->list_records($afields);
}
}
if (!empty($result) && !$result->count && $result->searchonly) {
$OUTPUT->show_message('contactsearchonly', 'notice');
}
else if (!empty($result) && $result->count > 0) {
// create javascript list
while ($row = $result->next()) {
$name = rcube_addressbook::compose_list_name($row);
// add record for every email address of the contact
$emails = $CONTACTS->get_col_values('email', $row, true);
foreach ($emails as $i => $email) {
$row_id = $row['ID'].$i;
$jsresult[$row_id] = format_email_recipient($email, $name);
+ $classname = $row['_type'] == 'group' ? 'group' : 'person';
+ $keyname = $row['_type'] == 'group' ? 'contactgroup' : 'contact';
+
$OUTPUT->command('add_contact_row', $row_id, array(
- 'contact' => html::span(array('title' => $email), Q($name ? $name : $email) .
+ $keyname => html::span(array('title' => $email), Q($name ? $name : $email) .
($name && count($emails) > 1 ? '&nbsp;' . html::span('email', Q($email)) : '')
- )), 'person');
+ )), $classname);
}
}
}
// update env
$OUTPUT->set_env('contactdata', $jsresult);
$OUTPUT->set_env('pagecount', ceil($result->count / $page_size));
$OUTPUT->command('set_page_buttons');
// send response
$OUTPUT->send();
diff --git a/skins/classic/addressbook.css b/skins/classic/addressbook.css
index 5afa4592f..415142e0c 100644
--- a/skins/classic/addressbook.css
+++ b/skins/classic/addressbook.css
@@ -1,432 +1,463 @@
/***** Roundcube|Mail address book task styles *****/
#abooktoolbar
{
position: absolute;
top: 45px;
left: 225px;
height: 35px;
}
#abooktoolbar a
{
padding-right: 10px;
}
#abooktoolbar a.button,
#abooktoolbar a.buttonPas,
#abooktoolbar span.separator {
display: block;
float: left;
width: 32px;
height: 32px;
padding: 0;
margin: 0 5px;
overflow: hidden;
background: url(images/abook_toolbar.png) 0 0 no-repeat transparent;
opacity: 0.99; /* this is needed to make buttons appear correctly in Chrome */
}
#abooktoolbar a.buttonPas {
opacity: 0.35;
}
#abooktoolbar a.addcontactSel {
background-position: 0 -32px;
}
#abooktoolbar a.compose {
background-position: -32px 0;
}
#abooktoolbar a.composeSel {
background-position: -32px -32px;
}
#abooktoolbar a.delete {
background-position: -64px 0;
}
#abooktoolbar a.deleteSel {
background-position: -64px -32px;
}
#abooktoolbar a.import {
background-position: -96px 0;
}
#abooktoolbar a.importSel {
background-position: -96px -32px;
}
#abooktoolbar a.export {
background-position: -128px 0;
}
#abooktoolbar a.exportSel {
background-position: -128px -32px;
}
#abooktoolbar a.exportAll {
background-position: -128px 0;
}
#abooktoolbar a.exportAllSel {
background-position: -128px -32px;
}
#abooktoolbar span.separator {
width: 5px;
background-position: -162px 0;
}
#abooktoolbar a.search {
background-position: -170px 0;
}
#abooktoolbar a.searchSel {
background-position: -170px -32px;
}
#abookcountbar
{
margin-top: 4px;
margin-left: 4px;
min-width: 250px;
}
#addressscreen
{
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 205px;
}
#directorylistbox
{
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 195px;
border: 1px solid #999999;
background-color: #F9F9F9;
overflow: hidden;
}
#directorylistbox input
{
margin: 0 0 0 20px;
font-size: 11px;
width: 90%;
}
#addresslist
{
position: absolute;
top: 0;
bottom: 0;
border: 1px solid #999999;
background-color: #F9F9F9;
overflow: hidden;
}
#contactgroupslist
{
border-top: 1px solid #999;
}
#addresslist
{
left: 0px;
width: 280px;
}
#directorylist,
#directorylist li ul
{
list-style: none;
margin: 0;
padding: 0;
background-color: #FFFFFF;
}
#directorylist li ul
{
border-top: 1px solid #EBEBEB;
}
#directorylist li
{
display: block;
font-size: 11px;
border-bottom: 1px solid #EBEBEB;
white-space: nowrap;
}
#directorylist li a
{
cursor: default;
display: block;
padding-left: 25px;
padding-top: 2px;
padding-bottom: 2px;
height: 16px;
text-decoration: none;
white-space: nowrap;
background: url(images/icons/folders.png) 5px -108px no-repeat;
}
#directorylist li ul li a
{
padding-left: 45px;
}
#directorylist li ul li:last-child
{
border-bottom: 0;
}
#directorylist li.contactgroup a
{
background-position: 22px -143px;
}
#directorylist li.contactsearch a
{
background-position: 6px -162px;
}
#directorylist li.selected > a
{
color: #FFF;
font-weight: bold;
background-color: #929292;
}
#directorylist li.droptarget
{
background-color: #FFFFA6;
}
#contacts-table
{
width: 100%;
table-layout: fixed;
}
#contacts-table tbody td
{
cursor: default;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
}
+#contacts-table .contact.readonly td
+{
+ font-style: italic;
+}
+
+#contacts-table td.name
+{
+ width: 95%;
+}
+
+#contacts-table td.action
+{
+ width: 12px;
+ padding: 0px 6px 0 4px;
+ text-align: right;
+}
+
+#contacts-table td.action a
+{
+ font-size: 16px;
+ font-weight: bold;
+ font-style: normal;
+ text-decoration: none;
+ color: #333;
+}
+
+#contacts-table .selected td.action a
+{
+ color: #fff;
+}
+
#contacts-box
{
position: absolute;
top: 0px;
left: 290px;
right: 0px;
bottom: 0px;
border: 1px solid #999999;
overflow: hidden;
}
body.iframe,
#contact-frame
{
background-color: #F2F2F2;
border: none;
min-height: 100%; /* Chrome 14 bug */
}
#contacttabs
{
position: relative;
padding-bottom: 12px;
}
#contacttabs div.tabsbar {
top: 0;
left: 2px;
}
#contacttabs fieldset.tabbed {
position: relative;
top: 22px;
min-height: 5em;
}
#contacthead
{
margin-bottom: 1em;
border: 0;
padding: 0;
}
#contacthead .names span.namefield,
#contacthead .names input
{
font-size: 140%;
}
#contacthead .displayname span.namefield
{
font-size: 120%;
}
#contacthead span.nickname:before,
#contacthead span.nickname:after,
#contacthead input.ff_nickname:before,
#contacthead input.ff_nickname:after
{
content: '"';
}
#contacthead input
{
margin-right: 6px;
margin-bottom: 0.2em;
}
#contacthead .names input,
#contacthead .addnames input
{
width: 180px;
}
#contacthead input.ff_prefix,
#contacthead input.ff_suffix
{
width: 90px;
}
#contacthead .addnames input.ff_name
{
width: 374px;
}
#contactphoto
{
float: right;
width: 60px;
margin-left: 3em;
margin-right: 4px;
}
#contactpic
{
width: 60px;
min-height: 60px;
border: 1px solid #ccc;
background: white;
}
#contactpic img {
width: 60px;
}
#contactpic.droptarget.hover {
background-color: #f0f0ee;
box-shadow: 0 0 5px 0 #999;
-moz-box-shadow: 0 0 5px 0 #999;
-o-box-shadow: 0 0 5px 0 #999;
}
#contactphoto .formlinks
{
margin-top: 0.5em;
text-align: center;
}
fieldset.contactfieldgroup
{
border: 0;
margin: 0.5em 0;
padding: 0.2em 2px;
}
fieldset.contactfieldgroupmulti
{
padding: 0.5em 2px;
}
fieldset.contactfieldgroup legend
{
font-size: 0.9em;
}
.contactfieldgroup .row
{
position: relative;
margin: 0.2em 0;
}
.contactfieldgroup .contactfieldlabel
{
position: absolute;
top: 0;
left: 2px;
width: 110px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #666;
font-weight: bold;
}
.contactfieldgroup .contactfieldlabel select
{
width: 100%;
background: none;
border: 0;
color: #666;
font-weight: bold;
padding-left: 0;
}
.contactfieldgroup .contactfieldcontent
{
padding-left: 120px;
min-height: 1em;
line-height: 1.3em;
}
.contactfieldgroup .contactfield {
line-height: 1.3em;
}
.contactcontrolleraddress .contactfieldcontent input {
margin-bottom: 0.1em;
}
.contactfieldcontent .contactfieldbutton {
vertical-align: middle;
margin-left: 0.5em;
}
#upload-form
{
padding: 6px;
}
#upload-form div
{
padding: 2px;
}
#sourcename
{
color: #666;
font-size: 10px;
margin: -5px 0 8px 2px;
}
#contact-title
{
/* fixes issue where tabs were overlapping box title when scrolling */
z-index: 10;
}
diff --git a/skins/classic/functions.js b/skins/classic/functions.js
index 5dd332ab7..d10812c57 100644
--- a/skins/classic/functions.js
+++ b/skins/classic/functions.js
@@ -1,992 +1,993 @@
/**
* Roundcube functions for default skin interface
*/
/**
* Settings
*/
function rcube_init_settings_tabs()
{
var el, cl, container = $('#tabsbar'),
last_tab = $('span:last', container),
tab = '#settingstabdefault',
action = window.rcmail && rcmail.env.action ? rcmail.env.action : null;
// move About tab to the end
if (last_tab && last_tab.attr('id') != 'settingstababout' && (el = $('#settingstababout'))) {
cl = el.clone(true);
el.remove();
last_tab.after(cl);
}
// get selected tab
if (action)
tab = '#settingstab' + (action == 'preferences' ? 'default' : (action.indexOf('identity')>0 ? 'identities' : action.replace(/\./g, '')));
$(tab).addClass('tablink-selected');
$('a', tab).removeAttr('onclick').click(function() { return false; });
}
// Fieldsets-to-tabs converter
// Warning: don't place "caller" <script> inside page element (id)
function rcube_init_tabs(id, current)
{
var content = $('#'+id),
fs = content.children('fieldset');
if (!fs.length)
return;
current = current ? current : 0;
// first hide not selected tabs
fs.each(function(idx) { if (idx != current) $(this).hide(); });
// create tabs container
var tabs = $('<div>').addClass('tabsbar').appendTo(content);
// convert fildsets into tabs
fs.each(function(idx) {
var tab, a, elm = $(this), legend = elm.children('legend');
// create a tab
a = $('<a>').text(legend.text()).attr('href', '#');
tab = $('<span>').attr({'id': 'tab'+idx, 'class': 'tablink'})
.click(function() { rcube_show_tab(id, idx); return false })
// remove legend
legend.remove();
// style fieldset
elm.addClass('tabbed');
// style selected tab
if (idx == current)
tab.addClass('tablink-selected');
// add the tab to container
tab.append(a).appendTo(tabs);
});
}
function rcube_show_tab(id, index)
{
var fs = $('#'+id).children('fieldset');
fs.each(function(idx) {
// Show/hide fieldset (tab content)
$(this)[index==idx ? 'show' : 'hide']();
// Select/unselect tab
$('#tab'+idx).toggleClass('tablink-selected', idx==index);
});
}
/**
* Mail UI
*/
function rcube_mail_ui()
{
this.popups = {
markmenu: {id:'markmessagemenu'},
replyallmenu: {id:'replyallmenu'},
forwardmenu: {id:'forwardmenu', editable:1},
searchmenu: {id:'searchmenu', editable:1},
messagemenu: {id:'messagemenu'},
attachmentmenu: {id:'attachmentmenu'},
listmenu: {id:'listmenu', editable:1},
dragmessagemenu:{id:'dragmessagemenu', sticky:1},
groupmenu: {id:'groupoptionsmenu', above:1},
mailboxmenu: {id:'mailboxoptionsmenu', above:1},
composemenu: {id:'composeoptionsmenu', editable:1, overlap:1},
spellmenu: {id:'spellmenu'},
// toggle: #1486823, #1486930
uploadmenu: {id:'attachment-form', editable:1, above:1, toggle:!bw.ie&&!bw.linux },
uploadform: {id:'upload-form', editable:1, toggle:!bw.ie&&!bw.linux }
};
var obj;
for (var k in this.popups) {
obj = $('#'+this.popups[k].id)
if (obj.length)
this.popups[k].obj = obj;
else {
delete this.popups[k];
}
}
}
rcube_mail_ui.prototype = {
show_popup: function(popup, show, config)
{
var obj;
// auto-register menu object
if (!this.popups[popup] && (obj = $('#'+popup)) && obj.length)
this.popups[popup] = $.extend(config, {id: popup, obj: obj});
if (typeof this[popup] == 'function')
return this[popup](show);
else
return this.show_popupmenu(popup, show);
},
show_popupmenu: function(popup, show)
{
var obj = this.popups[popup].obj,
above = this.popups[popup].above,
ref = $(this.popups[popup].link ? this.popups[popup].link : rcube_find_object(popup+'link'));
if (typeof show == 'undefined')
show = obj.is(':visible') ? false : true;
else if (this.popups[popup].toggle && show && this.popups[popup].obj.is(':visible') )
show = false;
if (show && ref.length) {
var parent = ref.parent(),
win = $(window),
pos = parent.hasClass('dropbutton') ? parent.offset() : ref.offset();
if (!above && pos.top + ref.height() + obj.height() > win.height())
above = true;
if (pos.left + obj.width() > win.width())
pos.left = win.width() - obj.width() - 30;
obj.css({ left:pos.left, top:(pos.top + (above ? -obj.height() : ref.height())) });
}
obj[show?'show':'hide']();
if (bw.ie6 && this.popups[popup].overlap) {
$('select').css('visibility', show?'hidden':'inherit');
$('select', obj).css('visibility', 'inherit');
}
},
dragmessagemenu: function(show)
{
this.popups.dragmessagemenu.obj[show?'show':'hide']();
},
forwardmenu: function(show)
{
$("input[name='forwardtype'][value="+(rcmail.env.forward_attachment ? 1 : 0)+"]", this.popups.forwardmenu.obj)
.prop('checked', true);
this.show_popupmenu('forwardmenu', show);
},
uploadmenu: function(show)
{
if (typeof show == 'object') // called as event handler
show = false;
// clear upload form
if (!show) {
try { $('#attachment-form form')[0].reset(); }
catch(e){} // ignore errors
}
this.show_popupmenu('uploadmenu', show);
if (!document.all && this.popups.uploadmenu.obj.is(':visible'))
$('#attachment-form input[type=file]').click();
},
searchmenu: function(show)
{
var obj = this.popups.searchmenu.obj,
ref = rcube_find_object('searchmenulink');
if (typeof show == 'undefined')
show = obj.is(':visible') ? false : true;
if (show && ref) {
var pos = $(ref).offset();
obj.css({left:pos.left, top:(pos.top + ref.offsetHeight + 2)});
if (rcmail.env.search_mods) {
var n, all,
list = $('input:checkbox[name="s_mods[]"]', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods;
if (rcmail.env.task == 'mail') {
mods = mods[mbox] ? mods[mbox] : mods['*'];
all = 'text';
}
else {
all = '*';
}
if (mods[all])
list.map(function() {
this.checked = true;
this.disabled = this.value != all;
});
else {
list.prop('disabled', false).prop('checked', false);
for (n in mods)
$('#s_mod_' + n).prop('checked', true);
}
}
}
obj[show?'show':'hide']();
},
set_searchmod: function(elem)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox;
if (!mods)
mods = {};
if (task == 'mail') {
if (!mods[mbox])
mods[mbox] = rcube_clone_object(mods['*']);
m = mods[mbox];
all = 'text';
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem.checked)
delete(m[elem.value]);
else
m[elem.value] = 1;
// mark all fields
if (elem.value != all)
return;
$('input:checkbox[name="s_mods[]"]').map(function() {
if (this == elem)
return;
this.checked = true;
if (elem.checked) {
this.disabled = true;
delete m[this.value];
}
else {
this.disabled = false;
m[this.value] = 1;
}
});
},
listmenu: function(show)
{
var obj = this.popups.listmenu.obj,
ref = rcube_find_object('listmenulink');
if (typeof show == 'undefined')
show = obj.is(':visible') ? false : true;
if (show && ref) {
var pos = $(ref).offset(),
menuwidth = obj.width(),
pagewidth = $(document).width();
if (pagewidth - pos.left < menuwidth && pos.left > menuwidth)
pos.left = pos.left - menuwidth;
obj.css({ left:pos.left, top:(pos.top + ref.offsetHeight + 2)});
// set form values
$('input[name="sort_col"][value="'+rcmail.env.sort_col+'"]').prop('checked', true);
$('input[name="sort_ord"][value="DESC"]').prop('checked', rcmail.env.sort_order == 'DESC');
$('input[name="sort_ord"][value="ASC"]').prop('checked', rcmail.env.sort_order != 'DESC');
$('input[name="view"][value="thread"]').prop('checked', rcmail.env.threading ? true : false);
$('input[name="view"][value="list"]').prop('checked', rcmail.env.threading ? false : true);
// set checkboxes
$('input[name="list_col[]"]').each(function() {
$(this).prop('checked', jQuery.inArray(this.value, rcmail.env.coltypes) != -1);
});
}
obj[show?'show':'hide']();
if (show) {
var maxheight=0;
$('#listmenu fieldset').each(function() {
var height = $(this).height();
if (height > maxheight) {
maxheight = height;
}
});
$('#listmenu fieldset').css("min-height", maxheight+"px")
// IE6 complains if you set this attribute using either method:
//$('#listmenu fieldset').css({'height':'auto !important'});
//$('#listmenu fieldset').css("height","auto !important");
.height(maxheight);
};
},
open_listmenu: function()
{
this.listmenu();
},
save_listmenu: function()
{
this.listmenu();
var sort = $('input[name="sort_col"]:checked').val(),
ord = $('input[name="sort_ord"]:checked').val(),
thread = $('input[name="view"]:checked').val(),
cols = $('input[name="list_col[]"]:checked')
.map(function(){ return this.value; }).get();
rcmail.set_list_options(cols, sort, ord, thread == 'thread' ? 1 : 0);
},
spellmenu: function(show)
{
var link, li,
lang = rcmail.spellcheck_lang(),
menu = this.popups.spellmenu.obj,
ul = $('ul', menu);
if (!ul.length) {
ul = $('<ul>');
for (i in rcmail.env.spell_langs) {
li = $('<li>');
link = $('<a href="#"></a>').text(rcmail.env.spell_langs[i])
.addClass('active').data('lang', i)
.click(function() {
rcmail.spellcheck_lang_set($(this).data('lang'));
});
link.appendTo(li);
li.appendTo(ul);
}
ul.appendTo(menu);
}
// select current language
$('li', ul).each(function() {
var el = $('a', this);
if (el.data('lang') == lang)
el.addClass('selected');
else if (el.hasClass('selected'))
el.removeClass('selected');
});
this.show_popupmenu('spellmenu', show);
},
show_attachmentmenu: function(elem)
{
var id = elem.parentNode.id.replace(/^attach/, '');
$('#attachmenuopen').unbind('click').attr('onclick', '').click(function(e) {
return rcmail.command('open-attachment', id, this);
});
$('#attachmenudownload').unbind('click').attr('onclick', '').click(function() {
rcmail.command('download-attachment', id, this);
});
this.popups.attachmentmenu.link = elem;
rcmail.command('menu-open', {menu: 'attachmentmenu', id: id});
},
menu_open: function(p)
{
if (p && p.props && p.props.menu == 'attachmentmenu')
this.show_popup('attachmentmenu');
else
this.open_listmenu();
},
menu_save: function(prop)
{
this.save_listmenu();
},
body_mouseup: function(evt, p)
{
var i, target = rcube_event.get_target(evt);
for (i in this.popups) {
if (this.popups[i].obj.is(':visible') && target != rcube_find_object(i+'link')
&& !this.popups[i].toggle
&& (!this.popups[i].editable || !this.target_overlaps(target, this.popups[i].id))
&& (!this.popups[i].sticky || !rcube_mouse_is_over(evt, rcube_find_object(this.popups[i].id)))
) {
window.setTimeout('rcmail_ui.show_popup("'+i+'",false);', 50);
}
}
},
target_overlaps: function (target, elementid)
{
var element = rcube_find_object(elementid);
while (target.parentNode) {
if (target.parentNode == element)
return true;
target = target.parentNode;
}
return false;
},
body_keydown: function(evt, p)
{
if (rcube_event.get_keycode(evt) == 27) {
for (var k in this.popups) {
if (this.popups[k].obj.is(':visible'))
this.show_popup(k, false);
}
}
},
switch_preview_pane: function(elem)
{
var uid, prev_frm = $('#mailpreviewframe');
if (elem.checked) {
rcmail.env.contentframe = 'messagecontframe';
if (mailviewsplit.layer) {
mailviewsplit.resize();
mailviewsplit.layer.elm.style.display = '';
}
else
mailviewsplit.init();
if (bw.opera) {
$('#messagelistcontainer').css({height: ''});
}
prev_frm.show();
if (uid = rcmail.message_list.get_single_selection())
rcmail.show_message(uid, false, true);
}
else {
prev_frm.hide();
if (bw.ie6 || bw.ie7) {
var fr = document.getElementById('mailcontframe');
fr.style.bottom = 0;
fr.style.height = parseInt(fr.parentNode.offsetHeight)+'px';
}
else {
$('#mailcontframe').css({height: 'auto', bottom: 0});
if (bw.opera)
$('#messagelistcontainer').css({height: 'auto'});
}
if (mailviewsplit.layer)
mailviewsplit.layer.elm.style.display = 'none';
rcmail.env.contentframe = null;
rcmail.show_contentframe(false);
}
rcmail.command('save-pref', {name: 'preview_pane', value: (elem.checked?1:0)});
},
/* Message composing */
init_compose_form: function()
{
var f, v, field, fields = ['cc', 'bcc', 'replyto', 'followupto'],
div = document.getElementById('compose-div'),
headers_div = document.getElementById('compose-headers-div');
// Show input elements with non-empty value
for (f=0; f<fields.length; f++) {
v = fields[f]; field = $('#_'+v);
if (field.length) {
field.on('change', {v:v}, function(e) { if (this.value) rcmail_ui.show_header_form(e.data.v); });
if (field.val() != '')
rcmail_ui.show_header_form(v);
}
}
// prevent from form data loss when pressing ESC key in IE
if (bw.ie) {
var form = rcube_find_object('form');
form.onkeydown = function (e) {
if (rcube_event.get_keycode(e) == 27)
rcube_event.cancel(e);
};
}
$(window).resize(function() {
rcmail_ui.resize_compose_body();
});
$('#compose-container').resize(function() {
rcmail_ui.resize_compose_body();
});
div.style.top = (parseInt(headers_div.offsetHeight, 10) + 3) + 'px';
$(window).resize();
// fixes contacts-table position when there's more than one addressbook
$('#contacts-table').css('top', $('#directorylist').height() + 24 + 'px');
// contacts search submit
$('#quicksearchbox').keydown(function(e) {
if (rcube_event.get_keycode(e) == 13)
rcmail.command('search');
});
},
resize_compose_body: function()
{
var div = $('#compose-div .boxlistcontent'),
w = div.width() - 2, h = div.height(),
x = bw.ie || bw.opera ? 4 : 0;
$('#compose-body_tbl').width((w+3)+'px').height('');
$('#compose-body_ifr').width((w+3)+'px').height((h-54)+'px');
$('#compose-body').width((w-x)+'px').height(h+'px');
$('#googie_edit_layer').height(h+'px');
},
resize_compose_body_ev: function()
{
window.setTimeout(function(){rcmail_ui.resize_compose_body();}, 100);
},
show_header_form: function(id)
{
var row, s,
link = document.getElementById(id + '-link');
if ((s = this.next_sibling(link)))
s.style.display = 'none';
else if ((s = this.prev_sibling(link)))
s.style.display = 'none';
link.style.display = 'none';
if ((row = document.getElementById('compose-' + id))) {
var div = document.getElementById('compose-div'),
headers_div = document.getElementById('compose-headers-div');
$(row).show();
div.style.top = (parseInt(headers_div.offsetHeight, 10) + 3) + 'px';
this.resize_compose_body();
}
return false;
},
hide_header_form: function(id)
{
var row, ns,
link = document.getElementById(id + '-link'),
parent = link.parentNode,
links = parent.getElementsByTagName('a');
link.style.display = '';
for (var i=0; i<links.length; i++)
if (links[i].style.display != 'none')
for (var j=i+1; j<links.length; j++)
if (links[j].style.display != 'none')
if ((ns = this.next_sibling(links[i]))) {
ns.style.display = '';
break;
}
document.getElementById('_' + id).value = '';
if ((row = document.getElementById('compose-' + id))) {
var div = document.getElementById('compose-div'),
headers_div = document.getElementById('compose-headers-div');
row.style.display = 'none';
div.style.top = (parseInt(headers_div.offsetHeight, 10) + 1) + 'px';
this.resize_compose_body();
}
return false;
},
next_sibling: function(elm)
{
var ns = elm.nextSibling;
while (ns && ns.nodeType == 3)
ns = ns.nextSibling;
return ns;
},
prev_sibling: function(elm)
{
var ps = elm.previousSibling;
while (ps && ps.nodeType == 3)
ps = ps.previousSibling;
return ps;
},
enable_command: function(p)
{
if (p.command == 'reply-list') {
var label = rcmail.gettext(p.status ? 'replylist' : 'replyall');
$('a.button.replyAll').attr('title', label);
}
}
};
/**
* Roundcube generic layer (floating box) class
*
* @constructor
*/
function rcube_layer(id, attributes)
{
this.name = id;
// create a new layer in the current document
this.create = function(arg)
{
var l = (arg.x) ? arg.x : 0,
t = (arg.y) ? arg.y : 0,
w = arg.width,
h = arg.height,
z = arg.zindex,
vis = arg.vis,
parent = arg.parent,
obj = document.createElement('DIV');
obj.id = this.name;
obj.style.position = 'absolute';
obj.style.visibility = (vis) ? (vis==2) ? 'inherit' : 'visible' : 'hidden';
obj.style.left = l+'px';
obj.style.top = t+'px';
if (w)
obj.style.width = w.toString().match(/\%$/) ? w : w+'px';
if (h)
obj.style.height = h.toString().match(/\%$/) ? h : h+'px';
if (z)
obj.style.zIndex = z;
if (parent)
parent.appendChild(obj);
else
document.body.appendChild(obj);
this.elm = obj;
};
// create new layer
if (attributes != null) {
this.create(attributes);
this.name = this.elm.id;
}
else // just refer to the object
this.elm = document.getElementById(id);
if (!this.elm)
return false;
// ********* layer object properties *********
this.css = this.elm.style;
this.event = this.elm;
this.width = this.elm.offsetWidth;
this.height = this.elm.offsetHeight;
this.x = parseInt(this.elm.offsetLeft);
this.y = parseInt(this.elm.offsetTop);
this.visible = (this.css.visibility=='visible' || this.css.visibility=='show' || this.css.visibility=='inherit') ? true : false;
// ********* layer object methods *********
// move the layer to a specific position
this.move = function(x, y)
{
this.x = x;
this.y = y;
this.css.left = Math.round(this.x)+'px';
this.css.top = Math.round(this.y)+'px';
};
// change the layers width and height
this.resize = function(w,h)
{
this.css.width = w+'px';
this.css.height = h+'px';
this.width = w;
this.height = h;
};
// show or hide the layer
this.show = function(a)
{
if(a == 1) {
this.css.visibility = 'visible';
this.visible = true;
}
else if(a == 2) {
this.css.visibility = 'inherit';
this.visible = true;
}
else {
this.css.visibility = 'hidden';
this.visible = false;
}
};
// write new content into a Layer
this.write = function(cont)
{
this.elm.innerHTML = cont;
};
};
/**
* Scroller
*/
function rcmail_scroller(list, top, bottom)
{
var ref = this;
this.list = $(list);
this.top = $(top);
this.bottom = $(bottom);
this.step_size = 6;
this.step_time = 20;
this.delay = 500;
this.top
.mouseenter(function() { ref.ts = window.setTimeout(function() { ref.scroll('down'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.bottom
.mouseenter(function() { ref.ts = window.setTimeout(function() { ref.scroll('up'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.scroll = function(dir)
{
var ref = this, size = this.step_size;
if (!rcmail.drag_active)
return;
if (dir == 'down')
size *= -1;
this.list.get(0).scrollTop += size;
this.ts = window.setTimeout(function() { ref.scroll(dir); }, this.step_time);
};
};
// Events handling in iframes (eg. preview pane)
function iframe_events()
{
// this==iframe
try {
var doc = this.contentDocument ? this.contentDocument : this.contentWindow ? this.contentWindow.document : null;
rcube_event.add_listener({ element: doc, object:rcmail_ui, method:'body_mouseup', event:'mouseup' });
}
catch (e) {
// catch possible "Permission denied" error in IE
};
};
// Abbreviate mailbox names to fit width of the container
function rcube_render_mailboxlist()
{
var list = $('#mailboxlist > li a, #mailboxlist ul:visible > li a');
// it's too slow with really big number of folders, especially on IE
if (list.length > (bw.ie ? 25 : 100))
return;
list.each(function(){
var elem = $(this),
text = elem.data('text');
if (!text) {
text = elem.text().replace(/\s+\(.+$/, '');
elem.data('text', text);
}
if (text.length < 6)
return;
var abbrev = fit_string_to_size(text, elem, elem.width() - elem.children('span.unreadcount').width());
if (abbrev != text)
elem.attr('title', text);
elem.contents().filter(function(){ return (this.nodeType == 3); }).get(0).data = abbrev;
});
};
// inspired by https://gist.github.com/24261/7fdb113f1e26111bd78c0c6fe515f6c0bf418af5
function fit_string_to_size(str, elem, len)
{
var w, span, result = str, ellip = '...';
if (!rcmail.env.tmp_span) {
// it should be appended to elem to use the same css style
// but for performance reasons we'll append it to body (once)
span = $('<b>').css({visibility: 'hidden', padding: '0px'})
.appendTo($('body', document)).get(0);
rcmail.env.tmp_span = span;
}
else {
span = rcmail.env.tmp_span;
}
span.innerHTML = result;
// on first run, check if string fits into the length already.
w = span.offsetWidth;
if (w > len) {
var cut = Math.max(1, Math.floor(str.length * ((w - len) / w) / 2)),
mid = Math.floor(str.length / 2),
offLeft = mid,
offRight = mid;
while (true) {
offLeft = mid - cut;
offRight = mid + cut;
span.innerHTML = str.substring(0,offLeft) + ellip + str.substring(offRight);
// break loop if string fits size
if (offLeft < 3 || span.offsetWidth)
break;
cut++;
}
// build resulting string
result = str.substring(0,offLeft) + ellip + str.substring(offRight);
}
return result;
};
function update_quota(data)
{
percent_indicator(rcmail.gui_objects.quotadisplay, data);
};
// percent (quota) indicator
function percent_indicator(obj, data)
{
if (!data || !obj)
return false;
var limit_high = 80,
limit_mid = 55,
width = data.width ? data.width : rcmail.env.indicator_width ? rcmail.env.indicator_width : 100,
height = data.height ? data.height : rcmail.env.indicator_height ? rcmail.env.indicator_height : 14,
quota = data.percent ? Math.abs(parseInt(data.percent)) : 0,
quota_width = parseInt(quota / 100 * width),
pos = $(obj).position();
// workarounds for Opera and Webkit bugs
pos.top = Math.max(0, pos.top);
pos.left = Math.max(0, pos.left);
rcmail.env.indicator_width = width;
rcmail.env.indicator_height = height;
// overlimit
if (quota_width > width) {
quota_width = width;
quota = 100;
}
if (data.title)
data.title = rcmail.get_label('quota') + ': ' + data.title;
// main div
var main = $('<div>');
main.css({position: 'absolute', top: pos.top, left: pos.left,
width: width + 'px', height: height + 'px', zIndex: 100, lineHeight: height + 'px'})
.attr('title', data.title).addClass('quota_text').html(quota + '%');
// used bar
var bar1 = $('<div>');
bar1.css({position: 'absolute', top: pos.top + 1, left: pos.left + 1,
width: quota_width + 'px', height: height + 'px', zIndex: 99});
// background
var bar2 = $('<div>');
bar2.css({position: 'absolute', top: pos.top + 1, left: pos.left + 1,
width: width + 'px', height: height + 'px', zIndex: 98})
.addClass('quota_bg');
if (quota >= limit_high) {
main.addClass(' quota_text_high');
bar1.addClass('quota_high');
}
else if(quota >= limit_mid) {
main.addClass(' quota_text_mid');
bar1.addClass('quota_mid');
}
else {
main.addClass(' quota_text_low');
bar1.addClass('quota_low');
}
// replace quota image
$(obj).html('').append(bar1).append(bar2).append(main);
// update #quotaimg title
$('#quotaimg').attr('title', data.title);
};
// Optional parameters used by TinyMCE
var rcmail_editor_settings = {
skin : "default", // "default", "o2k7"
skin_variant : "" // "", "silver", "black"
};
var rcmail_ui;
function rcube_init_mail_ui()
{
rcmail_ui = new rcube_mail_ui();
rcube_event.add_listener({ object:rcmail_ui, method:'body_mouseup', event:'mouseup' });
rcube_event.add_listener({ object:rcmail_ui, method:'body_keydown', event:'keydown' });
if (rcmail.env.quota_content)
update_quota(rcmail.env.quota_content);
rcmail.addEventListener('setquota', update_quota);
$('iframe').load(iframe_events)
.contents().mouseup(function(e){rcmail_ui.body_mouseup(e)});
if (rcmail.env.task == 'mail') {
rcmail.addEventListener('enable-command', 'enable_command', rcmail_ui);
rcmail.addEventListener('menu-open', 'menu_open', rcmail_ui);
rcmail.addEventListener('menu-save', 'menu_save', rcmail_ui);
rcmail.addEventListener('aftersend-attachment', 'uploadmenu', rcmail_ui);
rcmail.addEventListener('aftertoggle-editor', 'resize_compose_body_ev', rcmail_ui);
rcmail.gui_object('message_dragmenu', 'dragmessagemenu');
if (rcmail.gui_objects.mailboxlist) {
rcmail.addEventListener('responseaftermark', rcube_render_mailboxlist);
rcmail.addEventListener('responseaftergetunread', rcube_render_mailboxlist);
rcmail.addEventListener('responseaftercheck-recent', rcube_render_mailboxlist);
rcmail.addEventListener('aftercollapse-folder', rcube_render_mailboxlist);
+ rcmail.addEventListener('afterimport-messages', function(){ rcmail_ui.show_popup('uploadform', false); });
new rcmail_scroller('#mailboxlist-content', '#mailboxlist-title', '#mailboxlist-footer');
}
if (rcmail.env.action == 'compose')
rcmail_ui.init_compose_form();
else if (rcmail.env.action == 'show' || rcmail.env.action == 'preview')
// add menu link for each attachment
$('#attachment-list > li[id^="attach"]').each(function() {
$(this).append($('<a class="drop">').click(function() { rcmail_ui.show_attachmentmenu(this); }));
});
}
else if (rcmail.env.task == 'addressbook') {
rcmail.addEventListener('afterupload-photo', function(){ rcmail_ui.show_popup('uploadform', false); });
if (rcmail.gui_objects.folderlist)
new rcmail_scroller('#directorylist-content', '#directorylist-title', '#directorylist-footer');
}
else if (rcmail.env.task == 'settings') {
if (rcmail.gui_objects.subscriptionlist)
new rcmail_scroller('#folderlist-content', '#folderlist-title', '#folderlist-footer');
}
}
diff --git a/skins/classic/images/contactgroup.png b/skins/classic/images/contactgroup.png
new file mode 100644
index 000000000..c46383255
Binary files /dev/null and b/skins/classic/images/contactgroup.png differ
diff --git a/skins/classic/mail.css b/skins/classic/mail.css
index 7c350ca3d..0193e87ff 100644
--- a/skins/classic/mail.css
+++ b/skins/classic/mail.css
@@ -1,1741 +1,1752 @@
/***** Roundcube|Mail mail task styles *****/
#messagetoolbar
{
position: absolute;
top: 47px;
left: 205px;
right: 10px;
height: 35px;
min-width: 650px;
white-space: nowrap;
/* border: 1px solid #cccccc; */
}
.extwin #messagetoolbar
{
top: 5px;
left: 20px;
}
#messagetoolbar a,
#messagetoolbar select
{
display: block;
float: left;
padding-right: 10px;
}
#messagetoolbar a.button,
#messagetoolbar a.buttonPas {
display: block;
float: left;
width: 32px;
height: 32px;
padding: 0;
margin: 0 5px;
overflow: hidden;
background: url(images/mail_toolbar.png) 0 0 no-repeat transparent;
opacity: 0.99; /* this is needed to make buttons appear correctly in Chrome */
}
#messagetoolbar a.buttonPas {
opacity: 0.35;
}
#messagetoolbar a.button.selected {
background-color: #ddd;
margin-left: 4px;
margin-right: 4px;
margin-top: -1px;
border: 1px solid #ccc;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
}
#messagetoolbar a.checkmailSel {
background-position: 0 -32px;
}
#messagetoolbar a.back {
background-position: -32px 0;
}
#messagetoolbar a.backSel {
background-position: -32px -32px;
}
#messagetoolbar a.compose {
background-position: -64px 0;
}
#messagetoolbar a.composeSel {
background-position: -64px -32px;
}
#messagetoolbar a.reply {
background-position: -96px 0;
}
#messagetoolbar a.replySel {
background-position: -96px -32px;
}
#messagetoolbar a.replyAll {
background-position: -128px 0;
}
#messagetoolbar a.replyAllSel {
background-position: -128px -32px;
}
#messagetoolbar a.forward {
background-position: -160px 0;
}
#messagetoolbar a.forwardSel {
background-position: -160px -32px;
}
#messagetoolbar a.delete {
background-position: -192px 0;
}
#messagetoolbar a.deleteSel {
background-position: -192px -32px;
}
#messagetoolbar a.markmessage {
background-position: -256px 0;
}
#messagetoolbar a.messagemenu {
background-position: -288px 0;
}
#messagetoolbar a.spellcheck {
background-position: -384px 0;
}
#messagetoolbar a.spellcheckSel {
background-position: -384px -32px;
}
#messagetoolbar a.attach {
background-position: -352px 0;
}
#messagetoolbar a.attachSel {
background-position: -352px -32px;
}
#messagetoolbar a.insertsig {
background-position: -448px 0;
}
#messagetoolbar a.insertsigSel {
background-position: -448px -32px;
}
#messagetoolbar a.savedraft {
background-position: -322px 0;
}
#messagetoolbar a.savedraftSel {
background-position: -322px -32px;
}
#messagetoolbar a.send {
background-position: -416px 0;
}
#messagetoolbar a.sendSel {
background-position: -416px -32px;
}
#messagetoolbar select.mboxlist
{
position: relative;
margin: 0 8px;
top: 7px;
}
#messagetoolbar select.mboxlist option
{
padding-left: 15px;
}
#messagetoolbar select.mboxlist option[value=""]
{
padding-left: 2px;
}
#messagemenu li a.active:hover,
#attachmentmenu li a.active:hover,
#markmessagemenu li a.active:hover
{
color: #fff;
background-color: #c00;
}
#messagemenu li a,
#attachmentmenu li a
{
background: url(images/messageactions.png) no-repeat 7px 0;
background-position: 7px 20px;
}
#messagemenu li a.printlink
{
background-position: 7px 1px;
}
#messagemenu li a.downloadlink,
#attachmentmenu li a.downloadlink
{
background-position: 7px -17px;
}
#messagemenu li a.sourcelink
{
background-position: 7px -35px;
}
#messagemenu li a.openlink,
#attachmentmenu li a.openlink
{
background-position: 7px -53px;
}
#messagemenu li a.editlink
{
background-position: 7px -71px;
}
#markmessagemenu li a,
#compose-attachments li a
{
background: url(images/messageicons.png) no-repeat;
}
#markmessagemenu li a.readlink
{
background-position: 7px -51px;
}
#markmessagemenu li a.unreadlink
{
background-position: 7px -119px;
}
#markmessagemenu li a.flaggedlink
{
background-position: 7px -153px;
}
#markmessagemenu li a.unflaggedlink
{
background-position: 7px -136px;
}
#searchfilter
{
white-space: nowrap;
position: absolute;
right: 198px;
vertical-align: middle;
}
#searchfilter label
{
font-size: 11px;
}
#mailleftcontainer
{
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 160px;
}
#mailrightcontainer
{
position: absolute;
top: 0;
left: 170px;
bottom: 0;
right: 0;
min-width: 600px;
}
#mailrightcontent
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#messagepartcontainer
{
position: absolute;
top: 80px;
left: 20px;
right: 20px;
bottom: 20px;
}
#mailcontframe
{
position: absolute;
width: 100%;
top: 0;
bottom: 0;
border: 1px solid #999999;
background-color: #F9F9F9;
overflow: hidden;
}
#mailpreviewframe
{
position: absolute;
width: 100%;
top: 205px;
bottom: 0px;
border: 1px solid #999999;
background-color: #F9F9F9;
}
#messagecontframe
{
position: relative;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
width: 100%;
height: 100%;
min-height: 100%; /* Chrome 14 bug */
}
#messagepartframe
{
width: 100%;
height: 100%;
min-height: 100%; /* Chrome 14 bug */
border: 1px solid #999999;
background-color: #F9F9F9;
}
#partheader
{
position: absolute;
top: 10px;
left: 220px;
right: 20px;
height: 40px;
}
#partheader table td
{
padding-left: 2px;
padding-right: 4px;
vertical-align: middle;
font-size: 11px;
}
#partheader table td.title
{
color: #666666;
font-weight: bold;
}
/** mailbox list styles */
#mailboxlist-container
{
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
border: 1px solid #999;
background-color: #F9F9F9;
}
#mailboxlist
{
position:relative;
height: auto;
margin: 0px;
padding: 0px;
list-style-image: none;
list-style-type: none;
overflow: hidden;
white-space: nowrap;
background-color: #FFF;
}
#mailboxlist li
{
display: block;
position: relative;
font-size: 11px;
background: url(images/icons/folders.png) 5px 0 no-repeat;
border-bottom: 1px solid #EBEBEB;
}
#mailboxlist li ul li:last-child
{
border-bottom: none;
}
#mailboxlist li.inbox
{
background-position: 5px -18px;
}
#mailboxlist li.drafts
{
background-position: 5px -37px;
}
#mailboxlist li.sent
{
background-position: 5px -54px;
}
#mailboxlist li.junk
{
background-position: 5px -73px;
}
#mailboxlist li.trash
{
background-position: 5px -91px;
}
#mailboxlist li a
{
cursor: default;
display: block;
position: relative;
padding-left: 25px;
padding-top: 2px;
padding-bottom: 2px;
text-decoration: none;
height: 15px;
}
#mailboxlist li.unread
{
font-weight: bold;
}
#mailboxlist li.virtual > a
{
color: #666;
}
#mailboxlist li.recent > a
{
color: #0066FF;
}
#mailboxlist li.selected,
#mailboxlist li.droptarget li.selected
{
background-color: #929292;
}
#mailboxlist li.selected > a,
#mailboxlist li.droptarget li.selected a
{
color: #FFF;
font-weight: bold;
}
#mailboxlist li.droptarget
{
background-color: #FFFFA6;
}
/* styles for nested folders */
#mailboxlist ul {
list-style: none;
padding: 0;
margin: 0;
border-top: 1px solid #EBEBEB;
padding-left: 15px;
background-position: 25px 1px;
background-color: #FFF;
color: blue;
font-weight: normal;
}
#listcontrols
{
position: relative;
white-space: nowrap;
line-height: 22px;
padding: 0 4px;
width: auto;
min-width: 300px;
}
#listcontrols a,
#listcontrols span
{
display: block;
float: left;
font-size: 11px;
}
#listcontrols span input
{
vertical-align: middle;
}
#listcontrols a.button,
#listcontrols a.buttonPas
{
display: block;
float: left;
width: 15px;
height: 15px;
padding: 0;
margin-top: 4px;
margin-right: 2px;
overflow: hidden;
background: url(images/mail_footer.png) 0 0 no-repeat transparent;
opacity: 0.99; /* this is needed to make buttons appear correctly in Chrome */
}
#listcontrols a.buttonPas
{
opacity: 0.35;
}
#listcontrols a.all {
background-position: -30px 0;
}
#listcontrols a.allsel {
background-position: -30px -15px;
}
#listcontrols a.page {
background-position: -135px 0;
}
#listcontrols a.pagesel {
background-position: -135px -15px;
}
#listcontrols a.unread {
background-position: -45px 0;
}
#listcontrols a.unreadsel {
background-position: -45px -15px;
}
#listcontrols a.invert {
background-position: -60px 0;
}
#listcontrols a.invertsel {
background-position: -60px -15px;
}
#listcontrols a.none {
background-position: -75px 0;
}
#listcontrols a.nonesel {
background-position: -75px -15px;
}
#listcontrols a.expand-all {
background-position: -90px 0;
}
#listcontrols a.expand-allsel {
background-position: -90px -15px;
}
#listcontrols a.collapse-all {
background-position: -105px 0;
}
#listcontrols a.collapse-allsel {
background-position: -105px -15px;
}
#listcontrols a.expand-unread {
background-position: -120px 0;
}
#listcontrols a.expand-unreadsel {
background-position: -120px -15px;
}
#countcontrols
{
position: absolute;
top: 4px;
right: 4px;
white-space: nowrap;
font-size: 11px;
}
#countcontrols a.button,
#countcontrols a.buttonPas
{
float: right;
}
/** message list styles */
body.messagelist
{
margin: 0px;
background-color: #F9F9F9;
}
#messagelist
{
width: 100%;
display: table;
table-layout: fixed;
}
#messagelist thead tr td
{
height: 20px;
padding: 0 4px 0 2px;
vertical-align: middle;
border-bottom: 1px solid #999999;
color: #333333;
background: url(images/listheader.gif) top left repeat-x #CCC;
font-size: 11px;
font-weight: bold;
}
#messagelist thead tr td.sortedASC,
#messagelist thead tr td.sortedDESC
{
background-position: 0 -26px;
}
#messagelist thead tr td.sortedASC a
{
background: url(images/icons/sort.gif) right 0 no-repeat;
}
#messagelist thead tr td.sortedDESC a
{
background: url(images/icons/sort.gif) right -14px no-repeat;
}
#messagelist thead tr td a
{
display: block;
width: auto !important;
width: 100%;
color: #333333;
text-decoration: none;
}
#messagelist thead tr td.size
{
text-align: left;
}
#messagelist thead tr td.subject
{
padding-left: 18px;
width: 99%;
}
#messagelist tbody tr td
{
height: 20px;
padding: 0;
font-size: 11px;
overflow: hidden;
vertical-align: middle;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
border-bottom: 1px solid #EBEBEB;
cursor: default;
}
#messagelist tbody tr td a
{
color: #000;
text-decoration: none;
white-space: nowrap;
cursor: inherit;
}
#messagelist td img
{
vertical-align: middle;
display: inline-block;
}
#messagelist tbody tr td.flag,
#messagelist tbody tr td.status,
#messagelist tbody tr td.subject span.status
{
cursor: pointer;
}
#messagelist tr td.flag span,
#messagelist tr td.status span,
#messagelist tr td.attachment span,
#messagelist tr td.priority span
{
display: block;
width: 15px;
}
#messagelist tr td div.collapsed,
#messagelist tr td div.expanded,
#messagelist tr td.threads div.listmenu,
#messagelist tr td.attachment span.attachment,
#messagelist tr td.attachment span.report,
#messagelist tr td.priority span.priority,
#messagelist tr td.priority span.prio1,
#messagelist tr td.priority span.prio2,
#messagelist tr td.priority span.prio3,
#messagelist tr td.priority span.prio4,
#messagelist tr td.priority span.prio5,
#messagelist tr td.flag span.flagged,
#messagelist tr td.flag span.unflagged,
#messagelist tr td.flag span.unflagged:hover,
#messagelist tr td.status span.status,
#messagelist tr td.status span.msgicon,
#messagelist tr td.status span.deleted,
#messagelist tr td.status span.unread,
#messagelist tr td.status span.unreadchildren,
#messagelist tr td.subject span.msgicon,
#messagelist tr td.subject span.deleted,
#messagelist tr td.subject span.unread,
#messagelist tr td.subject span.replied,
#messagelist tr td.subject span.forwarded,
#messagelist tr td.subject span.unreadchildren
{
display: inline-block;
vertical-align: middle;
height: 17px;
width: 15px;
background: url(images/messageicons.png) center no-repeat;
}
#messagelist tr td.attachment span.attachment
{
background-position: 0 -170px;
}
#messagelist tr td.attachment span.report
{
background-position: 0 -255px;
}
#messagelist tr td.priority span.priority
{
background-position: 0 -309px;
}
#messagelist tr td.priority span.prio5
{
background-position: 0 -358px;
}
#messagelist tr td.priority span.prio4
{
background-position: 0 -340px;
}
#messagelist tr td.priority span.prio3
{
background-position: 0 -324px;
}
#messagelist tr td.priority span.prio2
{
background-position: 0 -309px;
}
#messagelist tr td.priority span.prio1
{
background-position: 0 -290px;
}
#messagelist tr td.flag span.flagged
{
background-position: 0 -153px;
}
#messagelist tr td.flag span.unflagged:hover
{
background-position: 0 -136px;
}
#messagelist tr td.subject span.msgicon,
#messagelist tr td.subject span.unreadchildren
{
background-position: 0 -51px;
margin: 0 2px;
}
#messagelist tr td.subject span.replied
{
background-position: 0 -85px;
}
#messagelist tr td.subject span.forwarded
{
background-position: 0 -68px;
}
#messagelist tr td.subject span.replied.forwarded
{
background-position: 0 -102px;
}
#messagelist tr td.status span.msgicon,
#messagelist tr td.flag span.unflagged,
#messagelist tr td.status span.unreadchildren
{
background-position: 0 17px; /* no icon */
}
#messagelist tr td.status span.msgicon:hover
{
background-position: 0 -272px;
}
#messagelist tr td.status span.deleted,
#messagelist tr td.subject span.deleted
{
background-position: 0 -187px;
}
#messagelist tr td.status span.status,
#messagelist tr td.status span.unread,
#messagelist tr td.subject span.unread
{
background-position: 0 -119px;
}
#messagelist tr td div.collapsed
{
background-position: 0 -221px;
cursor: pointer;
}
#messagelist tr td div.expanded
{
background-position: 0 -204px;
cursor: pointer;
}
#messagelist tr td.threads div.listmenu
{
background-position: 0 -238px;
cursor: pointer;
}
#messagelist tbody tr td.subject
{
width: 99%;
}
#messagelist tbody tr td.subject a
{
cursor: default;
vertical-align: middle; /* #1487091 */
}
/* thread parent message with unread children */
#messagelist tbody tr.unroot td.subject a
{
text-decoration: underline;
}
#messagelist tr td.attachment,
#messagelist tr td.threads,
#messagelist tr td.status,
#messagelist tr td.flag,
#messagelist tr td.priority
{
width: 17px;
padding: 0 0 0 2px;
}
#messagelist tr td.size
{
width: 60px;
text-align: right;
padding: 0 2px;
}
#messagelist tr td.fromto,
#messagelist tr td.from,
#messagelist tr td.to,
#messagelist tr td.cc,
#messagelist tr td.replyto
{
width: 180px;
padding: 0 2px;
}
#messagelist tr td.date
{
width: 118px;
padding: 0 2px;
}
#messagelist tr.message
{
background-color: #FFF;
}
#messagelist tr.unread
{
font-weight: bold;
background-color: #FFFFFF;
}
#messagelist tr.flagged td,
#messagelist tr.flagged td a
{
color: #CC0000;
}
#messagelist tr.selected td
{
color: #FFFFFF;
background-color: #CC3333;
}
#messagelist tr.unfocused td
{
color: #FFFFFF;
background-color: #929292;
}
#messagelist tr.selected td a
{
color: #FFFFFF;
}
#messagelist tr.unfocused td a
{
color: #FFFFFF;
}
#messagelist tr.deleted td,
#messagelist tr.deleted td a
{
color: #CCCCCC;
}
#listmenu
{
padding: 6px;
}
#listmenu legend
{
color: #999999;
}
#listmenu fieldset
{
border: 1px solid #999999;
margin: 0 5px;
float: left;
}
#listmenu div
{
padding: 8px 0 3px 0;
text-align: center;
clear: both;
}
/***** tree indicators *****/
td span.branch div
{
float: left;
height: 16px;
}
td span.branch div.tree
{
height: 17px;
width: 15px;
background: url(images/tree.gif) 0px 0px no-repeat;
}
td span.branch div.l1
{
background-position: 0px 0px; /* L */
}
td span.branch div.l2
{
background-position: -30px 0px; /* | */
}
td span.branch div.l3
{
background-position: -15px 0px; /* |- */
}
/** message view styles */
#messageframe
{
position: absolute;
top: 0;
left: 180px;
right: 0;
bottom: 0;
border: 1px solid #999;
background-color: #FFF;
overflow: auto;
z-index: 1;
}
.extwin #messageframe
{
left: 0;
}
div.messageheaderbox
{
margin: -14px 8px 0px 8px;
border: 1px solid #ccc;
}
table.headers-table
{
width: 100%;
background-color: #EBEBEB;
}
#messagebody #full-headers,
#messagebody table.headers-table
{
width: auto;
margin: 6px 8px;
background-color: #F4F4F4;
}
#messagebody table.headers-table
{
margin: 16px 6px 6px 6px;
}
div.message-partheaders + div.message-part
{
border-top: 0;
padding-top: 4px;
}
table.headers-table tr td
{
font-size: 11px;
border-bottom:1px solid #FFFFFF;
}
table.headers-table tr td.header-title
{
width: 1%;
color: #666666;
font-weight: bold;
text-align: right;
white-space: nowrap;
padding: 0 4px 0 8px;
}
table.headers-table tr td.header
{
width: 99%;
}
table.headers-table tr td.subject
{
font-weight: bold;
}
table.headers-table tr td.header span
{
white-space: nowrap;
}
#attachment-list
{
margin: 0;
padding: 0 4px 0 8px;
min-height: 16px;
list-style-image: none;
list-style-type: none;
background: url(images/icons/attachment.png) 4px 2px no-repeat #DFDFDF;
}
#messageframe #attachment-list
{
border-bottom: 1px solid #ccc;
}
.messageheaderbox #attachment-list
{
border-top: 1px solid #ccc;
}
#attachment-list:after
{
content: ".";
display: block;
height: 0;
font-size: 0;
clear: both;
visibility: hidden;
}
#attachment-list li
{
float: left;
height: 18px;
font-size: 11px;
padding: 2px 0px 0px 15px;
white-space: nowrap;
}
#attachment-list li a
{
text-decoration: none;
}
#attachment-list li a:hover
{
text-decoration: underline;
}
#attachment-list li a.drop {
background: url(images/icons/down_small.gif) no-repeat center 6px;
width: 12px;
height: 7px;
cursor: pointer;
padding: 5px 0 0;
margin-left: 3px;
display: inline-block;
}
#messagebody
{
position:relative;
padding-bottom: 10px;
background-color: #FFFFFF;
}
div.message-part,
div.message-htmlpart
{
padding: 10px 8px;
border-top: 1px solid #ccc;
/* overflow: hidden; */
}
#messagebody div:first-child
{
border-top: 0;
}
div.message-part a,
div.message-htmlpart a
{
color: #0000CC;
}
div.message-part pre,
div.message-htmlpart pre,
div.message-part div.pre
{
margin: 0px;
padding: 0px;
font-family: monospace;
font-size: 12px;
white-space: -moz-pre-wrap !important;
white-space: pre-wrap !important;
white-space: pre;
}
div.message-part span.sig
{
color: #666666;
}
div.message-part blockquote
{
color: blue;
border-left: 2px solid blue;
border-right: 2px solid blue;
background-color: #F6F6F6;
margin: 2px 0px;
padding: 1px 8px 1px 10px;
}
div.message-part blockquote blockquote
{
color: green;
border-left: 2px solid green;
border-right: 2px solid green;
}
div.message-part blockquote blockquote blockquote
{
color: #990000;
border-left: 2px solid #bb0000;
border-right: 2px solid #bb0000;
}
body.iframe div.message-htmlpart
{
margin: 8px;
}
div.message-htmlpart div.rcmBody
{
margin: 8px;
}
#messagebody span.part-notice
{
display: block;
}
#message-objects div,
#messagebody span.part-notice
{
margin: 8px;
min-height: 20px;
padding: 10px 10px 6px 46px;
}
#message-objects div a,
#messagebody span.part-notice a
{
color: #666666;
padding-left: 10px;
}
#message-objects div a:hover,
#messagebody span.part-notice a:hover
{
color: #333333;
}
#messagebody fieldset.image-attachment {
border: 0;
border-top: 1px solid #ccc;
margin: 1em 1em 0 1em;
}
#messagebody fieldset.image-attachment p > img
{
max-width: 80%;
}
#messagebody legend.image-filename
{
color: #999;
font-size: 0.9em;
}
#messagebody p.image-attachment
{
margin: 0 1em;
padding: 1em;
border-top: 1px solid #ccc;
}
#messagebody p.image-attachment a.image-link
{
float: left;
margin-right: 2em;
min-width: 160px;
min-height: 60px;
text-align: center;
}
#messagebody p.image-attachment .image-filename
{
display: block;
font-weight: bold;
line-height: 1.6em;
}
#messagebody p.image-attachment .image-filesize
{
font-size: 11px;
padding-right: 1em;
}
#messagebody p.image-attachment .attachment-links a
{
margin-right: 0.6em;
color: #cc0000;
font-size: 11px;
text-decoration: none;
}
#messagebody p.image-attachment .attachment-links a:hover
{
text-decoration: underline;
}
#messagelinks
{
position: absolute;
top: 8px;
right: 10px;
height: 16px;
text-align: right;
}
#messageframe #messagelinks
{
top: 2px;
right: 2px;
}
#compose-headers #openextwinlink
{
position: absolute;
height: 15px;
top: 4px;
right: 2px;
}
#full-headers
{
color: #666666;
text-align: center;
padding: 2px 6px;
border-bottom: 1px solid #ccc;
background-color: #EBEBEB;
}
.messageheaderbox #full-headers
{
border-bottom: 0;
}
div.more-headers
{
cursor: pointer;
height: 8px;
border-bottom: 0;
}
div.show-headers
{
background: url(images/icons/down_small.gif) no-repeat center;
}
div.hide-headers
{
background: url(images/icons/up_small.gif) no-repeat center;
}
#headers-source
{
margin: 2px 0;
padding: 0.5em;
height: 145px;
background: white;
overflow: auto;
font-size: 11px;
border: 1px solid #CCC;
display: none;
text-align: left;
color: #333;
}
/** message compose styles */
#compose-container
{
position: absolute;
top: 0;
left: 205px;
right: 0;
bottom: 0;
margin: 0;
}
#compose-div
{
position: absolute;
top: 85px;
right: 0;
left: 0;
bottom: 0;
margin: 0;
}
#compose-body-div
{
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 175px;
border: 1px solid #999;
}
#compose-div .boxlistcontent
{
bottom: 23px;
}
#compose-body
{
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: 0;
font-size: 9pt;
font-family: monospace;
resize: none;
border: none;
outline: none;
}
#compose-body_tbl,
#compose-body_tbl td
{
border: none;
}
#compose-body_tbl tr.mceFirst td.mceToolbar
{
border-bottom: 1px solid #ccc;
}
#compose-headers
{
width: 100%;
}
#compose-headers td.editfield
{
padding-right: 8px;
width: 95%;
}
#compose-headers td.top
{
vertical-align: top;
}
#compose-headers td.title,
#compose-subject td.title
{
width: 80px !important;
font-size: 11px;
font-weight: bold;
padding-right: 10px;
white-space: nowrap;
color: #666;
}
#compose-headers td textarea,
#compose-headers td input
{
resize: none;
width: 100%;
border: 1px solid #999;
}
#compose-headers td textarea
{
height: 32px;
}
input.from_address
{
width: 80% !important;
}
#compose-cc,
#compose-bcc,
#compose-replyto,
#compose-followupto
{
display: none;
}
#compose-editorfooter
{
position: absolute;
right: 5px;
bottom: 0;
text-align: right;
line-height: 20px;
}
#compose-editorfooter label
{
font-size: 11px;
font-weight: bold;
color: #666;
}
#compose-buttons
{
position: absolute;
left: 5px;
bottom: 1px;
width: auto;
}
#compose-contacts
{
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 195px;
border: 1px solid #999;
background-color: #F9F9F9;
}
#compose-attachments
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid #999;
background-color: #F9F9F9;
}
#compose-attachments.droptarget.hover
{
background-color: #F0F0EE;
box-shadow: 0 0 5px 0 #999;
-moz-box-shadow: 0 0 5px 0 #999;
-o-box-shadow: 0 0 5px 0 #999;
}
#compose-attachments ul
{
margin: 0px;
padding: 0px;
background-color: #FFF;
list-style-image: none;
list-style-type: none;
}
#compose-attachments ul li
{
height: 18px;
font-size: 11px;
padding-left: 2px;
padding-top: 2px;
padding-right: 4px;
border-bottom: 1px solid #EBEBEB;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
}
#compose-attachments li a
{
text-indent: -5000px;
width: 17px;
height: 16px;
display: inline-block;
text-decoration: none;
}
#compose-attachments li img
{
vertical-align: middle;
}
#compose-attachments li a.delete,
#compose-attachments li a.cancelupload
{
background-position: 0px -392px;
}
#compose-attachments li span
{
line-height: 18px;
vertical-align: middle;
}
+#upload-form,
#attachment-form
{
padding: 6px;
}
+#upload-form div,
#attachment-form div
{
padding: 2px;
}
+#upload-form div.buttons,
#attachment-form div.buttons
{
margin-top: 4px;
}
#quota
{
position: absolute;
top: 3px;
right: 8px;
width: 100px;
}
#quotaimg
{
position: absolute;
top: 3px;
right: 6px;
z-index: 101;
}
/* addressbook in compose - copy from addressbook.css */
#directorylist
{
list-style: none;
margin: 0;
padding: 0;
background-color: #FFFFFF;
}
#directorylist li
{
display: block;
font-size: 11px;
background: url(images/icons/folders.png) 5px -108px no-repeat;
border-bottom: 1px solid #EBEBEB;
white-space: nowrap;
}
#directorylist li a
{
cursor: default;
display: block;
padding-left: 25px;
padding-top: 2px;
padding-bottom: 2px;
text-decoration: none;
white-space: nowrap;
height: 15px;
}
#directorylist li.selected
{
background-color: #929292;
border-bottom: 1px solid #898989;
}
#directorylist li.selected a
{
color: #FFF;
font-weight: bold;
}
#contacts-table
{
width: 100%;
table-layout: fixed;
}
#contacts-table tbody td
{
cursor: default;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
}
+#contacts-table td span.email
+{
+ display: inline;
+ color: #ccc;
+ font-style: italic;
+ margin-left: 0.5em;
+}
+
#abookcountbar
{
margin-top: 4px;
margin-left: 4px;
position: absolute;
margin-right: 5px;
right: 0;
}
#abookactions
{
position: absolute;
text-underline: none;
}
#abookactions a
{
font-weight: bold;
line-height: 22px;
height: 22px;
width: auto;
margin: 0;
padding-left: 5px;
padding-right: 5px;
text-shadow: 1px 1px white;
background: url("images/icons/groupactions.png") no-repeat right -70px;
}
#abookactions a.disabled
{
color: #999;
}
#compose-contacts #quicksearchbar
{
top: 2px;
left: 7px;
}
#compose-contacts #directorylist
{
width: 100%;
top: 23px;
position: absolute;
border-top: 1px solid #eee;
}
#compose-contacts #contacts-table
{
top: 45px;
position: absolute;
}
diff --git a/skins/classic/templates/addressbook.html b/skins/classic/templates/addressbook.html
index ba119891c..fdcd1847f 100644
--- a/skins/classic/templates/addressbook.html
+++ b/skins/classic/templates/addressbook.html
@@ -1,120 +1,120 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<script type="text/javascript" src="/splitter.js"></script>
<script type="text/javascript" src="/functions.js"></script>
<style type="text/css">
#addresslist { width: <roundcube:exp expression="!empty(cookie:addressviewsplitter) ? cookie:addressviewsplitter-5 : 245" />px; }
#contacts-box { left: <roundcube:exp expression="!empty(cookie:addressviewsplitter) ? cookie:addressviewsplitter+5 : 255" />px;
<roundcube:exp expression="browser:ie ? ('width:expression((parseInt(this.parentNode.offsetWidth)-'.(!empty(cookie:addressviewsplitter) ? cookie:addressviewsplitter+5 : 255).')+\\'px\\');') : ''" />
}
#directorylistbox { width: <roundcube:exp expression="!empty(cookie:addressviewsplitterd) ? cookie:addressviewsplitterd-5 : 195" />px; }
#addressscreen { left: <roundcube:exp expression="!empty(cookie:addressviewsplitterd) ? cookie:addressviewsplitterd+5 : 205" />px;
<roundcube:exp expression="browser:ie ? ('width:expression((parseInt(this.parentNode.offsetWidth)-'.(!empty(cookie:addressviewsplitterd) ? cookie:addressviewsplitterd+5 : 205).')+\\'px\\');') : ''" />
}
</style>
</head>
<body onload="rcube_init_mail_ui()">
<roundcube:include file="/includes/taskbar.html" />
<roundcube:include file="/includes/header.html" />
<div id="abooktoolbar">
<roundcube:button command="add" type="link" class="buttonPas addcontact" classAct="button addcontact" classSel="button addcontactSel" title="newcontact" content=" " />
<roundcube:button command="compose" type="link" class="buttonPas compose" classAct="button compose" classSel="button composeSel" title="composeto" content=" " />
<roundcube:button command="delete" type="link" class="buttonPas delete" classAct="button delete" classSel="button deleteSel" title="deletecontact" content=" " />
<span class="separator">&nbsp;</span>
<roundcube:button command="import" type="link" class="buttonPas import" classAct="button import" classSel="button importSel" title="importcontacts" content=" " />
<span class="dropbutton">
<roundcube:button command="export" type="link" class="buttonPas export" classAct="button export" classSel="button exportSel" title="exportvcards" content=" " />
<span id="exportmenulink" onclick="rcmail_ui.show_popup('exportmenu');return false"></span>
</span>
<roundcube:button command="advanced-search" type="link" class="buttonPas search" classAct="button search" classSel="button searchSel" title="advsearch" content=" " />
<roundcube:container name="toolbar" id="abooktoolbar" />
</div>
<div id="quicksearchbar">
<roundcube:button name="searchmenulink" id="searchmenulink" image="/images/icons/glass_roll.png" onclick="rcmail_ui.show_popup('searchmenu');return false" title="searchmod" width="16" height="16" />
<roundcube:object name="searchform" id="quicksearchbox" />
<roundcube:button command="reset-search" id="searchreset" image="/images/icons/reset.gif" title="resetsearch" width="13" height="13" />
</div>
<div id="exportmenu" class="popupmenu">
<ul>
<li><roundcube:button command="export" label="exportall" prop="sub" classAct="exportalllink active" class="exportalllink" /></li>
<li><roundcube:button command="export-selected" label="exportsel" prop="sub" classAct="exportsellink active" class="exportsellink" /></li>
</ul>
</div>
<div id="searchmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><label><input type="checkbox" name="s_mods[]" value="name" id="s_mod_name" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="name" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="firstname" id="s_mod_firstname" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="firstname" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="surname" id="s_mod_surname" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="surname" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="email" id="s_mod_email" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="email" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="*" id="s_mod_all" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="allfields" /></span></label></li>
</ul>
</div>
<div id="mainscreen">
<div id="directorylistbox">
<div id="directorylist-title" class="boxtitle"><roundcube:label name="groups" /></div>
<div id="directorylist-content" class="boxlistcontent">
<roundcube:object name="directorylist" id="directorylist" class="treelist" />
</div>
<div id="directorylist-footer" class="boxfooter">
<roundcube:button command="group-create" type="link" title="newcontactgroup" class="buttonPas addgroup" classAct="button addgroup" content=" " />
<roundcube:button name="groupmenulink" id="groupmenulink" type="link" title="moreactions" class="button groupactions" onclick="rcmail_ui.show_popup('groupmenu');return false" content=" " />
</div>
</div>
<div id="addressscreen">
<div id="addresslist">
-<div class="boxtitle"><roundcube:label name="contacts" /></div>
+<roundcube:object name="addresslisttitle" label="contacts" tag="div" class="boxtitle" />
<div class="boxlistcontent">
<roundcube:object name="addresslist" id="contacts-table" class="records-table" cellspacing="0" summary="Contacts list" noheader="true" />
</div>
<div class="boxfooter">
<div id="abookcountbar" class="pagenav">
<roundcube:button command="firstpage" type="link" class="buttonPas firstpage" classAct="button firstpage" classSel="button firstpageSel" title="firstpage" content=" " />
<roundcube:button command="previouspage" type="link" class="buttonPas prevpage" classAct="button prevpage" classSel="button prevpageSel" title="previouspage" content=" " />
<roundcube:object name="recordsCountDisplay" style="padding:0 .5em; float:left" />
<roundcube:button command="nextpage" type="link" class="buttonPas nextpage" classAct="button nextpage" classSel="button nextpageSel" title="nextpage" content=" " />
<roundcube:button command="lastpage" type="link" class="buttonPas lastpage" classAct="button lastpage" classSel="button lastpageSel" title="lastpage" content=" " />
</div>
</div>
</div>
<script type="text/javascript">
var addrviewsplit = new rcube_splitter({id:'addressviewsplitter', p1: 'addresslist', p2: 'contacts-box', orientation: 'v', relative: true, start: 250});
rcmail.add_onload('addrviewsplit.init()');
var addrviewsplitd = new rcube_splitter({id:'addressviewsplitterd', p1: 'directorylistbox', p2: 'addressscreen', orientation: 'v', relative: true, start: 200});
rcmail.add_onload('addrviewsplitd.init()');
</script>
<div id="contacts-box">
<roundcube:object name="addressframe" id="contact-frame" width="100%" height="100%" frameborder="0" src="/watermark.html" />
</div>
</div>
</div>
<div id="groupoptionsmenu" class="popupmenu">
<ul>
<li><roundcube:button command="group-rename" label="grouprename" classAct="active" /></li>
<li><roundcube:button command="group-delete" label="groupdelete" classAct="active" /></li>
<li><roundcube:button command="group-remove-selected" label="groupremoveselected" classAct="active" /></li>
<li class="separator_above"><roundcube:button command="search-create" label="searchsave" classAct="active" /></li>
<li><roundcube:button command="search-delete" label="searchdelete" classAct="active" /></li>
<roundcube:container name="groupoptions" id="groupoptionsmenu" />
</ul>
</div>
</body>
</html>
diff --git a/skins/classic/templates/contact.html b/skins/classic/templates/contact.html
index d74a78b27..8be112b49 100644
--- a/skins/classic/templates/contact.html
+++ b/skins/classic/templates/contact.html
@@ -1,29 +1,29 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<script type="text/javascript" src="/functions.js"></script>
</head>
<body class="iframe">
<div id="contact-title" class="boxtitle"><roundcube:label name="contactproperties" /></div>
<div id="contact-details" class="boxcontent">
<roundcube:if condition="strlen(env:sourcename)" />
<div id="sourcename"><roundcube:label name="addressbook" />: <roundcube:var name="env:sourcename" /></div>
<roundcube:endif />
- <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div>
+ <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>
<roundcube:object name="contacthead" id="contacthead" />
<div style="clear:both"></div>
<div id="contacttabs">
<roundcube:object name="contactdetails" />
</div>
<p>
<roundcube:button command="edit" type="input" class="button" label="editcontact" condition="!ENV:readonly" />
</p>
</div>
<script type="text/javascript">rcube_init_tabs('contacttabs')</script>
</body>
</html>
diff --git a/skins/classic/templates/mail.html b/skins/classic/templates/mail.html
index c7010e87c..c3b4004cc 100644
--- a/skins/classic/templates/mail.html
+++ b/skins/classic/templates/mail.html
@@ -1,208 +1,211 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<script type="text/javascript" src="/splitter.js"></script>
<script type="text/javascript" src="/functions.js"></script>
<style type="text/css">
<roundcube:if condition="config:preview_pane == true" />
#mailcontframe { height: <roundcube:exp expression="!empty(cookie:mailviewsplitter) ? cookie:mailviewsplitter-5 : 195" />px; }
#mailpreviewframe { top: <roundcube:exp expression="!empty(cookie:mailviewsplitter) ? cookie:mailviewsplitter+5 : 205" />px;
<roundcube:exp expression="browser:ie ? ('height: expression((parseInt(this.parentNode.offsetHeight)-'.(!empty(cookie:mailviewsplitter) ? cookie:mailviewsplitter+25 : 245).')+\\'px\\');') : ''" />
}
<roundcube:endif />
#mailleftcontainer { width: <roundcube:exp expression="!empty(cookie:mailviewsplitterv) ? cookie:mailviewsplitterv-5 : 160" />px; }
#mailrightcontainer { left: <roundcube:exp expression="!empty(cookie:mailviewsplitterv) ? cookie:mailviewsplitterv+5 : 170" />px;
<roundcube:exp expression="browser:ie ? ('width: expression((parseInt(this.parentNode.offsetWidth)-'.(!empty(cookie:mailviewsplitterv) ? cookie:mailviewsplitterv+5 : 165).')+\\'px\\');') : ''" />
}
</style>
</head>
<body onload="rcube_init_mail_ui()">
<roundcube:include file="/includes/taskbar.html" />
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
<div id="mailleftcontainer">
<div id="mailboxlist-container">
<div id="mailboxlist-title" class="boxtitle"><roundcube:label name="mailboxlist" /></div>
<div id="mailboxlist-content" class="boxlistcontent">
<roundcube:object name="mailboxlist" id="mailboxlist" class="treelist" folder_filter="mail" />
</div>
<div id="mailboxlist-footer" class="boxfooter">
<roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="button groupactions" onclick="rcmail_ui.show_popup('mailboxmenu');return false" content=" " />
<roundcube:if condition="env:quota" />
<img id="quotaimg" src="/images/quota.<roundcube:exp expression="browser:ie && browser:ver < 7 ? 'gif' : 'png'" />" alt="" width="102" height="16" />
<div id="quota">
<roundcube:object name="quotaDisplay" display="image" width="100" height="14" id="quotadisplay" />
</div>
<roundcube:endif />
</div>
</div>
<script type="text/javascript">
var mailviewsplitv = new rcube_splitter({id:'mailviewsplitterv', p1: 'mailleftcontainer', p2: 'mailrightcontainer', orientation: 'v', relative: true, start: 165, callback: rcube_render_mailboxlist });
rcmail.add_onload('mailviewsplitv.init()');
</script>
</div>
<div id="mailrightcontainer">
<div id="mailrightcontent">
<div id="mailcontframe">
<div id="messagelistcontainer" class="boxlistcontent" style="top:0">
<roundcube:object name="messages"
id="messagelist"
cellspacing="0"
columns=""
summary="Message list"
optionsmenuIcon="true" />
</div>
<div class="boxfooter">
<div id="listcontrols" class="pagenav">
<span><roundcube:label name="select" />:&nbsp;</span>
<roundcube:button command="select-all" type="link" title="all" class="buttonPas all" classAct="button all" classSel="button allsel" content=" " />
<roundcube:button command="select-all" type="link" prop="page" title="currpage" class="buttonPas page" classAct="button page" classSel="button pagesel" content=" " />
<roundcube:button command="select-all" type="link" prop="unread" title="unread" class="buttonPas unread" classAct="button unread" classSel="button unreadsel" content=" " />
<roundcube:button command="select-all" type="link" prop="invert" title="invert" class="buttonPas invert" classAct="button invert" classSel="button invertsel" content=" " />
<roundcube:button command="select-none" type="link" title="none" class="buttonPas none" classAct="button none" classSel="button nonesel" content=" " />
<roundcube:container name="listcontrols" id="listcontrols" />
<roundcube:if condition="env:threads" />
<span style="margin-left: 12px"><roundcube:label name="threads" />:&nbsp;</span>
<roundcube:button command="expand-all" type="link" title="expand-all" class="buttonPas expand-all" classAct="button expand-all" classSel="button expand-allsel" content=" " />
<roundcube:button command="expand-unread" type="link" title="expand-unread" class="buttonPas expand-unread" classAct="button expand-unread" classSel="button expand-unreadsel" content=" " />
<roundcube:button command="collapse-all" type="link" title="collapse-all" class="buttonPas collapse-all" classAct="button collapse-all" classSel="button collapse-allsel" content=" " />
<roundcube:endif />
<roundcube:if condition="!in_array('preview_pane', (array)config:dont_override)" />
<span style="margin-left: 12px"><label for="prevpaneswitch"><span><roundcube:label name="previewpane" />:</span></label>
<input type="checkbox" id="prevpaneswitch" onclick="rcmail_ui.switch_preview_pane(this)"<roundcube:exp expression="config:preview_pane == true ? ' checked=checked' : ''" /> />
</span>
<roundcube:endif />
</div>
<div id="countcontrols" class="pagenav">
<roundcube:button command="lastpage" type="link" class="buttonPas lastpage" classAct="button lastpage" classSel="button lastpageSel" title="lastpage" content=" " />
<roundcube:button command="nextpage" type="link" class="buttonPas nextpage" classAct="button nextpage" classSel="button nextpageSel" title="nextpage" content=" " />
<roundcube:object name="messageCountDisplay" style="padding:0 .5em; float:right" />
<roundcube:button command="previouspage" type="link" class="buttonPas prevpage" classAct="button prevpage" classSel="button prevpageSel" title="previouspage" content=" " />
<roundcube:button command="firstpage" type="link" class="buttonPas firstpage" classAct="button firstpage" classSel="button firstpageSel" title="firstpage" content=" " />
</div>
</div>
</div>
<script type="text/javascript">
var mailviewsplit = new rcube_splitter({id:'mailviewsplitter', p1: 'mailcontframe', p2: 'mailpreviewframe', orientation: 'h', relative: true, start: 205});
<roundcube:if condition="config:preview_pane == true" />
rcmail.add_onload('mailviewsplit.init()');
<roundcube:endif />
</script>
<div id="mailpreviewframe"<roundcube:if condition="config:preview_pane != true" /> style="display:none"<roundcube:endif />>
<roundcube:object name="messagecontentframe" id="messagecontframe" width="100%" height="100%" frameborder="0" src="/watermark.html" />
</div>
</div>
</div>
</div>
<roundcube:include file="/includes/messagetoolbar.html" />
<div id="searchmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><label><input type="checkbox" name="s_mods[]" value="subject" id="s_mod_subject" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="subject" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="from" id="s_mod_from" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="from" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="to" id="s_mod_to" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="to" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="cc" id="s_mod_cc" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="cc" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="bcc" id="s_mod_bcc" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="bcc" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="body" id="s_mod_body" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="body" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="text" id="s_mod_text" onclick="rcmail_ui.set_searchmod(this)" /> <span><roundcube:label name="msgtext" /></span></label></li>
</ul>
</div>
<div id="quicksearchbar">
<div id="searchfilter">
<label for="rcmlistfilter"><roundcube:label name="filter" /></label>:
<roundcube:object name="searchfilter" class="searchfilter" />
</div>
<roundcube:button name="searchmenulink" id="searchmenulink" image="/images/icons/glass_roll.png" onclick="rcmail_ui.show_popup('searchmenu');return false" title="searchmod" width="16" height="16" />
<roundcube:object name="searchform" id="quicksearchbox" />
<roundcube:button command="reset-search" id="searchreset" image="/images/icons/reset.gif" title="resetsearch" width="13" height="13" />
</div>
<div id="dragmessagemenu" class="popupmenu">
<ul>
<li><roundcube:button command="moveto" onclick="return rcmail.drag_menu_action('moveto')" label="move" classAct="active" /></li>
<li><roundcube:button command="copy" onclick="return rcmail.drag_menu_action('copy')" label="copy" classAct="active" /></li>
</ul>
</div>
<div id="mailboxoptionsmenu" class="popupmenu">
<ul>
<li><roundcube:button command="expunge" type="link" label="compact" classAct="active" /></li>
- <li class="separator_below"><roundcube:button command="purge" type="link" label="empty" classAct="active" /></li>
+ <li><roundcube:button command="purge" type="link" label="empty" classAct="active" /></li>
+ <li class="separator_below"><roundcube:button name="messageimport" type="link" class="active" label="importmessages" id="uploadformlink" onclick="rcmail_ui.show_popup('uploadform', true); return false" /></li>
<li><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<roundcube:container name="mailboxoptions" id="mailboxoptionsmenu" />
</ul>
</div>
<div id="listmenu" class="popupmenu">
<roundcube:if condition="env:threads" />
<fieldset class="thinbordered"><legend><roundcube:label name="listmode" /></legend>
<ul class="toolbarmenu">
<li><label><input type="radio" name="view" value="list" id="view_default" /> <span><roundcube:label name="list" /></span></label></li>
<li><label><input type="radio" name="view" value="thread" id="view_thread" /> <span><roundcube:label name="threads" /></span></label></li>
</ul>
</fieldset>
<roundcube:endif />
<roundcube:if condition="!in_array('list_cols', (array)config:dont_override)" />
<fieldset class="thinbordered"><legend><roundcube:label name="listcolumns" /></legend>
<ul class="toolbarmenu">
<li><label><input type="checkbox" name="list_col[]" value="threads" id="cols_threads" checked="checked" disabled="disabled" /> <span class="disabled"><roundcube:label name="threads" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="subject" id="cols_subject" checked="checked" disabled="disabled" /> <span class="disabled"><roundcube:label name="subject" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="fromto" id="cols_fromto" /> <span><roundcube:label name="fromto" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="from" id="cols_from" /> <span><roundcube:label name="from" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="to" id="cols_to" /> <span><roundcube:label name="to" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="replyto" id="cols_replyto" /> <span><roundcube:label name="replyto" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="cc" id="cols_cc" /> <span><roundcube:label name="cc" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="date" id="cols_date" /> <span><roundcube:label name="date" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="size" id="cols_size" /> <span><roundcube:label name="size" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="status" id="cols_status" /> <span><roundcube:label name="readstatus" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="attachment" id="cols_attachment" /> <span><roundcube:label name="attachment" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="flag" id="cols_flag" /> <span><roundcube:label name="flag" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="priority" id="cols_priority" /> <span><roundcube:label name="priority" /></span></label></li>
</ul>
</fieldset>
<roundcube:endif />
<roundcube:if condition="!in_array('message_sort_col', (array)config:dont_override)" />
<fieldset class="thinbordered"><legend><roundcube:label name="listsorting" /></legend>
<ul class="toolbarmenu">
<li><label><input type="radio" name="sort_col" value="" id="sort_default" /> <span><roundcube:label name="nonesort" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="arrival" id="sort_arrival" /> <span><roundcube:label name="arrival" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="date" id="sort_date" /> <span><roundcube:label name="sentdate" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="subject" id="sort_subject" /> <span><roundcube:label name="subject" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="fromto" id="sort_fromto" /> <span><roundcube:label name="fromto" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="from" id="sort_from" /> <span><roundcube:label name="from" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="to" id="sort_to" /> <span><roundcube:label name="to" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="to" id="sort_replyto" /> <span><roundcube:label name="replyto" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="cc" id="sort_cc" /> <span><roundcube:label name="cc" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="size" id="sort_size" /> <span><roundcube:label name="size" /></span></label></li>
</ul>
</fieldset>
<roundcube:endif />
<roundcube:if condition="!in_array('message_sort_order', (array)config:dont_override)" />
<fieldset><legend><roundcube:label name="listorder" /></legend>
<ul class="toolbarmenu">
<li><label><input type="radio" name="sort_ord" value="ASC" id="sort_asc" /> <span><roundcube:label name="asc" /></span></label></li>
<li><label><input type="radio" name="sort_ord" value="DESC" id="sort_desc" /> <span><roundcube:label name="desc" /></span></label></li>
</ul>
</fieldset>
<roundcube:endif />
<div>
<roundcube:button command="menu-open" id="listmenucancel" type="input" class="button" label="cancel" />
<roundcube:button command="menu-save" id="listmenusave" type="input" class="button mainaction" label="save" />
</div>
</div>
+<roundcube:object name="messageimportform" id="upload-form" attachmentFieldSize="40" class="popupmenu" />
+
</body>
</html>
diff --git a/skins/larry/addressbook.css b/skins/larry/addressbook.css
index ff3951497..090e54c7b 100644
--- a/skins/larry/addressbook.css
+++ b/skins/larry/addressbook.css
@@ -1,348 +1,389 @@
/**
* Roundcube webmail styles for the Address Book section
*
* Copyright (c) 2012, The Roundcube Dev Team
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
#addressview-left {
position: absolute;
top: 0;
left: 0;
width: 220px;
bottom: 0;
z-index: 2;
}
#addressview-right {
position: absolute;
top: 0;
left: 232px;
right: 0;
bottom: 0;
z-index: 3;
}
#addressbooktoolbar {
position: absolute;
top: -6px;
left: 0;
height: 40px;
white-space: nowrap;
z-index: 10;
}
#directorylistbox {
position: absolute;
top: 42px;
left: 0;
width: 100%;
bottom: 0;
}
#addresslist {
position: absolute;
top: 42px;
left: 0;
width: 280px;
bottom: 0;
}
#contacts-box {
position: absolute;
top: 42px;
left: 292px;
right: 0;
bottom: 0;
}
#addressview-left #quicksearchbar input {
width: 156px;
}
#directorylist li a,
#contacts-table .contact td.name {
background-image: url(images/listicons.png);
background-position: -100px 0;
background-repeat: no-repeat;
overflow: hidden;
padding-left: 36px;
text-overflow: ellipsis;
}
-#contacts-table .contact.readonly td {
- font-style: italic;
-}
-
#directorylist li.addressbook a {
background-position: 6px -766px;
}
#directorylist li.addressbook.selected > a {
background-position: 6px -791px;
}
#directorylist li.addressbook ul li:last-child {
border-bottom: 0;
}
#directorylist li.addressbook ul.groups {
margin: 0;
padding: 0;
}
#directorylist li.addressbook ul.groups li {
width: 100%;
}
#directorylist li.contactgroup a {
padding-left: 62px;
background-position: 32px -1555px;
}
#directorylist li.contactgroup.selected a {
background-position: 32px -1579px;
}
#directorylist li.contactgroup input {
margin-left: 36px;
}
#directorylist li.contactsearch a {
background-position: 6px -1651px;
}
#directorylist li.contactsearch.selected a {
background-position: 6px -1675px;
}
#directorylist li.contactsearch input {
margin-left: 8px;
}
#directorylist li.addressbook div.collapsed,
#directorylist li.addressbook div.expanded {
top: 15px;
left: 20px;
}
+#contacts-table .contact.readonly td {
+ font-style: italic;
+}
+
+#contacts-table td.name {
+ width: 95%;
+}
+
+#contacts-table td.action {
+ width: 24px;
+ padding: 4px;
+}
+
+#contacts-table td.action a {
+ display: block;
+ width: 16px;
+ height: 14px;
+ text-indent: -5000px;
+ overflow: hidden;
+ background: url(images/listicons.png) -2px -1180px no-repeat;
+}
+
#contacts-table .contact td.name {
background-position: 6px -1603px;
}
#contacts-table .contact.selected td.name,
#contacts-table .contact.unfocused td.name {
background-position: 6px -1627px;
font-weight: bold;
}
+#contacts-table .group td.name {
+ background-position: 6px -1555px;
+}
+
+#contacts-table .group.selected td.name,
+#contacts-table .group.unfocused td.name {
+ background-position: 6px -1579px;
+ font-weight: bold;
+}
+
+#addresslist .boxtitle {
+ padding-right: 95px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#addresslist .boxtitle a.poplink {
+ color: #004458;
+ font-size: 14px;
+ line-height: 12px;
+ text-decoration: none;
+}
+
#contact-frame {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 28px;
border: 0;
border-radius: 4px;
}
#headerbuttons {
position: absolute;
top: 48px;
right: 10px;
width: auto;
z-index: 10;
}
#sourcename {
color: #999;
font-size: 10px;
margin: -5px 0 8px 2px;
}
#contactphoto {
float: left;
margin: 0 18px 20px 0;
width: 112px;
}
#contactpic {
width: 112px;
min-height: 112px;
background: white;
}
#contactpic img {
width: 112px;
visibility: inherit;
}
#contactpic.droptarget {
background-image: url(images/filedrop.png);
background-position: center;
background-repeat: no-repeat;
}
#contactpic.droptarget.hover {
background-color: #d9ecf4;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
}
#contactpic.droptarget.active img {
opacity: 0.15;
}
#contactpic.droptarget.hover img {
opacity: 0.05;
}
#contacthead {
border: 0;
margin: 0 16em 1em 0;
padding: 0;
line-height: 1.5em;
font-size: 12px;
}
form #contacthead {
margin-right: 0;
}
#contacthead .names span.namefield,
#contacthead .names input {
font-size: 140%;
font-weight: bold;
}
#contacthead .displayname span.namefield {
font-size: 120%;
font-weight: bold;
}
#contacthead span.nickname:before,
#contacthead span.nickname:after {
content: '"';
}
#contacthead input {
margin-right: 6px;
margin-bottom: 0.2em;
}
#contacthead .names input,
#contacthead .addnames input {
width: 180px;
}
#contacthead input.ff_prefix,
#contacthead input.ff_suffix {
width: 90px;
}
.contactfieldgroup {
border: 0;
border-radius: 5px;
background: #f7f7f7;
background: -moz-linear-gradient(top, #f7f7f7 0%, #eee 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f7f7f7), color-stop(100%,#eee));
background: -o-linear-gradient(top, #f7f7f7 0%, #eee 100%);
background: -ms-linear-gradient(top, #f7f7f7 0%, #eee 100%);
background: linear-gradient(top, #f7f7f7 0%, #eee 100%);
margin: 0 0 12px 0;
padding: 8px;
}
.contactfieldgroup legend {
display: block;
margin: 0 -8px;
width: 100%;
font-weight: bold;
text-shadow: 0px 1px 1px #fff;
padding: 6px 8px 3px 8px;
background: #f0f0f0;
background: -moz-linear-gradient(top, #f0f0f0 0%, #d6d6d6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f0f0), color-stop(100%,#d6d6d6));
background: -o-linear-gradient(top, #f0f0f0 0%, #d6d6d6 100%);
background: -ms-linear-gradient(top, #f0f0f0 0%, #d6d6d6 100%);
background: linear-gradient(top, #f0f0f0 0%, #d6d6d6 100%);
border-bottom: 1px solid #cfcfcf;
border-radius: 5px 5px 0 0;
}
.contactfieldgroup .row {
position: relative;
margin: 0.2em 0;
}
.contactfieldgroup .contactfieldlabel {
position: absolute;
top: 0;
left: 2px;
width: 110px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #666;
}
.contactfieldgroup .contactfieldlabel select {
width: 100%;
color: #666;
}
.contactfieldgroup .contactfieldcontent {
padding-left: 120px;
min-height: 1em;
line-height: 1.3em;
}
.contactfieldgroup .contactfield {
line-height: 1.3em;
}
.contactcontrolleraddress .contactfieldcontent input {
margin-bottom: 0.1em;
}
.contactfieldcontent.composite {
padding-bottom: 8px;
}
.contactfieldcontent .contactfieldbutton {
vertical-align: middle;
margin-left: 0.5em;
}
.contactfield .ff_notes {
width: 99%;
}
a.deletebutton {
position: relative;
left: 5px;
top: -3px;
display: inline-block;
width: 24px;
height: 18px;
text-decoration: none;
text-indent: -5000px;
background: url(images/buttons.png) -7px -377px no-repeat;
}
#import-box {
position: absolute;
bottom: 28px;
top: 34px;
left: 0;
right: 0;
overflow: auto;
padding: 10px;
}
diff --git a/skins/larry/ie7hacks.css b/skins/larry/ie7hacks.css
index 6d7af4787..fc4713361 100644
--- a/skins/larry/ie7hacks.css
+++ b/skins/larry/ie7hacks.css
@@ -1,211 +1,212 @@
/**
* Roundcube webmail CSS hacks for IE 7
*
* Copyright (c) 2012, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/* #1488618 */
#mainscreen {
height: expression((parseInt(document.documentElement.clientHeight)-108)+'px');
}
#mainscreen.offset {
height: expression((parseInt(document.documentElement.clientHeight)-150)+'px');
}
.minimal #mainscreen {
height: expression((parseInt(document.documentElement.clientHeight)-82)+'px');
}
.minimal #mainscreen.offset {
height: expression((parseInt(document.documentElement.clientHeight)-120)+'px');
}
#messagepartframe {
height: expression((parseInt(this.parentNode.offsetHeight)-1)+'px');
}
input.button {
display: inline;
font-size: 90%;
}
a.iconbutton,
a.deletebutton,
.boxpagenav a.icon,
.pagenav a.button span.inner,
.boxfooter .listbutton .inner,
.attachmentslist li a.delete,
.attachmentslist li a.cancelupload,
+#contacts-table td.action a,
.previewheader .iconlink,
.minimal #taskbar .button-inner {
/* workaround for text-indent which also offsets the background image */
text-indent: 0;
font-size: 0;
line-height: 0;
overflow: hidden;
text-align: right;
text-decoration: none;
}
.boxpagenav a.icon {
color: #bbd3da;
}
.pagenav a.button,
.pagenav a.button span.inner,
.previewheader .iconlink,
#uploadform a.iconlink {
display: inline;
}
.pagenavbuttons {
top: 4px;
}
.dropbutton .dropbuttontip {
right: -2px;
}
#login-form .box-inner form {
margin: 0;
}
#login-form #message div {
float: left;
display: block;
width: 200px;
margin-left: 130px;
white-space: nowrap;
text-align: left;
}
#messageheader.previewheader .iconlink {
color: #fff;
height: 14px;
}
#uploadform a.iconlink {
text-indent: 0px;
}
.boxfooter .countdisplay {
top: -12px;
}
ul.toolbarmenu li a {
width: 140px;
}
#threadselectmenu li a {
width: 160px;
}
#messagemenu li a {
width: 170px;
}
#rcmKSearchpane {
width: 400px;
}
#rcmKSearchpane ul li {
width: 380px;
text-overflow: ellipsis;
}
table.listing,
table.records-table {
display: block;
width: auto;
border-collapse: expression('separate', cellSpacing = '0');
}
.records-table tbody td span {
white-space: nowrap;
}
table.listing {
width: 100%;
}
ul.toolbarmenu li label {
margin: 0;
padding: 3px 8px;
}
.searchbox input {
padding-top: 4px;
padding-bottom: 2px;
}
#messagelistfooter #listcontrols,
#messagelistfooter #listselectors,
#messagelistfooter #countcontrols,
.pagenav .countdisplay {
display: inline;
}
#messagelistfooter #countcontrols {
position: relative;
top: -4px;
}
#messagecontframe,
#preferences-frame {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
#composeoptionstoggle {
display: inline;
top: 3px;
}
.propform {
margin: 0;
}
.propform fieldset legend {
color: #333;
margin-left: -5px;
padding-left: 0;
}
.contactfieldgroup legend {
margin-left: -14px;
}
.contactfieldcontent .contactfieldbutton {
top: -6px;
}
.tabsbar {
height: 15px;
padding-bottom: 15px;
}
.tabsbar .tablink {
padding: 0 1px 0 0;
}
.minimal #topline {
width: 100%;
height: 18px;
box-sizing: border-box;
}
.minimal #taskbar a:hover .tooltip {
right: 34px;
top: 1px;
}
diff --git a/skins/larry/images/contactgroup.png b/skins/larry/images/contactgroup.png
new file mode 100644
index 000000000..8303cf02f
Binary files /dev/null and b/skins/larry/images/contactgroup.png differ
diff --git a/skins/larry/images/listicons.png b/skins/larry/images/listicons.png
index f4505d4fa..e4ffef660 100644
Binary files a/skins/larry/images/listicons.png and b/skins/larry/images/listicons.png differ
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index fe9e56ea3..d653c7804 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -1,1502 +1,1515 @@
/**
* Roundcube webmail styles for the Email section
*
* Copyright (c) 2012, The Roundcube Dev Team
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
#mailview-left {
position: absolute;
top: 0;
left: 0;
width: 220px;
bottom: 0;
z-index: 2;
}
#mailview-right {
position: absolute;
top: 0;
left: 232px;
right: 0;
bottom: 0;
z-index: 3;
}
#mailview-right.fullwidth {
left: 0;
}
#mailview-top {
position: absolute;
top: 42px;
left: 0;
width: 100%;
bottom: 28px;
}
#mailview-top.fullheight {
border-radius: 4px 4px 0 0;
}
#mailview-bottom {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 27px;
border-radius: 4px;
border-top: none;
}
#composeview-right #mailview-bottom {
border-radius: 0 0 4px 4px;
}
#folderlist-header {
width: 100%;
height: 12px;
top: 32px;
}
#mailboxcontainer,
#messagelistcontainer {
position: absolute;
top: 42px;
left: 0;
width: 100%;
bottom: 0;
}
#messagelistcontainer {
top: 0;
bottom: 30px;
overflow: auto;
}
#messagelistfooter {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 22px;
padding: 4px 6px;
border-top: 1px solid #ddd;
background: #ebebeb;
background: -moz-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ebebeb), color-stop(100%,#c6c6c6));
background: -o-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: -ms-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
border-radius: 0 0 4px 4px;
}
#mailview-top.fullheight #messagelistfooter {
border-radius: 0;
}
#messagelistfooter.rightalign {
text-align: right;
}
#messagelistfooter #countcontrols {
display: inline-block;
}
#messagelistfooter #listcontrols,
#messagelistfooter #listselectors {
display: inline-block;
margin-right: 2em;
vertical-align: middle;
}
#messagelistfooter #listselectors .menuselector {
margin-top: -2px;
}
a.iconbutton.listmode {
width: 26px;
height: 20px;
background-position: 0 -477px;
}
a.iconbutton.threadmode {
width: 26px;
height: 20px;
background-position: 0 -497px;
}
a.iconbutton.listmode.selected {
background-position: -26px -477px;
}
a.iconbutton.threadmode.selected {
background-position: -26px -497px;
}
#mailboxlist li.mailbox {
position: relative;
background-repeat: no-repeat;
background-position: 6px 2px;
}
#mailboxlist > li:first-child {
border-radius: 4px 4px 0 0;
border-top: 0;
}
#mailboxlist li.mailbox a {
padding-left: 36px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background-image: url(images/listicons.png);
background-repeat: no-repeat;
background-position: 6px 3px;
}
#mailboxlist li.mailbox.unread > a {
padding-right: 36px;
}
#mailboxlist li.mailbox.selected > a {
background-position: 6px -21px;
}
#mailboxlist li.mailbox.inbox > a {
background-position: 6px -189px;
}
#mailboxlist li.mailbox.inbox.selected > a {
background-position: 6px -213px;
}
#mailboxlist li.mailbox.drafts > a {
background-position: 6px -238px;
}
#mailboxlist li.mailbox.drafts.selected > a {
background-position: 6px -262px;
}
#mailboxlist li.mailbox.sent > a {
background-position: 6px -286px;
}
#mailboxlist li.mailbox.sent.selected > a {
background-position: 6px -310px;
}
#mailboxlist li.mailbox.junk > a {
background-position: 6px -334px;
}
#mailboxlist li.mailbox.junk.selected > a {
background-position: 6px -358px;
}
#mailboxlist li.mailbox.trash > a {
background-position: 6px -382px;
}
#mailboxlist li.mailbox.trash.selected > a {
background-position: 6px -406px;
}
#mailboxlist li.mailbox.archive > a {
background-position: 6px -1699px;
}
#mailboxlist li.mailbox.archive.selected > a {
background-position: 6px -1723px;
}
#mailboxlist li.unread {
font-weight: bold;
}
#mailboxlist li.virtual > a {
color: #aaa;
}
#mailboxlist li.recent > a {
color: #017cb4;
}
#mailboxlist li.mailbox div.treetoggle {
top: 13px;
left: 19px;
}
#mailboxlist li.mailbox ul li:last-child {
border-bottom: 0;
}
/* nested mailboxes */
#mailboxlist li.mailbox ul {
list-style: none;
margin: 0;
padding: 0;
border-top: 1px solid #bbd3da;
}
#mailboxlist li.mailbox ul li a {
padding-left: 52px; /* 36 + 1 x 16 */
background-position: 22px -93px; /* 6 + 1 x 16 */
}
#mailboxlist li.mailbox ul li.selected > a {
background-position: 22px -117px;
}
#mailboxlist li.mailbox ul li div.treetoggle {
left: 33px;
top: 14px;
}
#mailboxlist li.mailbox ul ul li.mailbox a {
padding-left: 68px; /* 2x */
background-position: 38px -93px;
}
#mailboxlist li.mailbox ul ul li.selected > a {
background-position: 38px -117px;
}
#mailboxlist li.mailbox ul ul li div.treetoggle {
left: 48px;
}
#mailboxlist li.mailbox ul ul ul li.mailbox a {
padding-left: 84px; /* 3x */
background-position: 54px -93px;
}
#mailboxlist li.mailbox ul ul ul li.selected > a {
background-position: 54px -117px;
}
#mailboxlist li.mailbox ul ul ul li div.treetoggle {
left: 64px;
}
#mailboxlist li.mailbox ul ul ul ul li.mailbox a {
padding-left: 100px; /* 4x */
background-position: 70px -93px;
}
#mailboxlist li.mailbox ul ul ul ul li.selected > a {
background-position: 70px -117px;
}
#mailboxlist li.mailbox ul ul ul ul li div.treetoggle {
left: 80px;
}
/* indent folders on levels > 4 */
#mailboxlist li.mailbox ul ul ul ul ul li {
padding-left: 16px;
}
#mailboxlist li.mailbox ul ul ul ul ul li div.treetoggle {
left: 96px;
}
#mailboxlist li.mailbox .unreadcount {
position: absolute;
top: 3px;
right: 6px;
min-width: 1.8em;
padding: 2px 4px;
background: #82acb5;
background: -moz-linear-gradient(top, #82acb5 0%, #6a939f 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#82acb5), color-stop(100%,#6a939f));
background: -o-linear-gradient(top, #82acb5 0%, #6a939f 100%);
background: -ms-linear-gradient(top, #82acb5 0%, #6a939f 100%);
background: linear-gradient(top, #82acb5 0%, #6a939f 100%);
box-shadow: inset 0 1px 1px 0 #536d72;
-o-box-shadow: inset 0 1px 1px 0 #536d72;
-webkit-box-shadow: inset 0 1px 1px 0 #536d72;
-moz-box-shadow: inset 0 1px 1px 0 #536d72;
border-radius: 9px;
color: #fff;
text-align: center;
font-weight: bold;
text-shadow: none;
}
#mailboxlist li.mailbox.selected > a .unreadcount {
background: #005d76;
background: -moz-linear-gradient(top, #005d76 0%, #004558 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558));
background: -o-linear-gradient(top, #005d76 0%, #004558 100%);
background: -ms-linear-gradient(top, #005d76 0%, #004558 100%);
background: linear-gradient(top, #005d76 0%, #004558 100%);
box-shadow: inset 0 1px 1px 0 #003645;
-o-box-shadow: inset 0 1px 1px 0 #003645;
-webkit-box-shadow: inset 0 1px 1px 0 #003645;
-moz-box-shadow: inset 0 1px 1px 0 #003645;
}
#mailboxlist li.mailbox.recent > a .unreadcount {
background: #017cb4;
background: -moz-linear-gradient(top, #017cb4 0%, #006ca4 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#017cb4), color-stop(100%,#006ca4));
background: -o-linear-gradient(top, #017cb4 0%, #006ca4 100%);
background: -ms-linear-gradient(top, #017cb4 0%, #006ca4 100%);
background: linear-gradient(top, #017cb4 0%, #006ca4 100%);
box-shadow: inset 0 1px 1px 0 #005080;
-o-box-shadow: inset 0 1px 1px 0 #005080;
-webkit-box-shadow: inset 0 1px 1px 0 #005080;
-moz-box-shadow: inset 0 1px 1px 0 #005080;
}
#searchfilter {
position: absolute;
right: 256px;
width: auto;
top: 2px;
}
#searchfilter select {
height: 26px;
}
#mailview-left select.mailboxlist {
position: relative;
top: 10px;
width: 100%;
}
#messagetoolbar {
position: absolute;
top: -6px;
left: 0;
height: 40px;
white-space: nowrap;
z-index: 10;
}
#messagetoolbar.fullwidth {
right: 0;
}
#messagetoolbar .toolbarselect {
position: absolute;
bottom: 6px;
right: 3px;
}
#messagesearchtools {
position: absolute;
right: 0;
top: 0;
width: 400px;
}
#mailpreviewtoggle {
display: block;
position: absolute;
top: 6px;
right: 4px;
width: 20px;
height: 18px;
background: url(images/buttons.png) -3px -458px no-repeat;
}
#mailpreviewtoggle.enabled {
background-position: -28px -458px;
}
/*** message list ***/
#messagelist thead td:first-child {
border-radius: 4px 0 0 0; /* for Chrome */
}
#messagelist tr td.attachment,
#messagelist tr td.threads,
#messagelist tr td.status,
#messagelist tr td.flag,
#messagelist tr td.priority {
width: 20px;
padding: 2px 3px;
}
.webkit #messagelist tr td.attachment,
.webkit #messagelist tr td.threads,
.webkit #messagelist tr td.status,
.webkit #messagelist tr td.flag,
.webkit #messagelist tr td.priority {
width: 26px;
}
#messagelist tr td.threads {
width: 26px;
}
.webkit #messagelist tr td.threads {
width: 30px;
}
#messagelist tr td.threads,
#messagelist tr td.threads + td {
border-left: 0;
}
#messagelist tr td.size {
width: 60px;
text-align: right;
}
#messagelist thead tr td.size {
text-align: left;
}
#messagelist tr td.fromto,
#messagelist tr td.from,
#messagelist tr td.to,
#messagelist tr td.cc,
#messagelist tr td.replyto {
width: 200px;
}
#messagelist tr td.date {
width: 135px;
}
#messagelist tr.message {
/* background-color: #fff; */
}
#messagelist tr.thread.expanded td {
background-color: #ededed;
}
#messagelist tr.unread {
font-weight: bold;
/* background-color: #fff; */
}
#messagelist tr.flagged td,
#messagelist tr.flagged td a {
color: #f30;
}
#messagelist thead tr td.sortedASC a,
#messagelist thead tr td.sortedDESC a {
color: #004458;
text-decoration: underline;
background: url(images/listicons.png) right -912px no-repeat;
}
#messagelist thead tr td.sortedASC a {
background-position: right -944px;
}
#messagelist td img {
vertical-align: middle;
display: inline-block;
}
#messagelist tbody td a {
color: #333;
text-decoration: none;
white-space: nowrap;
cursor: default;
}
#messagelist tbody tr td.flag,
#messagelist tbody tr td.status,
#messagelist tbody tr td.subject span.status {
cursor: pointer;
}
#messagelist tr td.flag span,
#messagelist tr td.status span,
#messagelist tr td.attachment span,
#messagelist tr td.priority span {
display: block;
width: 20px;
}
#messagelist tr td div.collapsed,
#messagelist tr td div.expanded,
#messagelist tr td.threads div.listmenu,
#messagelist tr td.attachment span.attachment,
#messagelist tr td.attachment span.report,
#messagelist tr td.priority span.priority,
#messagelist tr td.priority span.prio1,
#messagelist tr td.priority span.prio2,
#messagelist tr td.priority span.prio3,
#messagelist tr td.priority span.prio4,
#messagelist tr td.priority span.prio5,
#messagelist tr td.flag span.flagged,
#messagelist tr td.flag span.unflagged,
#messagelist tr td.flag span.unflagged:hover,
#messagelist tr td.status span.status,
#messagelist tr td.status span.msgicon,
#messagelist tr td.status span.deleted,
#messagelist tr td.status span.unread,
#messagelist tr td.status span.unreadchildren,
#messagelist tr td.subject span.msgicon,
#messagelist tr td.subject span.deleted,
#messagelist tr td.subject span.unread,
#messagelist tr td.subject span.replied,
#messagelist tr td.subject span.forwarded,
#messagelist tr td.subject span.unreadchildren {
display: inline-block;
vertical-align: middle;
height: 18px;
width: 20px;
padding: 0;
background: url(images/listicons.png) -100px 0 no-repeat;
}
#messagelist tbody tr td.attachment span.attachment {
background-position: 0 -996px;
}
#messagelist thead tr td.attachment span.attachment {
background-position: -24px -997px;
}
#messagelist tbody tr td.attachment span.report {
background-position: -24px -1116px;
}
#messagelist tr td.priority span.prio5 {
background-position: 0 -1905px;
}
#messagelist tr td.priority span.prio4 {
background-position: 0 -1885px;
}
#messagelist tr td.priority span.prio2 {
background-position: 0 -1865px;
}
#messagelist tr td.priority span.prio1 {
background-position: 0 -1845px;
}
#messagelist tbody tr td.flag span.flagged {
background-position: 0 -1036px;
}
#messagelist thead tr td.flag span.flagged {
background-position: -24px -1036px;
}
#messagelist tr td.status span.msgicon:hover {
background-position: -23px -1056px;
}
#messagelist tr td.flag span.unflagged:hover {
background-position: -23px -1076px;
}
#messagelist tr td.subject span.msgicon,
#messagelist tr td.subject span.unreadchildren {
background-position: 0 -1056px;
margin: 0 1px 0 0;
width: 24px;
}
#messagelist tr td.subject span.replied {
background-position: 0 -1076px;
}
#messagelist tr td.subject span.forwarded {
background-position: 0 -1096px;
}
#messagelist tr td.subject span.replied.forwarded {
background-position: 0 -1116px;
}
#messagelist tr td.status span.msgicon,
#messagelist tr td.flag span.unflagged,
#messagelist tr td.status span.unreadchildren {
background-position: 0 1056px; /* no icon */
}
/*
#messagelist tr td.status span.msgicon:hover {
background-position: 0 -272px;
}
*/
#messagelist tr td.status span.deleted,
#messagelist tr td.status span.deleted:hover,
#messagelist tr td.subject span.deleted {
background-position: -22px -1096px;
}
#messagelist tr td.status span.status,
#messagelist tr td.status span.unread,
#messagelist tr td.subject span.unread,
#messagelist tr td.status span.unread:hover {
background-position: 0 -1016px;
}
#messagelist thead tr td.status span.status {
background-position: -24px -1016px;
}
#messagelist tr td div.collapsed {
background-position: 0 -1137px;
cursor: pointer;
}
#messagelist tr td div.expanded {
background-position: 0 -1157px;
cursor: pointer;
}
#messagelist tr td.threads div.listmenu {
background-position: 0 -976px;
cursor: pointer;
width: 26px;
}
#messagelist thead tr td.subject,
#messagelist tbody tr td.subject {
width: 99%;
white-space: nowrap;
}
#messagelist tbody tr td.subject a {
cursor: default;
vertical-align: middle; /* #1487091 */
}
/* thread parent message with unread children */
#messagelist tbody tr.unroot td.subject a {
text-decoration: underline;
}
/**** tree indicators ****/
#messagelist tbody tr td span.branch div {
display: inline-block;
}
#messagelist tbody tr td span.branch div.tree {
width: 15px;
}
#listoptions ul.proplist {
min-width: 16em;
}
/**** message view ****/
#mailpreviewframe {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 28px;
}
#messagecontframe {
border: 0;
border-radius: 4px 4px 0 0;
}
#messagecontent {
position: absolute;
top: 110px;
left: 0;
width: 100%;
bottom: 27px;
overflow: auto;
}
#messageheader,
#partheader,
#composeheaders {
position: relative;
padding: 3px 0;
background: #f9f9f9;
background: -moz-linear-gradient(top, #fff 0%, #f0f0f0 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fff), color-stop(100%,#f0f0f0));
background: -o-linear-gradient(top, #fff 0%, #f0f0f0 100%);
background: -ms-linear-gradient(top, #fff 0%, #f0f0f0 100%);
background: linear-gradient(top, #fff 0%, #f0f0f0 100%);
border-bottom: 1px solid #dfdfdf;
}
#mailview-right #messageheader {
border-radius: 4px 4px 0 0;
padding-left: 78px;
/* avoid headers eating up all the vertical space */
max-height: 50%;
overflow: auto;
}
h2.subject {
font-size: 15px;
margin: 0 15em 0 0;
padding: 4px 8px 2px 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#mailview-right #messageheader h2.subject {
margin-left: -56px;
}
h3.subject {
font-size: 14px;
margin: 0 12em 0 0;
padding: 8px 8px 4px 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.headers-table td {
color: #666;
padding: 2px 8px;
}
.headers-table td.header,
.ui-dialog-content.popup span.adr {
font-weight: bold;
}
.headers-table td.header-title {
white-space: nowrap;
}
.headers-table td.header a,
.ui-dialog-content.popup span.adr a {
color: #666;
text-decoration: none;
}
.headers-table td.header a:hover,
.ui-dialog-content.popup span.adr a:hover {
text-decoration: underline;
}
.headers-table td.subject {
color: #333;
font-size: 110%;
font-weight: bold;
}
.headers-table td.header span,
.ui-dialog-content.popup span.adr {
white-space: nowrap;
}
.headers-table td.header a.morelink {
color: #0069a6;
white-space: nowrap;
font-weight: normal;
}
.rcmaddcontact {
position: relative;
top: 1px;
margin-left: 0.5em;
}
.rcmaddcontact imp {
width: 20px;
height: 13px;
}
#preview-allheaders {
display: none;
}
#preview-allheaders td.header-title,
#preview-shortheaders td.header-title {
padding-left: 0;
}
#preview-shortheaders td.header {
padding-right: 18px;
}
.moreheaderstoggle {
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 18px;
padding: 0;
outline: none;
background: #f2f2f2;
background: -moz-linear-gradient(left, #fbfbfb 0, #e9e9e9 100%);
background: -webkit-gradient(linear, left top, right top, color-stop(0,#fbfbfb), color-stop(100%,#e9e9e9));
background: -o-linear-gradient(left, #fbfbfb 0, #e9e9e9 100%);
background: -ms-linear-gradient(left, #fbfbfb 0, #e9e9e9 100%);
background: linear-gradient(left, #fbfbfb 0, #e9e9e9 100%);
border-right: 1px solid #dfdfdf;
border-radius: 3px 0 0 0; /* for Opera */
}
.moreheaderstoggle .iconlink {
display: inline-block;
position: absolute;
top: 8px;
left: 0;
width: 18px;
height: 16px;
background: url(images/buttons.png) -27px -242px no-repeat;
}
.moreheaderstoggle.remove .iconlink {
top: auto;
bottom: 5px;
background-position: -5px -242px;
}
#full-headers {
position: relative;
}
div.more-headers {
position: absolute;
top: -12px;
right: 10px;
width: 12px;
height: 10px;
cursor: pointer;
background: url(images/buttons.png) center -1579px no-repeat;
}
div.hide-headers {
background-position: center -1590px;
}
#all-headers {
position: relative;
margin: 4px 10px;
padding: 0;
height: 180px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fdfdfd;
-moz-box-shadow: inset 0 0 1px 1px rgba(0,0,0, 0.1);
-webkit-box-shadow: inset 0 0 1px 1px rgba(0,0,0, 0.1);
-o-box-shadow: inset 0 0 1px 1px rgba(0,0,0, 0.1);
box-shadow: inset 0 0 1px 1px rgba(0,0,0, 0.1);
}
#headers-source {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 3px 6px;
overflow: auto;
text-align: left;
color: #333;
}
#messageheader.previewheader #all-headers {
margin-left: 0;
}
#messageheader.previewheader {
position: relative;
height: auto;
min-height: 52px;
padding: 0 0 3px 72px;
}
#messageheader.previewheader h3.subject {
padding: 8px 8px 2px 0;
}
#messageheader.previewheader #contactphoto {
display: block;
position: absolute;
top: 11px;
left: 30px;
width: 32px;
height: 32px;
overflow: hidden;
background: url(images/contactpic_32px.png) center center no-repeat #fff;
border-radius: 3px;
}
#messageheader.previewheader #contactphoto img {
width: 32px;
height: auto;
border-radius: 3px;
}
#messageheader .message-headers {
min-height: 60px;
}
#messageheader #contactphoto {
display: block;
position: absolute;
top: 34px;
left: 30px;
width: 48px;
height: 48px;
overflow: hidden;
border-radius: 4px;
border: 1px solid #e6e6e6;
background: url(images/contactpic_48px.png) center center no-repeat #fff;
}
#messageheader #contactphoto img {
width: 48px;
height: auto;
border-radius: 4px;
}
#messageheader #countcontrols,
#messageheader #formatcontrols {
position: absolute;
top: 8px;
right: 8px;
text-align: right;
white-space: nowrap;
}
#messageheader #formatcontrols {
top: 38px;
right: 8px;
}
#messageheader .pagenav .countdisplay {
min-width: 0;
padding-right: 0.5em;
white-space: nowrap;
}
#messagecontent .leftcol,
#messagepreview .leftcol {
margin-right: 252px;
overflow-x: auto;
}
#messagecontent .rightcol,
#messagepreview .rightcol {
float: right;
/*
position: absolute;
top: 10px;
right: 10px;
height: 90%;
*/
width: 230px;
margin: 8px;
min-height: 200px;
background: #f0f0f0;
padding: 8px;
border-radius: 4px;
}
#messagebody {
position: relative;
margin: 8px;
}
#message-objects div,
#messagebody span.part-notice {
margin: 8px;
}
#message-objects div.notice,
#message-buttons div.notice {
display: block;
color: #960;
border: 1px solid #ffdf0e;
background-color: #fef893;
background-position: 5px -83px;
padding: 6px 12px 4px 30px;
white-space: normal;
}
#message-objects div a.button,
#messagebody span.part-notice a.button {
margin-left: 10px;
}
div.message-part,
div.message-htmlpart,
div.message-partheaders {
padding: 10px 2px;
border-top: 1px solid #ccc;
}
#messagebody div:first-child {
padding-top: 0;
border-top: 0;
}
div.message-part pre,
div.message-htmlpart pre,
div.message-part div.pre {
margin: 0;
padding: 0;
font-family: monospace;
font-size: 12px;
white-space: -moz-pre-wrap !important;
white-space: pre-wrap !important;
white-space: pre;
}
div.message-part span.sig {
color: #666;
}
div.message-part blockquote {
color: blue;
border-left: 2px solid blue;
border-right: 2px solid blue;
background-color: #F6F6F6;
margin: 2px 0 2px 0;
padding: 1px 8px 1px 10px;
}
div.message-part blockquote blockquote {
color: green;
border-left: 2px solid green;
border-right: 2px solid green;
}
div.message-part blockquote blockquote blockquote {
color: #900;
border-left: 2px solid #b00;
border-right: 2px solid #b00;
}
div.message-partheaders {
margin-top: 8px;
padding: 8px 0;
}
div.message-partheaders .headers-table {
width: 100%;
}
div.message-partheaders .headers-table td.header-title {
width: auto;
padding-left: 0;
}
div.message-partheaders .headers-table td.header {
width: 88%;
}
#messagebody > hr {
color: #fff;
background: #fff;
border: 0;
border-bottom: 2px solid #f0f0f0;
}
#messagebody fieldset.image-attachment {
border: 0;
border-top: 1px solid #ccc;
margin-top: 1em;
}
#messagebody fieldset.image-attachment p > img {
max-width: 80%;
}
#messagebody legend.image-filename {
color: #999;
font-size: 0.9em;
margin: 0 1em;
}
#messagebody p.image-attachment {
position: relative;
padding: 1em;
border-top: 1px solid #ccc;
}
#messagebody p.image-attachment a.image-link {
float: left;
display: block;
margin-right: 2em;
min-width: 160px;
min-height: 60px;
text-align: center;
}
#messagebody p.image-attachment .image-filename {
display: block;
font-weight: bold;
line-height: 1.6em;
}
#messagebody p.image-attachment .image-filesize {
padding-right: 1em;
}
#messagebody p.image-attachment .attachment-links a {
margin-right: 0.6em;
}
#messagepartcontainer {
position: absolute;
top: 60px;
left: 0;
right: 0;
bottom: 0;
}
#messagepartframe {
border: 0;
width: 100%;
height: 100%;
}
/*** message composition ***/
#composeview-left {
position: absolute;
top: 0;
left: 0;
width: 250px;
bottom: 0;
}
#composeview-right {
position: absolute;
top: 0;
left: 262px;
right: 0;
bottom: 0;
}
#compose-contacts {
position: absolute;
top: 42px;
left: 0;
width: 100%;
bottom: 0;
}
#composequicksearch {
position: relative;
padding: 4px;
background: #c7e3ef;
}
#composequicksearch .searchbox input {
width: 100%;
height: 26px;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#composequicksearch #searchmenulink {
width: 15px;
}
#compose-contacts #directorylist {
border-bottom: 4px solid #c7e3ef;
}
#compose-contacts .scroller {
top: 65px;
border-top: 1px solid #fff;
}
#contacts-table {
table-layout: fixed;
}
#contacts-table td {
width: 100%;
}
#contacts-table td span {
display: block;
}
#contacts-table td span.email {
display: inline;
color: #69939e;
font-style: italic;
margin-left: 0.5em;
}
#compose-contacts li a, #contacts-table td {
background: url(images/listicons.png) -100px 0 no-repeat;
overflow: hidden;
padding-left: 36px;
text-overflow: ellipsis;
}
+#contacts-table td.contactgroup a {
+ color: #376572;
+ text-decoration: none;
+}
+
+#contacts-table td.contactgroup a span {
+ display: inline-block;
+ font-size: 16px;
+ font-weight: bold;
+ line-height: 11px;
+ margin-left: 0.3em;
+}
+
#contacts-table tr:first-child td {
border-top: 0;
}
#compose-contacts li.addressbook a {
background-position: 6px -766px;
}
#compose-contacts li.addressbook.selected a {
background-position: 6px -791px;
}
#contacts-table td.contactgroup {
background-position: 6px -1555px;
}
#contacts-table tr.unfocused td.contactgroup,
#contacts-table tr.selected td.contactgroup {
background-position: 6px -1579px;
}
#contacts-table td.contact {
background-position: 6px -1603px;
}
#contacts-table tr.unfocused td.contact,
#contacts-table tr.selected td.contact {
background-position: 6px -1627px;
}
#compose-content {
position: absolute;
top: 42px;
left: 0;
width: 100%;
bottom: 28px;
border-radius: 4px 4px 0 0;
border-bottom: none;
overflow: hidden;
}
#composeheaders {
border-radius: 4px 4px 0 0;
padding-left: 19px;
}
#composebuttons {
position: absolute;
top: 6px;
right: 6px;
width: auto;
white-space: nowrap;
z-index: 100;
}
#composebuttons a.button.extwin {
padding: 2px 3px;
}
.compose-headers {
width: 99%;
margin-bottom: 2px;
}
.compose-headers td {
padding: 2px 4px;
}
.compose-headers td.title {
width: 11%;
white-space: nowrap;
padding-left: 6px;
}
.compose-headers td.title label {
float: left;
}
.compose-headers td.title a.iconbutton {
float: right;
position: relative;
top: -2px;
width: 15px;
}
.compose-headers td.editfield {
width: 90%;
padding-left: 4px;
}
.compose-headers td.editfield a.iconlink {
margin-left: 0.5em;
}
.compose-headers td.formlinks {
padding: 0 4px;
}
.compose-headers td.top {
vertical-align: top;
padding-top: 10px;
}
.compose-headers td textarea,
.compose-headers td input {
width: 100%;
resize: none;
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
font-size: 11px;
}
#compose-cc, #compose-bcc, #compose-replyto, #compose-followupto {
display: none;
}
#composeoptions {
display: none;
padding: 2px 0 0 8px;
white-space: normal;
border-top: 1px solid #dfdfdf;
box-shadow: inset 0 1px 0 0 #fff;
-o-box-shadow: inset 0 1px 0 0 #fff;
-webkit-box-shadow: inset 0 1px 0 0 #fff;
-moz-box-shadow: inset 0 1px 0 0 #fff;
}
.composeoption {
color: #666;
padding-right: 22px;
white-space: nowrap;
}
#composeoptions .composeoption {
display: inline-block;
padding: 4px 22px 4px 0;
}
#composeoptions .composeoption:last-child {
padding-right: 4px;
}
.mozilla .composeoption input {
vertical-align: -3px;
}
#composeview-bottom {
position: relative;
width: 100%;
height: 200px;
}
#composebodycontainer {
position: absolute;
top: 0;
left: 0;
right: 260px;
bottom: 0;
}
#composebodycontainer.buttons {
bottom: 42px;
}
#composebody {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 99%;
border: 0;
border-radius: 0;
padding: 8px 0 8px 8px;
resize: none;
font-family: monospace;
font-size: 9pt;
outline: none;
box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
-moz-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
-webkit-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
-o-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
}
#composebody:active,
#composebody:focus {
box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
-o-box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
}
#compose-attachments {
position: absolute;
right: 0;
top: 1px;
bottom: 0;
width: 240px;
background: #f0f0f0;
border-style: solid;
border-color: #f0f0f0 #f0f0f0 #f0f0f0 #ddd;
border-width: 1px;
padding: 8px;
overflow: auto;
}
#compose-attachments.droptarget {
background-image: url(images/filedrop.png);
background-position: center bottom;
background-repeat: no-repeat;
}
#compose-attachments.droptarget.hover,
#compose-attachments.droptarget.active {
border-color: #019bc6;
box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-moz-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-webkit-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-o-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
}
#compose-attachments.droptarget.hover {
background-color: #d9ecf4;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
}
#composeview-bottom .formbuttons.floating {
position: absolute;
width: auto;
right: 260px;
z-index: 200;
padding-bottom: 8px;
}
.defaultSkin table.mceLayout,
.defaultSkin table.mceLayout tr.mceLast td {
border: 0 !important;
}
.defaultSkin td.mceToolbar {
border: 0 !important;
}
.defaultSkin table.mceLayout tr.mceFirst td {
background: #f0f0f0;
}
#composebody_toolbargroup {
border-bottom: 1px solid #ddd;
}
#uploadform a.iconlink {
margin-left: 1em;
text-indent: -5000px;
}
#uploadform form div {
margin: 4px 0;
}
diff --git a/skins/larry/styles.css b/skins/larry/styles.css
index cfbf9ac5f..8ddbb5960 100644
--- a/skins/larry/styles.css
+++ b/skins/larry/styles.css
@@ -1,2391 +1,2395 @@
/**
* Roundcube webmail styles for skin "Larry"
*
* Copyright (c) 2012, The Roundcube Dev Team
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
body {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
font-size: 11px;
color: #333;
background: url(images/linen.jpg) repeat #d1d5d8;
margin: 0;
}
body.noscroll {
/* also avoids bounce effect in Chrome and Safari */
overflow: hidden;
}
a {
color: #0069a6;
}
a:visited {
color: #0186ba;
}
img {
border: 0;
}
input[type="text"],
input[type="password"],
textarea {
margin: 0; /* Safari by default adds a margin */
padding: 4px;
border: 1px solid #b2b2b2;
border-radius: 4px;
box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-moz-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-webkit-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-o-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
}
input[type="text"]:focus,
input[type="password"]:focus,
input.button:focus,
textarea:focus {
border-color: #4787b1;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
outline: none;
}
input.placeholder,
textarea.placeholder {
color: #aaa;
}
.bold {
font-weight: bold;
}
/* fixes vertical alignment of checkboxes and labels */
label input,
label span {
vertical-align: middle;
}
/*** buttons ***/
input.button {
display: inline-block;
margin: 0 2px;
padding: 2px 5px;
color: #525252;
text-shadow: 0px 1px 1px #fff;
border: 1px solid #c0c0c0;
border-radius: 4px;
background: #f7f7f7;
background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6));
background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-o-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-webkit-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-moz-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
text-decoration: none;
outline: none;
}
.formbuttons input.button {
color: #ddd;
font-size: 110%;
text-shadow: 0px 1px 1px #333;
padding: 4px 12px;
border-color: #465864;
border-radius: 5px;
background: #7a7b7d;
background: -moz-linear-gradient(top, #7b7b7b 0%, #606060 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#7b7b7b), color-stop(100%,#606060)); /* Chrome,Safari4+ */
background: -o-linear-gradient(top, #7b7b7b 0%, #606060 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #7b7b7b 0%, #606060 100%); /* IE10+ */
background: linear-gradient(top, #7b7b7b 0%, #606060 100%); /* W3C */
box-shadow: 0 1px 1px 0 #ccc, inset 0 1px 0 0 #888;
-o-box-shadow: 0 1px 1px 0 #ccc, inset 0 1px 0 0 #888;
-webkit-box-shadow: 0 1px 1px 0 #ccc, inset 0 1px 0 0 #888;
-moz-box-shadow: 0 1px 1px 0 #ccc, inset 0 1px 0 0 #888;
}
.formbuttons input.button:hover,
.formbuttons input.button:focus,
input.button.mainaction:hover,
input.button.mainaction:focus {
color: #f2f2f2;
border-color: #465864;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.6), inset 0 1px 0 0 #888;
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.6), inset 0 1px 0 0 #888;
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.6), inset 0 1px 0 0 #888;
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.6), inset 0 1px 0 0 #888;
}
.formbuttons input.button:active {
color: #fff;
background: -moz-linear-gradient(top, #5c5c5c 0%, #7b7b7b 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#5c5c5c), color-stop(100%,#7b7b7b));
background: -o-linear-gradient(top, #5c5c5c 0%, #7b7b7b 100%);
background: -ms-linear-gradient(top, #5c5c5c 0%, #7b7b7b 100%);
background: linear-gradient(top, #5c5c5c 0%, #7b7b7b 100%);
}
input.button.mainaction {
color: #ededed;
text-shadow: 0px 1px 1px #333;
border-color: #1f262c;
background: #505050;
background: -moz-linear-gradient(top, #505050 0%, #2a2e31 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#505050), color-stop(100%,#2a2e31));
background: -o-linear-gradient(top, #505050 0%, #2a2e31 100%);
background: -ms-linear-gradient(top, #505050 0%, #2a2e31 100%);
background: linear-gradient(top, #505050 0%, #2a2e31 100%);
box-shadow: inset 0 1px 0 0 #777;
-moz-box-shadow: inset 0 1px 0 0 #777;
-webkit-box-shadow: inset 0 1px 0 0 #777;
-o-box-shadow: inset 0 1px 0 0 #777;
}
input.button.mainaction:active {
color: #fff;
background: #515151;
background: -moz-linear-gradient(top, #2a2e31 0%, #505050 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#2a2e31), color-stop(100%,#505050));
background: -o-linear-gradient(top, #2a2e31 0%, #505050 100%);
background: -ms-linear-gradient(top, #2a2e31 0%, #505050 100%);
background: linear-gradient(top, #2a2e31 0%, #505050 100%);
}
input.button[disabled],
input.button[disabled]:hover,
input.button.mainaction[disabled] {
color: #aaa !important;
}
input.mainaction {
font-weight: bold;
}
/** link buttons **/
a.button,
.buttongroup {
display: inline-block;
margin: 0 2px;
padding: 2px 5px;
color: #525252;
text-shadow: 0px 1px 1px #fff;
border: 1px solid #c6c6c6;
border-radius: 4px;
background: #f7f7f7;
background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6));
background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-o-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-webkit-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-moz-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
text-decoration: none;
}
.buttongroup {
padding: 0;
white-space: nowrap;
}
a.button:focus,
input.button:focus {
border-color: #4fadd5;
box-shadow: 0 0 2px 1px rgba(71,135,177, 0.6);
-moz-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.6);
-webkit-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.6);
-o-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.6);
outline: none;
}
label.disabled,
a.button.disabled {
color: #999;
}
a.button.disabled,
input.button.disabled,
input.button[disabled],
a.button.disabled:hover,
input.button.disabled:hover,
input.button[disabled]:hover {
border-color: #c6c6c6;
box-shadow: 0 1px 1px 0 rgba(160, 160, 160, 0.4);
-o-box-shadow: 0 1px 1px 0 rgba(160, 160, 160, 0.4);
-webkit-box-shadow: 0 1px 1px 0 rgba(160, 160, 160, 0.4);
-moz-box-shadow: 0 1px 1px 0 rgba(160, 160, 160, 0.4);
}
a.button.disabled span.inner {
opacity: 0.4;
filter: alpha(opacity=40);
}
.buttongroup a.button {
margin: 0;
border-width: 0 1px 0 0;
border-radius: 0;
background: none;
box-shadow: none;
-o-box-shadow: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
}
.buttongroup a.button.first,
.buttongroup a.button:first-child {
border-radius: 4px 0 0 4px;
border-left: 0;
}
.buttongroup a.button.last,
.buttongroup a.button:last-child {
border-radius: 0 4px 4px 0;
border-right: 0;
}
a.button.pressed,
a.button:active,
input.button:active {
background: #e6e6e6;
background: -moz-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e6e6e6), color-stop(100%,#f9f9f9));
background: -o-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
background: -ms-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
background: linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
}
.pagenav.dark a.button {
font-weight: bold;
border-color: #e6e6e6;
background: #d8d8d8;
background: -moz-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d8d8d8), color-stop(100%,#bababa));
background: -o-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: -ms-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: linear-gradient(top, #d8d8d8 0%, #bababa 100%);
box-shadow: 0 1px 1px 0 #999;
-o-box-shadow: 0 1px 1px 0 #999;
-webkit-box-shadow: 0 1px 1px 0 #999;
-moz-box-shadow: 0 1px 1px 0 #999;
}
.pagenav.dark a.button.pressed {
background: #bababa;
background: -moz-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bababa), color-stop(100%,#d8d8d8));
background: -o-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -ms-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: linear-gradient(top, #bababa 0%, #d8d8d8 100%);
}
.buttongroup a.button.selected,
.buttongroup a.button.selected:hover {
background: #8a8a8a;
background: -moz-linear-gradient(top, #909090 0%, #858585 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#909090), color-stop(100%,#858585));
background: -o-linear-gradient(top, #909090 0%, #858585 100%);
background: -ms-linear-gradient(top, #909090 0%, #858585 100%);
background: linear-gradient(top, #909090 0%, #858585 100%);
-webkit-box-shadow: inset 0 1px 2px 0 #555;
-moz-box-shadow: inset 0 1px 2px 0 #555;
box-shadow: inset 0 1px 2px 0 #555;
border-right-color: #555;
border-left-color: #555;
}
.pagenav a.button {
padding: 1px 3px;
height: 16px;
vertical-align: middle;
margin-bottom: 1px;
}
.pagenav .buttongroup a.button,
.pagenav .buttongroup a.button:hover {
padding: 1px 5px;
margin-bottom: 0;
}
.pagenav a.button span.inner {
display: inline-block;
width: 16px;
height: 13px;
text-indent: 1000px;
overflow: hidden;
background: url(images/buttons.png) -6px -211px no-repeat;
}
.pagenav a.prevpage span.inner {
background-position: -7px -226px;
}
.pagenav a.nextpage span.inner {
background-position: -28px -226px;
}
.pagenav a.lastpage span.inner {
background-position: -28px -211px;
}
.pagenav a.pageup span.inner {
background-position: -7px -241px;
}
.pagenav a.pagedown span.inner {
background-position: -29px -241px;
}
.pagenav a.reply span.inner {
background-position: -7px -256px;
}
.pagenav a.forward span.inner {
background-position: -29px -256px;
}
.pagenav a.replyall span.inner {
background-position: -7px -271px;
}
.pagenav a.extwin span.inner {
background-position: -29px -271px;
}
.pagenav a.changeformat.html span.inner {
background-position: -7px -1859px;
}
.pagenav a.changeformat.html.selected span.inner {
background-position: -29px -1859px;
}
.pagenav a.changeformat.text span.inner {
background-position: -7px -1874px;
}
.pagenav a.changeformat.text.selected span.inner {
background-position: -29px -1874px;
}
.pagenav .countdisplay {
display: inline-block;
padding: 3px 1em 0 1em;
text-shadow: 0px 1px 1px #fff;
min-width: 16em;
}
.pagenavbuttons {
position: relative;
top: -2px;
}
a.iconbutton {
display: inline-block;
width: 24px;
height: 18px;
text-decoration: none;
text-indent: -5000px;
background: url(images/buttons.png) -1000px 0 no-repeat;
}
a.iconbutton.disabled {
opacity: 0.4;
filter: alpha(opacity=40);
cursor: default;
}
a.iconbutton.searchoptions {
background-position: -2px -317px;
}
a.iconbutton.reset {
background-position: -25px -317px;
}
a.iconbutton.cancel {
background-position: -7px -377px;
}
a.iconlink {
display: inline-block;
color: #888;
text-decoration: none;
white-space: nowrap;
padding: 2px 8px 2px 20px;
background: url(images/buttons.png) -1000px 0 no-repeat;
}
a.iconlink:hover {
text-decoration: underline;
}
a.iconlink.delete {
background-position: -7px -337px;
}
a.iconlink.add {
background-position: -7px -357px;
}
a.iconlink.remove {
background-position: -7px -378px;
}
a.iconlink.cancel {
background-position: -7px -397px;
}
a.iconlink.edit {
background-position: -7px -417px;
}
a.iconlink.upload {
background-position: -6px -437px;
}
/*** message bar ***/
#message div.loading,
#message div.warning,
#message div.error,
#message div.notice,
#message div.confirmation,
#message-objects div.notice {
color: #555;
font-weight: bold;
padding: 6px 30px 6px 25px;
display: inline-block;
white-space: nowrap;
background: url(images/messages.png) 0 5px no-repeat;
cursor: default;
}
#message div.warning {
color: #960;
background-position: 0 -86px;
}
#message div.error {
color: #cf2734;
background-position: 0 -55px;
}
#message div.confirmation {
color: #093;
background-position: 0 -25px;
}
#message div.loading {
background: url(images/ajaxloader.gif) 2px 6px no-repeat;
}
#message div a,
#message div span {
padding-right: 0.5em;
text-decoration: none;
}
#message div a:hover {
text-decoration: underline;
cursor: pointer;
}
#message.statusbar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 27px;
padding-left: 8px;
border-top: 1px solid #ddd;
border-radius: 0 0 4px 4px;
background: #eaeaea;
background: -moz-linear-gradient(top, #eaeaea 0%, #c8c8c8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eaeaea), color-stop(100%,#c8c8c8));
background: -o-linear-gradient(top, #eaeaea 0%, #c8c8c8 100%);
background: -ms-linear-gradient(top, #eaeaea 0%, #c8c8c8 100%);
background: linear-gradient(top, #eaeaea 0%, #c8c8c8 100%);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ui-dialog.error .ui-dialog-title,
.ui-dialog.warning .ui-dialog-title,
.ui-dialog.confirmation .ui-dialog-title {
padding-left: 25px;
background: url(images/messages.png) 0 5px no-repeat;
text-shadow: 0 1px 1px #fff;
}
.ui-dialog.warning .ui-dialog-title {
color: #960;
background-position: 0 -90px;
}
.ui-dialog.error .ui-dialog-title {
color: #cf2734;
background-position: 0 -60px;
}
.ui-dialog.confirmation .ui-dialog-title {
color: #093;
background-position: 0 -30px;
}
.ui-dialog.popupmessage .ui-dialog-titlebar {
padding: 8px 1em 4px 1em;
background: #e3e3e3;
background: -moz-linear-gradient(top, #e3e3e3 0%, #cfcfcf 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e3e3e3), color-stop(100%,#cfcfcf));
background: -o-linear-gradient(top, #e3e3e3 0%, #cfcfcf 100%);
background: -ms-linear-gradient(top, #e3e3e3 0%, #cfcfcf 100%);
background: linear-gradient(top, #e3e3e3 0%, #cfcfcf 100%);
}
.ui-dialog.popupmessage .ui-widget-content {
font-size: 12px;
background: #eee;
background: -moz-linear-gradient(top, #eee 0%, #dcdcdc 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eee), color-stop(100%,#dcdcdc));
background: -o-linear-gradient(top, #eee 0%, #dcdcdc 100%);
background: -ms-linear-gradient(top, #eee 0%, #dcdcdc 100%);
background: linear-gradient(top, #eee 0%, #dcdcdc 100%);
}
/*** basic page layout ***/
#header {
overflow-x: hidden; /* Chrome bug #1488851 */
}
#topline {
height: 18px;
background: url(images/linen_header.jpg) repeat #666;
border-bottom: 1px solid #4f4f4f;
padding: 2px 0 2px 10px;
color: #aaa;
text-align: center;
}
#topnav {
position: relative;
height: 46px;
margin-bottom: 10px;
padding: 0 0 0 10px;
background: #111;
background: -moz-linear-gradient(top, #404040 0%, #060606 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#404040), color-stop(100%,#060606));
background: -o-linear-gradient(top, #404040 0%, #060606 100%);
background: -ms-linear-gradient(top, #404040 0%, #060606 100%);
background: linear-gradient(top, #404040 0%, #060606 100%);
}
#topline a,
#topnav a {
color: #eee;
text-decoration: none;
}
#topline a:hover {
text-decoration: underline;
}
#toplogo {
padding-top: 2px;
cursor: pointer;
}
.topleft {
float: left;
}
.topright {
float: right;
}
.closelink {
display: inline-block;
padding: 2px 10px 2px 20px;
}
#topline span.username {
padding-right: 1em;
}
#topline .topleft a {
display: inline-block;
padding: 2px 0.8em 0 0;
color: #aaa;
}
#topline a.button-logout {
display: inline-block;
padding: 2px 10px 2px 20px;
background: url(images/buttons.png) -6px -193px no-repeat;
color: #fff;
}
#taskbar .button-logout {
display: none;
}
#taskbar a.button-logout span.button-inner {
background-position: -2px -1791px;
}
#taskbar a.button-logout:hover span.button-inner {
background-position: -2px -1829px;
}
/*** minimal version of the page header ***/
.minimal #topline {
position: fixed;
top: -18px;
background: #444;
z-index: 5000;
width: 100%;
height: 22px;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.minimal #topline:hover {
top: 0px;
opacity: 0.94;
filter: alpha(opacity=94);
-webkit-transition: top 0.3s ease-in-out;
-moz-transition: top 0.3s ease-in-out;
-o-transition: top 0.3s ease-in-out;
transition: top 0.3s ease-in-out;
}
.extwin #topline,
.extwin #topline:hover {
position: static;
top: 0px;
height: 18px;
width: auto;
-moz-box-sizing: content-box;
box-sizing: content-box;
opacity: 0.999;
}
.partwin #topline {
position: absolute;
right: 6px;
top: 18px;
width: auto;
z-index: 100;
background: transparent;
background: none;
border: 0;
}
.minimal #topline a.button-logout {
display: none;
}
.minimal #topline span.username {
display: inline-block;
padding-top: 2px;
}
.minimal #topnav {
position: relative;
top: 4px;
height: 42px;
}
.minimal #taskbar a {
position: relative;
padding: 10px 10px 0 6px;
height: 32px;
}
.minimal #taskbar .button-logout {
display: inline-block;
}
.minimal #taskbar .button-inner {
top: -4px;
padding: 0;
height: 24px !important;
width: 27px;
text-indent: -5000px;
}
#taskbar .tooltip {
display: none;
}
.minimal #taskbar .tooltip {
position: absolute;
top: -500px;
right: 2px;
display: inline-block;
padding: 2px 8px 3px 8px;
background: #444;
background: -moz-linear-gradient(top, #444 0%, #333 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#444), color-stop(100%,#333));
background: -o-linear-gradient(top, #444 0%, #333 100%);
background: -ms-linear-gradient(top, #444 0%, #333 100%);
background: linear-gradient(top, #444 0%, #333 100%);
color: #eee;
font-weight: bold;
white-space: nowrap;
border: 1px solid #777;
box-shadow: 0 1px 5px 0 #333;
-moz-box-shadow: 0 1px 5px 0 #333;
-webkit-box-shadow: 0 1px 5px 0 #333;
-o-box-shadow: 0 1px 5px 0 #333;
z-index: 200;
white-space: nowrap;
text-shadow: 0px 1px 1px #000;
}
.minimal #taskbar .tooltip:after {
content: "";
position: absolute;
top: -4px;
right: 15px;
border-style: solid;
border-width: 0 4px 4px;
border-color: #888 transparent;
/* reduce the damage in FF3.0 */
display: block;
width: 0;
z-index: 251;
}
.ie8 .minimal #taskbar .tooltip:after {
top: -6px;
}
.minimal #taskbar a:hover .tooltip {
display: block;
top: 39px;
}
/*** taskbar ***/
#taskbar {
position: relative;
padding-right: 18px;
}
#taskbar a {
display: inline-block;
height: 34px;
padding: 12px 10px 0 6px;
}
#taskbar a span.button-inner {
display: inline-block;
font-size: 110%;
font-weight: normal;
text-shadow: 0px 1px 1px black;
padding: 5px 0 0 34px;
height: 19px;
background: url(images/buttons.png) -1000px 0 no-repeat;
}
#taskbar a.button-selected {
color: #3cf;
background-color: #2c2c2c;
}
#taskbar a.button-mail span.button-inner {
background-position: 0 2px;
}
#taskbar a.button-mail:hover span.button-inner,
#taskbar a.button-mail.button-selected span.button-inner {
background-position: 0 -22px;
}
#taskbar a.button-addressbook span.button-inner {
background-position: 0 -48px;
}
#taskbar a.button-addressbook:hover span.button-inner,
#taskbar a.button-addressbook.button-selected span.button-inner {
background-position: 0 -72px;
}
#taskbar a.button-settings span.button-inner {
background-position: 0 -96px;
}
#taskbar a.button-settings:hover span.button-inner,
#taskbar a.button-settings.button-selected span.button-inner {
background-position: 0 -120px;
}
#taskbar a.button-calendar span.button-inner {
background-position: 0 -144px;
}
#taskbar a.button-calendar:hover span.button-inner,
#taskbar a.button-calendar.button-selected span.button-inner {
background-position: 0 -168px;
}
#taskbar .minmodetoggle {
position: absolute;
top: 0;
right: 0;
display: block;
width: 19px;
height: 46px;
cursor: pointer;
background: url(images/buttons.png) -35px -1778px no-repeat;
}
.minimal #taskbar .minmodetoggle {
height: 42px;
background-position: -35px -1820px;
}
#mainscreen {
position: absolute;
top: 88px;
left: 10px;
right: 10px;
bottom: 20px;
}
.minimal #mainscreen {
top: 62px;
}
.minimal #mainscreen.offset {
top: 102px;
}
.partwin #mainscreen {
top: 60px
}
.extwin #mainscreen {
top: 40px;
}
#mainscreen.offset {
top: 132px;
}
#mainscreen .offset {
margin-top: 42px;
}
.uibox {
border: 1px solid #a3a3a3;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 0 2px #999;
-o-box-shadow: 0 0 2px #999;
-webkit-box-shadow: 0 0 2px #999;
-moz-box-shadow: 0 0 2px #999;
background: #fff;
}
.minwidth {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
min-width: 1024px;
}
.scroller {
overflow: auto;
}
.readtext {
width: 42em;
padding: 12px;
font-size: 12px;
}
.readtext > h1,
.readtext > h2,
.readtext > h3 {
margin-top: 0;
}
.watermark {
background-image: url(images/watermark.jpg);
background-position: center;
background-repeat: no-repeat;
}
/*** lists ***/
.listbox {
background: #d9ecf4;
overflow: hidden;
}
.listbox .scroller {
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
}
.listbox .scroller.withfooter {
bottom: 42px;
}
.listbox .boxtitle + .scroller {
top: 34px;
}
.boxtitle,
.uibox .listing thead td {
font-size: 12px;
font-weight: bold;
padding: 10px 8px 3px 8px;
height: 20px; /* doesn't affect table-cells in FF */
margin: 0;
text-shadow: 0px 1px 1px #fff;
border-bottom: 1px solid #bbd3da;
white-space: nowrap;
}
.uibox .listing thead td {
padding-bottom: 8px;
height: auto;
}
.uibox .boxtitle,
.uibox .listing thead td {
background: #b0ccd7;
color: #004458;
border-radius: 4px 4px 0 0;
}
.listbox .listitem,
.listbox .tablink,
.listing tbody td,
.listing li {
display: block;
border-top: 1px solid #fff;
border-bottom: 1px solid #bbd3da;
cursor: default;
font-weight: normal;
}
.listbox .listitem a,
.listbox .tablink a,
.listing tbody td,
.listing li a {
display: block;
color: #376572;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
cursor: default;
padding: 6px 8px 2px 8px;
height: 17px; /* doesn't affect table-cells in FF */
white-space: nowrap;
}
.listing tbody td {
display: table-cell;
padding-bottom: 5px;
height: auto;
min-height: 14px;
}
.webkit .listing tbody td {
height: 14px;
}
.listbox .listitem.selected,
.listbox .tablink.selected,
.listbox .listitem.selected > a,
.listbox .tablink.selected > a,
.listing tbody tr.unfocused td,
.listing tbody tr.selected td,
.listing li.selected,
.listing li.selected > a {
color: #004458;
font-weight: bold;
background-color: #c7e3ef;
}
ul.listing {
display: block;
list-style: none;
margin: 0;
padding: 0;
}
ul.listing li {
background-color: #d9ecf4;
}
ul.listing li ul {
border-top: 1px solid #bbd3da;
}
ul.listing li.droptarget,
table.listing tr.droptarget td {
background-color: #e8e798;
}
+.listbox table.listing {
+ background-color: #d9ecf4;
+}
+
table.listing,
table.layout {
border: 0;
width: 100%;
border-spacing: 0;
}
table.layout td {
vertical-align: top;
}
ul.treelist li {
position: relative;
}
ul.treelist li div.treetoggle {
position: absolute;
top: 13px;
left: 19px;
width: 13px;
height: 13px;
background: url(images/listicons.png) -3px -144px no-repeat;
cursor: pointer;
}
ul.treelist li div.treetoggle.expanded {
background-position: -3px -168px;
}
ul.treelist li.selected > div.collapsed {
background-position: -23px -144px;
}
ul.treelist li.selected > div.expanded {
background-position: -23px -168px;
}
.listbox .boxfooter {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 42px;
border-top: 1px solid #ccdde4;
background: #d9ecf4;
-webkit-box-shadow: inset 0 1px 0 0 #fff;
-moz-box-shadow: inset 0 1px 0 0 #fff;
box-shadow: inset 0 1px 0 0 #fff;
white-space: nowrap;
overflow: hidden;
}
.uibox .boxfooter {
border-radius: 0 0 4px 4px;
}
.boxfooter .listbutton {
display: inline-block;
text-decoration: none;
width: 48px;
border-right: 1px solid #fff;
background: #c7e3ef;
padding: 3px 0;
margin-top: 1px;
}
.uibox .boxfooter .listbutton:first-child {
border-radius: 0 0 0 4px;
}
.boxfooter .listbutton .inner {
display: inline-block;
width: 48px;
height: 35px;
text-indent: -5000px;
background: url(images/buttons.png) -1000px 0 no-repeat;
}
.boxfooter .listbutton.add .inner {
background-position: 10px -1301px;
}
.boxfooter .listbutton.delete .inner {
background-position: 10px -1342px;
}
.boxfooter .listbutton.groupactions .inner {
background-position: 5px -1382px;
}
.boxfooter .listbutton.addto .inner {
background-position: 5px -1422px;
}
.boxfooter .listbutton.addcc .inner {
background-position: 5px -1462px;
}
.boxfooter .listbutton.addbcc {
width: 54px;
}
.boxfooter .listbutton.addbcc .inner {
width: 54px;
background-position: 2px -1502px;
}
.boxfooter .listbutton.removegroup .inner {
background-position: 5px -1540px;
}
.boxfooter .listbutton.disabled .inner {
opacity: 0.4;
filter: alpha(opacity=40);
}
.boxfooter .countdisplay {
display: inline-block;
position: relative;
top: 10px;
color: #69929e;
padding: 3px 6px;
}
.boxpagenav {
position: absolute;
top: 10px;
right: 6px;
width: auto;
}
.boxpagenav a.icon {
display: inline-block;
padding: 1px 3px;
height: 13px;
width: 14px;
text-indent: 1000px;
vertical-align: bottom;
overflow: hidden;
background: url(images/buttons.png) -4px -286px no-repeat;
}
.boxpagenav a.icon.prevpage {
background-position: -4px -301px;
}
.boxpagenav a.icon.nextpage {
background-position: -28px -301px;
}
.boxpagenav a.icon.lastpage {
background-position: -28px -286px;
}
.boxpagenav a.icon.disabled {
opacity: 0.4;
filter: alpha(opacity=40);
}
.centerbox {
width: 40em;
margin: 16px auto;
}
.errorbox {
width: 40em;
padding: 20px;
}
.errorbox h3 {
font-size: 16px;
margin-top: 0;
}
/*** Records table ***/
table.records-table {
display: table;
width: 100%;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
border: 1px solid #bbd3da;
}
.boxlistcontent .records-table {
border: 0;
}
.records-table thead td {
color: #69939e;
font-size: 11px;
font-weight: bold;
background: #d6eaf3;
background: -moz-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -webkit-gradient(linear, left top, right top, color-stop(0,#e3f2f6), color-stop(8%,#d6eaf3), color-stop(100%,#d6eaf3));
background: -o-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -ms-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px ,#d6eaf3 100%);
background: linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
border-left: 1px solid #bbd3da;
padding: 8px 7px;
overflow: hidden;
text-overflow: ellipsis;
}
.records-table.sortheader thead td {
padding: 0;
}
.records-table thead td a,
.records-table thead td span {
display: block;
padding: 7px 7px;
color: #69939e;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
}
.records-table tbody td {
padding: 2px 7px;
border-bottom: 1px solid #ddd;
border-left: 1px dotted #bbd3da;
white-space: nowrap;
cursor: default;
overflow: hidden;
text-overflow: ellipsis;
background-color: #fff;
}
.records-table thead tr td:first-child,
.records-table tbody tr td:first-child {
border-left: 0;
}
.records-table tr.selected td {
color: #fff !important;
background: #019bc6;
background: -moz-linear-gradient(top, #019bc6 0%, #017cb4 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#019bc6), color-stop(100%,#017cb4));
background: -o-linear-gradient(top, #019bc6 0%, #017cb4 100%);
background: -ms-linear-gradient(top, #019bc6 0%, #017cb4 100%);
background: linear-gradient(top, #019bc6 0%, #017cb4 100%);
}
.records-table tr.selected td a,
.records-table tr.selected td span {
color: #fff !important;
}
.records-table tr.unfocused td {
color: #fff !important;
background: #4db0d2 !important;
}
.records-table tr.unfocused td a,
.records-table tr.unfocused td span {
color: #fff !important;
}
.records-table tr.deleted td,
.records-table tr.deleted td a {
color: #ccc !important;
}
/*** iFrames ***/
#aboutframe {
width: 97%;
height: 100%;
border: 0;
padding: 0;
}
body.iframe {
background: #fff;
margin: 38px 0 10px 0;
}
body.iframe.error {
background: #ededed;
}
body.iframe.floatingbuttons {
margin-bottom: 40px;
}
body.iframe.fullheight {
margin: 0;
}
.contentbox .boxtitle,
body.iframe .boxtitle {
color: #777;
background: #eee;
background: -moz-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eee), color-stop(100%,#dfdfdf));
background: -o-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: -ms-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: linear-gradient(top, #eee 0%, #dfdfdf 100%);
border-bottom: 1px solid #ccc;
}
body.iframe .boxtitle {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
}
body.iframe .footerleft.floating,
#composeview-bottom .formbuttons.floating {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
z-index: 110;
background: #fff;
padding-top: 8px;
padding-bottom: 12px;
}
body.iframe .footerleft.floating:before,
#composeview-bottom .formbuttons.floating:before {
content: " ";
position: absolute;
top: -6px;
left: 0;
width: 100%;
height: 6px;
background: url(images/overflowshadow.png) top center no-repeat;
}
.boxcontent {
padding: 10px;
}
.contentbox .scroller {
position: absolute;
top: 34px;
left: 0;
right: 0;
bottom: 28px;
overflow: auto;
}
.iframebox {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 28px;
}
.footerleft {
padding: 0 12px 4px 12px;
}
.propform fieldset {
margin-bottom: 20px;
border: 0;
padding: 0;
}
.propform fieldset legend {
display: block;
font-size: 14px;
font-weight: bold;
padding-bottom: 10px;
margin-bottom: 0;
}
.propform fieldset fieldset legend {
color: #666;
font-size: 12px;
}
fieldset.floating {
float: left;
margin-right: 10px;
margin-bottom: 10px;
}
table.propform {
width: 100%;
border-spacing: 0;
border-collapse: collapse;
}
ul.proplist li,
table.propform td {
width: 80%;
padding: 4px 10px;
background: #eee;
border-bottom: 2px solid #fff;
}
table.propform td.title {
width: 20%;
color: #333;
padding-right: 20px;
white-space: nowrap;
}
table.propform .mceLayout td {
padding: 0;
border-bottom: 0;
}
ul.proplist {
list-style: none;
margin: 0;
padding: 0;
}
ul.proplist li {
width: auto;
}
#pluginbody {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/*** Login form ***/
#login-form {
position: relative;
width: 580px;
margin: 20ex auto 2ex auto;
}
#login-form .box-inner {
width: 430px;
background: url(images/linen_login.jpg) top left no-repeat #5c5c5c;
margin: 0 50px;
padding: 10px 24px 24px 24px;
border: 1px solid #333;
border-radius: 5px;
box-shadow: inset 0 0 1px #ccc;
-o-box-shadow: inset 0 0 1px #ccc;
-webkit-box-shadow: inset 0 0 1px #ccc;
-moz-box-shadow: inset 0 0 1px #ccc;
}
#login-form .box-bottom {
background: url(images/login_shadow.png) top center no-repeat;
margin-top: -3px;
padding-top: 10px;
}
#login-form .noscriptwarning {
margin: 0 auto;
width: 430px;
color: #cf2734;
font-size: 110%;
font-weight: bold;
}
#login-form td.input {
width: 80%;
padding: 8px;
}
#login-form input[type="text"],
#login-form input[type="password"] {
width: 100%;
border-color: #666;
}
#login-form input.button {
color: #444;
text-shadow: 0px 1px 1px #fff;
border-color: #f9f9f9;
background: #f9f9f9;
background: -moz-linear-gradient(top, #f9f9f9 0%, #e2e2e2 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e2e2e2));
background: -o-linear-gradient(top, #f9f9f9 0%, #e2e2e2 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #e2e2e2 100%);
background: linear-gradient(top, #f9f9f9 0%, #e2e2e2 100%);
box-shadow: inset 0 1px 0 0 #fff;
-moz-box-shadow: inset 0 1px 0 0 #fff;
-webkit-box-shadow: inset 0 1px 0 0 #fff;
-o-box-shadow: inset 0 1px 0 0 #fff;
}
#login-form input.button:hover,
#login-form input.button:focus {
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9), inset 0 1px 0 0 #fff;
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9), inset 0 1px 0 0 #fff;
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9), inset 0 1px 0 0 #fff;
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9), inset 0 1px 0 0 #fff;
}
#login-form input.button:active {
color: #333;
background: -moz-linear-gradient(top, #dcdcdc 0%, #f9f9f9 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dcdcdc), color-stop(100%,#f9f9f9));
background: -o-linear-gradient(top, #dcdcdc 0%, #f9f9f9 100%);
background: -ms-linear-gradient(top, #dcdcdc 0%, #f9f9f9 100%);
background: linear-gradient(top, #dcdcdc 0%, #f9f9f9 100%);
}
#login-form form table {
width: 98%;
}
#login-form td.title {
width: 20%;
white-space: nowrap;
color: #cecece;
text-shadow: 0px 1px 1px black;
text-align: right;
padding-right: 1em;
}
#login-form p.formbuttons {
margin-top: 2em;
text-align: center;
}
#login-form #logo {
margin-bottom: 20px;
}
#login-form #message {
min-height: 40px;
padding: 5px 25px;
text-align: center;
}
#login-form #message div {
display: inline-block;
padding-right: 0;
}
#bottomline {
font-size: 90%;
text-align: center;
margin-top: 2em;
}
/*** quicksearch **/
.searchbox {
position: relative;
}
#quicksearchbar {
position: absolute;
right: 1px;
top: 2px;
width: 240px;
}
.searchbox input,
#quicksearchbar input {
width: 176px;
margin: 0;
padding: 3px 30px 3px 34px;
height: 18px;
background: #f1f1f1;
border-color: #ababab;
font-weight: bold;
font-size: 11px;
}
.searchbox #searchmenulink,
#quicksearchbar #searchmenulink {
position: absolute;
top: 5px;
left: 6px;
}
.searchbox #searchreset,
#quicksearchbar #searchreset {
position: absolute;
top: 4px;
right: 1px;
}
/*** toolbar ***/
.toolbar .spacer {
display: inline-block;
width: 24px;
height: 40px;
padding: 0;
}
.toolbar a.button {
text-align: center;
font-size: 10px;
color: #555;
min-width: 50px;
max-width: 75px;
height: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 28px 2px 0 2px;
text-shadow: 0px 1px 1px #eee;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
background: url(images/buttons.png) -100px 0 no-repeat transparent;
border: 0;
}
.toolbar a.button.disabled {
opacity: 0.4;
filter: alpha(opacity=40);
}
.dropbutton {
display: inline-block;
position: relative;
}
.dropbutton .dropbuttontip {
display: block;
position: absolute;
right: 0;
top: 0;
height: 42px;
width: 18px;
background: url(images/buttons.png) 0 -1255px no-repeat;
cursor: pointer;
}
.dropbutton .dropbuttontip:hover {
background-position: -26px -1255px;
}
.dropbutton a.button.disabled + .dropbuttontip {
opacity: 0.5;
filter: alpha(opacity=50);
}
.dropbutton a.button.disabled + .dropbuttontip:hover {
background-position: 0 -1255px;
}
.dropbutton a.button {
margin-left: 0;
padding-left: 0;
margin-right: 0;
padding-right: 0;
}
.toolbar a.button.back {
background-position: 0 -1216px;
}
.toolbar a.button.checkmail {
background-position: center -1176px;
}
.toolbar a.button.compose {
background-position: center -530px;
}
.toolbar a.button.reply {
background-position: center -570px;
}
.toolbar a.button.reply-all {
min-width: 64px;
background-position: left -610px;
}
.toolbar a.button.forward {
min-width: 64px;
background-position: left -650px;
}
.toolbar a.button.delete {
background-position: center -690px;
}
.toolbar a.button.archive {
background-position: center -730px;
}
.toolbar a.button.junk {
background-position: center -770px;
}
.toolbar a.button.print {
background-position: center -810px;
}
.toolbar a.button.markmessage {
background-position: center -1094px;
}
.toolbar a.button.more {
background-position: center -850px;
}
.toolbar a.button.attach {
background-position: center -890px;
}
.toolbar a.button.spellcheck {
min-width: 64px;
background-position: left -930px;
}
.toolbar a.button.spellcheck.selected {
background-position: left -1620px;
color: #1978a1;
}
.toolbar a.button.insertsig {
background-position: center -1135px;
}
.toolbar a.button.search {
background-position: center -970px;
}
.toolbar a.button.import {
background-position: center -1012px;
}
.toolbar a.button.export {
min-width: 74px;
background-position: center -1054px;
}
.toolbar a.button.send {
background-position: center -1660px;
}
.toolbar a.button.savedraft {
background-position: center -1700px;
}
.toolbar a.button.close {
background-position: 0 -1745px;
}
a.menuselector {
display: inline-block;
border: 1px solid #ababab;
border-radius: 4px;
background: #f8f8f8;
background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd));
background: -o-linear-gradient(top, #f8f8f8 0%, #dddddd 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #dddddd 100%);
background: linear-gradient(top, #f8f8f8 0%, #dddddd 100%);
text-decoration: none;
color: #333;
cursor: pointer;
white-space: nowrap;
}
a.menuselector .handle {
display: inline-block;
padding: 0 32px 0 6px;
height: 20px;
line-height: 19px;
text-shadow: 0px 1px 1px #fff;
background: url(images/selector.png) right center no-repeat;
border-radius: 4px;
}
a.menuselector:active {
background: #dddddd;
background: -moz-linear-gradient(top, #dddddd 0%, #f8f8f8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dddddd), color-stop(100%,#f8f8f8));
background: -o-linear-gradient(top, #dddddd 0%, #f8f8f8 100%);
background: -ms-linear-gradient(top, #dddddd 0%, #f8f8f8 100%);
background: linear-gradient(top, #dddddd 0%, #f8f8f8 100%);
text-decoration: none;
}
select.decorated {
position: relative;
z-index: 10;
opacity: 0;
height: 22px;
cursor: pointer;
filter: alpha(opacity=0);
-khtml-appearance: none;
-webkit-appearance: none;
}
html.opera select.decorated {
opacity: 1;
}
select.decorated option {
color: #fff;
background: #444;
border: 0;
border-top: 1px solid #5a5a5a;
border-bottom: 1px solid #333;
text-shadow: 0px 1px 1px #333;
padding: 4px 6px;
outline: none;
}
/*** quota indicator ***/
#quotadisplay {
left: 6px;
font-size: 12px;
font-weight: bold;
text-shadow: 0px 1px 1px #fff;
padding-left: 30px;
height: 18px;
background: url(images/quota.png) -100px 0 no-repeat;
}
/*** popup menus ***/
.popupmenu,
#rcmKSearchpane {
display: none;
position: absolute;
top: 32px;
left: 90px;
width: auto;
background: #444;
border: 1px solid #999;
z-index: 240;
border-radius: 4px;
box-shadow: 0 2px 6px 0 #333;
-moz-box-shadow: 0 2px 6px 0 #333;
-webkit-box-shadow: 0 2px 6px 0 #333;
-o-box-shadow: 0 2px 6px 0 #333;
}
.popupmenu.dropdown {
border-radius: 0 0 4px 4px;
border-top: 0;
}
ul.toolbarmenu,
#rcmKSearchpane ul {
margin: 0;
padding: 0;
list-style: none;
}
.googie_list td,
ul.toolbarmenu li,
#rcmKSearchpane ul li {
color: #fff;
white-space: nowrap;
min-width: 130px;
margin: 0;
border-top: 1px solid #5a5a5a;
border-bottom: 1px solid #333;
}
.googie_list tr:first-child td,
ul.toolbarmenu li:first-child,
select.decorated option:first-child {
border-top: 0;
}
.googie_list tr:last-child td,
ul.toolbarmenu li:last-child,
select.decorated option:last-child {
border-bottom: 0;
}
.googie_list td span,
ul.toolbarmenu li a {
display: block;
color: #666;
text-shadow: 0px 1px 1px #333;
text-decoration: none;
min-height: 14px;
padding: 6px 10px 6px 10px;
}
.googie_list td span {
padding: 3px 10px;
}
.googie_list td span,
ul.toolbarmenu li a.active {
color: #fff;
cursor: default;
}
.googie_list td.googie_list_onhover,
ul.toolbarmenu li a.active:hover,
#rcmKSearchpane ul li.selected,
select.decorated option:hover,
select.decorated option[selected='selected'] {
background-color: #00aad6;
background: -moz-linear-gradient(top, #00aad6 0%, #008fc9 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00aad6), color-stop(100%,#008fc9));
background: -o-linear-gradient(top, #00aad6 0%, #008fc9 100%);
background: -ms-linear-gradient(top, #00aad6 0%, #008fc9 100%);
background: linear-gradient(top, #00aad6 0%, #008fc9 100%);
}
ul.toolbarmenu.iconized li a,
ul.toolbarmenu.selectable li a {
padding-left: 30px;
}
ul.toolbarmenu.selectable li a.selected {
background: url(images/messages.png) 4px -27px no-repeat;
}
ul.toolbarmenu li label {
display: block;
color: #fff;
padding: 4px 8px;
text-shadow: 0px 1px 1px #333;
}
ul.toolbarmenu li a.icon {
color: #eee;
padding: 2px 6px;
}
ul.toolbarmenu li span.icon {
display: block;
min-height: 14px;
padding: 4px 4px 1px 24px;
height: 17px;
background-image: url(images/listicons.png);
background-position: -100px 0;
background-repeat: no-repeat;
opacity: 0.2;
filter: alpha(opacity=20);
}
ul.toolbarmenu li a.active span.icon {
opacity: 0.99;
filter: alpha(opacity=100);
}
ul.toolbarmenu li span.read {
background-position: 0 -1220px;
}
ul.toolbarmenu li span.unread {
background-position: 0 -1196px;
}
ul.toolbarmenu li span.flagged {
background-position: 0 -1244px;
}
ul.toolbarmenu li span.unflagged {
background-position: 0 -1268px;
}
ul.toolbarmenu li span.mail {
background-position: 0 -1293px;
}
ul.toolbarmenu li span.list {
background-position: 0 -1317px;
}
ul.toolbarmenu li span.invert {
background-position: 0 -1340px;
}
ul.toolbarmenu li span.cross {
background-position: 0 -1365px;
}
ul.toolbarmenu li span.print {
background-position: 0 -1436px;
}
ul.toolbarmenu li span.download {
background-position: 0 -1412px;
}
ul.toolbarmenu li span.edit {
background-position: 0 -1388px;
}
ul.toolbarmenu li span.viewsource {
background-position: 0 -1460px;
}
ul.toolbarmenu li span.extwin {
background-position: 0 -1484px;
}
ul.toolbarmenu li span.conversation {
background-position: 0 -1532px;
}
#rcmKSearchpane {
border-radius: 0 0 4px 4px;
border-top: 0;
}
#rcmKSearchpane ul li {
text-shadow: 0px 1px 1px #333;
text-decoration: none;
min-height: 14px;
padding: 6px 10px 6px 10px;
border: 0;
cursor: default;
}
.popupdialog {
display: none;
padding: 10px;
}
.popupdialog .formbuttons {
margin: 20px 0 4px 0;
}
.ui-dialog .prompt input {
display: block;
margin: 8px 0;
}
.hint {
margin: 4px 0;
color: #999;
text-shadow: 0px 1px 1px #fff;
}
.splitter {
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
position: absolute;
background: url(images/splitter.png) center no-repeat;
}
.splitter-h {
height: 10px;
width: 100%;
cursor: n-resize;
cursor: row-resize;
background-position: center 0;
}
.splitter-v {
width: 10px;
height: 100%;
cursor: e-resize;
cursor: col-resize;
background-position: 0 center;
}
#rcmdraglayer {
min-width: 260px;
width: auto !important;
width: 260px;
padding: 6px 8px;
background: #444;
border: 1px solid #555;
border-radius: 4px;
box-shadow: 0 2px 6px 0 #333;
-moz-box-shadow: 0 2px 6px 0 #333;
-webkit-box-shadow: 0 2px 6px 0 #333;
-o-box-shadow: 0 2px 6px 0 #333;
z-index: 250;
color: #ccc;
white-space: nowrap;
opacity: 0.92;
filter: alpha(opacity=92);
text-shadow: 0px 1px 1px #333;
}
#rcmdraglayer:after {
content: "";
position: absolute;
top: 6px;
left: -6px;
border-style: solid;
border-width: 6px 6px 6px 0;
border-color: transparent #444;
/* reduce the damage in FF3.0 */
display: block;
width: 0;
z-index: 251;
}
.draglayercopy:before {
position: absolute;
bottom: -6px;
left: -6px;
content: " ";
width: 16px;
height: 16px;
background: url(images/buttons.png) -7px -358px no-repeat;
z-index: 255;
}
/*** attachment list ***/
.attachmentslist {
list-style: none;
margin: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.attachmentslist li {
display: block;
position: relative;
background: url(images/filetypes.png) 0 0 no-repeat;
margin-bottom: 1px;
}
.attachmentslist li.pdf {
background-position: 0 -26px;
}
.attachmentslist li.doc,
.attachmentslist li.docx,
.attachmentslist li.msword {
background-position: 0 -52px;
}
.attachmentslist li.odt {
background-position: 0 -78px;
}
.attachmentslist li.xls,
.attachmentslist li.xlsx,
.attachmentslist li.msexcel {
background-position: 0 -104px;
}
.attachmentslist li.ods {
background-position: 0 -130px;
}
.attachmentslist li.zip,
.attachmentslist li.gz {
background-position: 0 -156px;
}
.attachmentslist li.rar {
background-position: 0 -182px;
}
.attachmentslist li.image {
background-position: 0 -208px;
}
.attachmentslist li.jpg,
.attachmentslist li.jpeg {
background-position: 0 -234px;
}
.attachmentslist li.png {
background-position: 0 -260px;
}
.attachmentslist li.m4p {
background-position: 0 -286px;
}
.attachmentslist li.mp3,
.attachmentslist li.audio {
background-position: 0 -312px;
}
.attachmentslist li.video {
background-position: 0 -338px;
}
.attachmentslist li.txt,
.attachmentslist li.text {
background-position: 0 -416px;
}
.attachmentslist li.ics,
.attachmentslist li.calendar {
background-position: 0 -364px;
}
.attachmentslist li.vcard {
background-position: 0 -390px;
}
.attachmentslist li.sig,
.attachmentslist li.pgp-signature,
.attachmentslist li.pkcs7-signature {
background-position: 0 -442px;
}
.attachmentslist li.html {
background-position: 0 -468px;
}
.attachmentslist li.eml,
.attachmentslist li.rfc822 {
background-position: 0 -494px;
}
.attachmentslist li a,
#compose-attachments ul li {
display: block;
color: #333;
font-weight: bold;
padding: 8px 15px 3px 30px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.attachmentslist li a.drop {
background: url(images/buttons.png) no-repeat scroll center -1570px;
width: 14px;
height: 20px;
cursor: pointer;
position: absolute;
right: 0;
top: 0;
padding: 0;
}
#compose-attachments ul li {
padding-right: 28px;
}
.attachmentslist li a:hover {
text-decoration: underline;
}
.attachmentslist li.uploading {
background: url(images/ajaxloader.gif) 2px 6px no-repeat;
}
.attachmentslist li a.delete,
.attachmentslist li a.cancelupload {
position: absolute;
top: 6px;
right: 0;
width: 24px;
height: 18px;
padding: 0;
text-decoration: none;
text-indent: -5000px;
background: url(images/buttons.png) -7px -337px no-repeat;
}
.attachmentslist li a.cancelupload {
background-position: -7px -377px;
}
/*** fieldset tabs ***/
.tabsbar {
margin-bottom: 12px;
padding-top: 15px;
height: 27px;
white-space: nowrap;
}
.ui-dialog-content .tabsbar {
margin-bottom: 0;
}
.tabsbar .tablink {
padding: 15px 1px 15px 0;
background: #f8f8f8;
background: -moz-linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(50%,#d3d3d3), color-stop(100%,#f8f8f8));
background: -webkit-linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
background: -o-linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
background: -ms-linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
background: linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
}
.tabsbar .tablink:last-child {
background: none;
}
.tabsbar .tablink:last-child a {
border-right: 0;
}
.tabsbar .tablink a {
padding: 15px;
color: #999;
font-size: 12px;
font-weight: bold;
text-decoration: none;
background: #fff;
border-right: 1px solid #fafafa;
}
.tabsbar .tablink.selected a {
color: #004458;
background: #f6f6f6;
background: -moz-linear-gradient(top, #fff 40%, #efefef 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(40%,#fff), color-stop(100%,#efefef));
background: -o-linear-gradient(top, #fff 40%, #efefef 100%);
background: -ms-linear-gradient(top, #fff 40%, #efefef 100%);
background: linear-gradient(top, #fff 40%, #efefef 100%);
}
fieldset.tab {
border: 0;
padding: 0;
margin-left: 0;
}
diff --git a/skins/larry/templates/addressbook.html b/skins/larry/templates/addressbook.html
index 401640f1f..d9e491f99 100644
--- a/skins/larry/templates/addressbook.html
+++ b/skins/larry/templates/addressbook.html
@@ -1,112 +1,112 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
</head>
<body class="noscroll">
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
<!-- toolbar -->
<div id="addressbooktoolbar" class="toolbar">
<roundcube:button command="import" type="link" class="button import disabled" classAct="button import" classSel="button import pressed" label="import" title="importcontacts" />
<span class="dropbutton">
<roundcube:button command="export" type="link" class="button export disabled" classAct="button export" classSel="button export pressed" label="export" title="exportvcards" />
<span class="dropbuttontip" id="exportmenulink" onclick="UI.show_popup('exportmenu');return false"></span>
</span>
<span class="spacer"></span>
<roundcube:button command="compose" type="link" class="button compose disabled" classAct="button compose" classSel="button compose pressed" label="compose" title="writenewmessage" />
<roundcube:button command="advanced-search" type="link" class="button search disabled" classAct="button search" classSel="button search pressed" label="advanced" title="advsearch" />
<roundcube:container name="toolbar" id="addressbooktoolbar" />
</div>
<div id="addressview-left">
<!-- sources/groups list -->
<div id="directorylistbox" class="uibox listbox">
<h2 id="directorylist-header" class="boxtitle"><roundcube:label name="groups" /></h2>
<div id="directorylist-content" class="scroller withfooter">
<roundcube:object name="directorylist" id="directorylist" class="treelist listing" />
</div>
<div id="directorylist-footer" class="boxfooter">
<roundcube:button command="group-create" type="link" title="newcontactgroup" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="groupoptions" id="groupoptionslink" type="link" title="moreactions" class="listbutton groupactions" onclick="UI.show_popup('groupoptions');return false" innerClass="inner" content="&#9881;" />
</div>
</div>
</div><!-- end addressview-left -->
<div id="addressview-right">
<!-- search box -->
<div id="quicksearchbar" class="searchbox">
<roundcube:object name="searchform" id="quicksearchbox" />
<roundcube:button name="searchmenulink" id="searchmenulink" class="iconbutton searchoptions" onclick="UI.show_popup('searchmenu');return false" title="searchmod" content=" " />
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
</div>
<!-- contacts list -->
<div id="addresslist" class="uibox listbox">
-<h2 class="boxtitle"><roundcube:label name="contacts" /></h2>
+<roundcube:object name="addresslisttitle" label="contacts" tag="h2" class="boxtitle" />
<div class="scroller withfooter">
<roundcube:object name="addresslist" id="contacts-table" class="listing" noheader="true" />
</div>
<div class="boxfooter">
<roundcube:button command="add" type="link" title="newcontact" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button command="delete" type="link" title="deletecontact" class="listbutton delete disabled" classAct="listbutton delete" innerClass="inner" content="x" /><roundcube:button command="group-remove-selected" type="link" title="groupremoveselected" class="listbutton removegroup disabled" classAct="listbutton removegroup" innerClass="inner" content="-" />
<roundcube:object name="recordsCountDisplay" class="countdisplay" label="fromtoshort" />
</div>
<div class="boxpagenav">
<roundcube:button command="firstpage" type="link" class="icon firstpage disabled" classAct="icon firstpage" title="firstpage" content="|&amp;lt;" />
<roundcube:button command="previouspage" type="link" class="icon prevpage disabled" classAct="icon prevpage" title="previouspage" content="&amp;lt;" />
<roundcube:button command="nextpage" type="link" class="icon nextpage disabled" classAct="icon nextpage" title="nextpage" content="&amp;gt;" />
<roundcube:button command="lastpage" type="link" class="icon lastpage disabled" classAct="icon lastpage" title="lastpage" content="&amp;gt;|" />
</div>
</div>
<div id="contacts-box" class="uibox">
<div class="iframebox">
<roundcube:object name="addressframe" id="contact-frame" style="width:100%; height:100%" frameborder="0" src="/watermark.html" />
</div>
<roundcube:object name="message" id="message" class="statusbar" />
</div>
</div><!-- end addressview-right -->
</div><!-- end mainscreen -->
<div id="exportmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><roundcube:button command="export" label="exportall" prop="sub" class="exportalllink" classAct="exportalllink active" /></li>
<li><roundcube:button command="export-selected" label="exportsel" prop="sub" class="exportsellink" classAct="exportsellink active" /></li>
</ul>
</div>
<div id="searchmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><label><input type="checkbox" name="s_mods[]" value="name" id="s_mod_name" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="name" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="firstname" id="s_mod_firstname" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="firstname" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="surname" id="s_mod_surname" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="surname" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="email" id="s_mod_email" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="email" /></span></label></li>
<li><label><input type="checkbox" name="s_mods[]" value="*" id="s_mod_all" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="allfields" /></span></label></li>
</ul>
</div>
<div id="groupoptions" class="popupmenu">
<ul id="groupoptionsmenu" class="toolbarmenu">
<li><roundcube:button command="group-rename" label="grouprename" classAct="active" /></li>
<li><roundcube:button command="group-delete" label="groupdelete" classAct="active" /></li>
<li><roundcube:button command="search-create" label="searchsave" classAct="active" /></li>
<li><roundcube:button command="search-delete" label="searchdelete" classAct="active" /></li>
<roundcube:container name="groupoptions" id="groupoptionsmenu" />
</ul>
</div>
<roundcube:include file="/includes/footer.html" />
</body>
</html>
diff --git a/skins/larry/templates/contact.html b/skins/larry/templates/contact.html
index d252049cd..59fe6f79f 100644
--- a/skins/larry/templates/contact.html
+++ b/skins/larry/templates/contact.html
@@ -1,33 +1,33 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
</head>
<body class="iframe">
<h1 class="boxtitle"><roundcube:label name="contactproperties" /></h1>
<div id="contact-details" class="boxcontent">
<roundcube:if condition="strlen(env:sourcename)" />
<div id="sourcename"><roundcube:label name="addressbook" />: <roundcube:var name="env:sourcename" /></div>
<roundcube:endif />
- <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div>
+ <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>
<roundcube:object name="contacthead" id="contacthead" />
<br style="clear:both" />
<div id="contacttabs" class="tabbed">
<roundcube:object name="contactdetails" />
</div>
</div>
<div id="headerbuttons" class="formbuttons">
<roundcube:button command="edit" type="input" class="button mainaction" label="editcontact" condition="!ENV:readonly" />
</div>
<roundcube:include file="/includes/footer.html" />
</body>
</html>
diff --git a/skins/larry/ui.js b/skins/larry/ui.js
index ec4d03d00..38d8539c7 100644
--- a/skins/larry/ui.js
+++ b/skins/larry/ui.js
@@ -1,1274 +1,1305 @@
/**
* Roundcube functions for default skin interface
*
* Copyright (c) 2013, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
function rcube_mail_ui()
{
var env = {};
var popups = {};
var popupconfig = {
forwardmenu: { editable:1 },
searchmenu: { editable:1, callback:searchmenu },
attachmentmenu: { },
listoptions: { editable:1 },
dragmessagemenu: { sticky:1 },
groupmenu: { above:1 },
mailboxmenu: { above:1 },
spellmenu: { callback: spellmenu },
// toggle: #1486823, #1486930
'attachment-form': { editable:1, above:1, toggle:!bw.ie&&!bw.linux },
'upload-form': { editable:1, toggle:!bw.ie&&!bw.linux }
};
var me = this;
var mailviewsplit;
var compose_headers = {};
// export public methods
this.set = setenv;
this.init = init;
this.init_tabs = init_tabs;
this.show_about = show_about;
this.show_popup = show_popup;
this.add_popup = add_popup;
this.set_searchmod = set_searchmod;
this.show_uploadform = show_uploadform;
this.show_header_row = show_header_row;
this.hide_header_row = hide_header_row;
this.update_quota = update_quota;
// set minimal mode on small screens (don't wait for document.ready)
if (window.$ && document.body) {
var minmode = rcmail.get_cookie('minimalmode');
if (parseInt(minmode) || (minmode === null && $(window).height() < 850)) {
$(document.body).addClass('minimal');
}
}
/**
*
*/
function setenv(key, val)
{
env[key] = val;
}
/**
* Initialize UI
* Called on document.ready
*/
function init()
{
rcmail.addEventListener('message', message_displayed);
/*** prepare minmode functions ***/
$('#taskbar a').each(function(i,elem){
$(elem).append('<span class="tooltip">' + $('.button-inner', this).html() + '</span>')
});
$('#taskbar .minmodetoggle').click(function(e){
var ismin = $(document.body).toggleClass('minimal').hasClass('minimal');
rcmail.set_cookie('minimalmode', ismin?1:0);
$(window).resize();
});
/*** mail task ***/
if (rcmail.env.task == 'mail') {
rcmail.addEventListener('menu-open', menu_open);
rcmail.addEventListener('menu-save', menu_save);
rcmail.addEventListener('responseafterlist', function(e){ switch_view_mode(rcmail.env.threading ? 'thread' : 'list') });
var dragmenu = $('#dragmessagemenu');
if (dragmenu.length) {
rcmail.gui_object('message_dragmenu', 'dragmessagemenu');
popups.dragmessagemenu = dragmenu;
}
if (rcmail.env.action == 'show' || rcmail.env.action == 'preview') {
rcmail.addEventListener('enable-command', enable_command);
rcmail.addEventListener('aftershow-headers', function() { layout_messageview(); });
rcmail.addEventListener('afterhide-headers', function() { layout_messageview(); });
$('#previewheaderstoggle').click(function(e){ toggle_preview_headers(); return false });
// add menu link for each attachment
$('#attachment-list > li').each(function() {
$(this).append($('<a class="drop">').click(function() { attachmentmenu(this); }));
});
}
else if (rcmail.env.action == 'compose') {
rcmail.addEventListener('aftertoggle-editor', function(){ window.setTimeout(function(){ layout_composeview() }, 200); });
rcmail.addEventListener('aftersend-attachment', show_uploadform);
rcmail.addEventListener('add-recipient', function(p){ show_header_row(p.field, true); });
layout_composeview();
// Show input elements with non-empty value
var f, v, field, fields = ['cc', 'bcc', 'replyto', 'followupto'];
for (f=0; f < fields.length; f++) {
v = fields[f]; field = $('#_'+v);
if (field.length) {
field.on('change', {v: v}, function(e) { if (this.value) show_header_row(e.data.v, true); });
if (field.val() != '')
show_header_row(v, true);
}
}
$('#composeoptionstoggle').click(function(){
$('#composeoptionstoggle').toggleClass('remove');
$('#composeoptions').toggle();
layout_composeview();
return false;
}).css('cursor', 'pointer');
// toggle compose options if opened in new window and they were visible before
var opener_rc = rcmail.opener();
if (opener_rc && opener_rc.env.action == 'compose' && $('#composeoptionstoggle', opener.document).hasClass('remove'))
$('#composeoptionstoggle').click();
new rcube_splitter({ id:'composesplitterv', p1:'#composeview-left', p2:'#composeview-right',
orientation:'v', relative:true, start:248, min:170, size:12, render:layout_composeview }).init();
}
else if (rcmail.env.action == 'list' || !rcmail.env.action) {
var previewframe = $('#mailpreviewframe').is(':visible');
$('#mailpreviewtoggle').addClass(previewframe ? 'enabled' : 'closed').click(function(e){ toggle_preview_pane(e); return false });
$('#maillistmode').addClass(rcmail.env.threading ? '' : 'selected').click(function(e){ switch_view_mode('list'); return false });
$('#mailthreadmode').addClass(rcmail.env.threading ? 'selected' : '').click(function(e){ switch_view_mode('thread'); return false });
mailviewsplit = new rcube_splitter({ id:'mailviewsplitter', p1:'#mailview-top', p2:'#mailview-bottom',
orientation:'h', relative:true, start:310, min:150, size:12, offset:4 });
if (previewframe)
mailviewsplit.init();
new rcube_scroller('#folderlist-content', '#folderlist-header', '#folderlist-footer');
rcmail.addEventListener('setquota', update_quota);
rcmail.addEventListener('enable-command', enable_command);
rcmail.addEventListener('afterimport-messages', show_uploadform);
}
if ($('#mailview-left').length) {
new rcube_splitter({ id:'mailviewsplitterv', p1:'#mailview-left', p2:'#mailview-right',
orientation:'v', relative:true, start:226, min:150, size:12, callback:render_mailboxlist, render:resize_leftcol }).init();
}
}
/*** settings task ***/
else if (rcmail.env.task == 'settings') {
rcmail.addEventListener('init', function(){
var tab = '#settingstabpreferences';
if (rcmail.env.action)
tab = '#settingstab' + (rcmail.env.action.indexOf('identity')>0 ? 'identities' : rcmail.env.action.replace(/\./g, ''));
$(tab).addClass('selected')
.children().first().removeAttr('onclick').click(function() { return false; });
});
if (rcmail.env.action == 'folders') {
new rcube_splitter({ id:'folderviewsplitter', p1:'#folderslist', p2:'#folder-details',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
new rcube_scroller('#folderslist-content', '#folderslist-header', '#folderslist-footer');
rcmail.addEventListener('setquota', update_quota);
}
else if (rcmail.env.action == 'identities') {
new rcube_splitter({ id:'identviewsplitter', p1:'#identitieslist', p2:'#identity-details',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
}
else if (rcmail.env.action == 'preferences' || !rcmail.env.action) {
new rcube_splitter({ id:'prefviewsplitter', p1:'#sectionslist', p2:'#preferences-box',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
}
}
/*** addressbook task ***/
else if (rcmail.env.task == 'addressbook') {
rcmail.addEventListener('afterupload-photo', show_uploadform);
+ rcmail.addEventListener('beforepushgroup', push_contactgroup);
+ rcmail.addEventListener('beforepopgroup', pop_contactgroup);
if (rcmail.env.action == '') {
new rcube_splitter({ id:'addressviewsplitterd', p1:'#addressview-left', p2:'#addressview-right',
orientation:'v', relative:true, start:226, min:150, size:12, render:resize_leftcol }).init();
new rcube_splitter({ id:'addressviewsplitter', p1:'#addresslist', p2:'#contacts-box',
orientation:'v', relative:true, start:286, min:270, size:12 }).init();
new rcube_scroller('#directorylist-content', '#directorylist-header', '#directorylist-footer');
}
}
// set min-width to show all toolbar buttons
var screen = $('.minwidth');
if (screen.length) {
screen.css('min-width', $('.toolbar').width() + $('#quicksearchbar').parent().width() + 20);
}
// turn a group of fieldsets into tabs
$('.tabbed').each(function(idx, elem){ init_tabs(elem); })
// decorate select elements
$('select.decorated').each(function(){
if (bw.opera) {
$(this).removeClass('decorated');
return;
}
var select = $(this),
height = Math.max(select.height(), 26) - 2,
width = select.width() - 22,
title = $('option', this).first().text();
if ($('option:selected', this).val() != '')
title = $('option:selected', this).text();
var overlay = $('<a class="menuselector"><span class="handle">' + title + '</span></a>')
.css('position', 'absolute')
.offset(select.position())
.insertAfter(select);
overlay.children().width(width).height(height).css('line-height', (height - 1) + 'px');
select.change(function() {
var val = $('option:selected', this).text();
$(this).next().children().html(val);
});
var parent = select.parent();
if (parent.css('position') != 'absolute')
parent.css('position', 'relative');
// re-set original select width to fix click action and options width in some browsers
if (!bw.mz)
select.width(overlay.width());
});
$(document.body)
.bind('mouseup', body_mouseup)
.bind('keyup', function(e){
if (e.keyCode == 27) {
for (var id in popups) {
if (popups[id].is(':visible'))
show_popup(id, false);
}
}
});
$('iframe').load(function(e){
// this = iframe
try {
var doc = this.contentDocument ? this.contentDocument : this.contentWindow ? this.contentWindow.document : null;
$(doc).mouseup(body_mouseup);
}
catch (e) {
// catch possible "Permission denied" error in IE
};
})
.contents().mouseup(body_mouseup);
// don't use $(window).resize() due to some unwanted side-effects
window.onresize = resize;
resize();
}
/**
* Handler for mouse-up events on the document body.
* This will close all open popup menus
*/
function body_mouseup(e)
{
var config, obj, target = e.target;
if (target.className == 'inner')
target = e.target.parentNode;
for (var id in popups) {
obj = popups[id];
config = popupconfig[id];
if (obj.is(':visible')
&& target.id != id+'link'
&& !config.toggle
&& (!config.editable || !target_overlaps(target, obj.get(0)))
&& (!config.sticky || !rcube_mouse_is_over(e, obj.get(0)))
) {
var myid = id+'';
window.setTimeout(function(){ show_popupmenu(myid, false) }, 10);
}
}
}
/**
* Update UI on window resize
*/
function resize(e)
{
// resize in intervals to prevent lags and double onresize calls in Chrome (#1489005)
var interval = e ? 10 : 0;
if (rcmail.resize_timeout)
window.clearTimeout(rcmail.resize_timeout);
rcmail.resize_timeout = window.setTimeout(function() {
if (rcmail.env.task == 'mail') {
if (rcmail.env.action == 'show' || rcmail.env.action == 'preview')
layout_messageview();
else if (rcmail.env.action == 'compose')
layout_composeview();
}
// make iframe footer buttons float if scrolling is active
$('body.iframe .footerleft').each(function(){
var footer = $(this),
body = $(document.body),
floating = footer.hasClass('floating'),
overflow = body.outerHeight(true) > $(window).height();
if (overflow != floating) {
var action = overflow ? 'addClass' : 'removeClass';
footer[action]('floating');
body[action]('floatingbuttons');
}
});
}, interval);
}
/**
* Triggered when a new user message is displayed
*/
function message_displayed(p)
{
// show a popup dialog on errors
if (p.type == 'error' && rcmail.env.task != 'login') {
if (me.message_timer) {
window.clearTimeout(me.message_timer);
}
if (!me.messagedialog) {
me.messagedialog = $('<div>').addClass('popupdialog').hide();
}
var msg = p.message,
pos = $(p.object).offset();
pos.top -= (rcmail.env.task == 'login' ? 20 : 160);
if (me.messagedialog.is(':visible'))
msg = me.messagedialog.html() + '<p>' + p.message + '</p>';
me.messagedialog.html(msg)
.dialog({
resizable: false,
closeOnEscape: true,
dialogClass: 'popupmessage ' + p.type,
title: env.errortitle,
close: function() {
me.messagedialog.dialog('destroy').hide();
},
position: ['center', pos.top],
hide: { effect:'drop', direction:'down' },
width: 420,
minHeight: 90
}).show();
me.message_timer = window.setTimeout(function(){ me.messagedialog.dialog('close'); }, Math.max(2000, p.timeout / 2));
}
}
/**
* Adjust UI objects of the mail view screen
*/
function layout_messageview()
{
$('#messagecontent').css('top', ($('#messageheader').outerHeight() + 1) + 'px');
$('#message-objects div a').addClass('button');
if (!$('#attachment-list li').length) {
$('div.rightcol').hide();
$('div.leftcol').css('margin-right', '0');
}
}
function render_mailboxlist(splitter)
{
// TODO: implement smart shortening of long folder names
}
function resize_leftcol(splitter)
{
// STUB
}
function layout_composeview()
{
var body = $('#composebody'),
form = $('#compose-content'),
bottom = $('#composeview-bottom'),
w, h, bh, ovflw, btns = 0,
minheight = 300,
bh = (form.height() - bottom.position().top);
ovflw = minheight - bh;
btns = ovflw > -100 ? 0 : 40;
bottom.css('height', Math.max(minheight, bh) + 'px');
form.css('overflow', ovflw > 0 ? 'auto' : 'hidden');
w = body.parent().width() - 5;
h = body.parent().height() - 16;
body.width(w).height(h);
$('#composebody_tbl').width((w+8)+'px').height('').css('margin-top', '1px');
$('#composebody_ifr').width((w+8)+'px').height((h-40)+'px');
$('#googie_edit_layer').height(h+'px');
// $('#composebodycontainer')[(btns ? 'addClass' : 'removeClass')]('buttons');
// $('#composeformbuttons')[(btns ? 'show' : 'hide')]();
var abooks = $('#directorylist');
$('#compose-contacts .scroller').css('top', abooks.position().top + abooks.outerHeight());
}
function update_quota(p)
{
var step = 24, step_count = 20,
y = p.total ? Math.ceil(p.percent / 100 * step_count) * step : 0;
// never show full-circle if quota is close to 100% but below.
if (p.total && y == step * step_count && p.percent < 100)
y -= step;
$('#quotadisplay').css('background-position', '0 -'+y+'px');
}
function enable_command(p)
{
if (p.command == 'reply-list') {
var label = rcmail.gettext(p.status ? 'replylist' : 'replyall');
if (rcmail.env.action == 'preview')
$('a.button.replyall').attr('title', label);
else
$('a.button.reply-all').text(label).attr('title', label);
}
}
/**
* Register a popup menu
*/
function add_popup(popup, config)
{
var obj = popups[popup] = $('#'+popup);
obj.appendTo(document.body); // move it to top for proper absolute positioning
if (obj.length)
popupconfig[popup] = $.extend(popupconfig[popup] || {}, config || {});
}
/**
* Trigger for popup menus
*/
function show_popup(popup, show, config)
{
// auto-register menu object
if (config || !popupconfig[popup])
add_popup(popup, config);
var visible = show_popupmenu(popup, show),
config = popupconfig[popup];
if (typeof config.callback == 'function')
config.callback(visible);
}
/**
* Show/hide a specific popup menu
*/
function show_popupmenu(popup, show)
{
var obj = popups[popup],
config = popupconfig[popup],
ref = $(config.link ? config.link : '#'+popup+'link'),
above = config.above;
if (!obj) {
obj = popups[popup] = $('#'+popup);
obj.appendTo(document.body); // move them to top for proper absolute positioning
}
if (!obj || !obj.length)
return false;
if (typeof show == 'undefined')
show = obj.is(':visible') ? false : true;
else if (config.toggle && show && obj.is(':visible'))
show = false;
if (show && ref.length) {
var parent = ref.parent(),
win = $(window),
pos;
if (parent.hasClass('dropbutton'))
ref = parent;
pos = ref.offset();
ref.offsetHeight = ref.outerHeight();
if (!above && pos.top + ref.offsetHeight + obj.height() > win.height())
above = true;
if (pos.left + obj.width() > win.width())
pos.left = win.width() - obj.width() - 12;
obj.css({ left:pos.left, top:(pos.top + (above ? -obj.height() : ref.offsetHeight)) });
}
obj[show?'show':'hide']();
// hide drop-down elements on buggy browsers
if (bw.ie6 && config.overlap) {
$('select').css('visibility', show?'hidden':'inherit');
$('select', obj).css('visibility', 'inherit');
}
return show;
}
/**
*
*/
function target_overlaps(target, elem)
{
while (target.parentNode) {
if (target.parentNode == elem)
return true;
target = target.parentNode;
}
return false;
}
/**
* Show/hide the preview pane
*/
function toggle_preview_pane(e)
{
var button = $(e.target),
frame = $('#mailpreviewframe'),
visible = !frame.is(':visible'),
splitter = mailviewsplit.pos || parseInt(rcmail.get_cookie('mailviewsplitter') || 320),
topstyles, bottomstyles, uid;
frame.toggle();
button.removeClass().addClass(visible ? 'enabled' : 'closed');
if (visible) {
$('#mailview-top').removeClass('fullheight').css({ bottom:'auto' });
$('#mailview-bottom').css({ height:'auto' });
rcmail.env.contentframe = 'messagecontframe';
if (uid = rcmail.message_list.get_single_selection())
rcmail.show_message(uid, false, true);
// let the splitter set the correct size and position
if (mailviewsplit.handle) {
mailviewsplit.handle.show();
mailviewsplit.resize();
}
else
mailviewsplit.init();
}
else {
rcmail.env.contentframe = null;
rcmail.show_contentframe(false);
$('#mailview-top').addClass('fullheight').css({ height:'auto', bottom:'28px' });
$('#mailview-bottom').css({ top:'auto', height:'26px' });
if (mailviewsplit.handle)
mailviewsplit.handle.hide();
}
if (visible && uid && rcmail.message_list)
rcmail.message_list.scrollto(uid);
rcmail.command('save-pref', { name:'preview_pane', value:(visible?1:0) });
}
/**
* Switch between short and full headers display in message preview
*/
function toggle_preview_headers()
{
$('#preview-shortheaders').toggle();
var full = $('#preview-allheaders').toggle(),
button = $('a#previewheaderstoggle');
// add toggle button to full headers table
if (full.is(':visible'))
button.attr('href', '#hide').removeClass('add').addClass('remove')
else
button.attr('href', '#details').removeClass('remove').addClass('add')
}
/**
*
*/
function switch_view_mode(mode)
{
if (rcmail.env.threading != (mode == 'thread'))
rcmail.set_list_options(null, undefined, undefined, mode == 'thread' ? 1 : 0);
$('#maillistmode, #mailthreadmode').removeClass('selected');
$('#mail'+mode+'mode').addClass('selected');
}
/**** popup callbacks ****/
function menu_open(p)
{
if (p && p.props && p.props.menu == 'attachmentmenu')
show_popupmenu('attachmentmenu');
else
show_listoptions();
}
function menu_save(prop)
{
save_listoptions();
}
function searchmenu(show)
{
if (show && rcmail.env.search_mods) {
var n, all,
obj = popups['searchmenu'],
list = $('input:checkbox[name="s_mods[]"]', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods;
if (rcmail.env.task == 'mail') {
mods = mods[mbox] ? mods[mbox] : mods['*'];
all = 'text';
}
else {
all = '*';
}
if (mods[all])
list.map(function() {
this.checked = true;
this.disabled = this.value != all;
});
else {
list.prop('disabled', false).prop('checked', false);
for (n in mods)
$('#s_mod_' + n).prop('checked', true);
}
}
}
function attachmentmenu(elem)
{
var id = elem.parentNode.id.replace(/^attach/, '');
$('#attachmenuopen').unbind('click').attr('onclick', '').click(function(e) {
return rcmail.command('open-attachment', id, this);
});
$('#attachmenudownload').unbind('click').attr('onclick', '').click(function() {
rcmail.command('download-attachment', id, this);
});
popupconfig.attachmentmenu.link = elem;
rcmail.command('menu-open', {menu: 'attachmentmenu', id: id});
}
function spellmenu(show)
{
var link, li,
lang = rcmail.spellcheck_lang(),
menu = popups.spellmenu,
ul = $('ul', menu);
if (!ul.length) {
ul = $('<ul class="toolbarmenu selectable">');
for (i in rcmail.env.spell_langs) {
li = $('<li>');
link = $('<a href="#"></a>').text(rcmail.env.spell_langs[i])
.addClass('active').data('lang', i)
.click(function() {
rcmail.spellcheck_lang_set($(this).data('lang'));
});
link.appendTo(li);
li.appendTo(ul);
}
ul.appendTo(menu);
}
// select current language
$('li', ul).each(function() {
var el = $('a', this);
if (el.data('lang') == lang)
el.addClass('selected');
else if (el.hasClass('selected'))
el.removeClass('selected');
});
}
/**
*
*/
function show_listoptions()
{
var $dialog = $('#listoptions');
// close the dialog
if ($dialog.is(':visible')) {
$dialog.dialog('close');
return;
}
// set form values
$('input[name="sort_col"][value="'+rcmail.env.sort_col+'"]').prop('checked', true);
$('input[name="sort_ord"][value="DESC"]').prop('checked', rcmail.env.sort_order == 'DESC');
$('input[name="sort_ord"][value="ASC"]').prop('checked', rcmail.env.sort_order != 'DESC');
// set checkboxes
$('input[name="list_col[]"]').each(function() {
$(this).prop('checked', $.inArray(this.value, rcmail.env.coltypes) != -1);
});
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: true,
title: null,
close: function() {
$dialog.dialog('destroy').hide();
},
minWidth: 500,
width: $dialog.width()+25
}).show();
}
/**
*
*/
function save_listoptions()
{
$('#listoptions').dialog('close');
var sort = $('input[name="sort_col"]:checked').val(),
ord = $('input[name="sort_ord"]:checked').val(),
cols = $('input[name="list_col[]"]:checked')
.map(function(){ return this.value; }).get();
rcmail.set_list_options(cols, sort, ord, rcmail.env.threading);
}
/**
*
*/
function set_searchmod(elem)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox;
if (!mods)
mods = {};
if (task == 'mail') {
if (!mods[mbox])
mods[mbox] = rcube_clone_object(mods['*']);
m = mods[mbox];
all = 'text';
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem.checked)
delete(m[elem.value]);
else
m[elem.value] = 1;
// mark all fields
if (elem.value != all)
return;
$('input:checkbox[name="s_mods[]"]').map(function() {
if (this == elem)
return;
this.checked = true;
if (elem.checked) {
this.disabled = true;
delete m[this.value];
}
else {
this.disabled = false;
m[this.value] = 1;
}
});
}
+ function push_contactgroup(p)
+ {
+ // lets the contacts list swipe to the left, nice!
+ var table = $('#contacts-table'),
+ scroller = table.parent().css('overflow', 'hidden');
+
+ table.clone()
+ .css({ position:'absolute', top:'0', left:'0', width:table.width()+'px', 'z-index':10 })
+ .appendTo(scroller)
+ .animate({ left: -(table.width()+5) + 'px' }, 300, 'swing', function(){
+ $(this).remove();
+ scroller.css('overflow', 'auto')
+ });
+ }
+
+ function pop_contactgroup(p)
+ {
+ // lets the contacts list swipe to the left, nice!
+ var table = $('#contacts-table'),
+ scroller = table.parent().css('overflow', 'hidden'),
+ clone = table.clone().appendTo(scroller);
+
+ table.css({ position:'absolute', top:'0', left:-(table.width()+5) + 'px', width:table.width()+'px', height:table.height()+'px', 'z-index':10 })
+ .animate({ left:'0' }, 300, 'linear', function(){
+ clone.remove();
+ $(this).css({ position:'relative', left:'0', width:'100%', height:'auto', 'z-index':1 });
+ scroller.css('overflow', 'auto')
+ });
+ }
function show_uploadform()
{
var $dialog = $('#upload-dialog');
// close the dialog
if ($dialog.is(':visible')) {
$dialog.dialog('close');
return;
}
-
+
// add icons to clone file input field
if (rcmail.env.action == 'compose' && !$dialog.data('extended')) {
$('<a>')
.addClass('iconlink add')
.attr('href', '#add')
.html('Add')
.appendTo($('input[type="file"]', $dialog).parent())
.click(add_uploadfile);
$dialog.data('extended', true);
}
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: true,
title: $dialog.attr('title'),
close: function() {
try { $('#upload-dialog form').get(0).reset(); }
catch(e){ } // ignore errors
$dialog.dialog('destroy').hide();
$('div.addline', $dialog).remove();
},
width: 480
}).show();
if (!document.all)
$('input[type=file]', $dialog).first().click();
}
function add_uploadfile(e)
{
var div = $(this).parent();
var clone = div.clone().addClass('addline').insertAfter(div);
clone.children('.iconlink').click(add_uploadfile);
clone.children('input').val('');
if (!document.all)
$('input[type=file]', clone).click();
}
/**
*
*/
function show_header_row(which, updated)
{
var row = $('#compose-' + which);
if (row.is(':visible'))
return; // nothing to be done here
if (compose_headers[which] && !updated)
$('#_' + which).val(compose_headers[which]);
row.show();
$('#' + which + '-link').hide();
layout_composeview();
return false;
}
/**
*
*/
function hide_header_row(which)
{
// copy and clear field value
var field = $('#_' + which);
compose_headers[which] = field.val();
field.val('');
$('#compose-' + which).hide();
$('#' + which + '-link').show();
layout_composeview();
return false;
}
/**
* Fieldsets-to-tabs converter
*/
function init_tabs(elem, current)
{
var content = $(elem),
id = content.get(0).id,
fs = content.children('fieldset');
if (!fs.length)
return;
if (!id) {
id = 'rcmtabcontainer';
content.attr('id', id);
}
// first hide not selected tabs
current = current || 0;
fs.each(function(idx) { if (idx != current) $(this).hide(); });
// create tabs container
var tabs = $('<div>').addClass('tabsbar').prependTo(content);
// convert fildsets into tabs
fs.each(function(idx) {
var tab, a, elm = $(this), legend = elm.children('legend');
// create a tab
a = $('<a>').text(legend.text()).attr('href', '#');
tab = $('<span>').attr({'id': 'tab'+idx, 'class': 'tablink'})
.click(function() { show_tab(id, idx); return false })
// remove legend
legend.remove();
// style fieldset
elm.addClass('tab');
// style selected tab
if (idx == current)
tab.addClass('selected');
// add the tab to container
tab.append(a).appendTo(tabs);
});
}
function show_tab(id, index)
{
var fs = $('#'+id).children('fieldset');
fs.each(function(idx) {
// Show/hide fieldset (tab content)
$(this)[index==idx ? 'show' : 'hide']();
// Select/unselect tab
$('#tab'+idx).toggleClass('selected', idx==index);
});
resize();
}
/**
* Show about page as jquery UI dialog
*/
function show_about(elem)
{
var frame = $('<iframe>').attr('id', 'aboutframe')
.attr('src', rcmail.url('settings/about'))
.attr('frameborder', '0')
.appendTo(document.body);
var h = Math.floor($(window).height() * 0.75);
var buttons = {};
var supportln = $('#supportlink');
if (supportln.length && (env.supporturl = supportln.attr('href')))
buttons[supportln.html()] = function(e){ env.supporturl.indexOf('mailto:') < 0 ? window.open(env.supporturl) : location.href = env.supporturl };
frame.dialog({
modal: true,
resizable: false,
closeOnEscape: true,
title: elem ? elem.title || elem.innerHTML : null,
close: function() {
frame.dialog('destroy').remove();
},
buttons: buttons,
width: 640,
height: h
}).width(640);
}
}
/**
* Roundcube Scroller class
*/
function rcube_scroller(list, top, bottom)
{
var ref = this;
this.list = $(list);
this.top = $(top);
this.bottom = $(bottom);
this.step_size = 6;
this.step_time = 20;
this.delay = 500;
this.top
.mouseenter(function() { if (rcmail.drag_active) ref.ts = window.setTimeout(function() { ref.scroll('down'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.bottom
.mouseenter(function() { if (rcmail.drag_active) ref.ts = window.setTimeout(function() { ref.scroll('up'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.scroll = function(dir)
{
var ref = this, size = this.step_size;
if (!rcmail.drag_active)
return;
if (dir == 'down')
size *= -1;
this.list.get(0).scrollTop += size;
this.ts = window.setTimeout(function() { ref.scroll(dir); }, this.step_time);
};
};
/**
* Roundcube UI splitter class
*
* @constructor
*/
function rcube_splitter(p)
{
this.p = p;
this.id = p.id;
this.horizontal = (p.orientation == 'horizontal' || p.orientation == 'h');
this.halfsize = (p.size !== undefined ? p.size : 10) / 2;
this.pos = p.start || 0;
this.min = p.min || 20;
this.offset = p.offset || 0;
this.relative = p.relative ? true : false;
this.drag_active = false;
this.render = p.render;
this.callback = p.callback;
var me = this;
rcube_splitter._instances[this.id] = me;
this.init = function()
{
this.p1 = $(this.p.p1);
this.p2 = $(this.p.p2);
// check if referenced elements exist, otherwise abort
if (!this.p1.length || !this.p2.length)
return;
// create and position the handle for this splitter
this.p1pos = this.relative ? this.p1.position() : this.p1.offset();
this.p2pos = this.relative ? this.p2.position() : this.p2.offset();
this.handle = $('<div>')
.attr('id', this.id)
.attr('unselectable', 'on')
.addClass('splitter ' + (this.horizontal ? 'splitter-h' : 'splitter-v'))
.appendTo(this.p1.parent())
.bind('mousedown', onDragStart);
if (this.horizontal) {
var top = this.p1pos.top + this.p1.outerHeight();
this.handle.css({ left:'0px', top:top+'px' });
}
else {
var left = this.p1pos.left + this.p1.outerWidth();
this.handle.css({ left:left+'px', top:'0px' });
}
// listen to window resize on IE
if (bw.ie)
$(window).resize(onResize);
// read saved position from cookie
var cookie = rcmail.get_cookie(this.id);
if (cookie && !isNaN(cookie)) {
this.pos = parseFloat(cookie);
this.resize();
}
else if (this.pos) {
this.resize();
this.set_cookie();
}
};
/**
* Set size and position of all DOM objects
* according to the saved splitter position
*/
this.resize = function()
{
if (this.horizontal) {
this.p1.css('height', Math.floor(this.pos - this.p1pos.top - this.halfsize) + 'px');
this.p2.css('top', Math.ceil(this.pos + this.halfsize + 2) + 'px');
this.handle.css('top', Math.round(this.pos - this.halfsize + this.offset)+'px');
if (bw.ie) {
var new_height = parseInt(this.p2.parent().outerHeight(), 10) - parseInt(this.p2.css('top'), 10) - (bw.ie8 ? 2 : 0);
this.p2.css('height', (new_height > 0 ? new_height : 0) + 'px');
}
}
else {
this.p1.css('width', Math.floor(this.pos - this.p1pos.left - this.halfsize) + 'px');
this.p2.css('left', Math.ceil(this.pos + this.halfsize) + 'px');
this.handle.css('left', Math.round(this.pos - this.halfsize + this.offset + 3)+'px');
if (bw.ie) {
var new_width = parseInt(this.p2.parent().outerWidth(), 10) - parseInt(this.p2.css('left'), 10) ;
this.p2.css('width', (new_width > 0 ? new_width : 0) + 'px');
}
}
this.p2.resize();
this.p1.resize();
// also resize iframe covers
if (this.drag_active) {
$('iframe').each(function(i, elem) {
var pos = $(this).offset();
$('#iframe-splitter-fix-'+i).css({ top: pos.top+'px', left: pos.left+'px', width:elem.offsetWidth+'px', height: elem.offsetHeight+'px' });
});
}
if (typeof this.render == 'function')
this.render(this);
};
/**
* Handler for mousedown events
*/
function onDragStart(e)
{
// disable text selection while dragging the splitter
if (bw.konq || bw.chrome || bw.safari)
document.body.style.webkitUserSelect = 'none';
me.p1pos = me.relative ? me.p1.position() : me.p1.offset();
me.p2pos = me.relative ? me.p2.position() : me.p2.offset();
me.drag_active = true;
// start listening to mousemove events
$(document).bind('mousemove.'+this.id, onDrag).bind('mouseup.'+this.id, onDragStop);
// enable dragging above iframes
$('iframe').each(function(i, elem) {
$('<div>')
.attr('id', 'iframe-splitter-fix-'+i)
.addClass('iframe-splitter-fix')
.css({ background: '#fff',
width: elem.offsetWidth+'px', height: elem.offsetHeight+'px',
position: 'absolute', opacity: '0.001', zIndex: 1000
})
.css($(this).offset())
.appendTo('body');
});
};
/**
* Handler for mousemove events
*/
function onDrag(e)
{
if (!me.drag_active)
return false;
var pos = rcube_event.get_mouse_pos(e);
if (me.relative) {
var parent = me.p1.parent().offset();
pos.x -= parent.left;
pos.y -= parent.top;
}
if (me.horizontal) {
if (((pos.y - me.halfsize) > me.p1pos.top) && ((pos.y + me.halfsize) < (me.p2pos.top + me.p2.outerHeight()))) {
me.pos = Math.max(me.min, pos.y - me.offset);
me.resize();
}
}
else {
if (((pos.x - me.halfsize) > me.p1pos.left) && ((pos.x + me.halfsize) < (me.p2pos.left + me.p2.outerWidth()))) {
me.pos = Math.max(me.min, pos.x - me.offset);
me.resize();
}
}
me.p1pos = me.relative ? me.p1.position() : me.p1.offset();
me.p2pos = me.relative ? me.p2.position() : me.p2.offset();
return false;
};
/**
* Handler for mouseup events
*/
function onDragStop(e)
{
// resume the ability to highlight text
if (bw.konq || bw.chrome || bw.safari)
document.body.style.webkitUserSelect = 'auto';
// cancel the listening for drag events
$(document).unbind('.'+me.id);
me.drag_active = false;
// remove temp divs
$('div.iframe-splitter-fix').remove();
me.set_cookie();
if (typeof me.callback == 'function')
me.callback(me);
return bw.safari ? true : rcube_event.cancel(e);
};
/**
* Handler for window resize events
*/
function onResize(e)
{
if (me.horizontal) {
var new_height = parseInt(me.p2.parent().outerHeight(), 10) - parseInt(me.p2[0].style.top, 10) - (bw.ie8 ? 2 : 0);
me.p2.css('height', (new_height > 0 ? new_height : 0) +'px');
}
else {
var new_width = parseInt(me.p2.parent().outerWidth(), 10) - parseInt(me.p2[0].style.left, 10);
me.p2.css('width', (new_width > 0 ? new_width : 0) + 'px');
}
};
/**
* Saves splitter position in cookie
*/
this.set_cookie = function()
{
var exp = new Date();
exp.setYear(exp.getFullYear() + 1);
rcmail.set_cookie(this.id, this.pos, exp);
};
} // end class rcube_splitter
// static getter for splitter instances
rcube_splitter._instances = {};
rcube_splitter.get_instance = function(id)
{
return rcube_splitter._instances[id];
};

File Metadata

Mime Type
text/x-diff
Expires
Sat, Mar 1, 5:21 AM (1 d, 16 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
ed/c9/1d3df8fb9af697562cdb50388cf8
Default Alt Text
(976 KB)

Event Timeline