- 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
release-0.7
alecpl 13 years ago
parent b104e39f34
commit 80152b333c

@ -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

@ -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

@ -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

@ -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` (

@ -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 */;

@ -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)
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"

@ -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);

@ -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);

@ -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);

@ -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));
}

File diff suppressed because it is too large Load Diff

@ -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;
}
}
}

@ -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;
@ -238,11 +236,11 @@ class rcube_imap_generic
function multLine($line, $escape = false)
{
$line = rtrim($line);
if (preg_match('/\{[0-9]+\}$/', $line)) {
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;
@ -878,11 +876,11 @@ class rcube_imap_generic
* Executes SELECT command (if mailbox is already not in selected state)
*
* @param string $mailbox Mailbox name
* @param array $qresync_data QRESYNC data (RFC5162)
*
* @return boolean True on success, false on error
* @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,13 +921,41 @@ 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)) {
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('/^\* OK \[PERMANENTFLAGS \(([^\)]+)\)\]/iU', $line, $match)) {
else if (preg_match('/^(HIGHESTMODSEQ) ([0-9]+)/i', $line, $match)) {
$this->data[strtoupper($match[1])] = (string) $match[2];
}
else if (preg_match('/^(NOMODSEQ)/i', $line, $match)) {
$this->data[strtoupper($match[1])] = true;
}
else if (preg_match('/^PERMANENTFLAGS \(([^\)]+)\)/iU', $line, $match)) {
$this->data['PERMANENTFLAGS'] = explode(' ', $match[1]);
}
}
// QRESYNC FETCH response (RFC5162)
else if (preg_match('/^\* ([0-9+]) FETCH/i', $line, $match)) {
$line = substr($line, strlen($match[0]));
$fetch_data = $this->tokenizeResponse($line, 1);
$data = array('id' => $match[1]);
for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) {
$data[strtolower($fetch_data[$i])] = $fetch_data[$i+1];
}
$this->data['QRESYNC'][$data['uid']] = $data;
}
// QRESYNC VANISHED response (RFC5162)
else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
$line = substr($line, strlen($match[0]));
$v_data = $this->tokenizeResponse($line, 1);
$this->data['VANISHED'] = $v_data;
}
}
$this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
@ -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,7 +1551,6 @@ class rcube_imap_generic
* @param int $uid Message sequence identifier
*
* @return int Message unique identifier
* @access public
*/
function ID2UID($mailbox, $id)
{
@ -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();
$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 = '';
while (strlen($out) < $bytes) {
$out = $this->readBytes($bytes);
if ($out === NULL)
break;
$line .= $out;
}
if (preg_match('/^\* [0-9]+ FETCH \((.*) BODY/sU', $line, $matches)) {
$str = $matches[1];
$str = $this->readLine(4096);
if ($str === false)
break;
while (list($name, $value) = $this->tokenizeResponse($str, 2)) {
$line .= $str;
}
// Tokenize response and assign to object properties
while (list($name, $value) = $this->tokenizeResponse($line, 2)) {
if ($name == 'UID') {
$result[$id]->uid = intval($value);
}
else if ($name == 'RFC822.SIZE') {
$result[$id]->size = intval($value);
}
else if ($name == 'RFC822.TEXT') {
$result[$id]->body = $value;
}
else if ($name == 'INTERNALDATE') {
$result[$id]->internaldate = $value;
$result[$id]->date = $value;
$result[$id]->timestamp = $this->StrToTime($value);
}
else if ($name == 'FLAGS') {
$flags_a = $value;
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;
}
}
// 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];
}
// 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);
else if ($name == 'MODSEQ') {
$result[$id]->modseq = $value[0];
}
else if ($name == 'ENVELOPE') {
$result[$id]->envelope = $value;
}
else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) {
if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) {
$value = array($value);
}
$result[$id]->bodystructure = $value;
}
// 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 == 'RFC822') {
$result[$id]->body = $value;
}
// handle FLAGS reply after headers (AOL, Zimbra?)
if (preg_match('/\s+FLAGS \((.*)\)\)$/', $line, $matches)) {
$flags_a = $this->tokenizeResponse($matches[1]);
break;
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;
}
if (ord($line[0])<=32) {
$lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($line);
} else {
$lines[++$ln] = trim($line);
}
// patch from "Maksim Rubis" <siburny@hotmail.com>
} while ($line[0] != ')' && !$this->startsWith($line, $key, true));
if (strncmp($line, $key, strlen($key))) {
// process header, fill rcube_mail_header obj.
// initialize
if (is_array($headers)) {
reset($headers);
while (list($k, $bar) = each($headers)) {
$headers[$k] = '';
// create array with header field:data
if (!empty($headers)) {
$headers = explode("\n", trim($headers));
foreach ($headers as $hid => $resln) {
if (ord($resln[0]) <= 32) {
$lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
} else {
$lines[++$ln] = trim($resln);
}
}
// create array with header field:data
while (list($lines_key, $str) = each($lines)) {
list($field, $string) = explode(':', $str, 2);
@ -1723,44 +1804,41 @@ 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);
@ -2043,6 +2121,7 @@ class rcube_imap_generic
$params .= 'RETURN (' . implode(' ', $items) . ')';
}
if (!empty($criteria)) {
$modseq = stripos($criteria, 'MODSEQ') !== false;
$params .= ($params ? ' ' : '') . $criteria;
}
else {
@ -2061,13 +2140,20 @@ class rcube_imap_generic
$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)
@ -3092,11 +3137,104 @@ class rcube_imap_generic
return NULL;
}
/**
* Returns BODYSTRUCTURE for the specified message.
*
* @param string $mailbox Folder name
* @param int $id Message sequence number or UID
* @param bool $is_uid True if $id is an UID
*
* @return array/bool Body structure array or False on error.
* @since 0.6
*/
function getStructure($mailbox, $id, $is_uid = false)
{
$result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE'));
if (is_array($result)) {
$result = array_shift($result);
return $result->bodystructure;
}
return false;
}
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)

@ -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);

@ -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;
@ -141,5 +71,3 @@ class rcube_mime_struct
return $a;
}
}
}

@ -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();

@ -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'])

Loading…
Cancel
Save