summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoralecpl <alec@alec.pl>2011-09-07 11:07:03 +0000
committeralecpl <alec@alec.pl>2011-09-07 11:07:03 +0000
commit80152b333ca5d856dcf09f5ca10a9ffd80ba117f (patch)
tree084aa3c8aabb3d2b1783dbb01170840ccefc0c62
parentb104e39f3425faf77cae67101c734fcfc3b0c1e9 (diff)
- 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
-rw-r--r--CHANGELOG7
-rw-r--r--SQL/mssql.initial.sql144
-rw-r--r--SQL/mssql.upgrade.sql96
-rw-r--r--SQL/mysql.initial.sql76
-rw-r--r--SQL/mysql.update.sql42
-rw-r--r--SQL/postgres.initial.sql76
-rw-r--r--SQL/postgres.update.sql43
-rw-r--r--SQL/sqlite.initial.sql87
-rw-r--r--SQL/sqlite.update.sql41
-rw-r--r--program/include/main.inc14
-rw-r--r--program/include/rcube_imap.php1191
-rw-r--r--program/include/rcube_imap_cache.php907
-rw-r--r--program/include/rcube_imap_generic.php593
-rw-r--r--program/include/rcube_message.php8
-rw-r--r--program/include/rcube_mime_struct.php88
-rw-r--r--program/steps/mail/func.inc2
-rw-r--r--program/steps/mail/show.inc2
17 files changed, 2165 insertions, 1252 deletions
diff --git a/CHANGELOG b/CHANGELOG
index f959c71a0..7a8973155 100644
--- a/CHANGELOG
+++ b/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
diff --git a/SQL/mssql.initial.sql b/SQL/mssql.initial.sql
index 8e103eb55..eb5f20bcc 100644
--- a/SQL/mssql.initial.sql
+++ b/SQL/mssql.initial.sql
@@ -7,6 +7,37 @@ CREATE TABLE [dbo].[cache] (
) 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 ,
@@ -53,25 +84,6 @@ CREATE TABLE [dbo].[identities] (
) 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
-
CREATE TABLE [dbo].[session] (
[sess_id] [varchar] (32) COLLATE Latin1_General_CI_AI NOT NULL ,
[created] [datetime] NOT NULL ,
@@ -116,6 +128,27 @@ ALTER TABLE [dbo].[cache] WITH NOCHECK ADD
) 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
(
@@ -144,13 +177,6 @@ ALTER TABLE [dbo].[identities] WITH NOCHECK ADD
) ON [PRIMARY]
GO
-ALTER TABLE [dbo].[messages] WITH NOCHECK ADD
- PRIMARY KEY CLUSTERED
- (
- [message_id]
- ) ON [PRIMARY]
-GO
-
ALTER TABLE [dbo].[session] WITH NOCHECK ADD
CONSTRAINT [PK_session_sess_id] PRIMARY KEY CLUSTERED
(
@@ -187,6 +213,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
CONSTRAINT [DF_contacts_user_id] DEFAULT (0) FOR [user_id],
CONSTRAINT [DF_contacts_changed] DEFAULT (getdate()) FOR [changed],
@@ -238,33 +291,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
CONSTRAINT [DF_session_sess_id] DEFAULT ('') FOR [sess_id],
CONSTRAINT [DF_session_created] DEFAULT (getdate()) FOR [created],
@@ -318,7 +344,17 @@ ALTER TABLE [dbo].[cache] ADD CONSTRAINT [FK_cache_user_id]
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
diff --git a/SQL/mssql.upgrade.sql b/SQL/mssql.upgrade.sql
index 258b1d78a..29440f950 100644
--- a/SQL/mssql.upgrade.sql
+++ b/SQL/mssql.upgrade.sql
@@ -151,3 +151,99 @@ ALTER TABLE [dbo].[searches] ADD CONSTRAINT [FK_searches_user_id]
ON DELETE CASCADE ON UPDATE CASCADE
GO
+DROP TABLE [dbo].[messages]
+GO
+CREATE TABLE [dbo].[cache_index] (
+ [user_id] [int] NOT NULL ,
+ [mailbox] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
+ [changed] [datetime] NOT NULL ,
+ [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
+ \ No newline at end of file
diff --git a/SQL/mysql.initial.sql b/SQL/mysql.initial.sql
index a50e28c35..32b3991e0 100644
--- a/SQL/mysql.initial.sql
+++ b/SQL/mysql.initial.sql
@@ -33,33 +33,6 @@ CREATE TABLE `users` (
) /*!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 @@ CREATE TABLE `cache` (
) /*!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` (
diff --git a/SQL/mysql.update.sql b/SQL/mysql.update.sql
index 7f8ce6154..fee18d640 100644
--- a/SQL/mysql.update.sql
+++ b/SQL/mysql.update.sql
@@ -170,3 +170,45 @@ CREATE TABLE `searches` (
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 */;
diff --git a/SQL/postgres.initial.sql b/SQL/postgres.initial.sql
index 01221c4e3..a1864c6c9 100644
--- a/SQL/postgres.initial.sql
+++ b/SQL/postgres.initial.sql
@@ -67,7 +67,7 @@ CREATE SEQUENCE identity_ids
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 SEQUENCE cache_ids
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_user_id_idx ON "cache" (user_id, cache_key);
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;
+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);
--
--- Table "messages"
--- Name: messages; Type: TABLE; Schema: public; Owner: postgres
+-- Table "cache_thread"
+-- Name: cache_thread; Type: TABLE; Schema: public; Owner: postgres
--
-CREATE TABLE messages (
- message_id integer DEFAULT nextval('message_ids'::text) PRIMARY KEY,
+CREATE TABLE cache_thread (
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 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 messages_index_idx ON messages (user_id, cache_key, idx);
-CREATE INDEX messages_created_idx ON messages (created);
+CREATE INDEX cache_messages_changed_idx ON cache_messages (changed);
--
-- Table "dictionary"
diff --git a/SQL/postgres.update.sql b/SQL/postgres.update.sql
index e316ff540..4bd0400c9 100644
--- a/SQL/postgres.update.sql
+++ b/SQL/postgres.update.sql
@@ -126,3 +126,46 @@ CREATE TABLE searches (
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);
diff --git a/SQL/sqlite.initial.sql b/SQL/sqlite.initial.sql
index 46ee5301b..7ec82015d 100644
--- a/SQL/sqlite.initial.sql
+++ b/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 @@ CREATE TABLE cache (
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 @@ CREATE INDEX ix_session_changed ON session (changed);
-- --------------------------------------------------------
---
--- 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 TABLE searches (
);
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);
diff --git a/SQL/sqlite.update.sql b/SQL/sqlite.update.sql
index 41ab0200d..378072c03 100644
--- a/SQL/sqlite.update.sql
+++ b/SQL/sqlite.update.sql
@@ -223,11 +223,11 @@ INSERT INTO contacts (contact_id, user_id, changed, del, name, email, firstname,
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
CREATE TABLE dictionary (
@@ -247,3 +247,42 @@ CREATE TABLE searches (
);
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);
diff --git a/program/include/main.inc b/program/include/main.inc
index 4c24ce31e..3513a07c2 100644
--- a/program/include/main.inc
+++ b/program/include/main.inc
@@ -169,11 +169,17 @@ function rcmail_cache_gc()
// 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));
}
diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php
index 97b080a74..a15a5923c 100644
--- a/program/include/rcube_imap.php
+++ b/program/include/rcube_imap.php
@@ -48,11 +48,11 @@ class rcube_imap
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 @@ class rcube_imap
* @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 @@ class rcube_imap
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 @@ class rcube_imap
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 @@ class rcube_imap
function close()
{
$this->conn->closeConnection();
+ if ($this->mcache)
+ $this->mcache->close();
}
@@ -689,7 +697,7 @@ class rcube_imap
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 @@ class rcube_imap
$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 @@ class rcube_imap
$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 @@ class rcube_imap
'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 @@ class rcube_imap
* @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 @@ class rcube_imap
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';
-
- 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);
- }
- }
+ $page = $page ? $page : $this->list_page;
- // retrieve headers from IMAP
- $a_msg_headers = array();
+ // Use messages cache
+ if ($mcache = $this->get_mcache_engine()) {
+ $msg_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
+
+ if (empty($msg_index))
+ return array();
+
+ $from = ($page-1) * $this->page_size;
+ $to = $from + $this->page_size;
+ $msg_index = array_values($msg_index); // UIDs
+ $is_uid = true;
+ $sorted = true;
+ if ($from || $to)
+ $msg_index = array_slice($msg_index, $from, $to - $from);
+
+ if ($slice)
+ $msg_index = array_slice($msg_index, -$slice, $slice);
+
+ $a_msg_headers = $mcache->get_messages($mailbox, $msg_index);
+ }
+ // retrieve headers from IMAP
// use message index sort as default sorting (for better performance)
- 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,57 +888,53 @@ class rcube_imap
// 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))
return array();
// 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 @@ class rcube_imap
* @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 @@ class rcube_imap
/**
- * Private method for fetching threads data
+ * Method for fetching threads data
+ *
+ * @param string $mailbox Folder name
+ * @param bool $force Use IMAP server, no cache
*
- * @param string $mailbox Mailbox/folder name
* @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 @@ class rcube_imap
* @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 @@ class rcube_imap
}
// 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 @@ class rcube_imap
// 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 @@ class rcube_imap
$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 @@ class rcube_imap
$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 @@ class rcube_imap
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 @@ class rcube_imap
$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 @@ class rcube_imap
/**
- * 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)
{
- // fetch reqested headers from server
- $a_header_index = $this->conn->fetchHeaders(
- $mailbox, $msgs, false, false, $this->get_fetch_headers());
-
- if (empty($a_header_index))
- return 0;
+ if (empty($msgs))
+ return array();
- foreach ($a_header_index as $i => $headers) {
- $a_msg_headers[$headers->uid] = $headers;
+ if (!$force && ($mcache = $this->get_mcache_engine())) {
+ return $mcache->get_messages($mailbox, $msgs, $is_uid);
}
- // 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;
- }
+ // fetch reqested headers from server
+ $index = $this->conn->fetchHeaders(
+ $mailbox, $msgs, $is_uid, false, $this->get_fetch_headers());
- if ($for_remove)
- $this->remove_message_cache($cache_key, $for_remove);
+ if (empty($index))
+ return array();
- // 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);
- }
+ foreach ($index as $headers) {
+ $a_msg_headers[$headers->uid] = $headers;
}
- return count($a_msg_headers);
+ return $a_msg_headers;
}
@@ -1471,7 +1454,7 @@ class rcube_imap
}
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 @@ class rcube_imap
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 @@ class rcube_imap
// 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 @@ class rcube_imap
* 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 @@ class rcube_imap
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);
@@ -1623,99 +1608,6 @@ class rcube_imap
/**
- * @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);
- }
- }
- }
- }
- }
-
-
- /**
* Invoke search request to IMAP server
*
* @param string $mailbox Mailbox name to search in
@@ -1750,8 +1642,8 @@ class rcube_imap
* @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 @@ class rcube_imap
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 @@ class rcube_imap
$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 @@ class rcube_imap
* @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 @@ class rcube_imap
* @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 @@ class rcube_imap
/**
* 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 @@ class rcube_imap
{
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 @@ class rcube_imap
/**
* 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 @@ class rcube_imap
/**
- * 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);
+ if (!strlen($mailbox)) {
+ $mailbox = $this->mailbox;
+ }
- // return cached message structure
- if (is_object($headers) && is_object($headers->structure)) {
- return $headers->structure;
+ // Check internal cache
+ if (!empty($this->icache['message'])) {
+ if (($headers = $this->icache['message']) && $headers->uid == $uid) {
+ return $headers;
+ }
}
- if (!$structure_str) {
- $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
+ $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 = 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;
+ $structure = $headers->bodystructure;
+
+ if (empty($structure))
+ return $headers;
// set message charset from message headers
if ($headers->charset)
@@ -2090,7 +1995,7 @@ class rcube_imap
$structure[1] = $m[2];
}
else
- return false;
+ return $headers;
}
$struct = &$this->_structure_part($structure, 0, '', $headers);
@@ -2103,13 +2008,9 @@ class rcube_imap
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 @@ class rcube_imap
// 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 @@ class rcube_imap
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 @@ class rcube_imap
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 @@ class rcube_imap
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 @@ class rcube_imap
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 @@ class rcube_imap
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 @@ class rcube_imap
{
// 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 @@ class rcube_imap
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 @@ class rcube_imap
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 @@ class rcube_imap
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 @@ class rcube_imap
$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 @@ class rcube_imap
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 @@ class rcube_imap
}
// 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 @@ class rcube_imap
$mailbox = $this->mailbox;
}
- return $this->_uid2id($uid, $mailbox);
+ return $this->uid2id($uid, $mailbox);
}
@@ -3004,7 +2897,7 @@ class rcube_imap
$mailbox = $this->mailbox;
}
- return $this->_id2uid($id, $mailbox);
+ return $this->id2uid($id, $mailbox);
}
@@ -3302,11 +3195,14 @@ class rcube_imap
$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 @@ class rcube_imap
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);
}
@@ -3508,6 +3404,36 @@ class rcube_imap
/**
+ * 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;
+ }
+
+
+ /**
* Returns extended information about the folder
*
* @param string $mailbox Folder name
@@ -3848,7 +3774,7 @@ class rcube_imap
else {
if ($this->cache)
$this->cache->close();
- $this->cache = null;
+ $this->cache = null;
$this->caching = false;
}
}
@@ -3918,419 +3844,50 @@ class rcube_imap
* 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
- *
- * @param string $mailbox Mailbox name
- * @param string $cache_key Internal cache key
- * @return int Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
+ * Getter for messages cache object
*/
- private function check_cache_status($mailbox, $cache_key)
+ private function get_mcache_engine()
{
- 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 ($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']));
+ 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->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']);
+ return $this->mcache;
}
-
- /**
- * @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
+ * Clears the messages cache.
+ *
+ * @param string $mailbox Folder name
+ * @param array $uids Optional message UIDs to remove from cache
*/
- private function get_message_cache_index_min($key, $uids=NULL)
+ function clear_message_cache($mailbox = null, $uids = null)
{
- if (!$this->messages_caching)
- return;
-
- if (!empty($uids) && !is_array($uids)) {
- if ($uids == '*' || $uids == '1:*')
- $uids = NULL;
- else
- $uids = explode(',', $uids);
+ if ($mcache = $this->get_mcache_engine()) {
+ $mcache->clear($mailbox, $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;
- }
-
/* --------------------------------
* encoding/decoding methods
@@ -4627,36 +4184,46 @@ class rcube_imap
/**
- * @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 @@ class rcube_imap
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_message_part
*/
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 @@ class rcube_header_sorter
*/
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 @@ class rcube_header_sorter
$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;
diff --git a/program/include/rcube_imap_cache.php b/program/include/rcube_imap_cache.php
new file mode 100644
index 000000000..f0c3fbb8d
--- /dev/null
+++ b/program/include/rcube_imap_cache.php
@@ -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;
+ }
+ }
+
+}
diff --git a/program/include/rcube_imap_generic.php b/program/include/rcube_imap_generic.php
index 75acc5a3d..65b20254e 100644
--- a/program/include/rcube_imap_generic.php
+++ b/program/include/rcube_imap_generic.php
@@ -48,22 +48,20 @@ class rcube_mail_header
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 @@ class rcube_imap_generic
public $errornum;
public $result;
public $resultcode;
+ public $selected;
public $data = array();
public $flags = array(
'SEEN' => '\\Seen',
@@ -96,7 +95,6 @@ class rcube_imap_generic
'*' => '\\*',
);
- private $selected;
private $fp;
private $host;
private $logged = false;
@@ -235,14 +233,14 @@ class rcube_imap_generic
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 @@ class rcube_imap_generic
$out .= $line;
}
- $line = $a[1][0] . ($escape ? $this->escape($out) : $out);
+ $line = $str . ($escape ? $this->escape($out) : $out);
}
return $line;
@@ -877,12 +875,12 @@ class rcube_imap_generic
/**
* 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 @@ class rcube_imap_generic
}
}
*/
- 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 @@ class rcube_imap_generic
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 @@ class rcube_imap_generic
* 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 @@ class rcube_imap_generic
}
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 @@ class rcube_imap_generic
* @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 @@ class rcube_imap_generic
* Executes CLOSE command
*
* @return boolean True on success, False on error
- * @access public
* @since 0.5
*/
function close()
@@ -1043,7 +1080,6 @@ class rcube_imap_generic
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
- * @access public
*/
function subscribe($mailbox)
{
@@ -1059,7 +1095,6 @@ class rcube_imap_generic
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
- * @access public
*/
function unsubscribe($mailbox)
{
@@ -1075,7 +1110,6 @@ class rcube_imap_generic
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
- * @access public
*/
function deleteFolder($mailbox)
{
@@ -1091,7 +1125,6 @@ class rcube_imap_generic
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
- * @access public
*/
function clearFolder($mailbox)
{
@@ -1116,7 +1149,6 @@ class rcube_imap_generic
* @param string $mailbox Mailbox name
*
* @return int Number of messages, False on error
- * @access public
*/
function countMessages($mailbox, $refresh = false)
{
@@ -1149,7 +1181,6 @@ class rcube_imap_generic
* @param string $mailbox Mailbox name
*
* @return int Number of messages, False on error
- * @access public
*/
function countRecent($mailbox)
{
@@ -1172,7 +1203,6 @@ class rcube_imap_generic
* @param string $mailbox Mailbox name
*
* @return int Number of messages, False on error
- * @access public
*/
function countUnseen($mailbox)
{
@@ -1203,7 +1233,6 @@ class rcube_imap_generic
* @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())
@@ -1235,6 +1264,37 @@ class rcube_imap_generic
return false;
}
+ /**
+ * Executes ENABLE command (RFC5161)
+ *
+ * @param mixed $extension Extension name to enable (or array of names)
+ *
+ * @return array|bool List of enabled extensions, False on error
+ * @since 0.6
+ */
+ function enable($extension)
+ {
+ if (empty($extension))
+ return false;
+
+ if (!$this->hasCapability('ENABLE'))
+ return false;
+
+ if (!is_array($extension))
+ $extension = array($extension);
+
+ list($code, $response) = $this->execute('ENABLE', $extension);
+
+ if ($code == self::ERROR_OK && preg_match('/\* ENABLED /i', $response)) {
+ $response = substr($response, 10); // remove prefix "* ENABLED "
+ $result = (array) $this->tokenizeResponse($response);
+
+ return $result;
+ }
+
+ return false;
+ }
+
function sort($mailbox, $field, $add='', $is_uid=FALSE, $encoding = 'US-ASCII')
{
$field = strtoupper($field);
@@ -1472,7 +1532,6 @@ class rcube_imap_generic
* @param int $uid Message unique identifier (UID)
*
* @return int Message sequence identifier
- * @access public
*/
function UID2ID($mailbox, $uid)
{
@@ -1492,12 +1551,11 @@ class rcube_imap_generic
* @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,47 +1573,59 @@ class rcube_imap_generic
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 @@ class rcube_imap_generic
$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;
+
+ $line .= $str;
+ }
- // 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);
+ // 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;
+ }
}
}
}
- }
-
- // 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;
+ else if ($name == 'MODSEQ') {
+ $result[$id]->modseq = $value[0];
}
-
- // handle FLAGS reply after headers (AOL, Zimbra?)
- if (preg_match('/\s+FLAGS \((.*)\)\)$/', $line, $matches)) {
- $flags_a = $this->tokenizeResponse($matches[1]);
- break;
+ else if ($name == 'ENVELOPE') {
+ $result[$id]->envelope = $value;
}
-
- if (ord($line[0])<=32) {
- $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($line);
- } else {
- $lines[++$ln] = trim($line);
+ 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;
}
- // 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 @@ class rcube_imap_generic
$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 @@ class rcube_imap_generic
$params .= 'RETURN (' . implode(' ', $items) . ')';
}
if (!empty($criteria)) {
+ $modseq = stripos($criteria, 'MODSEQ') !== false;
$params .= ($params ? ' ' : '') . $criteria;
}
else {
@@ -2054,20 +2133,27 @@ class rcube_imap_generic
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) {
// Skip prefix: ... (TAG "A285") UID ...
$this->tokenizeResponse($response, $return_uid ? 2 : 1);
$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 @@ class rcube_imap_generic
*
* @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 @@ class rcube_imap_generic
*
* @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 @@ class rcube_imap_generic
*
* @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 @@ class rcube_imap_generic
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 @@ class rcube_imap_generic
$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 @@ class rcube_imap_generic
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 @@ class rcube_imap_generic
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 @@ class rcube_imap_generic
*
* @return boolean True on success, False on failure
*
- * @access public
* @since 0.5-beta
*/
function setACL($mailbox, $user, $acl)
@@ -2684,7 +2739,6 @@ class rcube_imap_generic
*
* @return boolean True on success, False on failure
*
- * @access public
* @since 0.5-beta
*/
function deleteACL($mailbox, $user)
@@ -2702,7 +2756,6 @@ class rcube_imap_generic
* @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 @@ class rcube_imap_generic
* @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 @@ class rcube_imap_generic
* @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 @@ class rcube_imap_generic
* @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 @@ class rcube_imap_generic
*
* @return boolean True on success, False on failure
*
- * @access public
* @since 0.5-beta
*/
function deleteMetadata($mailbox, $entries)
@@ -2862,7 +2911,6 @@ class rcube_imap_generic
*
* @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 @@ class rcube_imap_generic
* 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 @@ class rcube_imap_generic
*
* @return boolean True on success, False on failure
*
- * @access public
* @since 0.5-beta
*/
function deleteAnnotation($mailbox, $data)
@@ -3008,7 +3054,6 @@ class rcube_imap_generic
*
* @return array Annotations result on success, NULL on error
*
- * @access public
* @since 0.5-beta
*/
function getAnnotation($mailbox, $entries, $attribs)
@@ -3093,10 +3138,103 @@ class rcube_imap_generic
}
/**
+ * 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 @@ class rcube_imap_generic
* @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 @@ class rcube_imap_generic
$response = $noresp ? null : '';
if (!empty($arguments)) {
- $query .= ' ' . implode(' ', $arguments);
+ foreach ($arguments as $arg) {
+ $query .= ' ' . self::r_implode($arg);
+ }
}
// Send command
@@ -3173,7 +3312,6 @@ class rcube_imap_generic
* @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 @@ class rcube_imap_generic
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 @@ class rcube_imap_generic
// 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 @@ class rcube_imap_generic
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]));
}
@@ -3255,6 +3395,23 @@ class rcube_imap_generic
return $num == 1 ? $result[0] : $result;
}
+ static function r_implode($element)
+ {
+ $string = '';
+
+ if (is_array($element)) {
+ reset($element);
+ while (list($key, $value) = each($element)) {
+ $string .= ' ' . self::r_implode($value);
+ }
+ }
+ else {
+ return $element;
+ }
+
+ return '(' . trim($string) . ')';
+ }
+
private function _xor($string, $string2)
{
$result = '';
@@ -3348,7 +3505,6 @@ class rcube_imap_generic
*
* @param boolean $debug New value for the debugging flag.
*
- * @access public
* @since 0.5-stable
*/
function setDebug($debug, $handler = null)
@@ -3362,7 +3518,6 @@ class rcube_imap_generic
*
* @param string $message Debug mesage text.
*
- * @access private
* @since 0.5-stable
*/
private function debug($message)
diff --git a/program/include/rcube_message.php b/program/include/rcube_message.php
index 4e2595550..2ec386c6b 100644
--- a/program/include/rcube_message.php
+++ b/program/include/rcube_message.php
@@ -77,7 +77,7 @@ class rcube_message
$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 @@ class rcube_message
'_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);
diff --git a/program/include/rcube_mime_struct.php b/program/include/rcube_mime_struct.php
index c64942566..ed28af31f 100644
--- a/program/include/rcube_mime_struct.php
+++ b/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)
- {
- 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)
+ function getStructurePartType($structure, $part)
{
- 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 @@ class rcube_mime_struct
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 @@ class rcube_mime_struct
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 @@ class rcube_mime_struct
return '';
}
- function getPartArray($a, $part)
+ function getStructurePartArray($a, $part)
{
if (!is_array($a)) {
return false;
@@ -137,9 +67,7 @@ class rcube_mime_struct
else
return $a;
}
- else if (($part==0) || (empty($part))) {
+ else if (($part == 0) || (empty($part))) {
return $a;
}
}
-
-}
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index ae0d3a55c..ab4b2907a 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -1454,7 +1454,7 @@ function rcmail_send_mdn($message, &$smtp_error)
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();
diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc
index 1472a9e61..ba172c7ae 100644
--- a/program/steps/mail/show.inc
+++ b/program/steps/mail/show.inc
@@ -77,7 +77,7 @@ if ($uid = get_input_value('_uid', RCUBE_INPUT_GET)) {
// 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'])