Added ability to FETCH partial body sections and headers.

This adds facilities to issue FETCH BODY<partial> syntax commands to IMAP server.  Only
thing that is missing is the ability to specify body MIME parts, which can be added later.
(At that time we'll want to include support for the FETCH BODYSTRUCTURE command.)

This is a necessary component for threading (#3808).
This commit is contained in:
Jim Nelson 2011-10-13 18:45:24 -07:00
parent 8ae5dbb20a
commit d74ed8456a
5 changed files with 229 additions and 36 deletions

View file

@ -87,6 +87,7 @@ class ImapConsole : Gtk.Window {
"examine",
"fetch",
"uid-fetch",
"fetch-fields",
"help",
"exit",
"quit",
@ -167,6 +168,10 @@ class ImapConsole : Gtk.Window {
close(cmd, args);
break;
case "fetch-fields":
fetch_fields(cmd, args);
break;
case "help":
foreach (string cmdname in cmdnames)
print_console_line(cmdname);
@ -393,7 +398,19 @@ class ImapConsole : Gtk.Window {
for (int ctr = 1; ctr < args.length; ctr++)
data_items += Geary.Imap.FetchDataType.decode(args[ctr]);
cx.send_async.begin(new Geary.Imap.FetchCommand(msg_set, data_items), null, on_fetch);
cx.send_async.begin(new Geary.Imap.FetchCommand(msg_set, data_items, null), null, on_fetch);
}
private void fetch_fields(string cmd, string[] args) throws Error {
check_min_connected(cmd, args, 2, "<message-span> <field-name...>");
status("Fetching fields %s".printf(args[0]));
Geary.Imap.FetchBodyDataType fields = new Geary.Imap.FetchBodyDataType(
Geary.Imap.FetchBodyDataType.SectionPart.HEADER_FIELDS, args[1:args.length]);
cx.send_async.begin(new Geary.Imap.FetchCommand(
new Geary.Imap.MessageSet.custom(args[0]), null, { fields }), null, on_fetch);
}
private void on_fetch(Object? source, AsyncResult result) {

View file

@ -92,39 +92,72 @@ 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) {
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());
assert(data_items.length > 0);
if (data_items.length == 1) {
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 data_item_list = new ListParameter(this);
foreach (FetchDataType data_item in data_items)
data_item_list.add(data_item.to_parameter());
ListParameter list = new ListParameter(this);
add(data_item_list);
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) {
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());
assert(data_items.size > 0);
if (data_items.size == 1) {
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);
foreach (FetchDataType data_item in data_items)
data_item_list.add(data_item.to_parameter());
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);
}

View file

@ -0,0 +1,113 @@
/* 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.FetchBodyDataType {
public enum SectionPart {
NONE,
HEADER,
HEADER_FIELDS,
HEADER_FIELDS_NOT,
MIME,
TEXT;
public string serialize() {
switch (this) {
case NONE:
return "";
case HEADER:
return "header";
case HEADER_FIELDS:
return "header.fields";
case HEADER_FIELDS_NOT:
return "header.fields.not";
case MIME:
return "mime";
case TEXT:
return "text";
default:
assert_not_reached();
}
}
public string to_string() {
return serialize();
}
}
private SectionPart section_part;
private string[]? field_names;
private bool is_peek;
/**
* field_names are required for SectionPart.HEADER_FIELDS and SectionPart.HEADER_FIELDS_NOT
* and must be null for all other SectionParts.
*/
public FetchBodyDataType(SectionPart section_part, string[]? field_names) {
init(section_part, field_names, false);
}
/**
* Like FetchBodyDataType, but the /seen flag will not be set when used on a message.
*/
public FetchBodyDataType.peek(SectionPart section_part, string[]? field_names) {
init(section_part, field_names, true);
}
private void init(SectionPart section_part, string[]? field_names, bool is_peek) {
switch (section_part) {
case SectionPart.HEADER_FIELDS:
case SectionPart.HEADER_FIELDS_NOT:
assert(field_names != null && field_names.length > 0);
break;
default:
assert(field_names == null);
break;
}
this.section_part = section_part;
this.field_names = field_names;
this.is_peek = is_peek;
}
public string serialize() {
return to_string();
}
public Parameter to_parameter() {
// Because of the kooky formatting of the Body[section]<partial> fetch field, use an
// unquoted string and format it ourselves.
return new UnquotedStringParameter(serialize());
}
private string serialize_field_names() {
if (field_names == null || field_names.length == 0)
return "";
// note that the leading space is supplied here
StringBuilder builder = new StringBuilder(" (");
for (int ctr = 0; ctr < field_names.length; ctr++) {
builder.append(field_names[ctr]);
if (ctr < (field_names.length - 1))
builder.append_c(' ');
}
builder.append_c(')');
return builder.str;
}
public string to_string() {
return (!is_peek ? "body[%s%s]" : "body.peek[%s%s]").printf(section_part.serialize(),
serialize_field_names());
}
}

View file

@ -57,10 +57,12 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
if (fields == Geary.Email.Field.NONE)
throw new EngineError.BAD_PARAMETERS("No email fields specified");
Gee.Set<FetchDataType> data_type_set = new Gee.HashSet<FetchDataType>();
fields_to_fetch_data_types(fields, data_type_set);
Gee.List<FetchDataType> data_type_list = new Gee.ArrayList<FetchDataType>();
Gee.List<FetchBodyDataType> body_data_type_list = new Gee.ArrayList<FetchBodyDataType>();
fields_to_fetch_data_types(fields, data_type_list, body_data_type_list, false);
FetchCommand fetch_cmd = new FetchCommand.from_collection(msg_set, data_type_set);
FetchCommand fetch_cmd = new FetchCommand.from_collection(msg_set, data_type_list,
body_data_type_list);
CommandResponse resp = yield context.session.send_command_async(fetch_cmd, cancellable);
@ -77,7 +79,8 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
// see fields_to_fetch_data_types() for why this is guaranteed
assert(uid != null);
Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(folder, res.msg_num, uid),
Geary.Email email = new Geary.Email(
new Geary.Imap.EmailLocation(folder, res.msg_num, uid),
new Geary.Imap.EmailIdentifier(uid));
fetch_results_to_email(res, fields, email);
@ -92,14 +95,12 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
if (context.is_closed())
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
Gee.Set<FetchDataType> data_type_set = new Gee.HashSet<FetchDataType>();
fields_to_fetch_data_types(fields, data_type_set);
// no need to fetch the UID we're asking for
data_type_set.remove(FetchDataType.UID);
Gee.List<FetchDataType> data_type_list = new Gee.ArrayList<FetchDataType>();
Gee.List<FetchBodyDataType> body_data_type_list = new Gee.ArrayList<FetchBodyDataType>();
fields_to_fetch_data_types(fields, data_type_list, body_data_type_list, true);
FetchCommand fetch_cmd = new FetchCommand.from_collection(new MessageSet.uid(uid),
data_type_set);
data_type_list, body_data_type_list);
CommandResponse resp = yield context.session.send_command_async(fetch_cmd, cancellable);
@ -112,7 +113,8 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
if (results.length != 1)
throw new ImapError.SERVER_ERROR("Too many responses from server: %d", results.length);
Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(folder, results[0].msg_num, uid),
Geary.Email email = new Geary.Email(
new Geary.Imap.EmailLocation(folder, results[0].msg_num, uid),
new Geary.Imap.EmailIdentifier(uid));
fetch_results_to_email(results[0], fields, email);
@ -145,37 +147,64 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
// store FetchDataTypes in a set because the same data type may be requested multiple times
// by different fields (i.e. ENVELOPE)
private static void fields_to_fetch_data_types(Geary.Email.Field fields,
Gee.Set<FetchDataType> data_type_set) {
// UID is always fetched
// TODO: Detect when FETCH is addressed by UID instead of position and *not* fetch the
// UID, on the assumption that the caller will not need it
data_type_set.add(FetchDataType.UID);
private void fields_to_fetch_data_types(Geary.Email.Field fields, Gee.List<FetchDataType> data_types_list,
Gee.List<FetchBodyDataType> body_data_types_list, bool is_specific_uid) {
// always fetch UID because it's needed for EmailIdentifier (unless single message is being
// fetched by UID, in which case, obviously not necessary)
if (!is_specific_uid)
data_types_list.add(FetchDataType.UID);
// 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
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);
}
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" }));
break;
case Geary.Email.Field.ORIGINATORS:
body_data_types_list.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.HEADER_FIELDS, { "From", "Sender", "Reply-To" }));
break;
case Geary.Email.Field.RECEIVERS:
body_data_types_list.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.HEADER_FIELDS, { "To", "Cc", "Bcc" }));
break;
case Geary.Email.Field.REFERENCES:
body_data_types_list.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.HEADER_FIELDS, { "Message-ID", "In-Reply-To" }));
break;
case Geary.Email.Field.SUBJECT:
data_type_set.add(FetchDataType.ENVELOPE);
body_data_types_list.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.HEADER_FIELDS, { "Subject" }));
break;
case Geary.Email.Field.HEADER:
data_type_set.add(FetchDataType.RFC822_HEADER);
data_types_list.add(FetchDataType.RFC822_HEADER);
break;
case Geary.Email.Field.BODY:
data_type_set.add(FetchDataType.RFC822_TEXT);
data_types_list.add(FetchDataType.RFC822_TEXT);
break;
case Geary.Email.Field.PROPERTIES:
// Gmail doesn't like using FAST when combined with other fetch types, so
// do this manually
data_type_set.add(FetchDataType.FLAGS);
data_type_set.add(FetchDataType.INTERNALDATE);
data_type_set.add(FetchDataType.RFC822_SIZE);
data_types_list.add(FetchDataType.FLAGS);
data_types_list.add(FetchDataType.INTERNALDATE);
data_types_list.add(FetchDataType.RFC822_SIZE);
break;
case Geary.Email.Field.NONE:

View file

@ -62,6 +62,7 @@ def build(bld):
'../engine/imap/imap-error.vala',
'../engine/imap/message/imap-data-format.vala',
'../engine/imap/message/imap-fetch-data-type.vala',
'../engine/imap/message/imap-fetch-body-data-type.vala',
'../engine/imap/message/imap-flag.vala',
'../engine/imap/message/imap-message-data.vala',
'../engine/imap/message/imap-message-set.vala',