Commit db62ed5d authored by Jim Nelson's avatar Jim Nelson

FETCH BODY[section]<partial> support.

This adds support for retrieving partial header and body blocks straight from the email, and
therefore support to pull the References header from a message (which, for some reason, IMAP
doesn't support or include in the FETCH ENVELOPE command).  This is necessary for email conversations (#3808).

This required a change to the database schema, meaning old databases will need to be blown
away before starting.
parent ab69b20b
......@@ -33,6 +33,7 @@ CREATE TABLE MessageTable (
message_id TEXT,
in_reply_to TEXT,
reference_ids TEXT,
subject TEXT,
......
......@@ -408,9 +408,12 @@ class ImapConsole : Gtk.Window {
Geary.Imap.FetchBodyDataType fields = new Geary.Imap.FetchBodyDataType(
Geary.Imap.FetchBodyDataType.SectionPart.HEADER_FIELDS, args[1:args.length]);
Gee.List<Geary.Imap.FetchBodyDataType> list = new Gee.ArrayList<Geary.Imap.FetchBodyDataType>();
list.add(fields);
cx.send_async.begin(new Geary.Imap.FetchCommand(
new Geary.Imap.MessageSet.custom(args[0]), null, { fields }), null, on_fetch);
new Geary.Imap.MessageSet.custom(args[0]), null, list), null, on_fetch);
}
private void on_fetch(Object? source, AsyncResult result) {
......
......@@ -76,6 +76,7 @@ public class Geary.Email : Object {
// REFERENCES
public Geary.RFC822.MessageID? message_id { get; private set; default = null; }
public Geary.RFC822.MessageID? in_reply_to { get; private set; default = null; }
public Geary.RFC822.MessageIDList? references { get; private set; default = null; }
// SUBJECT
public Geary.RFC822.Subject? subject { get; private set; default = null; }
......@@ -126,9 +127,11 @@ public class Geary.Email : Object {
fields |= Field.RECEIVERS;
}
public void set_references(Geary.RFC822.MessageID? message_id, Geary.RFC822.MessageID? in_reply_to) {
public void set_full_references(Geary.RFC822.MessageID? message_id, Geary.RFC822.MessageID? in_reply_to,
Geary.RFC822.MessageIDList? references) {
this.message_id = message_id;
this.in_reply_to = in_reply_to;
this.references = references;
fields |= Field.REFERENCES;
}
......
......@@ -88,82 +88,6 @@ public class Geary.Imap.CloseCommand : Command {
}
}
public class Geary.Imap.FetchCommand : Command {
public const string NAME = "fetch";
public const string UID_NAME = "uid fetch";
public FetchCommand(MessageSet msg_set, FetchDataType[]? data_items,
FetchBodyDataType[]? body_data_items) {
base (msg_set.is_uid ? UID_NAME : NAME);
add(msg_set.to_parameter());
int data_items_length = (data_items != null) ? data_items.length : 0;
int body_data_items_length = (body_data_items != null) ? body_data_items.length : 0;
// if only one item being fetched, pass that as a singleton parameter, otherwise pass them
// all as a list
if (data_items_length == 1 && body_data_items_length == 0) {
add(data_items[0].to_parameter());
} else if (data_items_length == 0 && body_data_items_length == 1) {
add(body_data_items[0].to_parameter());
} else {
ListParameter list = new ListParameter(this);
if (data_items != null) {
foreach (FetchDataType data_item in data_items)
list.add(data_item.to_parameter());
}
if (body_data_items != null) {
foreach (FetchBodyDataType body_data_item in body_data_items)
list.add(body_data_item.to_parameter());
}
add(list);
}
}
public FetchCommand.from_collection(MessageSet msg_set, Gee.Collection<FetchDataType>? data_items,
Gee.Collection<FetchBodyDataType>? body_data_items) {
base (msg_set.is_uid ? UID_NAME : NAME);
add(msg_set.to_parameter());
int data_items_length = (data_items != null) ? data_items.size : 0;
int body_data_items_length = (body_data_items != null) ? body_data_items.size : 0;
// see note in unadorned ctor for reasoning here
if (data_items_length == 1 && body_data_items_length == 0) {
foreach (FetchDataType data_type in data_items) {
add(data_type.to_parameter());
break;
}
} else if (data_items_length == 0 && body_data_items_length == 1) {
foreach (FetchBodyDataType body_data_type in body_data_items) {
add(body_data_type.to_parameter());
break;
}
} else {
ListParameter data_item_list = new ListParameter(this);
if (data_items != null) {
foreach (FetchDataType data_item in data_items)
data_item_list.add(data_item.to_parameter());
}
if (body_data_items != null) {
foreach (FetchBodyDataType body_data_item in body_data_items)
data_item_list.add(body_data_item.to_parameter());
}
add(data_item_list);
}
}
}
public class Geary.Imap.StatusCommand : Command {
public const string NAME = "status";
......
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Imap.FetchCommand : Command {
public const string NAME = "fetch";
public const string UID_NAME = "uid fetch";
public FetchCommand(MessageSet msg_set, FetchDataType[]? data_items,
Gee.List<FetchBodyDataType>? body_data_items) {
base (msg_set.is_uid ? UID_NAME : NAME);
add(msg_set.to_parameter());
int data_items_length = (data_items != null) ? data_items.length : 0;
int body_items_length = (body_data_items != null) ? body_data_items.size : 0;
// if only one item being fetched, pass that as a singleton parameter, otherwise pass them
// as a list
if (data_items_length == 1 && body_items_length == 0) {
add(data_items[0].to_parameter());
} else if (data_items_length == 0 && body_items_length == 1) {
add(body_data_items[0].to_parameter());
} else {
ListParameter list = new ListParameter(this);
if (data_items_length > 0) {
foreach (FetchDataType data_item in data_items)
list.add(data_item.to_parameter());
}
if (body_items_length > 0) {
foreach (FetchBodyDataType body_item in body_data_items)
list.add(body_item.to_parameter());
}
add(list);
}
}
public FetchCommand.from_collection(MessageSet msg_set, Gee.List<FetchDataType>? data_items,
Gee.List<FetchBodyDataType>? body_data_items) {
base (msg_set.is_uid ? UID_NAME : NAME);
add(msg_set.to_parameter());
int data_items_length = (data_items != null) ? data_items.size : 0;
int body_items_length = (body_data_items != null) ? body_data_items.size : 0;
// see note in unadorned ctor for reasoning here
if (data_items_length == 1 && body_items_length == 0) {
add(data_items[0].to_parameter());
} else if (data_items_length == 0 && body_items_length == 1) {
add(body_data_items[0].to_parameter());
} else {
ListParameter list = new ListParameter(this);
if (data_items_length > 0) {
foreach (FetchDataType data_item in data_items)
list.add(data_item.to_parameter());
}
if (body_items_length > 0) {
foreach (FetchBodyDataType body_item in body_data_items)
list.add(body_item.to_parameter());
}
add(list);
}
}
}
......@@ -40,7 +40,7 @@ public abstract class Geary.Imap.FetchDataDecoder {
return decode_literal(literalp);
// bad news; this means this function isn't handling a Parameter type properly
assert_not_reached();;
assert_not_reached();
}
protected virtual MessageData decode_string(StringParameter param) throws ImapError {
......
......@@ -16,6 +16,7 @@ public class Geary.Imap.FetchResults : Geary.Imap.CommandResults {
public int msg_num { get; private set; }
private Gee.Map<FetchDataType, MessageData> map = new Gee.HashMap<FetchDataType, MessageData>();
private Gee.List<Memory.AbstractBuffer> body_data = new Gee.ArrayList<Memory.AbstractBuffer>();
public FetchResults(StatusResponse status_response, int msg_num) {
base (status_response);
......@@ -40,16 +41,23 @@ public class Geary.Imap.FetchResults : Geary.Imap.CommandResults {
// and the structured data itself
for (int ctr = 0; ctr < list.get_count(); ctr += 2) {
StringParameter data_item_param = list.get_as_string(ctr);
FetchDataType data_item = FetchDataType.decode(data_item_param.value);
FetchDataDecoder? decoder = data_item.get_decoder();
if (decoder == null) {
debug("Unable to decode fetch response for \"%s\": No decoder available",
data_item.to_string());
if (FetchBodyDataType.is_fetch_body(data_item_param)) {
// FETCH body data items are merely a literal of all requested fields formatted
// in RFC822 header format
results.body_data.add(list.get_as_literal(ctr + 1).get_buffer());
} else {
FetchDataType data_item = FetchDataType.decode(data_item_param.value);
FetchDataDecoder? decoder = data_item.get_decoder();
if (decoder == null) {
debug("Unable to decode fetch response for \"%s\": No decoder available",
data_item.to_string());
continue;
}
continue;
results.set_data(data_item, decoder.decode(list.get_required(ctr + 1)));
}
results.set_data(data_item, decoder.decode(list.get_required(ctr + 1)));
}
return results;
......@@ -85,6 +93,10 @@ public class Geary.Imap.FetchResults : Geary.Imap.CommandResults {
return map.get(data_item);
}
public Gee.List<Memory.AbstractBuffer> get_body_data() {
return body_data.read_only_view;
}
public int get_count() {
return map.size;
}
......
......@@ -105,6 +105,12 @@ public class Geary.Imap.FetchBodyDataType {
return builder.str;
}
public static bool is_fetch_body(StringParameter items) {
string strd = items.value.down();
return strd.has_prefix("body[") || strd.has_prefix("body.peek[");
}
public string to_string() {
return (!is_peek ? "body[%s%s]" : "body.peek[%s%s]").printf(section_part.serialize(),
serialize_field_names());
......
......@@ -65,7 +65,6 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
body_data_type_list);
CommandResponse resp = yield context.session.send_command_async(fetch_cmd, cancellable);
if (resp.status_response.status != Status.OK) {
throw new ImapError.SERVER_ERROR("Server error for %s: %s", fetch_cmd.to_string(),
resp.to_string());
......@@ -103,7 +102,6 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
data_type_list, body_data_type_list);
CommandResponse resp = yield context.session.send_command_async(fetch_cmd, cancellable);
if (resp.status_response.status != Status.OK) {
throw new ImapError.SERVER_ERROR("Server error for %s: %s", fetch_cmd.to_string(),
resp.to_string());
......@@ -157,38 +155,47 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
// The assumption here is that because ENVELOPE is such a common fetch command, the
// server will have optimizations for it, whereas if we called for each header in the
// envelope separately, the server has to chunk harder parsing the RFC822 header
bool using_envelope = false;
if (fields.is_all_set(Geary.Email.Field.ENVELOPE)) {
data_types_list.add(FetchDataType.ENVELOPE);
// remove those flags and process any remaining
fields = fields.clear(Geary.Email.Field.ENVELOPE);
using_envelope = true;
}
// pack all the needed headers into a single FetchBodyDataType
string[] field_names = new string[0];
foreach (Geary.Email.Field field in Geary.Email.Field.all()) {
switch (fields & field) {
case Geary.Email.Field.DATE:
body_data_types_list.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.HEADER_FIELDS, { "Date" }));
field_names += "Date";
break;
case Geary.Email.Field.ORIGINATORS:
body_data_types_list.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.HEADER_FIELDS, { "From", "Sender", "Reply-To" }));
field_names += "From";
field_names += "Sender";
field_names += "Reply-To";
break;
case Geary.Email.Field.RECEIVERS:
body_data_types_list.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.HEADER_FIELDS, { "To", "Cc", "Bcc" }));
field_names += "To";
field_names += "Cc";
field_names += "Bcc";
break;
case Geary.Email.Field.REFERENCES:
body_data_types_list.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.HEADER_FIELDS, { "Message-ID", "In-Reply-To" }));
field_names += "References";
if (!using_envelope) {
field_names += "Message-ID";
field_names += "In-Reply-To";
}
break;
case Geary.Email.Field.SUBJECT:
body_data_types_list.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.HEADER_FIELDS, { "Subject" }));
field_names += "Subject";
break;
case Geary.Email.Field.HEADER:
......@@ -215,15 +222,25 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
assert_not_reached();
}
}
if (field_names.length > 0) {
body_data_types_list.add(new FetchBodyDataType(
FetchBodyDataType.SectionPart.HEADER_FIELDS, field_names));
}
}
private static void fetch_results_to_email(FetchResults res, Geary.Email.Field fields,
Geary.Email email) {
Geary.Email email) throws Error {
// accumulate these to submit Imap.EmailProperties all at once
Geary.Imap.MessageFlags? flags = null;
InternalDate? internaldate = null;
RFC822.Size? rfc822_size = null;
// accumulate these to submit References all at once
RFC822.MessageID? message_id = null;
RFC822.MessageID? in_reply_to = null;
RFC822.MessageIDList? references = null;
foreach (FetchDataType data_type in res.get_all_types()) {
MessageData? data = res.get_data(data_type);
if (data == null)
......@@ -245,8 +262,10 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
if ((fields & Geary.Email.Field.RECEIVERS) != 0)
email.set_receivers(envelope.to, envelope.cc, envelope.bcc);
if ((fields & Geary.Email.Field.REFERENCES) != 0)
email.set_references(envelope.message_id, envelope.in_reply_to);
if ((fields & Geary.Email.Field.REFERENCES) != 0) {
message_id = envelope.message_id;
in_reply_to = envelope.in_reply_to;
}
break;
case FetchDataType.RFC822_HEADER:
......@@ -275,8 +294,81 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
}
}
// fields_to_fetch_data_types() will always generate a single FetchBodyDataType for all
// the header fields it needs
Gee.List<Memory.AbstractBuffer> body_data = res.get_body_data();
if (body_data.size > 0) {
assert(body_data.size == 1);
RFC822.Header headers = new RFC822.Header(body_data[0]);
// DATE
string? value = headers.get_header("Date");
if (!String.is_empty(value))
email.set_send_date(new RFC822.Date(value));
// ORIGINATORS
RFC822.MailboxAddresses? from = null;
RFC822.MailboxAddresses? sender = null;
RFC822.MailboxAddresses? reply_to = null;
value = headers.get_header("From");
if (!String.is_empty(value))
from = new RFC822.MailboxAddresses.from_rfc822_string(value);
value = headers.get_header("Sender");
if (!String.is_empty(value))
sender = new RFC822.MailboxAddresses.from_rfc822_string(value);
value = headers.get_header("Reply-To");
if (!String.is_empty(value))
reply_to = new RFC822.MailboxAddresses.from_rfc822_string(value);
email.set_originators(from, sender, reply_to);
// RECEIVERS
RFC822.MailboxAddresses? to = null;
RFC822.MailboxAddresses? cc = null;
RFC822.MailboxAddresses? bcc = null;
value = headers.get_header("To");
if (!String.is_empty(value))
to = new RFC822.MailboxAddresses.from_rfc822_string(value);
value = headers.get_header("Cc");
if (!String.is_empty(value))
cc = new RFC822.MailboxAddresses.from_rfc822_string(value);
value = headers.get_header("Bcc");
if (!String.is_empty(value))
bcc = new RFC822.MailboxAddresses.from_rfc822_string(value);
email.set_receivers(to, cc, bcc);
// REFERENCES
// (Note that it's possible the request used an IMAP ENVELOPE, in which case only the
// References header will be present if REFERENCES were required.)
value = headers.get_header("Message-ID");
if (!String.is_empty(value))
message_id = new RFC822.MessageID(value);
value = headers.get_header("In-Reply-To");
if (!String.is_empty(value))
in_reply_to = new RFC822.MessageID(value);
value = headers.get_header("References");
if (!String.is_empty(value))
references = new RFC822.MessageIDList(value);
// SUBJECT
value = headers.get_header("Subject");
if (!String.is_empty(value))
email.set_message_subject(new RFC822.Subject(value));
}
if (flags != null && internaldate != null && rfc822_size != null)
email.set_email_properties(new Geary.Imap.EmailProperties(flags, internaldate, rfc822_size));
email.set_full_references(message_id, in_reply_to, references);
}
}
......
......@@ -13,10 +13,46 @@
public interface Geary.RFC822.MessageData : Geary.Common.MessageData {
}
public class Geary.RFC822.MessageID : Geary.Common.StringMessageData, Geary.RFC822.MessageData {
public class Geary.RFC822.MessageID : Geary.Common.StringMessageData, Geary.RFC822.MessageData,
Geary.Equalable {
public MessageID(string value) {
base (value);
}
public bool equals(Equalable e) {
MessageID? message_id = e as MessageID;
if (message_id == null)
return false;
if (this == message_id)
return true;
return value == message_id.value;
}
}
public class Geary.RFC822.MessageIDList : Geary.Common.StringMessageData, Geary.RFC822.MessageData {
private Gee.List<MessageID>? list = null;
public MessageIDList(string value) {
base (value);
}
public Gee.List<MessageID> decoded() {
if (list != null)
return list;
list = new Gee.ArrayList<MessageID>(Equalable.equal_func);
string[] ids = value.split(" ");
foreach (string id in ids) {
id = id.strip();
if (!String.is_empty(id))
list.add(new MessageID(id));
}
return list;
}
}
public class Geary.RFC822.Date : Geary.RFC822.MessageData, Geary.Common.MessageData {
......@@ -55,9 +91,49 @@ public class Geary.RFC822.Subject : Geary.Common.StringMessageData, Geary.RFC822
}
public class Geary.RFC822.Header : Geary.Common.BlockMessageData, Geary.RFC822.MessageData {
private GMime.Message? message = null;
private string[]? names = null;
public Header(Geary.Memory.AbstractBuffer buffer) {
base ("RFC822.Header", buffer);
}
private unowned GMime.HeaderList get_headers() throws RFC822Error {
if (message != null)
return message.get_header_list();
GMime.Parser parser = new GMime.Parser.with_stream(
new GMime.StreamMem.with_buffer(buffer.get_array()));
parser.set_respect_content_length(false);
parser.set_scan_from(false);
message = parser.construct_message();
if (message == null)
throw new RFC822Error.INVALID("Unable to parse RFC 822 headers");
return message.get_header_list();
}
public string get_header(string name) throws RFC822Error {
return get_headers().get(name);
}
public string[] get_header_names() throws RFC822Error {
if (names != null)
return names;
names = new string[0];
unowned GMime.HeaderIter iter;
if (!get_headers().get_iter(out iter))
return names;
do {
names += iter.get_name();
} while (iter.next());
return names;
}
}
public class Geary.RFC822.Text : Geary.Common.BlockMessageData, Geary.RFC822.MessageData {
......
......@@ -21,6 +21,7 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
public string? message_id { get; set; }
public string? in_reply_to { get; set; }
public string? references { get; set; }
public string? subject { get; set; }
......@@ -97,8 +98,10 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
}
if ((fields & Geary.Email.Field.REFERENCES) != 0) {
email.set_references((message_id != null) ? new RFC822.MessageID(message_id) : null,
(in_reply_to != null) ? new RFC822.MessageID(in_reply_to) : null);
email.set_full_references(
(message_id != null) ? new RFC822.MessageID(message_id) : null,
(in_reply_to != null) ? new RFC822.MessageID(in_reply_to) : null,
(references != null) ? new RFC822.MessageIDList(references) : null);
}
if (((fields & Geary.Email.Field.SUBJECT) != 0) && (subject != null))
......
......@@ -23,6 +23,7 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
MESSAGE_ID,
IN_REPLY_TO,
REFERENCES,
SUBJECT,
......@@ -43,8 +44,8 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
SQLHeavy.Query query = locked.prepare(
"INSERT INTO MessageTable "
+ "(fields, date_field, date_time_t, from_field, sender, reply_to, to_field, cc, bcc, "
+ "message_id, in_reply_to, subject, header, body) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ "message_id, in_reply_to, reference_ids, subject, header, body) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
query.bind_int(0, row.fields);
query.bind_string(1, row.date);
query.bind_int64(2, row.date_time_t);
......@@ -56,9 +57,10 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
query.bind_string(8, row.bcc);
query.bind_string(9, row.message_id);
query.bind_string(10, row.in_reply_to);
query.bind_string(11, row.subject);
query.bind_string(12, row.header);
query.bind_string(13, row.body);
query.bind_string(11, row.references);
query.bind_string(12, row.subject);
query.bind_string(13, row.header);
query.bind_string(14, row.body);
int64 id = yield query.execute_insert_async(cancellable);
locked.set_commit_required();
......@@ -116,10 +118,11 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
if (row.fields.is_any_set(Geary.Email.Field.REFERENCES)) {
query = locked.prepare(
"UPDATE MessageTable SET message_id=?, in_reply_to=? WHERE id=?");
"UPDATE MessageTable SET message_id=?, in_reply_to=?, reference_ids = ? WHERE id=?");
query.bind_string(0, row.message_id);
query.bind_string(1, row.in_reply_to);
query.bind_int64(2, row.id);
query.bind_string(2, row.references);
query.bind_int64(3, row.id);
yield query.execute_async(cancellable);
}
......@@ -242,7 +245,7 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
break;
case Geary.Email.Field.REFERENCES:
append = "message_id, in_reply_to";
append = "message_id, in_reply_to, reference_ids";
break;
case Geary.Email.Field.SUBJECT:
......
......@@ -42,6 +42,7 @@ def build(bld):
'../engine/imap/command/imap-command-response.vala',
'../engine/imap/command/imap-commands.vala',
'../engine/imap/command/imap-command.vala',
'../engine/imap/command/imap-fetch-command.vala',
'../engine/imap/decoders/imap-command-results.vala',
'../engine/imap/decoders/imap-fetch-data-decoder.vala',
'../engine/imap/decoders/imap-fetch-results.vala',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment