alecpl
2011-09-07 80152b333ca5d856dcf09f5ca10a9ffd80ba117f
- Rewritten messages caching (merged devel-mcache branch):
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
- Partial QRESYNC support
- Improved FETCH response handling
- Improvements in response tokenization method


1 files added
16 files modified
3443 ■■■■■ changed files
CHANGELOG 7 ●●●●● patch | view | raw | blame | history
SQL/mssql.initial.sql 144 ●●●●● patch | view | raw | blame | history
SQL/mssql.upgrade.sql 96 ●●●●● patch | view | raw | blame | history
SQL/mysql.initial.sql 76 ●●●●● patch | view | raw | blame | history
SQL/mysql.update.sql 42 ●●●●● patch | view | raw | blame | history
SQL/postgres.initial.sql 84 ●●●●● patch | view | raw | blame | history
SQL/postgres.update.sql 43 ●●●●● patch | view | raw | blame | history
SQL/sqlite.initial.sql 87 ●●●●● patch | view | raw | blame | history
SQL/sqlite.update.sql 41 ●●●●● patch | view | raw | blame | history
program/include/main.inc 14 ●●●● patch | view | raw | blame | history
program/include/rcube_imap.php 1207 ●●●●● patch | view | raw | blame | history
program/include/rcube_imap_cache.php 907 ●●●●● patch | view | raw | blame | history
program/include/rcube_imap_generic.php 595 ●●●●● patch | view | raw | blame | history
program/include/rcube_message.php 8 ●●●● patch | view | raw | blame | history
program/include/rcube_mime_struct.php 88 ●●●●● patch | view | raw | blame | history
program/steps/mail/func.inc 2 ●●● patch | view | raw | blame | history
program/steps/mail/show.inc 2 ●●● patch | view | raw | blame | history
CHANGELOG
@@ -1,6 +1,13 @@
CHANGELOG Roundcube Webmail
===========================
- 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
- Partial QRESYNC support
- 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
SQL/mssql.initial.sql
@@ -7,6 +7,37 @@
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[cache_index] (
    [user_id] [int] NOT NULL ,
    [mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
    [changed] [datetime] NOT NULL ,
    [data] [text] COLLATE Latin1_General_CI_AI NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[cache_thread] (
    [user_id] [int] NOT NULL ,
    [mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
    [changed] [datetime] NOT NULL ,
    [data] [text] COLLATE Latin1_General_CI_AI NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[cache_messages] (
    [user_id] [int] NOT NULL ,
    [mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
    [uid] [int] NOT NULL ,
    [changed] [datetime] NOT NULL ,
    [data] [text] COLLATE Latin1_General_CI_AI NOT NULL
    [seen] [char](1) NOT NULL ,
    [deleted] [char](1) NOT NULL ,
    [answered] [char](1) NOT NULL ,
    [forwarded] [char](1) NOT NULL ,
    [flagged] [char](1) NOT NULL ,
    [mdnsent] [char](1) NOT NULL ,
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[contacts] (
    [contact_id] [int] IDENTITY (1, 1) NOT NULL ,
    [user_id] [int] NOT NULL ,
@@ -50,25 +81,6 @@
    [bcc] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
    [signature] [text] COLLATE Latin1_General_CI_AI NULL, 
    [html_signature] [char] (1) COLLATE Latin1_General_CI_AI NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[messages] (
    [message_id] [int] IDENTITY (1, 1) NOT NULL ,
    [user_id] [int] NOT NULL ,
    [del] [tinyint] NOT NULL ,
    [cache_key] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
    [created] [datetime] NOT NULL ,
    [idx] [int] NOT NULL ,
    [uid] [int] NOT NULL ,
    [subject] [varchar] (255) COLLATE Latin1_General_CI_AI NOT NULL ,
    [from] [varchar] (255) COLLATE Latin1_General_CI_AI NOT NULL ,
    [to] [varchar] (255) COLLATE Latin1_General_CI_AI NOT NULL ,
    [cc] [varchar] (255) COLLATE Latin1_General_CI_AI NOT NULL ,
    [date] [datetime] NOT NULL ,
    [size] [int] NOT NULL ,
    [headers] [text] COLLATE Latin1_General_CI_AI NOT NULL ,
    [structure] [text] COLLATE Latin1_General_CI_AI NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
@@ -116,6 +128,27 @@
    )  ON [PRIMARY] 
GO
ALTER TABLE [dbo].[cache_index] WITH NOCHECK ADD
     PRIMARY KEY CLUSTERED
    (
        [user_id],[mailbox]
    ) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_thread] WITH NOCHECK ADD
     PRIMARY KEY CLUSTERED
    (
        [user_id],[mailbox]
    ) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_messages] WITH NOCHECK ADD
     PRIMARY KEY CLUSTERED
    (
        [user_id],[mailbox],[uid]
    ) ON [PRIMARY]
GO
ALTER TABLE [dbo].[contacts] WITH NOCHECK ADD 
    CONSTRAINT [PK_contacts_contact_id] PRIMARY KEY  CLUSTERED 
    (
@@ -141,13 +174,6 @@
     PRIMARY KEY  CLUSTERED 
    (
        [identity_id]
    )  ON [PRIMARY]
GO
ALTER TABLE [dbo].[messages] WITH NOCHECK ADD
     PRIMARY KEY  CLUSTERED
    (
        [message_id]
    )  ON [PRIMARY] 
GO
@@ -185,6 +211,33 @@
GO
CREATE  INDEX [IX_cache_created] ON [dbo].[cache]([created]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_index] ADD
    CONSTRAINT [DF_cache_index_changed] DEFAULT (getdate()) FOR [changed]
GO
CREATE  INDEX [IX_cache_index_user_id] ON [dbo].[cache_index]([user_id]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_thread] ADD
    CONSTRAINT [DF_cache_thread_changed] DEFAULT (getdate()) FOR [changed]
GO
CREATE  INDEX [IX_cache_thread_user_id] ON [dbo].[cache_thread]([user_id]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_messages] ADD
    CONSTRAINT [DF_cache_messages_changed] DEFAULT (getdate()) FOR [changed]
    CONSTRAINT [DF_cache_messages_seen] DEFAULT (0) FOR [seen],
    CONSTRAINT [DF_cache_messages_deleted] DEFAULT (0) FOR [deleted],
    CONSTRAINT [DF_cache_messages_answered] DEFAULT (0) FOR [answered],
    CONSTRAINT [DF_cache_messages_forwarded] DEFAULT (0) FOR [forwarded],
    CONSTRAINT [DF_cache_messages_flagged] DEFAULT (0) FOR [flagged],
    CONSTRAINT [DF_cache_messages_mdnsent] DEFAULT (0) FOR [mdnsent],
GO
CREATE  INDEX [IX_cache_messages_user_id] ON [dbo].[cache_messages]([user_id]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[contacts] ADD 
@@ -236,33 +289,6 @@
GO
CREATE  INDEX [IX_identities_user_id] ON [dbo].[identities]([user_id]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[messages] ADD
    CONSTRAINT [DF_messages_user_id] DEFAULT (0) FOR [user_id],
    CONSTRAINT [DF_messages_del] DEFAULT (0) FOR [del],
    CONSTRAINT [DF_messages_cache_key] DEFAULT ('') FOR [cache_key],
    CONSTRAINT [DF_messages_created] DEFAULT (getdate()) FOR [created],
    CONSTRAINT [DF_messages_idx] DEFAULT (0) FOR [idx],
    CONSTRAINT [DF_messages_uid] DEFAULT (0) FOR [uid],
    CONSTRAINT [DF_messages_subject] DEFAULT ('') FOR [subject],
    CONSTRAINT [DF_messages_from] DEFAULT ('') FOR [from],
    CONSTRAINT [DF_messages_to] DEFAULT ('') FOR [to],
    CONSTRAINT [DF_messages_cc] DEFAULT ('') FOR [cc],
    CONSTRAINT [DF_messages_date] DEFAULT (getdate()) FOR [date],
    CONSTRAINT [DF_messages_size] DEFAULT (0) FOR [size]
GO
CREATE  INDEX [IX_messages_user_id] ON [dbo].[messages]([user_id]) ON [PRIMARY]
GO
CREATE  INDEX [IX_messages_cache_key] ON [dbo].[messages]([cache_key]) ON [PRIMARY]
GO
CREATE  INDEX [IX_messages_uid] ON [dbo].[messages]([uid]) ON [PRIMARY]
GO
CREATE  INDEX [IX_messages_created] ON [dbo].[messages]([created]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[session] ADD 
@@ -318,7 +344,17 @@
    ON DELETE CASCADE ON UPDATE CASCADE
GO
ALTER TABLE [dbo].[messages] ADD CONSTRAINT [FK_messages_user_id]
ALTER TABLE [dbo].[cache_index] ADD CONSTRAINT [FK_cache_index_user_id]
    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
    ON DELETE CASCADE ON UPDATE CASCADE
GO
ALTER TABLE [dbo].[cache_thread] ADD CONSTRAINT [FK_cache_thread_user_id]
    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
    ON DELETE CASCADE ON UPDATE CASCADE
GO
ALTER TABLE [dbo].[cache_messages] ADD CONSTRAINT [FK_cache_messages_user_id]
    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
    ON DELETE CASCADE ON UPDATE CASCADE
GO
SQL/mssql.upgrade.sql
@@ -151,3 +151,99 @@
    ON DELETE CASCADE ON UPDATE CASCADE
GO
DROP TABLE [dbo].[messages]
GO
CREATE TABLE [dbo].[cache_index] (
    [user_id] [int] NOT NULL ,
    [mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
    [changed] [datetime] NOT NULL ,
    [data] [text] COLLATE Latin1_General_CI_AI NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[cache_thread] (
    [user_id] [int] NOT NULL ,
    [mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
    [changed] [datetime] NOT NULL ,
    [data] [text] COLLATE Latin1_General_CI_AI NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[cache_messages] (
    [user_id] [int] NOT NULL ,
    [mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
    [uid] [int] NOT NULL ,
    [changed] [datetime] NOT NULL ,
    [data] [text] COLLATE Latin1_General_CI_AI NOT NULL
    [seen] [char](1) NOT NULL ,
    [deleted] [char](1) NOT NULL ,
    [answered] [char](1) NOT NULL ,
    [forwarded] [char](1) NOT NULL ,
    [flagged] [char](1) NOT NULL ,
    [mdnsent] [char](1) NOT NULL ,
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_index] WITH NOCHECK ADD
     PRIMARY KEY CLUSTERED
    (
        [user_id],[mailbox]
    ) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_thread] WITH NOCHECK ADD
     PRIMARY KEY CLUSTERED
    (
        [user_id],[mailbox]
    ) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_messages] WITH NOCHECK ADD
     PRIMARY KEY CLUSTERED
    (
        [user_id],[mailbox],[uid]
    ) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_index] ADD
    CONSTRAINT [DF_cache_index_changed] DEFAULT (getdate()) FOR [changed]
GO
CREATE  INDEX [IX_cache_index_user_id] ON [dbo].[cache_index]([user_id]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_thread] ADD
    CONSTRAINT [DF_cache_thread_changed] DEFAULT (getdate()) FOR [changed]
GO
CREATE  INDEX [IX_cache_thread_user_id] ON [dbo].[cache_thread]([user_id]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_messages] ADD
    CONSTRAINT [DF_cache_messages_changed] DEFAULT (getdate()) FOR [changed]
    CONSTRAINT [DF_cache_messages_seen] DEFAULT (0) FOR [seen],
    CONSTRAINT [DF_cache_messages_deleted] DEFAULT (0) FOR [deleted],
    CONSTRAINT [DF_cache_messages_answered] DEFAULT (0) FOR [answered],
    CONSTRAINT [DF_cache_messages_forwarded] DEFAULT (0) FOR [forwarded],
    CONSTRAINT [DF_cache_messages_flagged] DEFAULT (0) FOR [flagged],
    CONSTRAINT [DF_cache_messages_mdnsent] DEFAULT (0) FOR [mdnsent],
GO
CREATE  INDEX [IX_cache_messages_user_id] ON [dbo].[cache_messages]([user_id]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[cache_index] ADD CONSTRAINT [FK_cache_index_user_id]
    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
    ON DELETE CASCADE ON UPDATE CASCADE
GO
ALTER TABLE [dbo].[cache_thread] ADD CONSTRAINT [FK_cache_thread_user_id]
    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
    ON DELETE CASCADE ON UPDATE CASCADE
GO
ALTER TABLE [dbo].[cache_messages] ADD CONSTRAINT [FK_cache_messages_user_id]
    FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
    ON DELETE CASCADE ON UPDATE CASCADE
GO
SQL/mysql.initial.sql
@@ -33,33 +33,6 @@
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-- Table structure for table `messages`
CREATE TABLE `messages` (
 `message_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
 `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
 `del` tinyint(1) NOT NULL DEFAULT '0',
 `cache_key` varchar(128) /*!40101 CHARACTER SET ascii COLLATE ascii_general_ci */ NOT NULL,
 `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
 `idx` int(11) UNSIGNED NOT NULL DEFAULT '0',
 `uid` int(11) UNSIGNED NOT NULL DEFAULT '0',
 `subject` varchar(255) NOT NULL,
 `from` varchar(255) NOT NULL,
 `to` varchar(255) NOT NULL,
 `cc` varchar(255) NOT NULL,
 `date` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
 `size` int(11) UNSIGNED NOT NULL DEFAULT '0',
 `headers` text NOT NULL,
 `structure` text,
 PRIMARY KEY(`message_id`),
 CONSTRAINT `user_id_fk_messages` FOREIGN KEY (`user_id`)
   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
 INDEX `created_index` (`created`),
 INDEX `index_index` (`user_id`, `cache_key`, `idx`),
 UNIQUE `uniqueness` (`user_id`, `cache_key`, `uid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-- Table structure for table `cache`
CREATE TABLE `cache` (
@@ -76,6 +49,55 @@
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-- Table structure for table `cache_index`
CREATE TABLE `cache_index` (
 `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
 `mailbox` varchar(255) BINARY NOT NULL,
 `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
 `data` longtext NOT NULL,
 CONSTRAINT `user_id_fk_cache_index` FOREIGN KEY (`user_id`)
   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
 INDEX `changed_index` (`changed`),
 PRIMARY KEY (`user_id`, `mailbox`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-- Table structure for table `cache_thread`
CREATE TABLE `cache_thread` (
 `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
 `mailbox` varchar(255) BINARY NOT NULL,
 `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
 `data` longtext NOT NULL,
 CONSTRAINT `user_id_fk_cache_thread` FOREIGN KEY (`user_id`)
   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
 INDEX `changed_index` (`changed`),
 PRIMARY KEY (`user_id`, `mailbox`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-- Table structure for table `cache_messages`
CREATE TABLE `cache_messages` (
 `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
 `mailbox` varchar(255) BINARY NOT NULL,
 `uid` int(11) UNSIGNED NOT NULL DEFAULT '0',
 `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
 `data` longtext NOT NULL,
 `seen` tinyint(1) NOT NULL DEFAULT '0',
 `deleted` tinyint(1) NOT NULL DEFAULT '0',
 `answered` tinyint(1) NOT NULL DEFAULT '0',
 `forwarded` tinyint(1) NOT NULL DEFAULT '0',
 `flagged` tinyint(1) NOT NULL DEFAULT '0',
 `mdnsent` tinyint(1) NOT NULL DEFAULT '0',
 CONSTRAINT `user_id_fk_cache_messages` FOREIGN KEY (`user_id`)
   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
 INDEX `changed_index` (`changed`),
 PRIMARY KEY (`user_id`, `mailbox`, `uid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-- Table structure for table `contacts`
CREATE TABLE `contacts` (
SQL/mysql.update.sql
@@ -170,3 +170,45 @@
    REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
  UNIQUE `uniqueness` (`user_id`, `type`, `name`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE `messages`;
CREATE TABLE `cache_index` (
 `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
 `mailbox` varchar(255) BINARY NOT NULL,
 `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
 `data` longtext NOT NULL,
 CONSTRAINT `user_id_fk_cache_index` FOREIGN KEY (`user_id`)
   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
 INDEX `changed_index` (`changed`),
 PRIMARY KEY (`user_id`, `mailbox`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE `cache_thread` (
 `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
 `mailbox` varchar(255) BINARY NOT NULL,
 `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
 `data` longtext NOT NULL,
 CONSTRAINT `user_id_fk_cache_thread` FOREIGN KEY (`user_id`)
   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
 INDEX `changed_index` (`changed`),
 PRIMARY KEY (`user_id`, `mailbox`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE `cache_messages` (
 `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
 `mailbox` varchar(255) BINARY NOT NULL,
 `uid` int(11) UNSIGNED NOT NULL DEFAULT '0',
 `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
 `data` longtext NOT NULL,
 `seen` tinyint(1) NOT NULL DEFAULT '0',
 `deleted` tinyint(1) NOT NULL DEFAULT '0',
 `answered` tinyint(1) NOT NULL DEFAULT '0',
 `forwarded` tinyint(1) NOT NULL DEFAULT '0',
 `flagged` tinyint(1) NOT NULL DEFAULT '0',
 `mdnsent` tinyint(1) NOT NULL DEFAULT '0',
 CONSTRAINT `user_id_fk_cache_messages` FOREIGN KEY (`user_id`)
   REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
 INDEX `changed_index` (`changed`),
 PRIMARY KEY (`user_id`, `mailbox`, `uid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
SQL/postgres.initial.sql
@@ -67,7 +67,7 @@
CREATE TABLE identities (
    identity_id integer DEFAULT nextval('identity_ids'::text) PRIMARY KEY,
    user_id integer NOT NULL
    REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
    changed timestamp with time zone DEFAULT now() NOT NULL,
    del smallint DEFAULT 0 NOT NULL,
    standard smallint DEFAULT 0 NOT NULL,
@@ -178,7 +178,7 @@
CREATE TABLE "cache" (
    cache_id integer DEFAULT nextval('cache_ids'::text) PRIMARY KEY,
    user_id integer NOT NULL
    REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
    cache_key varchar(128) DEFAULT '' NOT NULL,
    created timestamp with time zone DEFAULT now() NOT NULL,
    data text NOT NULL
@@ -188,43 +188,59 @@
CREATE INDEX cache_created_idx ON "cache" (created);
--
-- Sequence "message_ids"
-- Name: message_ids; Type: SEQUENCE; Schema: public; Owner: postgres
-- Table "cache_index"
-- Name: cache_index; Type: TABLE; Schema: public; Owner: postgres
--
CREATE SEQUENCE message_ids
    INCREMENT BY 1
    NO MAXVALUE
    NO MINVALUE
    CACHE 1;
--
-- Table "messages"
-- Name: messages; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE messages (
    message_id integer DEFAULT nextval('message_ids'::text) PRIMARY KEY,
CREATE TABLE cache_index (
    user_id integer NOT NULL
    REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
    del smallint DEFAULT 0 NOT NULL,
    cache_key varchar(128) DEFAULT '' NOT NULL,
    created timestamp with time zone DEFAULT now() NOT NULL,
    idx integer DEFAULT 0 NOT NULL,
    uid integer DEFAULT 0 NOT NULL,
    subject varchar(128) DEFAULT '' NOT NULL,
    "from" varchar(128) DEFAULT '' NOT NULL,
    "to" varchar(128) DEFAULT '' NOT NULL,
    cc varchar(128) DEFAULT '' NOT NULL,
    date timestamp with time zone NOT NULL,
    size integer DEFAULT 0 NOT NULL,
    headers text NOT NULL,
    structure text,
    CONSTRAINT messages_user_id_key UNIQUE (user_id, cache_key, uid)
        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
    mailbox varchar(255) NOT NULL,
    changed timestamp with time zone DEFAULT now() NOT NULL,
    data text NOT NULL,
    PRIMARY KEY (user_id, mailbox)
);
CREATE INDEX messages_index_idx ON messages (user_id, cache_key, idx);
CREATE INDEX messages_created_idx ON messages (created);
CREATE INDEX cache_index_changed_idx ON cache_index (changed);
--
-- Table "cache_thread"
-- Name: cache_thread; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE cache_thread (
    user_id integer NOT NULL
        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
    mailbox varchar(255) NOT NULL,
    changed timestamp with time zone DEFAULT now() NOT NULL,
    data text NOT NULL,
    PRIMARY KEY (user_id, mailbox)
);
CREATE INDEX cache_thread_changed_idx ON cache_thread (changed);
--
-- Table "cache_messages"
-- Name: cache_messages; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE cache_messages (
    user_id integer NOT NULL
        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
    mailbox varchar(255) NOT NULL,
    uid integer NOT NULL,
    changed timestamp with time zone DEFAULT now() NOT NULL,
    data text NOT NULL,
    seen smallint NOT NULL DEFAULT 0,
    deleted smallint NOT NULL DEFAULT 0,
    answered smallint NOT NULL DEFAULT 0,
    forwarded smallint NOT NULL DEFAULT 0,
    flagged smallint NOT NULL DEFAULT 0,
    mdnsent smallint NOT NULL DEFAULT 0,
    PRIMARY KEY (user_id, mailbox, uid)
);
CREATE INDEX cache_messages_changed_idx ON cache_messages (changed);
--
-- Table "dictionary"
SQL/postgres.update.sql
@@ -126,3 +126,46 @@
    data text NOT NULL,
    CONSTRAINT searches_user_id_key UNIQUE (user_id, "type", name)
);
DROP SEQUENCE messages_ids;
DROP TABLE messages;
CREATE TABLE cache_index (
    user_id integer NOT NULL
        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
    mailbox varchar(255) NOT NULL,
    changed timestamp with time zone DEFAULT now() NOT NULL,
    data text NOT NULL,
    PRIMARY KEY (user_id, mailbox)
);
CREATE INDEX cache_index_changed_idx ON cache_index (changed);
CREATE TABLE cache_thread (
    user_id integer NOT NULL
        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
    mailbox varchar(255) NOT NULL,
    changed timestamp with time zone DEFAULT now() NOT NULL,
    data text NOT NULL,
    PRIMARY KEY (user_id, mailbox)
);
CREATE INDEX cache_thread_changed_idx ON cache_thread (changed);
CREATE TABLE cache_messages (
    user_id integer NOT NULL
        REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
    mailbox varchar(255) NOT NULL,
    uid integer NOT NULL,
    changed timestamp with time zone DEFAULT now() NOT NULL,
    data text NOT NULL,
    seen smallint NOT NULL DEFAULT 0,
    deleted smallint NOT NULL DEFAULT 0,
    answered smallint NOT NULL DEFAULT 0,
    forwarded smallint NOT NULL DEFAULT 0,
    flagged smallint NOT NULL DEFAULT 0,
    mdnsent smallint NOT NULL DEFAULT 0,
    PRIMARY KEY (user_id, mailbox, uid)
);
CREATE INDEX cache_messages_changed_idx ON cache_messages (changed);
SQL/sqlite.initial.sql
@@ -1,7 +1,7 @@
-- Roundcube Webmail initial database structure
-- 
-- Table structure for table `cache`
-- Table structure for table cache
-- 
CREATE TABLE cache (
@@ -9,7 +9,7 @@
  user_id integer NOT NULL default 0,
  cache_key varchar(128) NOT NULL default '',
  created datetime NOT NULL default '0000-00-00 00:00:00',
  data longtext NOT NULL
  data text NOT NULL
);
CREATE INDEX ix_cache_user_cache_key ON cache(user_id, cache_key);
@@ -121,34 +121,6 @@
-- --------------------------------------------------------
--
-- Table structure for table messages
--
CREATE TABLE messages (
  message_id integer NOT NULL PRIMARY KEY,
  user_id integer NOT NULL default '0',
  del tinyint NOT NULL default '0',
  cache_key varchar(128) NOT NULL default '',
  created datetime NOT NULL default '0000-00-00 00:00:00',
  idx integer NOT NULL default '0',
  uid integer NOT NULL default '0',
  subject varchar(255) NOT NULL default '',
  "from" varchar(255) NOT NULL default '',
  "to" varchar(255) NOT NULL default '',
  "cc" varchar(255) NOT NULL default '',
  "date" datetime NOT NULL default '0000-00-00 00:00:00',
  size integer NOT NULL default '0',
  headers text NOT NULL,
  structure text
);
CREATE UNIQUE INDEX ix_messages_user_cache_uid ON messages (user_id,cache_key,uid);
CREATE INDEX ix_messages_index ON messages (user_id,cache_key,idx);
CREATE INDEX ix_messages_created ON messages (created);
-- --------------------------------------------------------
--
-- Table structure for table dictionary
--
@@ -176,3 +148,58 @@
);
CREATE UNIQUE INDEX ix_searches_user_type_name (user_id, type, name);
-- --------------------------------------------------------
--
-- Table structure for table cache_index
--
CREATE TABLE cache_index (
    user_id integer NOT NULL,
    mailbox varchar(255) NOT NULL,
    changed datetime NOT NULL default '0000-00-00 00:00:00',
    data text NOT NULL,
    PRIMARY KEY (user_id, mailbox)
);
CREATE INDEX ix_cache_index_changed ON cache_index (changed);
-- --------------------------------------------------------
--
-- Table structure for table cache_thread
--
CREATE TABLE cache_thread (
    user_id integer NOT NULL,
    mailbox varchar(255) NOT NULL,
    changed datetime NOT NULL default '0000-00-00 00:00:00',
    data text NOT NULL,
    PRIMARY KEY (user_id, mailbox)
);
CREATE INDEX ix_cache_thread_changed ON cache_thread (changed);
-- --------------------------------------------------------
--
-- Table structure for table cache_messages
--
CREATE TABLE cache_messages (
    user_id integer NOT NULL,
    mailbox varchar(255) NOT NULL,
    uid integer NOT NULL,
    changed datetime NOT NULL default '0000-00-00 00:00:00',
    data text NOT NULL,
    seen smallint NOT NULL DEFAULT '0',
    deleted smallint NOT NULL DEFAULT '0',
    answered smallint NOT NULL DEFAULT '0',
    forwarded smallint NOT NULL DEFAULT '0',
    flagged smallint NOT NULL DEFAULT '0',
    mdnsent smallint NOT NULL DEFAULT '0',
    PRIMARY KEY (user_id, mailbox, uid)
);
CREATE INDEX ix_cache_messages_changed ON cache_messages (changed);
SQL/sqlite.update.sql
@@ -223,10 +223,10 @@
CREATE INDEX ix_contacts_user_id ON contacts(user_id, email);
DROP TABLE contacts_tmp;
DELETE FROM messages;
DELETE FROM cache;
CREATE INDEX ix_contactgroupmembers_contact_id ON contactgroupmembers (contact_id);
-- Updates from version 0.6-stable
@@ -247,3 +247,42 @@
);
CREATE UNIQUE INDEX ix_searches_user_type_name (user_id, type, name);
DROP TABLE messages;
CREATE TABLE cache_index (
    user_id integer NOT NULL,
    mailbox varchar(255) NOT NULL,
    changed datetime NOT NULL default '0000-00-00 00:00:00',
    data text NOT NULL,
    PRIMARY KEY (user_id, mailbox)
);
CREATE INDEX ix_cache_index_changed ON cache_index (changed);
CREATE TABLE cache_thread (
    user_id integer NOT NULL,
    mailbox varchar(255) NOT NULL,
    changed datetime NOT NULL default '0000-00-00 00:00:00',
    data text NOT NULL,
    PRIMARY KEY (user_id, mailbox)
);
CREATE INDEX ix_cache_thread_changed ON cache_thread (changed);
CREATE TABLE cache_messages (
    user_id integer NOT NULL,
    mailbox varchar(255) NOT NULL,
    uid integer NOT NULL,
    changed datetime NOT NULL default '0000-00-00 00:00:00',
    data text NOT NULL,
    seen smallint NOT NULL DEFAULT '0',
    deleted smallint NOT NULL DEFAULT '0',
    answered smallint NOT NULL DEFAULT '0',
    forwarded smallint NOT NULL DEFAULT '0',
    flagged smallint NOT NULL DEFAULT '0',
    mdnsent smallint NOT NULL DEFAULT '0',
    PRIMARY KEY (user_id, mailbox, uid)
);
CREATE INDEX ix_cache_messages_changed ON cache_messages (changed);
program/include/main.inc
@@ -169,11 +169,17 @@
  // get target timestamp
  $ts = get_offset_time($rcmail->config->get('message_cache_lifetime', '30d'), -1);
  $db->query("DELETE FROM ".get_table_name('messages')."
             WHERE  created < " . $db->fromunixtime($ts));
  $db->query("DELETE FROM ".get_table_name('cache_messages')
        ." WHERE changed < " . $db->fromunixtime($ts));
  $db->query("DELETE FROM ".get_table_name('cache')."
              WHERE  created < " . $db->fromunixtime($ts));
  $db->query("DELETE FROM ".get_table_name('cache_index')
        ." WHERE changed < " . $db->fromunixtime($ts));
  $db->query("DELETE FROM ".get_table_name('cache_thread')
        ." WHERE changed < " . $db->fromunixtime($ts));
  $db->query("DELETE FROM ".get_table_name('cache')
        ." WHERE created < " . $db->fromunixtime($ts));
}
program/include/rcube_imap.php
@@ -48,11 +48,11 @@
    public $conn;
    /**
     * Instance of rcube_mdb2
     * Instance of rcube_imap_cache
     *
     * @var rcube_mdb2
     * @var rcube_imap_cache
     */
    private $db;
    private $mcache;
    /**
     * Instance of rcube_cache
@@ -60,6 +60,14 @@
     * @var rcube_cache
     */
    private $cache;
    /**
     * Internal (in-memory) cache
     *
     * @var array
     */
    private $icache = array();
    private $mailbox = 'INBOX';
    private $delimiter = NULL;
    private $namespace = NULL;
@@ -68,8 +76,6 @@
    private $default_charset = 'ISO-8859-1';
    private $struct_charset = NULL;
    private $default_folders = array('INBOX');
    private $messages_caching = false;
    private $icache = array();
    private $uid_id_map = array();
    private $msg_headers = array();
    public  $search_set = NULL;
@@ -78,10 +84,10 @@
    private $search_sort_field = '';
    private $search_threads = false;
    private $search_sorted = false;
    private $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
    private $options = array('auth_method' => 'check');
    private $host, $user, $pass, $port, $ssl;
    private $caching = false;
    private $messages_caching = false;
    /**
     * All (additional) headers used (in any way) by Roundcube
@@ -214,6 +220,8 @@
    function close()
    {
        $this->conn->closeConnection();
        if ($this->mcache)
            $this->mcache->close();
    }
@@ -689,7 +697,7 @@
            if ($status) {
                $this->set_folder_stats($mailbox, 'cnt', $res['msgcount']);
                $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->_id2uid($res['maxuid'], $mailbox) : 0);
                $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->id2uid($res['maxuid'], $mailbox) : 0);
            }
        }
        // RECENT count is fetched a bit different
@@ -722,9 +730,9 @@
            $count = is_array($index) ? $index['COUNT'] : 0;
            if ($mode == 'ALL') {
                if ($need_uid && $this->messages_caching) {
                    // Save messages index for check_cache_status()
                    $this->icache['all_undeleted_idx'] = $index['ALL'];
                if ($this->messages_caching) {
                    // Save additional info required by cache status check
                    $this->icache['undeleted_idx'] = array($mailbox, $index['ALL'], $index['COUNT']);
                }
                if ($status) {
                    $this->set_folder_stats($mailbox, 'cnt', $count);
@@ -739,7 +747,7 @@
                $count = $this->conn->countMessages($mailbox);
                if ($status) {
                    $this->set_folder_stats($mailbox,'cnt', $count);
                    $this->set_folder_stats($mailbox, 'maxuid', $count ? $this->_id2uid($count, $mailbox) : 0);
                    $this->set_folder_stats($mailbox, 'maxuid', $count ? $this->id2uid($count, $mailbox) : 0);
                }
            }
        }
@@ -774,7 +782,7 @@
                'maxuid'   => $dcount ? max(array_keys($this->icache['threads']['depth'])) : 0,
            );
        }
        else if (is_array($result = $this->_fetch_threads($mailbox))) {
        else if (is_array($result = $this->fetch_threads($mailbox))) {
            $dcount = count($result[1]);
            $result = array(
                'count'    => count($result[0]),
@@ -817,11 +825,11 @@
     * @param   string   $sort_field Header field to sort by
     * @param   string   $sort_order Sort order [ASC|DESC]
     * @param   int      $slice      Number of slice items to extract from result array
     *
     * @return  array    Indexed array with message header objects
     * @access  private
     * @see     rcube_imap::list_headers
     */
    private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
    private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
    {
        if (!strlen($mailbox))
            return array();
@@ -831,41 +839,39 @@
            return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
        if ($this->threading)
            return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $recursive, $slice);
            return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $slice);
        $this->_set_sort_order($sort_field, $sort_order);
        $page         = $page ? $page : $this->list_page;
        $cache_key    = $mailbox.'.msg';
        $page = $page ? $page : $this->list_page;
        if ($this->messages_caching) {
            // cache is OK, we can get messages from local cache
            // (assume cache is in sync when in recursive mode)
            if ($recursive || $this->check_cache_status($mailbox, $cache_key)>0) {
                $start_msg = ($page-1) * $this->page_size;
                $a_msg_headers = $this->get_message_cache($cache_key, $start_msg,
                    $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
                $result = array_values($a_msg_headers);
                if ($slice)
                    $result = array_slice($result, -$slice, $slice);
                return $result;
            }
            // cache is incomplete, sync it (all messages in the folder)
            else if (!$recursive) {
                $this->sync_header_index($mailbox);
                return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, true, $slice);
            }
        // Use messages cache
        if ($mcache = $this->get_mcache_engine()) {
            $msg_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
            if (empty($msg_index))
                return array();
            $from      = ($page-1) * $this->page_size;
            $to        = $from + $this->page_size;
            $msg_index = array_values($msg_index); // UIDs
            $is_uid    = true;
            $sorted    = true;
            if ($from || $to)
                $msg_index = array_slice($msg_index, $from, $to - $from);
            if ($slice)
                $msg_index = array_slice($msg_index, -$slice, $slice);
            $a_msg_headers = $mcache->get_messages($mailbox, $msg_index);
        }
        // retrieve headers from IMAP
        $a_msg_headers = array();
        // use message index sort as default sorting (for better performance)
        if (!$this->sort_field) {
        else if (!$this->sort_field) {
            if ($this->skip_deleted) {
                // @TODO: this could be cached
                if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
                    $max = max($msg_index);
                    list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
                    $msg_index = array_slice($msg_index, $begin, $end-$begin);
                }
@@ -882,46 +888,42 @@
            // fetch reqested headers from server
            if ($msg_index)
                $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
                $a_msg_headers = $this->fetch_headers($mailbox, $msg_index);
        }
        // use SORT command
        else if ($this->get_capability('SORT') &&
            // Courier-IMAP provides SORT capability but allows to disable it by admin (#1486959)
            ($msg_index = $this->conn->sort($mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
            ($msg_index = $this->conn->sort($mailbox, $this->sort_field,
                $this->skip_deleted ? 'UNDELETED' : '', true)) !== false
        ) {
            if (!empty($msg_index)) {
                list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
                $max = max($msg_index);
                $msg_index = array_slice($msg_index, $begin, $end-$begin);
                $is_uid    = true;
                if ($slice)
                    $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
                // fetch reqested headers from server
                $this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
                $a_msg_headers = $this->fetch_headers($mailbox, $msg_index, true);
            }
        }
        // fetch specified header for all messages and sort
        else if ($a_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
            asort($a_index); // ASC
            $msg_index = array_keys($a_index);
            $max = max($msg_index);
        else if ($msg_index = $this->conn->fetchHeaderIndex($mailbox, "1:*",
            $this->sort_field, $this->skip_deleted, true)
        ) {
            asort($msg_index); // ASC
            $msg_index = array_keys($msg_index);
            list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
            $msg_index = array_slice($msg_index, $begin, $end-$begin);
            $is_uid    = true;
            if ($slice)
                $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
            // fetch reqested headers from server
            $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
            $a_msg_headers = $this->fetch_headers($mailbox, $msg_index, true);
        }
        // delete cached messages with a higher index than $max+1
        // Changed $max to $max+1 to fix this bug : #1484295
        $this->clear_message_cache($cache_key, $max + 1);
        // kick child process to sync cache
        // ...
        // return empty array if no messages found
        if (!is_array($a_msg_headers) || empty($a_msg_headers))
@@ -929,10 +931,10 @@
        // use this class for message sorting
        $sorter = new rcube_header_sorter();
        $sorter->set_sequence_numbers($msg_index);
        $sorter->set_index($msg_index, $is_uid);
        $sorter->sort_headers($a_msg_headers);
        if ($this->sort_order == 'DESC')
        if ($this->sort_order == 'DESC' && !$sorted)
            $a_msg_headers = array_reverse($a_msg_headers);
        return array_values($a_msg_headers);
@@ -946,27 +948,28 @@
     * @param   int      $page       Current page to list
     * @param   string   $sort_field Header field to sort by
     * @param   string   $sort_order Sort order [ASC|DESC]
     * @param   boolean  $recursive  True if called recursively
     * @param   int      $slice      Number of slice items to extract from result array
     *
     * @return  array    Indexed array with message header objects
     * @access  private
     * @see     rcube_imap::list_headers
     */
    private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
    private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
    {
        $this->_set_sort_order($sort_field, $sort_order);
        $page = $page ? $page : $this->list_page;
//    $cache_key = $mailbox.'.msg';
//    $cache_status = $this->check_cache_status($mailbox, $cache_key);
        $page   = $page ? $page : $this->list_page;
        $mcache = $this->get_mcache_engine();
        // get all threads (default sort order)
        list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
        // get all threads (not sorted)
        if ($mcache)
            list ($thread_tree, $msg_depth, $has_children) = $mcache->get_thread($mailbox);
        else
            list ($thread_tree, $msg_depth, $has_children) = $this->fetch_threads($mailbox);
        if (empty($thread_tree))
            return array();
        $msg_index = $this->_sort_threads($mailbox, $thread_tree);
        $msg_index = $this->sort_threads($mailbox, $thread_tree);
        return $this->_fetch_thread_headers($mailbox,
            $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
@@ -974,14 +977,20 @@
    /**
     * Private method for fetching threads data
     * Method for fetching threads data
     *
     * @param   string   $mailbox Mailbox/folder name
     * @param  string $mailbox  Folder name
     * @param  bool   $force    Use IMAP server, no cache
     *
     * @return  array    Array with thread data
     * @access  private
     */
    private function _fetch_threads($mailbox)
    function fetch_threads($mailbox, $force = false)
    {
        if (!$force && ($mcache = $this->get_mcache_engine())) {
            // don't store in self's internal cache, cache has it's own internal cache
            return $mcache->get_thread($mailbox);
        }
        if (empty($this->icache['threads'])) {
            // get all threads
            $result = $this->conn->thread($mailbox, $this->threading,
@@ -1012,12 +1021,12 @@
     * @param array   $msg_index    Messages index
     * @param int     $page         List page number
     * @param int     $slice        Number of threads to slice
     *
     * @return array  Messages headers
     * @access  private
     */
    private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
    {
        $cache_key = $mailbox.'.msg';
        // now get IDs for current page
        list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
        $msg_index = array_slice($msg_index, $begin, $end-$begin);
@@ -1038,7 +1047,7 @@
        }
        // fetch reqested headers from server
        $this->_fetch_headers($mailbox, $all_ids, $a_msg_headers, $cache_key);
        $a_msg_headers = $this->fetch_headers($mailbox, $all_ids);
        // return empty array if no messages found
        if (!is_array($a_msg_headers) || empty($a_msg_headers))
@@ -1046,7 +1055,7 @@
        // use this class for message sorting
        $sorter = new rcube_header_sorter();
        $sorter->set_sequence_numbers($all_ids);
        $sorter->set_index($all_ids);
        $sorter->sort_headers($a_msg_headers);
        // Set depth, has_children and unread_children fields in headers
@@ -1135,11 +1144,11 @@
                $msgs = array_slice($msgs, -$slice, $slice);
            // fetch headers
            $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
            $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
            // I didn't found in RFC that FETCH always returns messages sorted by index
            $sorter = new rcube_header_sorter();
            $sorter->set_sequence_numbers($msgs);
            $sorter->set_index($msgs);
            $sorter->sort_headers($a_msg_headers);
            return array_values($a_msg_headers);
@@ -1165,10 +1174,10 @@
                $msgs = array_slice($msgs, -$slice, $slice);
            // fetch headers
            $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
            $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
            $sorter = new rcube_header_sorter();
            $sorter->set_sequence_numbers($msgs);
            $sorter->set_index($msgs);
            $sorter->sort_headers($a_msg_headers);
            return array_values($a_msg_headers);
@@ -1184,21 +1193,22 @@
                if ($slice)
                    $msgs = array_slice($msgs, -$slice, $slice);
                // ...and fetch headers
                $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
                $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
                // return empty array if no messages found
                if (!is_array($a_msg_headers) || empty($a_msg_headers))
                    return array();
                $sorter = new rcube_header_sorter();
                $sorter->set_sequence_numbers($msgs);
                $sorter->set_index($msgs);
                $sorter->sort_headers($a_msg_headers);
                return array_values($a_msg_headers);
            }
            else {
                // for small result set we can fetch all messages headers
                $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
                $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
                // return empty array if no messages found
                if (!is_array($a_msg_headers) || empty($a_msg_headers))
@@ -1256,7 +1266,7 @@
        $this->_set_sort_order($sort_field, $sort_order);
        $msg_index = $this->_sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
        $msg_index = $this->sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
        return $this->_fetch_thread_headers($mailbox,
            $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
@@ -1297,64 +1307,37 @@
    /**
     * Fetches message headers (used for loop)
     * Fetches messages headers
     *
     * @param  string  $mailbox       Mailbox name
     * @param  string  $msgs          Message index to fetch
     * @param  array   $a_msg_headers Reference to message headers array
     * @param  string  $cache_key     Cache index key
     * @return int     Messages count
     * @param  string  $mailbox  Mailbox name
     * @param  array   $msgs     Messages sequence numbers
     * @param  bool    $is_uid   Enable if $msgs numbers are UIDs
     * @param  bool    $force    Disables cache use
     *
     * @return array Messages headers indexed by UID
     * @access private
     */
    private function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
    function fetch_headers($mailbox, $msgs, $is_uid = false, $force = false)
    {
        if (empty($msgs))
            return array();
        if (!$force && ($mcache = $this->get_mcache_engine())) {
            return $mcache->get_messages($mailbox, $msgs, $is_uid);
        }
        // fetch reqested headers from server
        $a_header_index = $this->conn->fetchHeaders(
            $mailbox, $msgs, false, false, $this->get_fetch_headers());
        $index = $this->conn->fetchHeaders(
            $mailbox, $msgs, $is_uid, false, $this->get_fetch_headers());
        if (empty($a_header_index))
            return 0;
        if (empty($index))
            return array();
        foreach ($a_header_index as $i => $headers) {
        foreach ($index as $headers) {
            $a_msg_headers[$headers->uid] = $headers;
        }
        // Update cache
        if ($this->messages_caching && $cache_key) {
            // cache is incomplete?
            $cache_index = $this->get_message_cache_index($cache_key);
            foreach ($a_header_index as $headers) {
                // message in cache
                if ($cache_index[$headers->id] == $headers->uid) {
                    unset($cache_index[$headers->id]);
                    continue;
                }
                // wrong UID at this position
                if ($cache_index[$headers->id]) {
                    $for_remove[] = $cache_index[$headers->id];
                    unset($cache_index[$headers->id]);
                }
                // message UID in cache but at wrong position
                if (is_int($key = array_search($headers->uid, $cache_index))) {
                    $for_remove[] = $cache_index[$key];
                    unset($cache_index[$key]);
                }
                $for_create[] = $headers->uid;
            }
            if ($for_remove)
                $this->remove_message_cache($cache_key, $for_remove);
            // add messages to cache
            foreach ((array)$for_create as $uid) {
                $headers = $a_msg_headers[$uid];
                $this->add_message_cache($cache_key, $headers->id, $headers, NULL, true);
            }
        }
        return count($a_msg_headers);
        return $a_msg_headers;
    }
@@ -1471,7 +1454,7 @@
            }
            else {
                $a_index = $this->conn->fetchHeaderIndex($mailbox,
                    join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
                    join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
                if (is_array($a_index)) {
                    if ($this->sort_order=="ASC")
@@ -1492,50 +1475,62 @@
            return $this->icache[$key];
        // check local cache
        $cache_key = $mailbox.'.msg';
        $cache_status = $this->check_cache_status($mailbox, $cache_key);
        // cache is OK
        if ($cache_status>0) {
            $a_index = $this->get_message_cache_index($cache_key,
                $this->sort_field, $this->sort_order);
            return array_keys($a_index);
        if ($mcache = $this->get_mcache_engine()) {
            $a_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
            $this->icache[$key] = array_keys($a_index);
        }
        // fetch from IMAP server
        else {
            $this->icache[$key] = $this->message_index_direct(
                $mailbox, $this->sort_field, $this->sort_order);
        }
        return $this->icache[$key];
    }
    /**
     * Return sorted array of message IDs (not UIDs) directly from IMAP server.
     * Doesn't use cache and ignores current search settings.
     *
     * @param string $mailbox    Mailbox to get index from
     * @param string $sort_field Sort column
     * @param string $sort_order Sort order [ASC, DESC]
     *
     * @return array Indexed array with message IDs
     */
    function message_index_direct($mailbox, $sort_field = null, $sort_order = null)
    {
        // use message index sort as default sorting
        if (!$this->sort_field) {
        if (!$sort_field) {
            if ($this->skip_deleted) {
                $a_index = $this->_search_index($mailbox, 'ALL');
            } else if ($max = $this->_messagecount($mailbox)) {
                $a_index = range(1, $max);
            }
            if ($a_index !== false && $this->sort_order == 'DESC')
            if ($a_index !== false && $sort_order == 'DESC')
                $a_index = array_reverse($a_index);
            $this->icache[$key] = $a_index;
        }
        // fetch complete message index
        else if ($this->get_capability('SORT') &&
            ($a_index = $this->conn->sort($mailbox,
                $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
                $sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
        ) {
            if ($this->sort_order == 'DESC')
            if ($sort_order == 'DESC')
                $a_index = array_reverse($a_index);
            $this->icache[$key] = $a_index;
        }
        else if ($a_index = $this->conn->fetchHeaderIndex(
            $mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
            if ($this->sort_order=="ASC")
            $mailbox, "1:*", $sort_field, $skip_deleted)) {
            if ($sort_order=="ASC")
                asort($a_index);
            else if ($this->sort_order=="DESC")
            else if ($sort_order=="DESC")
                arsort($a_index);
            $this->icache[$key] = array_keys($a_index);
            $a_index = array_keys($a_index);
        }
        return $this->icache[$key] !== false ? $this->icache[$key] : array();
        return $a_index !== false ? $a_index : array();
    }
@@ -1567,19 +1562,9 @@
        // have stored it in RAM
        if (isset($this->icache[$key]))
            return $this->icache[$key];
/*
        // check local cache
        $cache_key = $mailbox.'.msg';
        $cache_status = $this->check_cache_status($mailbox, $cache_key);
        // cache is OK
        if ($cache_status>0) {
            $a_index = $this->get_message_cache_index($cache_key, $this->sort_field, $this->sort_order);
            return array_keys($a_index);
        }
*/
        // get all threads (default sort order)
        list ($thread_tree) = $this->_fetch_threads($mailbox);
        list ($thread_tree) = $this->fetch_threads($mailbox);
        $this->icache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
@@ -1591,7 +1576,7 @@
     * Return array of threaded messages (all, not only roots)
     *
     * @param string $mailbox     Mailbox to get index from
     * @param array  $thread_tree Threaded messages array (see _fetch_threads())
     * @param array  $thread_tree Threaded messages array (see fetch_threads())
     * @param array  $ids         Message IDs if we know what we need (e.g. search result)
     *                            for better performance
     * @return array Indexed array with message IDs
@@ -1603,7 +1588,7 @@
        if (empty($thread_tree))
            return array();
        $msg_index = $this->_sort_threads($mailbox, $thread_tree, $ids);
        $msg_index = $this->sort_threads($mailbox, $thread_tree, $ids);
        if ($this->sort_order == 'DESC')
            $msg_index = array_reverse($msg_index);
@@ -1619,99 +1604,6 @@
        }
        return $all_ids;
    }
    /**
     * @param string $mailbox Mailbox name
     * @access private
     */
    private function sync_header_index($mailbox)
    {
        $cache_key = $mailbox.'.msg';
        $cache_index = $this->get_message_cache_index($cache_key);
        $chunk_size = 1000;
        // cache is empty, get all messages
        if (is_array($cache_index) && empty($cache_index)) {
            $max = $this->_messagecount($mailbox);
            // syncing a big folder maybe slow
            @set_time_limit(0);
            $start = 1;
            $end   = min($chunk_size, $max);
            while (true) {
                // do this in loop to save memory (1000 msgs ~= 10 MB)
                if ($headers = $this->conn->fetchHeaders($mailbox,
                    "$start:$end", false, false, $this->get_fetch_headers())
                ) {
                    foreach ($headers as $header) {
                        $this->add_message_cache($cache_key, $header->id, $header, NULL, true);
                    }
                }
                if ($end - $start < $chunk_size - 1)
                    break;
                $end   = min($end+$chunk_size, $max);
                $start += $chunk_size;
            }
            return;
        }
        // fetch complete message index
        if (isset($this->icache['folder_index']))
            $a_message_index = &$this->icache['folder_index'];
        else
            $a_message_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", 'UID', $this->skip_deleted);
        if ($a_message_index === false || $cache_index === null)
            return;
        // compare cache index with real index
        foreach ($a_message_index as $id => $uid) {
            // message in cache at correct position
            if ($cache_index[$id] == $uid) {
                unset($cache_index[$id]);
                continue;
            }
            // other message at this position
            if (isset($cache_index[$id])) {
                $for_remove[] = $cache_index[$id];
                unset($cache_index[$id]);
            }
            // message in cache but at wrong position
            if (is_int($key = array_search($uid, $cache_index))) {
                $for_remove[] = $uid;
                unset($cache_index[$key]);
            }
            $for_update[] = $id;
        }
        // remove messages at wrong positions and those deleted that are still in cache_index
        if (!empty($for_remove))
            $cache_index = array_merge($cache_index, $for_remove);
        if (!empty($cache_index))
            $this->remove_message_cache($cache_key, $cache_index);
        // fetch complete headers and add to cache
        if (!empty($for_update)) {
            // syncing a big folder maybe slow
            @set_time_limit(0);
            // To save memory do this in chunks
            $for_update = array_chunk($for_update, $chunk_size);
            foreach ($for_update as $uids) {
                if ($headers = $this->conn->fetchHeaders($mailbox,
                    $uids, false, false, $this->get_fetch_headers())
                ) {
                    foreach ($headers as $header) {
                        $this->add_message_cache($cache_key, $header->id, $header, NULL, true);
                    }
                }
            }
        }
    }
@@ -1750,8 +1642,8 @@
     * @param string $criteria   Search criteria
     * @param string $charset    Charset
     * @param string $sort_field Sorting field
     *
     * @return array   search results as list of message ids
     * @access private
     * @see rcube_imap::search()
     */
    private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
@@ -1773,9 +1665,9 @@
            if ($a_messages !== false) {
                list ($thread_tree, $msg_depth, $has_children) = $a_messages;
                $a_messages = array(
                    'tree'     => $thread_tree,
                    'depth'    => $msg_depth,
                    'children' => $has_children
                    'tree' => $thread_tree,
                    'depth'=> $msg_depth,
                    'children' => $has_children
                );
            }
@@ -1787,7 +1679,7 @@
            $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
            // but I've seen that Courier doesn't support UTF-8)
            // but I've seen Courier with disabled UTF-8 support)
            if ($a_messages === false && $charset && $charset != 'US-ASCII')
                $a_messages = $this->conn->sort($mailbox, $sort_field,
                    $this->convert_criteria($criteria, $charset), false, 'US-ASCII');
@@ -1829,8 +1721,8 @@
     * @param  string  $mailbox Mailbox name to search in
     * @param  string  $str     Search string
     * @param  boolean $ret_uid True if UIDs should be returned
     *
     * @return array   Search results as list of message IDs or UIDs
     * @access public
     */
    function search_once($mailbox='', $str=NULL, $ret_uid=false)
    {
@@ -1884,43 +1776,53 @@
     * @param string $mailbox     Mailbox name
     * @param  array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result)
     * @param  array $ids         Message IDs if we know what we need (e.g. search result)
     *
     * @return array Sorted roots IDs
     * @access private
     */
    private function _sort_threads($mailbox, $thread_tree, $ids=NULL)
    function sort_threads($mailbox, $thread_tree, $ids = null)
    {
        // THREAD=ORDEREDSUBJECT:     sorting by sent date of root message
        // THREAD=REFERENCES:     sorting by sent date of root message
        // THREAD=REFS:         sorting by the most recent date in each thread
        // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
        // THREAD=REFERENCES:     sorting by sent date of root message
        // THREAD=REFS:           sorting by the most recent date in each thread
        // default sorting
        if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
            return array_keys((array)$thread_tree);
          }
        // here we'll implement REFS sorting, for performance reason
        else { // ($sort_field == 'date' && $this->threading != 'REFS')
        }
        // here we'll implement REFS sorting
        else {
            if ($mcache = $this->get_mcache_engine()) {
                $a_index = $mcache->get_index($mailbox, $this->sort_field, 'ASC');
                if (is_array($a_index)) {
                    $a_index = array_keys($a_index);
                    // now we must remove IDs that doesn't exist in $ids
                    if (!empty($ids))
                        $a_index = array_intersect($a_index, $ids);
                }
            }
            // use SORT command
            if ($this->get_capability('SORT') &&
            else if ($this->get_capability('SORT') &&
                ($a_index = $this->conn->sort($mailbox, $this->sort_field,
                    !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
                    !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
            ) {
                // return unsorted tree if we've got no index data
                if (!$a_index)
                    return array_keys((array)$thread_tree);
                // do nothing
            }
            else {
                // fetch specified headers for all messages and sort them
                $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
                    $this->sort_field, $this->skip_deleted);
                    $this->sort_field, $this->skip_deleted);
                // return unsorted tree if we've got no index data
                if (!$a_index)
                    return array_keys((array)$thread_tree);
                asort($a_index); // ASC
                $a_index = array_values($a_index);
                // return unsorted tree if we've got no index data
                if (!empty($a_index)) {
                    asort($a_index); // ASC
                    $a_index = array_values($a_index);
                }
            }
            return $this->_sort_thread_refs($thread_tree, $a_index);
            if (empty($a_index))
                return array_keys((array)$thread_tree);
            return $this->_sort_thread_refs($thread_tree, $a_index);
        }
    }
@@ -1928,10 +1830,10 @@
    /**
     * THREAD=REFS sorting implementation
     *
     * @param  array $tree  Thread tree array (message identifiers as keys)
     * @param  array $index Array of sorted message identifiers
     * @param  array $tree   Thread tree array (message identifiers as keys)
     * @param  array $index  Array of sorted message identifiers
     *
     * @return array   Array of sorted roots messages
     * @access private
     */
    private function _sort_thread_refs($tree, $index)
    {
@@ -1980,7 +1882,7 @@
    {
        if (!empty($this->search_string))
            $this->search_set = $this->search('', $this->search_string, $this->search_charset,
                $this->search_sort_field, $this->search_threads, $this->search_sorted);
                $this->search_sort_field, $this->search_threads, $this->search_sorted);
        return $this->get_search_set();
    }
@@ -2008,32 +1910,25 @@
    /**
     * Return message headers object of a specific message
     *
     * @param int     $id       Message ID
     * @param int     $id       Message sequence ID or UID
     * @param string  $mailbox  Mailbox to read from
     * @param boolean $is_uid   True if $id is the message UID
     * @param boolean $bodystr  True if we need also BODYSTRUCTURE in headers
     * @return object Message headers representation
     * @param bool    $force    True to skip cache
     *
     * @return rcube_mail_header Message headers
     */
    function get_headers($id, $mailbox=null, $is_uid=true, $bodystr=false)
    function get_headers($uid, $mailbox = null, $force = false)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
        }
        $uid = $is_uid ? $id : $this->_id2uid($id, $mailbox);
        // get cached headers
        if ($uid && ($headers = &$this->get_cached_message($mailbox.'.msg', $uid)))
            return $headers;
        $headers = $this->conn->fetchHeader(
            $mailbox, $id, $is_uid, $bodystr, $this->get_fetch_headers());
        // write headers cache
        if ($headers) {
            if ($headers->uid && $headers->id)
                $this->uid_id_map[$mailbox][$headers->uid] = $headers->id;
            $this->add_message_cache($mailbox.'.msg', $headers->id, $headers, NULL, false, true);
        if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
            $headers = $mcache->get_message($mailbox, $uid);
        }
        else {
            $headers = $this->conn->fetchHeader(
                $mailbox, $uid, true, true, $this->get_fetch_headers());
        }
        return $headers;
@@ -2041,33 +1936,43 @@
    /**
     * Fetch body structure from the IMAP server and build
     * Fetch message headers and body structure from the IMAP server and build
     * an object structure similar to the one generated by PEAR::Mail_mimeDecode
     *
     * @param int    $uid           Message UID to fetch
     * @param string $structure_str Message BODYSTRUCTURE string (optional)
     * @return object rcube_message_part Message part tree or False on failure
     * @param int     $uid      Message UID to fetch
     * @param string  $mailbox  Mailbox to read from
     *
     * @return object rcube_mail_header Message data
     */
    function &get_structure($uid, $structure_str='')
    function get_message($uid, $mailbox = null)
    {
        $cache_key = $this->mailbox.'.msg';
        $headers = &$this->get_cached_message($cache_key, $uid);
        // return cached message structure
        if (is_object($headers) && is_object($headers->structure)) {
            return $headers->structure;
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
        }
        if (!$structure_str) {
            $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
        // Check internal cache
        if (!empty($this->icache['message'])) {
            if (($headers = $this->icache['message']) && $headers->uid == $uid) {
                return $headers;
            }
        }
        $structure = rcube_mime_struct::parseStructure($structure_str);
        $struct = false;
        // parse structure and add headers
        if (!empty($structure)) {
            $headers = $this->get_headers($uid);
            $this->_msg_id = $headers->id;
        $headers = $this->get_headers($uid, $mailbox);
        // structure might be cached
        if (!empty($headers->structure))
            return $headers;
        $this->_msg_uid = $uid;
        if (empty($headers->bodystructure)) {
            $headers->bodystructure = $this->conn->getStructure($mailbox, $uid, true);
        }
        $structure = $headers->bodystructure;
        if (empty($structure))
            return $headers;
        // set message charset from message headers
        if ($headers->charset)
@@ -2090,7 +1995,7 @@
                $structure[1] = $m[2];
            }
            else
                return false;
                return $headers;
        }
        $struct = &$this->_structure_part($structure, 0, '', $headers);
@@ -2103,13 +2008,9 @@
            list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
        }
        // write structure to cache
        if ($this->messages_caching)
            $this->add_message_cache($cache_key, $this->_msg_id, $headers, $struct,
                $this->icache['message.id'][$uid], true);
        }
        $headers->structure = $struct;
        return $struct;
        return $this->icache['message'] = $headers;
    }
@@ -2174,7 +2075,7 @@
            // headers for parts on all levels
            if ($mime_part_headers) {
                $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
                    $this->_msg_id, $mime_part_headers);
                    $this->_msg_uid, $mime_part_headers);
            }
            $struct->parts = array();
@@ -2276,7 +2177,7 @@
        if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
            if (empty($mime_headers)) {
                $mime_headers = $this->conn->fetchPartHeader(
                    $this->mailbox, $this->_msg_id, false, $struct->mime_id);
                    $this->mailbox, $this->_msg_uid, true, $struct->mime_id);
            }
            if (is_string($mime_headers))
@@ -2339,7 +2240,7 @@
            if ($i<2) {
                if (!$headers) {
                    $headers = $this->conn->fetchPartHeader(
                        $this->mailbox, $this->_msg_id, false, $part->mime_id);
                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
                }
                $filename_mime = '';
                $i = 0;
@@ -2358,7 +2259,7 @@
            if ($i<2) {
                if (!$headers) {
                    $headers = $this->conn->fetchPartHeader(
                            $this->mailbox, $this->_msg_id, false, $part->mime_id);
                            $this->mailbox, $this->_msg_uid, true, $part->mime_id);
                }
                $filename_encoded = '';
                $i = 0; $matches = array();
@@ -2377,7 +2278,7 @@
            if ($i<2) {
                if (!$headers) {
                    $headers = $this->conn->fetchPartHeader(
                        $this->mailbox, $this->_msg_id, false, $part->mime_id);
                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
                }
                $filename_mime = '';
                $i = 0; $matches = array();
@@ -2396,7 +2297,7 @@
            if ($i<2) {
                if (!$headers) {
                    $headers = $this->conn->fetchPartHeader(
                        $this->mailbox, $this->_msg_id, false, $part->mime_id);
                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
                }
                $filename_encoded = '';
                $i = 0; $matches = array();
@@ -2465,17 +2366,12 @@
    {
        // get part encoding if not provided
        if (!is_object($o_part)) {
            $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
            $structure = new rcube_mime_struct();
            // error or message not found
            if (!$structure->loadStructure($structure_str)) {
                return false;
            }
            $structure = $this->conn->getStructure($this->mailbox, $uid, true);
            $o_part = new rcube_message_part;
            $o_part->ctype_primary = strtolower($structure->getPartType($part));
            $o_part->encoding      = strtolower($structure->getPartEncoding($part));
            $o_part->charset       = $structure->getPartCharset($part);
            $o_part->ctype_primary = strtolower(rcube_imap_generic::getStructurePartType($structure, $part));
            $o_part->encoding      = strtolower(rcube_imap_generic::getStructurePartEncoding($structure, $part));
            $o_part->charset       = rcube_imap_generic::getStructurePartCharset($structure, $part);
        }
        // TODO: Add caching for message parts
@@ -2584,12 +2480,12 @@
        if ($result) {
            // reload message headers if cached
            if ($this->messages_caching && !$skip_cache) {
                $cache_key = $mailbox.'.msg';
                if ($all_mode)
                    $this->clear_message_cache($cache_key);
                else
                    $this->remove_message_cache($cache_key, explode(',', $uids));
            // @TODO: update flags instead removing from cache
            if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
                $status = strpos($flag, 'UN') !== 0;
                $mflag  = preg_replace('/^UN/', '', $flag);
                $mcache->change_flag($mailbox, $all_mode ? null : explode(',', $uids),
                    $mflag, $status);
            }
            // clear cached counters
@@ -2721,19 +2617,18 @@
                if ($this->search_threads || $all_mode)
                    $this->refresh_search();
                else {
                    $uids = explode(',', $uids);
                    foreach ($uids as $uid)
                        $a_mids[] = $this->_uid2id($uid, $from_mbox);
                    $a_uids = explode(',', $uids);
                    foreach ($a_uids as $uid)
                        $a_mids[] = $this->uid2id($uid, $from_mbox);
                    $this->search_set = array_diff($this->search_set, $a_mids);
                }
                unset($a_mids);
                unset($a_uids);
            }
            // update cached message headers
            $cache_key = $from_mbox.'.msg';
            if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
                // clear cache from the lowest index on
                $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
            }
            // remove cached messages
            // @TODO: do cache update instead of clearing it
            $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
        }
        return $moved;
@@ -2818,19 +2713,17 @@
                if ($this->search_threads || $all_mode)
                    $this->refresh_search();
                else {
                    $uids = explode(',', $uids);
                    foreach ($uids as $uid)
                        $a_mids[] = $this->_uid2id($uid, $mailbox);
                    $a_uids = explode(',', $uids);
                    foreach ($a_uids as $uid)
                        $a_mids[] = $this->uid2id($uid, $mailbox);
                    $this->search_set = array_diff($this->search_set, $a_mids);
                    unset($a_uids);
                    unset($a_mids);
                }
            }
            // remove deleted messages from cache
            $cache_key = $mailbox.'.msg';
            if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
                // clear cache from the lowest index on
                $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
            }
            // remove cached messages
            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
        }
        return $deleted;
@@ -2855,9 +2748,9 @@
            $cleared = $this->conn->clearFolder($mailbox);
        }
        // make sure the message count cache is cleared as well
        // make sure the cache is cleared as well
        if ($cleared) {
            $this->clear_message_cache($mailbox.'.msg');
            $this->clear_message_cache($mailbox);
            $a_mailbox_cache = $this->get_cache('messagecount');
            unset($a_mailbox_cache[$mailbox]);
            $this->update_cache('messagecount', $a_mailbox_cache);
@@ -2898,9 +2791,9 @@
    private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
    {
        if ($uids && $this->get_capability('UIDPLUS'))
            $a_uids = is_array($uids) ? join(',', $uids) : $uids;
            list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
        else
            $a_uids = NULL;
            $uids = null;
        // force mailbox selection and check if mailbox is writeable
        // to prevent a situation when CLOSE is executed on closed
@@ -2915,13 +2808,13 @@
        }
        // CLOSE(+SELECT) should be faster than EXPUNGE
        if (empty($a_uids) || $a_uids == '1:*')
        if (empty($uids) || $all_mode)
            $result = $this->conn->close();
        else
            $result = $this->conn->expunge($mailbox, $a_uids);
            $result = $this->conn->expunge($mailbox, $uids);
        if ($result && $clear_cache) {
            $this->clear_message_cache($mailbox.'.msg');
            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
            $this->_clear_messagecount($mailbox);
        }
@@ -2986,7 +2879,7 @@
            $mailbox = $this->mailbox;
        }
        return $this->_uid2id($uid, $mailbox);
        return $this->uid2id($uid, $mailbox);
    }
@@ -3004,7 +2897,7 @@
            $mailbox = $this->mailbox;
        }
        return $this->_id2uid($id, $mailbox);
        return $this->id2uid($id, $mailbox);
    }
@@ -3302,11 +3195,14 @@
                    $this->conn->unsubscribe($c_subscribed);
                    $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
                        $new_name, $c_subscribed));
                    // clear cache
                    $this->clear_message_cache($c_subscribed);
                }
            }
            // clear cache
            $this->clear_message_cache($mailbox.'.msg');
            $this->clear_message_cache($mailbox);
            $this->clear_cache('mailboxes', true);
        }
@@ -3342,13 +3238,13 @@
                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
                    $this->conn->unsubscribe($c_mbox);
                    if ($this->conn->deleteFolder($c_mbox)) {
                        $this->clear_message_cache($c_mbox.'.msg');
                        $this->clear_message_cache($c_mbox);
                    }
                }
            }
            // clear mailbox-related cache
            $this->clear_message_cache($mailbox.'.msg');
            $this->clear_message_cache($mailbox);
            $this->clear_cache('mailboxes', true);
        }
@@ -3504,6 +3400,36 @@
        $opts = $this->conn->data['LIST'][$mailbox];
        return is_array($opts) ? $opts : array();
    }
    /**
     * Gets connection (and current mailbox) data: UIDVALIDITY, EXISTS, RECENT,
     * PERMANENTFLAGS, UIDNEXT, UNSEEN
     *
     * @param string $mailbox Folder name
     *
     * @return array Data
     */
    function mailbox_data($mailbox)
    {
        if (!strlen($mailbox))
            $mailbox = $this->mailbox !== null ? $this->mailbox : 'INBOX';
        if ($this->conn->selected != $mailbox) {
            if ($this->conn->select($mailbox))
                $this->mailbox = $mailbox;
        }
        $data = $this->conn->data;
        // add (E)SEARCH result for ALL UNDELETED query
        if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx'][0] == $mailbox) {
            $data['ALL_UNDELETED']   = $this->icache['undeleted_idx'][1];
            $data['COUNT_UNDELETED'] = $this->icache['undeleted_idx'][2];
        }
        return $data;
    }
@@ -3848,7 +3774,7 @@
        else {
            if ($this->cache)
                $this->cache->close();
            $this->cache = null;
            $this->cache   = null;
            $this->caching = false;
        }
    }
@@ -3918,418 +3844,49 @@
     * Enable or disable messages caching
     *
     * @param boolean $set Flag
     * @access public
     */
    function set_messages_caching($set)
    {
        $rcmail = rcmail::get_instance();
        if ($set && ($dbh = $rcmail->get_dbh())) {
            $this->db = $dbh;
        if ($set) {
            $this->messages_caching = true;
        }
        else {
            if ($this->mcache)
                $this->mcache->close();
            $this->mcache = null;
            $this->messages_caching = false;
        }
    }
    /**
     * Checks if the cache is up-to-date
     * Getter for messages cache object
     */
    private function get_mcache_engine()
    {
        if ($this->messages_caching && !$this->mcache) {
            $rcmail = rcmail::get_instance();
            if ($dbh = $rcmail->get_dbh()) {
                $this->mcache = new rcube_imap_cache(
                    $dbh, $this, $rcmail->user->ID, $this->skip_deleted);
            }
        }
        return $this->mcache;
    }
    /**
     * Clears the messages cache.
     *
     * @param string $mailbox   Mailbox name
     * @param string $cache_key Internal cache key
     * @return int   Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
     * @param string $mailbox Folder name
     * @param array  $uids    Optional message UIDs to remove from cache
     */
    private function check_cache_status($mailbox, $cache_key)
    function clear_message_cache($mailbox = null, $uids = null)
    {
        if (!$this->messages_caching)
            return -3;
        $cache_index = $this->get_message_cache_index($cache_key);
        $msg_count = $this->_messagecount($mailbox);
        $cache_count = count($cache_index);
        // empty mailbox
        if (!$msg_count) {
            return $cache_count ? -2 : 1;
        if ($mcache = $this->get_mcache_engine()) {
            $mcache->clear($mailbox, $uids);
        }
        if ($cache_count == $msg_count) {
            if ($this->skip_deleted) {
                if (!empty($this->icache['all_undeleted_idx'])) {
                    $uids = rcube_imap_generic::uncompressMessageSet($this->icache['all_undeleted_idx']);
                    $uids = array_flip($uids);
                    foreach ($cache_index as $uid) {
                        unset($uids[$uid]);
                    }
                }
                else {
                    // get all undeleted messages excluding cached UIDs
                    $uids = $this->search_once($mailbox, 'ALL UNDELETED NOT UID '.
                        rcube_imap_generic::compressMessageSet($cache_index));
                }
                if (empty($uids)) {
                    return 1;
                }
            } else {
                // get UID of the message with highest index
                $uid = $this->_id2uid($msg_count, $mailbox);
                $cache_uid = array_pop($cache_index);
                // uids of highest message matches -> cache seems OK
                if ($cache_uid == $uid) {
                    return 1;
                }
            }
            // cache is dirty
            return -1;
        }
        // if cache count differs less than 10% report as dirty
        return (abs($msg_count - $cache_count) < $msg_count/10) ? -1 : -2;
    }
    /**
     * @param string $key Cache key
     * @param string $from
     * @param string $to
     * @param string $sort_field
     * @param string $sort_order
     * @access private
     */
    private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
    {
        if (!$this->messages_caching)
            return NULL;
        // use idx sort as default sorting
        if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
            $sort_field = 'idx';
        }
        $result = array();
        $sql_result = $this->db->limitquery(
                "SELECT idx, uid, headers".
                " FROM ".get_table_name('messages').
                " WHERE user_id=?".
                " AND cache_key=?".
                " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
                $from,
                $to - $from,
                $_SESSION['user_id'],
                $key);
        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $uid = intval($sql_arr['uid']);
            $result[$uid] = $this->db->decode(unserialize($sql_arr['headers']));
            // featch headers if unserialize failed
            if (empty($result[$uid]))
                $result[$uid] = $this->conn->fetchHeader(
                    preg_replace('/.msg$/', '', $key), $uid, true, false, $this->get_fetch_headers());
        }
        return $result;
    }
    /**
     * @param string $key Cache key
     * @param int    $uid Message UID
     * @return mixed
     * @access private
     */
    private function &get_cached_message($key, $uid)
    {
        $internal_key = 'message';
        if ($this->messages_caching && !isset($this->icache[$internal_key][$uid])) {
            $sql_result = $this->db->query(
                "SELECT idx, headers, structure, message_id".
                " FROM ".get_table_name('messages').
                " WHERE user_id=?".
                " AND cache_key=?".
                " AND uid=?",
                $_SESSION['user_id'],
                $key,
                $uid);
            if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                $this->icache['message.id'][$uid] = intval($sql_arr['message_id']);
                $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = intval($sql_arr['idx']);
                $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
                if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
                    $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
            }
        }
        return $this->icache[$internal_key][$uid];
    }
    /**
     * @param string  $key        Cache key
     * @param string  $sort_field Sorting column
     * @param string  $sort_order Sorting order
     * @return array Messages index
     * @access private
     */
    private function get_message_cache_index($key, $sort_field='idx', $sort_order='ASC')
    {
        if (!$this->messages_caching || empty($key))
            return NULL;
        // use idx sort as default
        if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
            $sort_field = 'idx';
        if (array_key_exists('index', $this->icache)
            && $this->icache['index']['key'] == $key
            && $this->icache['index']['sort_field'] == $sort_field
        ) {
            if ($this->icache['index']['sort_order'] == $sort_order)
                return $this->icache['index']['result'];
            else
                return array_reverse($this->icache['index']['result'], true);
        }
        $this->icache['index'] = array(
            'result'     => array(),
            'key'        => $key,
            'sort_field' => $sort_field,
            'sort_order' => $sort_order,
        );
        $sql_result = $this->db->query(
            "SELECT idx, uid".
            " FROM ".get_table_name('messages').
            " WHERE user_id=?".
            " AND cache_key=?".
            " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
            $_SESSION['user_id'],
            $key);
        while ($sql_arr = $this->db->fetch_assoc($sql_result))
            $this->icache['index']['result'][$sql_arr['idx']] = intval($sql_arr['uid']);
        return $this->icache['index']['result'];
    }
    /**
     * @access private
     */
    private function add_message_cache($key, $index, $headers, $struct=null, $force=false, $internal_cache=false)
    {
        if (empty($key) || !is_object($headers) || empty($headers->uid))
            return;
        // add to internal (fast) cache
        if ($internal_cache) {
            $this->icache['message'][$headers->uid] = clone $headers;
            $this->icache['message'][$headers->uid]->structure = $struct;
        }
        // no further caching
        if (!$this->messages_caching)
            return;
        // known message id
        if (is_int($force) && $force > 0) {
            $message_id = $force;
        }
        // check for an existing record (probably headers are cached but structure not)
        else if (!$force) {
            $sql_result = $this->db->query(
                "SELECT message_id".
                " FROM ".get_table_name('messages').
                " WHERE user_id=?".
                " AND cache_key=?".
                " AND uid=?",
                $_SESSION['user_id'],
                $key,
                $headers->uid);
            if ($sql_arr = $this->db->fetch_assoc($sql_result))
                $message_id = $sql_arr['message_id'];
        }
        // update cache record
        if ($message_id) {
            $this->db->query(
                "UPDATE ".get_table_name('messages').
                " SET idx=?, headers=?, structure=?".
                " WHERE message_id=?",
                $index,
                serialize($this->db->encode(clone $headers)),
                is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
                $message_id
            );
        }
        else { // insert new record
            $this->db->query(
                "INSERT INTO ".get_table_name('messages').
                " (user_id, del, cache_key, created, idx, uid, subject, ".
                $this->db->quoteIdentifier('from').", ".
                $this->db->quoteIdentifier('to').", ".
                "cc, date, size, headers, structure)".
                " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
                $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
                $_SESSION['user_id'],
                $key,
                $index,
                $headers->uid,
                (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
                (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
                (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
                (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
                (int)$headers->size,
                serialize($this->db->encode(clone $headers)),
                is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
            );
        }
        unset($this->icache['index']);
    }
    /**
     * @access private
     */
    private function remove_message_cache($key, $ids, $idx=false)
    {
        if (!$this->messages_caching)
            return;
        $this->db->query(
            "DELETE FROM ".get_table_name('messages').
            " WHERE user_id=?".
            " AND cache_key=?".
            " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
            $_SESSION['user_id'],
            $key);
        unset($this->icache['index']);
    }
    /**
     * @param string $key         Cache key
     * @param int    $start_index Start index
     * @access private
     */
    private function clear_message_cache($key, $start_index=1)
    {
        if (!$this->messages_caching)
            return;
        $this->db->query(
            "DELETE FROM ".get_table_name('messages').
            " WHERE user_id=?".
            " AND cache_key=?".
            " AND idx>=?",
            $_SESSION['user_id'], $key, $start_index);
        unset($this->icache['index']);
    }
    /**
     * @access private
     */
    private function get_message_cache_index_min($key, $uids=NULL)
    {
        if (!$this->messages_caching)
            return;
        if (!empty($uids) && !is_array($uids)) {
            if ($uids == '*' || $uids == '1:*')
                $uids = NULL;
            else
                $uids = explode(',', $uids);
        }
        $sql_result = $this->db->query(
            "SELECT MIN(idx) AS minidx".
            " FROM ".get_table_name('messages').
            " WHERE  user_id=?".
            " AND    cache_key=?"
            .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
            $_SESSION['user_id'],
            $key);
        if ($sql_arr = $this->db->fetch_assoc($sql_result))
            return $sql_arr['minidx'];
        else
            return 0;
    }
    /**
     * @param string $key Cache key
     * @param int    $id  Message (sequence) ID
     * @return int Message UID
     * @access private
     */
    private function get_cache_id2uid($key, $id)
    {
        if (!$this->messages_caching)
            return null;
        if (array_key_exists('index', $this->icache)
            && $this->icache['index']['key'] == $key
        ) {
            return $this->icache['index']['result'][$id];
        }
        $sql_result = $this->db->query(
            "SELECT uid".
            " FROM ".get_table_name('messages').
            " WHERE user_id=?".
            " AND cache_key=?".
            " AND idx=?",
            $_SESSION['user_id'], $key, $id);
        if ($sql_arr = $this->db->fetch_assoc($sql_result))
            return intval($sql_arr['uid']);
        return null;
    }
    /**
     * @param string $key Cache key
     * @param int    $uid Message UID
     * @return int Message (sequence) ID
     * @access private
     */
    private function get_cache_uid2id($key, $uid)
    {
        if (!$this->messages_caching)
            return null;
        if (array_key_exists('index', $this->icache)
            && $this->icache['index']['key'] == $key
        ) {
            return array_search($uid, $this->icache['index']['result']);
        }
        $sql_result = $this->db->query(
            "SELECT idx".
            " FROM ".get_table_name('messages').
            " WHERE user_id=?".
            " AND cache_key=?".
            " AND uid=?",
            $_SESSION['user_id'], $key, $uid);
        if ($sql_arr = $this->db->fetch_assoc($sql_result))
            return intval($sql_arr['idx']);
        return null;
    }
    /* --------------------------------
@@ -4627,36 +4184,46 @@
    /**
     * @param int    $uid     Message UID
     * @param string $mailbox Mailbox name
     * Finds message sequence ID for specified UID
     *
     * @param int    $uid      Message UID
     * @param string $mailbox  Mailbox name
     * @param bool   $force    True to skip cache
     *
     * @return int Message (sequence) ID
     * @access private
     */
    private function _uid2id($uid, $mailbox=NULL)
    function uid2id($uid, $mailbox = null, $force = false)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
        }
        if (!isset($this->uid_id_map[$mailbox][$uid])) {
            if (!($id = $this->get_cache_uid2id($mailbox.'.msg', $uid)))
                $id = $this->conn->UID2ID($mailbox, $uid);
            $this->uid_id_map[$mailbox][$uid] = $id;
        if (!empty($this->uid_id_map[$mailbox][$uid])) {
            return $this->uid_id_map[$mailbox][$uid];
        }
        return $this->uid_id_map[$mailbox][$uid];
        if (!$force && ($mcache = $this->get_mcache_engine()))
            $id = $mcache->uid2id($mailbox, $uid);
        if (empty($id))
            $id = $this->conn->UID2ID($mailbox, $uid);
        $this->uid_id_map[$mailbox][$uid] = $id;
        return $id;
    }
    /**
     * @param int    $id      Message (sequence) ID
     * @param string $mailbox Mailbox name
     * Find UID of the specified message sequence ID
     *
     * @param int    $id       Message (sequence) ID
     * @param string $mailbox  Mailbox name
     * @param bool   $force    True to skip cache
     *
     * @return int Message UID
     * @access private
     */
    private function _id2uid($id, $mailbox=null)
    function id2uid($id, $mailbox = null, $force = false)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
@@ -4666,9 +4233,11 @@
            return $uid;
        }
        if (!($uid = $this->get_cache_id2uid($mailbox.'.msg', $id))) {
        if (!$force && ($mcache = $this->get_mcache_engine()))
            $uid = $mcache->id2uid($mailbox, $id);
        if (empty($uid))
            $uid = $this->conn->ID2UID($mailbox, $id);
        }
        $this->uid_id_map[$mailbox][$uid] = $id;
@@ -4954,16 +4523,24 @@
 */
class rcube_header_sorter
{
    var $sequence_numbers = array();
    private $seqs = array();
    private $uids = array();
    /**
     * Set the predetermined sort order.
     *
     * @param array $seqnums Numerically indexed array of IMAP message sequence numbers
     * @param array $index  Numerically indexed array of IMAP ID or UIDs
     * @param bool  $is_uid Set to true if $index contains UIDs
     */
    function set_sequence_numbers($seqnums)
    function set_index($index, $is_uid = false)
    {
        $this->sequence_numbers = array_flip($seqnums);
        $index = array_flip($index);
        if ($is_uid)
            $this->uids = $index;
        else
            $this->seqs = $index;
    }
    /**
@@ -4973,14 +4550,10 @@
     */
    function sort_headers(&$headers)
    {
        /*
        * uksort would work if the keys were the sequence number, but unfortunately
        * the keys are the UIDs.  We'll use uasort instead and dereference the value
        * to get the sequence number (in the "id" field).
        *
        * uksort($headers, array($this, "compare_seqnums"));
        */
        uasort($headers, array($this, "compare_seqnums"));
        if (!empty($this->uids))
            uksort($headers, array($this, "compare_uids"));
        else
            uasort($headers, array($this, "compare_seqnums"));
    }
    /**
@@ -4996,8 +4569,24 @@
        $seqb = $b->id;
        // then find each sequence number in my ordered list
        $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
        $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
        $posa = isset($this->seqs[$seqa]) ? intval($this->seqs[$seqa]) : -1;
        $posb = isset($this->seqs[$seqb]) ? intval($this->seqs[$seqb]) : -1;
        // return the relative position as the comparison value
        return $posa - $posb;
    }
    /**
     * Sort method called by uksort()
     *
     * @param int $a Array key (UID)
     * @param int $b Array key (UID)
     */
    function compare_uids($a, $b)
    {
        // then find each sequence number in my ordered list
        $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
        $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
        // return the relative position as the comparison value
        return $posa - $posb;
program/include/rcube_imap_cache.php
New file
@@ -0,0 +1,907 @@
<?php
/*
 +-----------------------------------------------------------------------+
 | program/include/rcube_imap_cache.php                                  |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
 | Licensed under the GNU GPL                                            |
 |                                                                       |
 | PURPOSE:                                                              |
 |   Caching of IMAP folder contents (messages and index)                |
 |                                                                       |
 +-----------------------------------------------------------------------+
 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
 | Author: Aleksander Machniak <alec@alec.pl>                            |
 +-----------------------------------------------------------------------+
 $Id$
*/
/**
 * Interface class for accessing Roundcube messages cache
 *
 * @package    Cache
 * @author     Thomas Bruederli <roundcube@gmail.com>
 * @author     Aleksander Machniak <alec@alec.pl>
 * @version    1.0
 */
class rcube_imap_cache
{
    /**
     * Instance of rcube_imap
     *
     * @var rcube_imap
     */
    private $imap;
    /**
     * Instance of rcube_mdb2
     *
     * @var rcube_mdb2
     */
    private $db;
    /**
     * User ID
     *
     * @var int
     */
    private $userid;
    /**
     * Internal (in-memory) cache
     *
     * @var array
     */
    private $icache = array();
    private $skip_deleted = false;
    public $flag_fields = array('seen', 'deleted', 'answered', 'forwarded', 'flagged', 'mdnsent');
    /**
     * Object constructor.
     */
    function __construct($db, $imap, $userid, $skip_deleted)
    {
        $this->db           = $db;
        $this->imap         = $imap;
        $this->userid       = (int)$userid;
        $this->skip_deleted = $skip_deleted;
    }
    /**
     * Cleanup actions (on shutdown).
     */
    public function close()
    {
        $this->save_icache();
        $this->icache = null;
    }
    /**
     * Return (sorted) messages index.
     * If index doesn't exist or is invalid, will be updated.
     *
     * @param string  $mailbox     Folder name
     * @param string  $sort_field  Sorting column
     * @param string  $sort_order  Sorting order (ASC|DESC)
     * @param bool    $exiting     Skip index initialization if it doesn't exist in DB
     *
     * @return array Messages index
     */
    function get_index($mailbox, $sort_field = null, $sort_order = null, $existing = false)
    {
        if (empty($this->icache[$mailbox]))
            $this->icache[$mailbox] = array();
        $sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC';
        // Seek in internal cache
        if (array_key_exists('index', $this->icache[$mailbox])
            && ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field)
        ) {
            if ($this->icache[$mailbox]['index']['sort_order'] == $sort_order)
                return $this->icache[$mailbox]['index']['result'];
            else
                return array_reverse($this->icache[$mailbox]['index']['result'], true);
        }
        // Get index from DB (if DB wasn't already queried)
        if (empty($this->icache[$mailbox]['index_queried'])) {
            $index = $this->get_index_row($mailbox);
            // set the flag that DB was already queried for index
            // this way we'll be able to skip one SELECT, when
            // get_index() is called more than once
            $this->icache[$mailbox]['index_queried'] = true;
        }
        $data  = null;
        // @TODO: Think about skipping validation checks.
        // If we could check only every 10 minutes, we would be able to skip
        // expensive checks, mailbox selection or even IMAP connection, this would require
        // additional logic to force cache invalidation in some cases
        // and many rcube_imap changes to connect when needed
        // Entry exist, check cache status
        if (!empty($index)) {
            $exists = true;
            if ($sort_field == 'ANY') {
                $sort_field = $index['sort_field'];
            }
            if ($sort_field != $index['sort_field']) {
                $is_valid = false;
            }
            else {
                $is_valid = $this->validate($mailbox, $index, $exists);
            }
            if ($is_valid) {
                // build index, assign sequence IDs to unique IDs
                $data = array_combine($index['seq'], $index['uid']);
                // revert the order if needed
                if ($index['sort_order'] != $sort_order)
                    $data = array_reverse($data, true);
            }
        }
        else {
            // Got it in internal cache, so the row already exist
            $exists = array_key_exists('index', $this->icache[$mailbox]);
            if ($existing) {
                return null;
            }
            else if ($sort_field == 'ANY') {
                $sort_field = '';
            }
        }
        // Index not found, not valid or sort field changed, get index from IMAP server
        if ($data === null) {
            // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
            $mbox_data = $this->imap->mailbox_data($mailbox);
            $data      = array();
            // Prevent infinite loop.
            // It happens when rcube_imap::message_index_direct() is called.
            // There id2uid() is called which will again call get_index() and so on.
            if (!$sort_field && !$this->skip_deleted)
                $this->icache['pending_index_update'] = true;
            if ($mbox_data['EXISTS']) {
                // fetch sorted sequence numbers
                $data_seq = $this->imap->message_index_direct($mailbox, $sort_field, $sort_order);
                // fetch UIDs
                if (!empty($data_seq)) {
                    // Seek in internal cache
                    if (array_key_exists('index', (array)$this->icache[$mailbox]))
                        $data_uid = $this->icache[$mailbox]['index']['result'];
                    else
                        $data_uid = $this->imap->conn->fetchUIDs($mailbox, $data_seq);
                    // build index
                    if (!empty($data_uid)) {
                        foreach ($data_seq as $seq)
                            if ($uid = $data_uid[$seq])
                                $data[$seq] = $uid;
                    }
                }
            }
            // Reset internal flags
            $this->icache['pending_index_update'] = false;
            // insert/update
            $this->add_index_row($mailbox, $sort_field, $sort_order, $data, $mbox_data, $exists);
        }
        $this->icache[$mailbox]['index'] = array(
            'result'     => $data,
            'sort_field' => $sort_field,
            'sort_order' => $sort_order,
        );
        return $data;
    }
    /**
     * Return messages thread.
     * If threaded index doesn't exist or is invalid, will be updated.
     *
     * @param string  $mailbox     Folder name
     * @param string  $sort_field  Sorting column
     * @param string  $sort_order  Sorting order (ASC|DESC)
     *
     * @return array Messages threaded index
     */
    function get_thread($mailbox)
    {
        if (empty($this->icache[$mailbox]))
            $this->icache[$mailbox] = array();
        // Seek in internal cache
        if (array_key_exists('thread', $this->icache[$mailbox])) {
            return array(
                $this->icache[$mailbox]['thread']['tree'],
                $this->icache[$mailbox]['thread']['depth'],
                $this->icache[$mailbox]['thread']['children'],
            );
        }
        // Get index from DB
        $index = $this->get_thread_row($mailbox);
        $data  = null;
        // Entry exist, check cache status
        if (!empty($index)) {
            $exists   = true;
            $is_valid = $this->validate($mailbox, $index, $exists);
            if (!$is_valid) {
                $index = null;
            }
        }
        // Index not found or not valid, get index from IMAP server
        if ($index === null) {
            // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
            $mbox_data = $this->imap->mailbox_data($mailbox);
            if ($mbox_data['EXISTS']) {
                // get all threads (default sort order)
                list ($thread_tree, $msg_depth, $has_children) = $this->imap->fetch_threads($mailbox, true);
            }
            $index = array(
                'tree'     => !empty($thread_tree) ? $thread_tree : array(),
                'depth'    => !empty($msg_depth) ? $msg_depth : array(),
                'children' => !empty($has_children) ? $has_children : array(),
            );
            // insert/update
            $this->add_thread_row($mailbox, $index, $mbox_data, $exists);
        }
        $this->icache[$mailbox]['thread'] = $index;
        return array($index['tree'], $index['depth'], $index['children']);
    }
    /**
     * Returns list of messages (headers). See rcube_imap::fetch_headers().
     *
     * @param string $mailbox  Folder name
     * @param array  $msgs     Message sequence numbers
     * @param bool   $is_uid   True if $msgs contains message UIDs
     *
     * @return array The list of messages (rcube_mail_header) indexed by UID
     */
    function get_messages($mailbox, $msgs = array(), $is_uid = true)
    {
        if (empty($msgs)) {
            return array();
        }
        // Convert IDs to UIDs
        // @TODO: it would be nice if we could work with UIDs only
        // then, e.g. when fetching search result, index would be not needed
        if (!$is_uid) {
            $index = $this->get_index($mailbox, 'ANY');
            foreach ($msgs as $idx => $msgid)
                if ($uid = $index[$msgid])
                    $msgs[$idx] = $uid;
        }
        $flag_fields = implode(', ', array_map(array($this->db, 'quoteIdentifier'), $this->flag_fields));
        // Fetch messages from cache
        $sql_result = $this->db->query(
            "SELECT uid, data, ".$flag_fields
            ." FROM ".get_table_name('cache_messages')
            ." WHERE user_id = ?"
                ." AND mailbox = ?"
                ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
            $this->userid, $mailbox);
        $msgs   = array_flip($msgs);
        $result = array();
        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $uid          = intval($sql_arr['uid']);
            $result[$uid] = $this->build_message($sql_arr);
            // save memory, we don't need a body here
            $result[$uid]->body = null;
//@TODO: update message ID according to index data?
            if (!empty($result[$uid])) {
                unset($msgs[$uid]);
            }
        }
        // Fetch not found messages from IMAP server
        if (!empty($msgs)) {
            $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), true, true);
            // Insert to DB and add to result list
            if (!empty($messages)) {
                foreach ($messages as $msg) {
                    $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
                    $result[$msg->uid] = $msg;
                }
            }
        }
        return $result;
    }
    /**
     * Returns message data.
     *
     * @param string $mailbox  Folder name
     * @param int    $uid      Message UID
     *
     * @return rcube_mail_header Message data
     */
    function get_message($mailbox, $uid)
    {
        // Check internal cache
        if (($message = $this->icache['message'])
            && $message['mailbox'] == $mailbox && $message['object']->uid == $uid
        ) {
            return $this->icache['message']['object'];
        }
        $flag_fields = implode(', ', array_map(array($this->db, 'quoteIdentifier'), $this->flag_fields));
        $sql_result = $this->db->query(
            "SELECT data, ".$flag_fields
            ." FROM ".get_table_name('cache_messages')
            ." WHERE user_id = ?"
                ." AND mailbox = ?"
                ." AND uid = ?",
                $this->userid, $mailbox, (int)$uid);
        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $message = $this->build_message($sql_arr);
            $found   = true;
//@TODO: update message ID according to index data?
        }
        // Get the message from IMAP server
        if (empty($message)) {
            $message = $this->imap->get_headers($uid, $mailbox, true);
            // cache will be updated in close(), see below
        }
        // Save the message in internal cache, will be written to DB in close()
        // Common scenario: user opens unseen message
        // - get message (SELECT)
        // - set message headers/structure (INSERT or UPDATE)
        // - set \Seen flag (UPDATE)
        // This way we can skip one UPDATE
        if (!empty($message)) {
            // Save current message from internal cache
            $this->save_icache();
            $this->icache['message'] = array(
                'object'  => $message,
                'mailbox' => $mailbox,
                'exists'  => $found,
                'md5sum'  => md5(serialize($message)),
            );
        }
        return $message;
    }
    /**
     * Saves the message in cache.
     *
     * @param string            $mailbox  Folder name
     * @param rcube_mail_header $message  Message data
     * @param bool              $force    Skips message in-cache existance check
     */
    function add_message($mailbox, $message, $force = false)
    {
        if (!is_object($message) || empty($message->uid))
            return;
        $msg = serialize($this->db->encode(clone $message));
        $flag_fields = array_map(array($this->db, 'quoteIdentifier'), $this->flag_fields);
        $flag_values = array();
        foreach ($this->flag_fields as $flag)
            $flag_values[] = (int) $message->$flag;
        // update cache record (even if it exists, the update
        // here will work as select, assume row exist if affected_rows=0)
        if (!$force) {
            foreach ($flag_fields as $key => $val)
                $flag_data[] = $val . " = " . $flag_values[$key];
            $res = $this->db->query(
                "UPDATE ".get_table_name('cache_messages')
                ." SET data = ?, changed = ".$this->db->now()
                .", " . implode(', ', $flag_data)
                ." WHERE user_id = ?"
                    ." AND mailbox = ?"
                    ." AND uid = ?",
                $msg, $this->userid, $mailbox, (int) $message->uid);
            if ($this->db->affected_rows())
                return;
        }
        // insert new record
        $this->db->query(
            "INSERT INTO ".get_table_name('cache_messages')
            ." (user_id, mailbox, uid, changed, data, " . implode(', ', $flag_fields) . ")"
            ." VALUES (?, ?, ?, ".$this->db->now().", ?, " . implode(', ', $flag_values) . ")",
            $this->userid, $mailbox, (int) $message->uid, $msg);
    }
    /**
     * Sets the flag for specified message.
     *
     * @param string  $mailbox  Folder name
     * @param array   $uids     Message UIDs or null to change flag
     *                          of all messages in a folder
     * @param string  $flag     The name of the flag
     * @param bool    $enabled  Flag state
     */
    function change_flag($mailbox, $uids, $flag, $enabled = false)
    {
        $flag = strtolower($flag);
        if (in_array($flag, $this->flag_fields)) {
            // Internal cache update
            if ($uids && count($uids) == 1 && ($uid = current($uids))
                && ($message = $this->icache['message'])
                && $message['mailbox'] == $mailbox && $message['object']->uid == $uid
            ) {
                $message['object']->$flag = $enabled;
                return;
            }
            $this->db->query(
                "UPDATE ".get_table_name('cache_messages')
                ." SET changed = ".$this->db->now()
                .", " .$this->db->quoteIdentifier($flag) . " = " . intval($enabled)
                ." WHERE user_id = ?"
                    ." AND mailbox = ?"
                    .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
                $this->userid, $mailbox);
        }
        else {
            // @TODO: SELECT+UPDATE?
            $this->remove_message($mailbox, $uids);
        }
    }
    /**
     * Removes message(s) from cache.
     *
     * @param string $mailbox  Folder name
     * @param array  $uids     Message UIDs, NULL removes all messages
     */
    function remove_message($mailbox = null, $uids = null)
    {
        if (!strlen($mailbox)) {
            $this->db->query(
                "DELETE FROM ".get_table_name('cache_messages')
                ." WHERE user_id = ?",
                $this->userid);
        }
        else {
            // Remove the message from internal cache
            if (!empty($uids) && !is_array($uids) && ($message = $this->icache['message'])
                && $message['mailbox'] == $mailbox && $message['object']->uid == $uids
            ) {
                $this->icache['message'] = null;
            }
            $this->db->query(
                "DELETE FROM ".get_table_name('cache_messages')
                ." WHERE user_id = ?"
                    ." AND mailbox = ".$this->db->quote($mailbox)
                    .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
                $this->userid);
        }
    }
    /**
     * Clears index cache.
     *
     * @param string  $mailbox     Folder name
     */
    function remove_index($mailbox = null)
    {
        $this->db->query(
            "DELETE FROM ".get_table_name('cache_index')
            ." WHERE user_id = ".intval($this->userid)
                .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
        );
        if (strlen($mailbox))
            unset($this->icache[$mailbox]['index']);
        else
            $this->icache = array();
    }
    /**
     * Clears thread cache.
     *
     * @param string  $mailbox     Folder name
     */
    function remove_thread($mailbox = null)
    {
        $this->db->query(
            "DELETE FROM ".get_table_name('cache_thread')
            ." WHERE user_id = ".intval($this->userid)
                .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
        );
        if (strlen($mailbox))
            unset($this->icache[$mailbox]['thread']);
        else
            $this->icache = array();
    }
    /**
     * Clears the cache.
     *
     * @param string $mailbox  Folder name
     * @param array  $uids     Message UIDs, NULL removes all messages in a folder
     */
    function clear($mailbox = null, $uids = null)
    {
        $this->remove_index($mailbox);
        $this->remove_thread($mailbox);
        $this->remove_message($mailbox, $uids);
    }
    /**
     * @param string $mailbox Folder name
     * @param int    $id      Message (sequence) ID
     *
     * @return int Message UID
     */
    function id2uid($mailbox, $id)
    {
        if (!empty($this->icache['pending_index_update']))
            return null;
        // get index if it exists
        $index = $this->get_index($mailbox, 'ANY', null, true);
        return $index[$id];
    }
    /**
     * @param string $mailbox Folder name
     * @param int    $uid     Message UID
     *
     * @return int Message (sequence) ID
     */
    function uid2id($mailbox, $uid)
    {
        if (!empty($this->icache['pending_index_update']))
            return null;
        // get index if it exists
        $index = $this->get_index($mailbox, 'ANY', null, true);
        return array_search($uid, (array)$index);
    }
    /**
     * Fetches index data from database
     */
    private function get_index_row($mailbox)
    {
        // Get index from DB
        $sql_result = $this->db->query(
            "SELECT data"
            ." FROM ".get_table_name('cache_index')
            ." WHERE user_id = ?"
                ." AND mailbox = ?",
            $this->userid, $mailbox);
        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $data = explode('@', $sql_arr['data']);
            return array(
                'seq'        => explode(',', $data[0]),
                'uid'        => explode(',', $data[1]),
                'sort_field' => $data[2],
                'sort_order' => $data[3],
                'deleted'    => $data[4],
                'validity'   => $data[5],
                'uidnext'    => $data[6],
            );
        }
        return null;
    }
    /**
     * Fetches thread data from database
     */
    private function get_thread_row($mailbox)
    {
        // Get thread from DB
        $sql_result = $this->db->query(
            "SELECT data"
            ." FROM ".get_table_name('cache_thread')
            ." WHERE user_id = ?"
                ." AND mailbox = ?",
            $this->userid, $mailbox);
        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            $data = explode('@', $sql_arr['data']);
            $data[0] = unserialize($data[0]);
            // build 'depth' and 'children' arrays
            $depth = $children = array();
            $this->build_thread_data($data[0], $depth, $children);
            return array(
                'tree'     => $data[0],
                'depth'    => $depth,
                'children' => $children,
                'deleted'  => $data[1],
                'validity' => $data[2],
                'uidnext'  => $data[3],
            );
        }
        return null;
    }
    /**
     * Saves index data into database
     */
    private function add_index_row($mailbox, $sort_field, $sort_order,
        $data = array(), $mbox_data = array(), $exists = false)
    {
        $data = array(
            implode(',', array_keys($data)),
            implode(',', array_values($data)),
            $sort_field,
            $sort_order,
            (int) $this->skip_deleted,
            (int) $mbox_data['UIDVALIDITY'],
            (int) $mbox_data['UIDNEXT'],
        );
        $data = implode('@', $data);
        if ($exists)
            $sql_result = $this->db->query(
                "UPDATE ".get_table_name('cache_index')
                ." SET data = ?, changed = ".$this->db->now()
                ." WHERE user_id = ?"
                    ." AND mailbox = ?",
                $data, $this->userid, $mailbox);
        else
            $sql_result = $this->db->query(
                "INSERT INTO ".get_table_name('cache_index')
                ." (user_id, mailbox, data, changed)"
                ." VALUES (?, ?, ?, ".$this->db->now().")",
                $this->userid, $mailbox, $data);
    }
    /**
     * Saves thread data into database
     */
    private function add_thread_row($mailbox, $data = array(), $mbox_data = array(), $exists = false)
    {
        $data = array(
            serialize($data['tree']),
            (int) $this->skip_deleted,
            (int) $mbox_data['UIDVALIDITY'],
            (int) $mbox_data['UIDNEXT'],
        );
        $data = implode('@', $data);
        if ($exists)
            $sql_result = $this->db->query(
                "UPDATE ".get_table_name('cache_thread')
                ." SET data = ?, changed = ".$this->db->now()
                ." WHERE user_id = ?"
                    ." AND mailbox = ?",
                $data, $this->userid, $mailbox);
        else
            $sql_result = $this->db->query(
                "INSERT INTO ".get_table_name('cache_thread')
                ." (user_id, mailbox, data, changed)"
                ." VALUES (?, ?, ?, ".$this->db->now().")",
                $this->userid, $mailbox, $data);
    }
    /**
     * Checks index/thread validity
     */
    private function validate($mailbox, $index, &$exists = true)
    {
        $is_thread = isset($index['tree']);
        // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
        $mbox_data = $this->imap->mailbox_data($mailbox);
        // @TODO: Think about skipping validation checks.
        // If we could check only every 10 minutes, we would be able to skip
        // expensive checks, mailbox selection or even IMAP connection, this would require
        // additional logic to force cache invalidation in some cases
        // and many rcube_imap changes to connect when needed
        // Check UIDVALIDITY
        // @TODO: while we're storing message sequence numbers in thread
        //        index, should UIDVALIDITY invalidate the thread data?
        if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
            // the whole cache (all folders) is invalid
            $this->clear();
            $exists = false;
            return false;
        }
        // Folder is empty but cache isn't
        if (empty($mbox_data['EXISTS']) && (!empty($index['seq']) || !empty($index['tree']))) {
            $this->clear($mailbox);
            $exists = false;
            return false;
        }
        // Check UIDNEXT
        if ($index['uidnext'] != $mbox_data['UIDNEXT']) {
            unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
            return false;
        }
        // Index was created with different skip_deleted setting
        if ($this->skip_deleted != $index['deleted']) {
            return false;
        }
        // @TODO: find better validity check for threaded index
        if ($is_thread) {
            // check messages number...
            if ($mbox_data['EXISTS'] != max(array_keys($index['depth']))) {
                return false;
            }
            return true;
        }
        // The rest of checks, more expensive
        if (!empty($this->skip_deleted)) {
            // compare counts if available
            if ($mbox_data['COUNT_UNDELETED'] != null
                && $mbox_data['COUNT_UNDELETED'] != count($index['uid'])) {
                return false;
            }
            // compare UID sets
            if ($mbox_data['ALL_UNDELETED'] != null) {
                $uids_new = rcube_imap_generic::uncompressMessageSet($mbox_data['ALL_UNDELETED']);
                $uids_old = $index['uid'];
                if (count($uids_new) != count($uids_old)) {
                    return false;
                }
                sort($uids_new, SORT_NUMERIC);
                sort($uids_old, SORT_NUMERIC);
                if ($uids_old != $uids_new)
                    return false;
            }
            else {
                // get all undeleted messages excluding cached UIDs
                $ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '.
                    rcube_imap_generic::compressMessageSet($index['uid']));
                if (!empty($ids)) {
                    $index = null; // cache invalid
                }
            }
        }
        else {
            // check messages number...
            if ($mbox_data['EXISTS'] != max($index['seq'])) {
                return false;
            }
            // ... and max UID
            if (max($index['uid']) != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
                return false;
            }
        }
        return true;
    }
    /**
     * Converts cache row into message object.
     *
     * @param array $sql_arr Message row data
     *
     * @return rcube_mail_header Message object
     */
    private function build_message($sql_arr)
    {
        $message = $this->db->decode(unserialize($sql_arr['data']));
        if ($message) {
            foreach ($this->flag_fields as $field)
                $message->$field = (bool) $sql_arr[$field];
        }
        return $message;
    }
    /**
     * Creates 'depth' and 'children' arrays from stored thread 'tree' data.
     */
    private function build_thread_data($data, &$depth, &$children, $level = 0)
    {
        foreach ((array)$data as $key => $val) {
            $children[$key] = !empty($val);
            $depth[$key] = $level;
            if (!empty($val))
                $this->build_thread_data($val, $depth, $children, $level + 1);
        }
    }
    /**
     * Saves message stored in internal cache
     */
    private function save_icache()
    {
        // Save current message from internal cache
        if ($message = $this->icache['message']) {
            $object = $message['object'];
            // remove body too big (>500kB)
            if ($object->body && strlen($object->body) > 500 * 1024)
                $object->body = null;
            // calculate current md5 sum
            $md5sum = md5(serialize($object));
            if ($message['md5sum'] != $md5sum) {
                $this->add_message($message['mailbox'], $object, !$message['exists']);
            }
            $this->icache['message']['md5sum'] = $md5sum;
        }
    }
}
program/include/rcube_imap_generic.php
@@ -48,22 +48,20 @@
    public $encoding;
    public $charset;
    public $ctype;
    public $flags;
    public $timestamp;
    public $body_structure;
    public $bodystructure;
    public $internaldate;
    public $references;
    public $priority;
    public $mdn_to;
    public $mdn_sent = false;
    public $flags;
    public $mdnsent = false;
    public $seen = false;
    public $deleted = false;
    public $answered = false;
    public $forwarded = false;
    public $flagged = false;
    public $has_children = false;
    public $depth = 0;
    public $unread_children = 0;
    public $others = array();
}
@@ -84,6 +82,7 @@
    public $errornum;
    public $result;
    public $resultcode;
    public $selected;
    public $data = array();
    public $flags = array(
        'SEEN'     => '\\Seen',
@@ -96,7 +95,6 @@
        '*'        => '\\*',
    );
    private $selected;
    private $fp;
    private $host;
    private $logged = false;
@@ -235,14 +233,14 @@
        return $line;
    }
    function multLine($line, $escape=false)
    function multLine($line, $escape = false)
    {
        $line = rtrim($line);
        if (preg_match('/\{[0-9]+\}$/', $line)) {
            $out = '';
        if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
            $out   = '';
            $str   = substr($line, 0, -strlen($m[0]));
            $bytes = $m[1];
            preg_match_all('/(.*)\{([0-9]+)\}$/', $line, $a);
            $bytes = $a[2][0];
            while (strlen($out) < $bytes) {
                $line = $this->readBytes($bytes);
                if ($line === NULL)
@@ -250,7 +248,7 @@
                $out .= $line;
            }
            $line = $a[1][0] . ($escape ? $this->escape($out) : $out);
            $line = $str . ($escape ? $this->escape($out) : $out);
        }
        return $line;
@@ -877,12 +875,12 @@
    /**
     * Executes SELECT command (if mailbox is already not in selected state)
     *
     * @param string $mailbox Mailbox name
     * @param string $mailbox      Mailbox name
     * @param array  $qresync_data QRESYNC data (RFC5162)
     *
     * @return boolean True on success, false on error
     * @access public
     */
    function select($mailbox)
    function select($mailbox, $qresync_data = null)
    {
        if (!strlen($mailbox)) {
            return false;
@@ -901,7 +899,21 @@
            }
        }
*/
        list($code, $response) = $this->execute('SELECT', array($this->escape($mailbox)));
        $params = array($this->escape($mailbox));
        // QRESYNC data items
        //    0. the last known UIDVALIDITY,
        //    1. the last known modification sequence,
        //    2. the optional set of known UIDs, and
        //    3. an optional parenthesized list of known sequence ranges and their
        //       corresponding UIDs.
        if (!empty($qresync_data)) {
            if (!empty($qresync_data[2]))
                $qresync_data[2] = self::compressMessageSet($qresync_data[2]);
            $params[] = array('QRESYNC', $qresync_data);
        }
        list($code, $response) = $this->execute('SELECT', $params);
        if ($code == self::ERROR_OK) {
            $response = explode("\r\n", $response);
@@ -909,11 +921,39 @@
                if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/i', $line, $m)) {
                    $this->data[strtoupper($m[2])] = (int) $m[1];
                }
                else if (preg_match('/^\* OK \[(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)\]/i', $line, $match)) {
                    $this->data[strtoupper($match[1])] = (int) $match[2];
                else if (preg_match('/^\* OK \[/i', $line, $match)) {
                    $line = substr($line, 6);
                    if (preg_match('/^(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)/i', $line, $match)) {
                        $this->data[strtoupper($match[1])] = (int) $match[2];
                    }
                    else if (preg_match('/^(HIGHESTMODSEQ) ([0-9]+)/i', $line, $match)) {
                        $this->data[strtoupper($match[1])] = (string) $match[2];
                    }
                    else if (preg_match('/^(NOMODSEQ)/i', $line, $match)) {
                        $this->data[strtoupper($match[1])] = true;
                    }
                    else if (preg_match('/^PERMANENTFLAGS \(([^\)]+)\)/iU', $line, $match)) {
                        $this->data['PERMANENTFLAGS'] = explode(' ', $match[1]);
                    }
                }
                else if (preg_match('/^\* OK \[PERMANENTFLAGS \(([^\)]+)\)\]/iU', $line, $match)) {
                    $this->data['PERMANENTFLAGS'] = explode(' ', $match[1]);
                // QRESYNC FETCH response (RFC5162)
                else if (preg_match('/^\* ([0-9+]) FETCH/i', $line, $match)) {
                    $line       = substr($line, strlen($match[0]));
                    $fetch_data = $this->tokenizeResponse($line, 1);
                    $data       = array('id' => $match[1]);
                    for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) {
                        $data[strtolower($fetch_data[$i])] = $fetch_data[$i+1];
                    }
                    $this->data['QRESYNC'][$data['uid']] = $data;
                }
                // QRESYNC VANISHED response (RFC5162)
                else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
                    $line   = substr($line, strlen($match[0]));
                    $v_data = $this->tokenizeResponse($line, 1);
                    $this->data['VANISHED'] = $v_data;
                }
            }
@@ -935,7 +975,6 @@
     *                        in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
     *
     * @return array Status item-value hash
     * @access public
     * @since 0.5-beta
     */
    function status($mailbox, $items=array())
@@ -971,7 +1010,7 @@
            }
            for ($i=0, $len=count($items); $i<$len; $i += 2) {
                $result[$items[$i]] = (int) $items[$i+1];
                $result[$items[$i]] = $items[$i+1];
            }
            $this->data['STATUS:'.$mailbox] = $result;
@@ -989,7 +1028,6 @@
     * @param string $messages Message UIDs to expunge
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function expunge($mailbox, $messages=NULL)
    {
@@ -1022,7 +1060,6 @@
     * Executes CLOSE command
     *
     * @return boolean True on success, False on error
     * @access public
     * @since 0.5
     */
    function close()
@@ -1043,7 +1080,6 @@
     * @param string $mailbox Mailbox name
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function subscribe($mailbox)
    {
@@ -1059,7 +1095,6 @@
     * @param string $mailbox Mailbox name
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function unsubscribe($mailbox)
    {
@@ -1075,7 +1110,6 @@
     * @param string $mailbox Mailbox name
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function deleteFolder($mailbox)
    {
@@ -1091,7 +1125,6 @@
     * @param string $mailbox Mailbox name
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function clearFolder($mailbox)
    {
@@ -1116,7 +1149,6 @@
     * @param string $mailbox Mailbox name
     *
     * @return int Number of messages, False on error
     * @access public
     */
    function countMessages($mailbox, $refresh = false)
    {
@@ -1149,7 +1181,6 @@
     * @param string $mailbox Mailbox name
     *
     * @return int Number of messages, False on error
     * @access public
     */
    function countRecent($mailbox)
    {
@@ -1172,7 +1203,6 @@
     * @param string $mailbox Mailbox name
     *
     * @return int Number of messages, False on error
     * @access public
     */
    function countUnseen($mailbox)
    {
@@ -1203,7 +1233,6 @@
     * @param array $items Client identification information key/value hash
     *
     * @return array Server identification information key/value hash
     * @access public
     * @since 0.6
     */
    function id($items=array())
@@ -1228,6 +1257,37 @@
            for ($i=0, $len=count($items); $i<$len; $i += 2) {
                $result[$items[$i]] = $items[$i+1];
            }
            return $result;
        }
        return false;
    }
    /**
     * Executes ENABLE command (RFC5161)
     *
     * @param mixed $extension Extension name to enable (or array of names)
     *
     * @return array|bool List of enabled extensions, False on error
     * @since 0.6
     */
    function enable($extension)
    {
        if (empty($extension))
            return false;
        if (!$this->hasCapability('ENABLE'))
            return false;
        if (!is_array($extension))
            $extension = array($extension);
        list($code, $response) = $this->execute('ENABLE', $extension);
        if ($code == self::ERROR_OK && preg_match('/\* ENABLED /i', $response)) {
            $response = substr($response, 10); // remove prefix "* ENABLED "
            $result   = (array) $this->tokenizeResponse($response);
            return $result;
        }
@@ -1472,7 +1532,6 @@
     * @param int    $uid     Message unique identifier (UID)
     *
     * @return int Message sequence identifier
     * @access public
     */
    function UID2ID($mailbox, $uid)
    {
@@ -1492,12 +1551,11 @@
     * @param int    $uid     Message sequence identifier
     *
     * @return int Message unique identifier
     * @access public
     */
    function ID2UID($mailbox, $id)
    {
        if (empty($id) || $id < 0) {
            return     null;
            return null;
        }
        if (!$this->select($mailbox)) {
@@ -1515,46 +1573,58 @@
    function fetchUIDs($mailbox, $message_set=null)
    {
        if (is_array($message_set))
            $message_set = join(',', $message_set);
        else if (empty($message_set))
        if (empty($message_set))
            $message_set = '1:*';
        return $this->fetchHeaderIndex($mailbox, $message_set, 'UID', false);
    }
    function fetchHeaders($mailbox, $message_set, $uidfetch=false, $bodystr=false, $add='')
    /**
     * FETCH command (RFC3501)
     *
     * @param string $mailbox     Mailbox name
     * @param mixed  $message_set Message(s) sequence identifier(s) or UID(s)
     * @param bool   $is_uid      True if $message_set contains UIDs
     * @param array  $query_items FETCH command data items
     * @param string $mod_seq     Modification sequence for CHANGEDSINCE (RFC4551) query
     * @param bool   $vanished    Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query
     *
     * @return array List of rcube_mail_header elements, False on error
     * @since 0.6
     */
    function fetch($mailbox, $message_set, $is_uid = false, $query_items = array(),
        $mod_seq = null, $vanished = false)
    {
        $result = array();
        if (!$this->select($mailbox)) {
            return false;
        }
        $message_set = $this->compressMessageSet($message_set);
        $result      = array();
        if ($add)
            $add = ' '.trim($add);
        /* FETCH uid, size, flags and headers */
        $key      = $this->nextTag();
        $request  = $key . ($uidfetch ? ' UID' : '') . " FETCH $message_set ";
        $request .= "(UID RFC822.SIZE FLAGS INTERNALDATE ";
        if ($bodystr)
            $request .= "BODYSTRUCTURE ";
        $request .= "BODY.PEEK[HEADER.FIELDS (DATE FROM TO SUBJECT CONTENT-TYPE ";
        $request .= "CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY".$add.")])";
        $request  = $key . ($is_uid ? ' UID' : '') . " FETCH $message_set ";
        $request .= "(" . implode(' ', $query_items) . ")";
        if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) {
            $request .= " (CHANGEDSINCE $mod_seq" . ($vanished ? " VANISHED" : '') .")";
        }
        if (!$this->putLine($request)) {
            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
            return false;
        }
        do {
            $line = $this->readLine(4096);
            $line = $this->multLine($line);
            if (!$line)
                break;
            // Sample reply line:
            // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
            // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
            // BODY[HEADER.FIELDS ...
            if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
                $id = intval($m[1]);
@@ -1565,101 +1635,112 @@
                $result[$id]->messageID = 'mid:' . $id;
                $lines = array();
                $ln = 0;
                $line  = substr($line, strlen($m[0]) + 2);
                $ln    = 0;
                // Sample reply line:
                // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
                // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
                // BODY[HEADER.FIELDS ...
                // get complete entry
                while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
                    $bytes = $m[1];
                    $out   = '';
                if (preg_match('/^\* [0-9]+ FETCH \((.*) BODY/sU', $line, $matches)) {
                    $str = $matches[1];
                    while (list($name, $value) = $this->tokenizeResponse($str, 2)) {
                        if ($name == 'UID') {
                            $result[$id]->uid = intval($value);
                        }
                        else if ($name == 'RFC822.SIZE') {
                            $result[$id]->size = intval($value);
                        }
                        else if ($name == 'INTERNALDATE') {
                            $result[$id]->internaldate = $value;
                            $result[$id]->date         = $value;
                            $result[$id]->timestamp    = $this->StrToTime($value);
                        }
                        else if ($name == 'FLAGS') {
                            $flags_a = $value;
                        }
                    while (strlen($out) < $bytes) {
                        $out = $this->readBytes($bytes);
                        if ($out === NULL)
                            break;
                        $line .= $out;
                    }
                    // BODYSTRUCTURE
                    if ($bodystr) {
                        while (!preg_match('/ BODYSTRUCTURE (.*) BODY\[HEADER.FIELDS/sU', $line, $m)) {
                            $line2 = $this->readLine(1024);
                            $line .= $this->multLine($line2, true);
                        }
                        $result[$id]->body_structure = $m[1];
                    }
                    $str = $this->readLine(4096);
                    if ($str === false)
                        break;
                    // the rest of the result
                    if (preg_match('/ BODY\[HEADER.FIELDS \(.*?\)\]\s*(.*)$/s', $line, $m)) {
                        $reslines = explode("\n", trim($m[1], '"'));
                        // re-parse (see below)
                        foreach ($reslines as $resln) {
                            if (ord($resln[0])<=32) {
                                $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($resln);
                            } else {
                                $lines[++$ln] = trim($resln);
                    $line .= $str;
                }
                // Tokenize response and assign to object properties
                while (list($name, $value) = $this->tokenizeResponse($line, 2)) {
                    if ($name == 'UID') {
                        $result[$id]->uid = intval($value);
                    }
                    else if ($name == 'RFC822.SIZE') {
                        $result[$id]->size = intval($value);
                    }
                    else if ($name == 'RFC822.TEXT') {
                        $result[$id]->body = $value;
                    }
                    else if ($name == 'INTERNALDATE') {
                        $result[$id]->internaldate = $value;
                        $result[$id]->date         = $value;
                        $result[$id]->timestamp    = $this->StrToTime($value);
                    }
                    else if ($name == 'FLAGS') {
                        if (!empty($value)) {
                            foreach ((array)$value as $flag) {
                                $flag = str_replace('\\', '', $flag);
                                switch (strtoupper($flag)) {
                                case 'SEEN':
                                    $result[$id]->seen = true;
                                    break;
                                case 'DELETED':
                                    $result[$id]->deleted = true;
                                    break;
                                case 'ANSWERED':
                                    $result[$id]->answered = true;
                                    break;
                                case '$FORWARDED':
                                    $result[$id]->forwarded = true;
                                    break;
                                case '$MDNSENT':
                                    $result[$id]->mdnsent = true;
                                    break;
                                case 'FLAGGED':
                                    $result[$id]->flagged = true;
                                    break;
                                default:
                                    $result[$id]->flags[] = $flag;
                                    break;
                                }
                            }
                        }
                    }
                    else if ($name == 'MODSEQ') {
                        $result[$id]->modseq = $value[0];
                    }
                    else if ($name == 'ENVELOPE') {
                        $result[$id]->envelope = $value;
                    }
                    else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) {
                        if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) {
                            $value = array($value);
                        }
                        $result[$id]->bodystructure = $value;
                    }
                    else if ($name == 'RFC822') {
                        $result[$id]->body = $value;
                    }
                    else if ($name == 'BODY') {
                        $body = $this->tokenizeResponse($line, 1);
                        if ($value[0] == 'HEADER.FIELDS')
                            $headers = $body;
                        else if (!empty($value))
                            $result[$id]->bodypart[$value[0]] = $body;
                        else
                            $result[$id]->body = $body;
                    }
                }
                // Start parsing headers.  The problem is, some header "lines" take up multiple lines.
                // So, we'll read ahead, and if the one we're reading now is a valid header, we'll
                // process the previous line.  Otherwise, we'll keep adding the strings until we come
                // to the next valid header line.
                do {
                    $line = rtrim($this->readLine(300), "\r\n");
                    // The preg_match below works around communigate imap, which outputs " UID <number>)".
                    // Without this, the while statement continues on and gets the "FH0 OK completed" message.
                    // If this loop gets the ending message, then the outer loop does not receive it from radline on line 1249.
                    // This in causes the if statement on line 1278 to never be true, which causes the headers to end up missing
                    // If the if statement was changed to pick up the fh0 from this loop, then it causes the outer loop to spin
                    // An alternative might be:
                    // if (!preg_match("/:/",$line) && preg_match("/\)$/",$line)) break;
                    // however, unsure how well this would work with all imap clients.
                    if (preg_match("/^\s*UID [0-9]+\)$/", $line)) {
                        break;
                    }
                    // handle FLAGS reply after headers (AOL, Zimbra?)
                    if (preg_match('/\s+FLAGS \((.*)\)\)$/', $line, $matches)) {
                        $flags_a = $this->tokenizeResponse($matches[1]);
                        break;
                    }
                    if (ord($line[0])<=32) {
                        $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($line);
                    } else {
                        $lines[++$ln] = trim($line);
                    }
                // patch from "Maksim Rubis" <siburny@hotmail.com>
                } while ($line[0] != ')' && !$this->startsWith($line, $key, true));
                if (strncmp($line, $key, strlen($key))) {
                    // process header, fill rcube_mail_header obj.
                    // initialize
                    if (is_array($headers)) {
                        reset($headers);
                        while (list($k, $bar) = each($headers)) {
                            $headers[$k] = '';
                // create array with header field:data
                if (!empty($headers)) {
                    $headers = explode("\n", trim($headers));
                    foreach ($headers as $hid => $resln) {
                        if (ord($resln[0]) <= 32) {
                            $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
                        } else {
                            $lines[++$ln] = trim($resln);
                        }
                    }
                    // create array with header field:data
                    while (list($lines_key, $str) = each($lines)) {
                        list($field, $string) = explode(':', $str, 2);
@@ -1723,47 +1804,44 @@
                                $result[$id]->others[$field] = $string;
                            }
                            break;
                        } // end switch ()
                    } // end while ()
                }
                // process flags
                if (!empty($flags_a)) {
                    foreach ($flags_a as $flag) {
                        $flag = str_replace('\\', '', $flag);
                        $result[$id]->flags[] = $flag;
                        switch (strtoupper($flag)) {
                        case 'SEEN':
                            $result[$id]->seen = true;
                            break;
                        case 'DELETED':
                            $result[$id]->deleted = true;
                            break;
                        case 'ANSWERED':
                            $result[$id]->answered = true;
                            break;
                        case '$FORWARDED':
                            $result[$id]->forwarded = true;
                            break;
                        case '$MDNSENT':
                            $result[$id]->mdn_sent = true;
                            break;
                        case 'FLAGGED':
                            $result[$id]->flagged = true;
                            break;
                        }
                    }
                }
            }
            // VANISHED response (QRESYNC RFC5162)
            // Sample: * VANISHED (EARLIER) 300:310,405,411
            else if (preg_match('/^\* VANISHED [EARLIER]*/i', $line, $match)) {
                $line   = substr($line, strlen($match[0]));
                $v_data = $this->tokenizeResponse($line, 1);
                $this->data['VANISHED'] = $v_data;
            }
        } while (!$this->startsWith($line, $key, true));
        return $result;
    }
    function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add = '')
    {
        $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE');
        if ($bodystr)
            $query_items[] = 'BODYSTRUCTURE';
        $query_items[] = 'BODY.PEEK[HEADER.FIELDS ('
            . 'DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY'
            . ($add ? ' ' . trim($add) : '')
            . ')]';
        $result = $this->fetch($mailbox, $message_set, $is_uid, $query_items);
        return $result;
    }
    function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='')
    {
        $a  = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add);
        $a = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add);
        if (is_array($a)) {
            return array_shift($a);
        }
@@ -2043,6 +2121,7 @@
            $params .= 'RETURN (' . implode(' ', $items) . ')';
        }
        if (!empty($criteria)) {
            $modseq = stripos($criteria, 'MODSEQ') !== false;
            $params .= ($params ? ' ' : '') . $criteria;
        }
        else {
@@ -2054,11 +2133,18 @@
        if ($code == self::ERROR_OK) {
            // remove prefix...
            $response = substr($response, stripos($response,
            $response = substr($response, stripos($response,
                $esearch ? '* ESEARCH' : '* SEARCH') + ($esearch ? 10 : 9));
            // ...and unilateral untagged server responses
            if ($pos = strpos($response, '*')) {
                $response = rtrim(substr($response, 0, $pos));
            }
            // remove MODSEQ response
            if ($modseq) {
                if (preg_match('/\(MODSEQ ([0-9]+)\)$/', $response, $m)) {
                    $response = substr($response, 0, -strlen($m[0]));
                }
            }
            if ($esearch) {
@@ -2067,7 +2153,7 @@
                $result = array();
                for ($i=0; $i<count($items); $i++) {
                    // If the SEARCH results in no matches, the server MUST NOT
                    // If the SEARCH returns no matches, the server MUST NOT
                    // include the item result option in the ESEARCH response
                    if ($ret = $this->tokenizeResponse($response, 2)) {
                        list ($name, $value) = $ret;
@@ -2116,7 +2202,6 @@
     *
     * @return array List of mailboxes or hash of options if $status_opts argument
     *               is non-empty.
     * @access public
     */
    function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
    {
@@ -2132,7 +2217,6 @@
     *
     * @return array List of mailboxes or hash of options if $status_opts argument
     *               is non-empty.
     * @access public
     */
    function listSubscribed($ref, $mailbox, $status_opts=array())
    {
@@ -2152,7 +2236,6 @@
     *
     * @return array List of mailboxes or hash of options if $status_ops argument
     *               is non-empty.
     * @access private
     */
    private function _listMailboxes($ref, $mailbox, $subscribed=false,
        $status_opts=array(), $select_opts=array())
@@ -2231,7 +2314,7 @@
        return false;
    }
    function fetchMIMEHeaders($mailbox, $id, $parts, $mime=true)
    function fetchMIMEHeaders($mailbox, $uid, $parts, $mime=true)
    {
        if (!$this->select($mailbox)) {
            return false;
@@ -2249,7 +2332,7 @@
            $peeks[] = "BODY.PEEK[$part.$type]";
        }
        $request = "$key FETCH $id (" . implode(' ', $peeks) . ')';
        $request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')';
        // send request
        if (!$this->putLine($request)) {
@@ -2263,7 +2346,7 @@
            if (preg_match('/BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
                $idx = $matches[1];
                $result[$idx] = preg_replace('/^(\* '.$id.' FETCH \()?\s*BODY\['.$idx.'\.'.$type.'\]\s+/', '', $line);
                $result[$idx] = preg_replace('/^(\* [0-9]+ FETCH \()?\s*BODY\['.$idx.'\.'.$type.'\]\s+/', '', $line);
                $result[$idx] = trim($result[$idx], '"');
                $result[$idx] = rtrim($result[$idx], "\t\r\n\0\x0B");
            }
@@ -2570,33 +2653,6 @@
        return false;
    }
    function fetchStructureString($mailbox, $id, $is_uid=false)
    {
        if (!$this->select($mailbox)) {
            return false;
        }
        $key = $this->nextTag();
        $result = false;
        $command = $key . ($is_uid ? ' UID' : '') ." FETCH $id (BODYSTRUCTURE)";
        if ($this->putLine($command)) {
            do {
                $line = $this->readLine(5000);
                $line = $this->multLine($line, true);
                if (!preg_match("/^$key /", $line))
                    $result .= $line;
            } while (!$this->startsWith($line, $key, true, true));
            $result = trim(substr($result, strpos($result, 'BODYSTRUCTURE')+13, -1));
        }
        else {
            $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
        }
        return $result;
    }
    function getQuota()
    {
        /*
@@ -2660,7 +2716,6 @@
     *
     * @return boolean True on success, False on failure
     *
     * @access public
     * @since 0.5-beta
     */
    function setACL($mailbox, $user, $acl)
@@ -2684,7 +2739,6 @@
     *
     * @return boolean True on success, False on failure
     *
     * @access public
     * @since 0.5-beta
     */
    function deleteACL($mailbox, $user)
@@ -2702,7 +2756,6 @@
     * @param string $mailbox Mailbox name
     *
     * @return array User-rights array on success, NULL on error
     * @access public
     * @since 0.5-beta
     */
    function getACL($mailbox)
@@ -2743,7 +2796,6 @@
     * @param string $user    User name
     *
     * @return array List of user rights
     * @access public
     * @since 0.5-beta
     */
    function listRights($mailbox, $user)
@@ -2775,7 +2827,6 @@
     * @param string $mailbox Mailbox name
     *
     * @return array MYRIGHTS response on success, NULL on error
     * @access public
     * @since 0.5-beta
     */
    function myRights($mailbox)
@@ -2802,7 +2853,6 @@
     * @param array  $entries Entry-value array (use NULL value as NIL)
     *
     * @return boolean True on success, False on failure
     * @access public
     * @since 0.5-beta
     */
    function setMetadata($mailbox, $entries)
@@ -2832,7 +2882,6 @@
     *
     * @return boolean True on success, False on failure
     *
     * @access public
     * @since 0.5-beta
     */
    function deleteMetadata($mailbox, $entries)
@@ -2862,7 +2911,6 @@
     *
     * @return array GETMETADATA result on success, NULL on error
     *
     * @access public
     * @since 0.5-beta
     */
    function getMetadata($mailbox, $entries, $options=array())
@@ -2954,7 +3002,6 @@
     *                        three elements: entry name, attribute name, value
     *
     * @return boolean True on success, False on failure
     * @access public
     * @since 0.5-beta
     */
    function setAnnotation($mailbox, $data)
@@ -2986,7 +3033,6 @@
     *
     * @return boolean True on success, False on failure
     *
     * @access public
     * @since 0.5-beta
     */
    function deleteAnnotation($mailbox, $data)
@@ -3008,7 +3054,6 @@
     *
     * @return array Annotations result on success, NULL on error
     *
     * @access public
     * @since 0.5-beta
     */
    function getAnnotation($mailbox, $entries, $attribs)
@@ -3093,10 +3138,103 @@
    }
    /**
     * Returns BODYSTRUCTURE for the specified message.
     *
     * @param string $mailbox Folder name
     * @param int    $id      Message sequence number or UID
     * @param bool   $is_uid  True if $id is an UID
     *
     * @return array/bool Body structure array or False on error.
     * @since 0.6
     */
    function getStructure($mailbox, $id, $is_uid = false)
    {
        $result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE'));
        if (is_array($result)) {
            $result = array_shift($result);
            return $result->bodystructure;
        }
        return false;
    }
    static function getStructurePartType($structure, $part)
    {
        $part_a = self::getStructurePartArray($structure, $part);
        if (!empty($part_a)) {
            if (is_array($part_a[0]))
                return 'multipart';
            else if ($part_a[0])
                return $part_a[0];
        }
        return 'other';
    }
    static function getStructurePartEncoding($structure, $part)
    {
        $part_a = self::getStructurePartArray($structure, $part);
        if ($part_a) {
            if (!is_array($part_a[0]))
                return $part_a[5];
        }
        return '';
    }
    static function getStructurePartCharset($structure, $part)
    {
        $part_a = self::getStructurePartArray($structure, $part);
        if ($part_a) {
            if (is_array($part_a[0]))
                return '';
            else {
                if (is_array($part_a[2])) {
                    $name = '';
                    while (list($key, $val) = each($part_a[2]))
                        if (strcasecmp($val, 'charset') == 0)
                            return $part_a[2][$key+1];
                }
            }
        }
        return '';
    }
    static function getStructurePartArray($a, $part)
    {
        if (!is_array($a)) {
            return false;
        }
        if (strpos($part, '.') > 0) {
            $original_part = $part;
            $pos = strpos($part, '.');
            $rest = substr($original_part, $pos+1);
            $part = substr($original_part, 0, $pos);
            if ((strcasecmp($a[0], 'message') == 0) && (strcasecmp($a[1], 'rfc822') == 0)) {
                $a = $a[8];
            }
            return self::getStructurePartArray($a[$part-1], $rest);
        }
        else if ($part>0) {
            if (!is_array($a[0]) && (strcasecmp($a[0], 'message') == 0)
                && (strcasecmp($a[1], 'rfc822') == 0)) {
                $a = $a[8];
            }
            if (is_array($a[$part-1]))
                return $a[$part-1];
            else
                return $a;
        }
        else if (($part == 0) || (empty($part))) {
            return $a;
        }
    }
    /**
     * Creates next command identifier (tag)
     *
     * @return string Command identifier
     * @access public
     * @since 0.5-beta
     */
    function nextTag()
@@ -3115,7 +3253,6 @@
     * @param int    $options   Execution options
     *
     * @return mixed Response code or list of response code and data
     * @access public
     * @since 0.5-beta
     */
    function execute($command, $arguments=array(), $options=0)
@@ -3126,7 +3263,9 @@
        $response = $noresp ? null : '';
        if (!empty($arguments)) {
            $query .= ' ' . implode(' ', $arguments);
            foreach ($arguments as $arg) {
                $query .= ' ' . self::r_implode($arg);
            }
        }
        // Send command
@@ -3173,7 +3312,6 @@
     * @param int    $num  Number of tokens to return
     *
     * @return mixed Tokens array or string if $num=1
     * @access public
     * @since 0.5-beta
     */
    static function tokenizeResponse(&$str, $num=0)
@@ -3194,7 +3332,7 @@
                if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
                    // error
                }
                $result[] = substr($str, $epos + 3, $bytes);
                $result[] = $bytes ? substr($str, $epos + 3, $bytes) : '';
                // Advance the string
                $str = substr($str, $epos + 3 + $bytes);
                break;
@@ -3223,10 +3361,12 @@
            // Parenthesized list
            case '(':
            case '[':
                $str = substr($str, 1);
                $result[] = self::tokenizeResponse($str);
                break;
            case ')':
            case ']':
                $str = substr($str, 1);
                return $result;
                break;
@@ -3243,8 +3383,8 @@
                    break;
                }
                // excluded chars: SP, CTL, )
                if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) {
                // excluded chars: SP, CTL, ), [, ]
                if (preg_match('/^([^\x00-\x20\x29\x5B\x5D\x7F]+)/', $str, $m)) {
                    $result[] = $m[1] == 'NIL' ? NULL : $m[1];
                    $str = substr($str, strlen($m[1]));
                }
@@ -3253,6 +3393,23 @@
        }
        return $num == 1 ? $result[0] : $result;
    }
    static function r_implode($element)
    {
        $string = '';
        if (is_array($element)) {
            reset($element);
            while (list($key, $value) = each($element)) {
                $string .= ' ' . self::r_implode($value);
            }
        }
        else {
            return $element;
        }
        return '(' . trim($string) . ')';
    }
    private function _xor($string, $string2)
@@ -3348,7 +3505,6 @@
     *
     * @param   boolean $debug      New value for the debugging flag.
     *
     * @access  public
     * @since   0.5-stable
     */
    function setDebug($debug, $handler = null)
@@ -3362,7 +3518,6 @@
     *
     * @param   string  $message    Debug mesage text.
     *
     * @access  private
     * @since   0.5-stable
     */
    private function debug($message)
program/include/rcube_message.php
@@ -77,7 +77,7 @@
        $this->imap->get_all_headers = true;
        $this->uid = $uid;
        $this->headers = $this->imap->get_headers($uid, NULL, true, true);
        $this->headers = $this->imap->get_message($uid);
        if (!$this->headers)
            return;
@@ -94,9 +94,9 @@
                '_mbox' => $this->imap->get_mailbox_name(), '_uid' => $uid))
        );
        if ($this->structure = $this->imap->get_structure($uid, $this->headers->body_structure)) {
            $this->get_mime_numbers($this->structure);
            $this->parse_structure($this->structure);
        if (!empty($this->headers->structure)) {
            $this->get_mime_numbers($this->headers->structure);
            $this->parse_structure($this->headers->structure);
        }
        else {
            $this->body = $this->imap->get_body($uid);
program/include/rcube_mime_struct.php
@@ -1,77 +1,7 @@
<?php
/*
 +-----------------------------------------------------------------------+
 | program/include/rcube_mime_struct.php                                 |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
 | Licensed under the GNU GPL                                            |
 |                                                                       |
 | PURPOSE:                                                              |
 |   Provide functions for handling mime messages structure              |
 |                                                                       |
 |   Based on Iloha MIME Library. See http://ilohamail.org/ for details  |
 |                                                                       |
 +-----------------------------------------------------------------------+
 | Author: Aleksander Machniak <alec@alec.pl>                            |
 | Author: Ryo Chijiiwa <Ryo@IlohaMail.org>                              |
 +-----------------------------------------------------------------------+
 $Id$
*/
/**
 * Helper class to process IMAP's BODYSTRUCTURE string
 *
 * @package    Mail
 * @author     Aleksander Machniak <alec@alec.pl>
 */
class rcube_mime_struct
{
    private $structure;
    function __construct($str=null)
    function getStructurePartType($structure, $part)
    {
        if ($str)
            $this->structure = $this->parseStructure($str);
    }
    /*
     * Parses IMAP's BODYSTRUCTURE string into array
    */
    function parseStructure($str)
    {
        $line = substr($str, 1, strlen($str) - 2);
        $line = str_replace(')(', ') (', $line);
        $struct = rcube_imap_generic::tokenizeResponse($line);
        if (!is_array($struct[0]) && (strcasecmp($struct[0], 'message') == 0)
            && (strcasecmp($struct[1], 'rfc822') == 0)) {
            $struct = array($struct);
        }
        return $struct;
    }
    /*
     * Parses IMAP's BODYSTRUCTURE string into array and loads it into class internal variable
    */
    function loadStructure($str)
    {
        if (empty($str))
            return true;
        $this->structure = $this->parseStructure($str);
        return (!empty($this->structure));
    }
    function getPartType($part)
    {
        $part_a = $this->getPartArray($this->structure, $part);
        $part_a = self::getPartArray($structure, $part);
        if (!empty($part_a)) {
            if (is_array($part_a[0]))
                return 'multipart';
@@ -82,9 +12,9 @@
        return 'other';
    }
    function getPartEncoding($part)
    function getStructurePartEncoding($structure, $part)
    {
        $part_a = $this->getPartArray($this->structure, $part);
        $part_a = self::getPartArray($structure, $part);
        if ($part_a) {
            if (!is_array($part_a[0]))
                return $part_a[5];
@@ -93,9 +23,9 @@
        return '';
    }
    function getPartCharset($part)
    function getStructurePartCharset($structure, $part)
    {
        $part_a = $this->getPartArray($this->structure, $part);
        $part_a = self::getPartArray($structure, $part);
        if ($part_a) {
            if (is_array($part_a[0]))
                return '';
@@ -112,7 +42,7 @@
        return '';
    }
    function getPartArray($a, $part)
    function getStructurePartArray($a, $part)
    {
        if (!is_array($a)) {
            return false;
@@ -137,9 +67,7 @@
            else
                return $a;
        }
        else if (($part==0) || (empty($part))) {
        else if (($part == 0) || (empty($part))) {
            return $a;
        }
    }
}
program/steps/mail/func.inc
@@ -1454,7 +1454,7 @@
  if (!is_object($message) || !is_a($message, rcube_message))
    $message = new rcube_message($message);
  if ($message->headers->mdn_to && !$message->headers->mdn_sent &&
  if ($message->headers->mdn_to && !$message->headers->mdnsent &&
    ($IMAP->check_permflag('MDNSENT') || $IMAP->check_permflag('*')))
  {
    $identity = $RCMAIL->user->get_identity();
program/steps/mail/show.inc
@@ -77,7 +77,7 @@
  // check for unset disposition notification
  if ($MESSAGE->headers->mdn_to &&
      !$MESSAGE->headers->mdn_sent && !$MESSAGE->headers->seen &&
      !$MESSAGE->headers->mdnsent && !$MESSAGE->headers->seen &&
      ($IMAP->check_permflag('MDNSENT') || $IMAP->check_permflag('*')) &&
      $mbox_name != $CONFIG['drafts_mbox'] &&
      $mbox_name != $CONFIG['sent_mbox'])