Added NOOP idling support to ClientSession.

ClientSession can now automatically send NOOP commands as keepalives at a specified interval.  Also, the CommandResponse decoders have been moved into their own directory (they will soon be fertile and multiply).  More work ahead on the FetchResults object, which should be like NoopResults and completely encapsulate all the information returned from a FETCH.
This commit is contained in:
Jim Nelson 2011-05-23 18:58:34 -07:00
parent e56a6a599a
commit 5021d8372d
13 changed files with 408 additions and 53 deletions

View file

@ -3,7 +3,7 @@ BUILD_ROOT = 1
VALAC := valac
APPS := console syntax lsmbox readmail
APPS := console syntax lsmbox readmail watchmbox
ENGINE_SRC := \
src/engine/Engine.vala \
@ -20,20 +20,22 @@ ENGINE_SRC := \
src/engine/imap/Tag.vala \
src/engine/imap/Command.vala \
src/engine/imap/Commands.vala \
src/engine/imap/FetchCommand.vala \
src/engine/imap/ResponseCode.vala \
src/engine/imap/ServerResponse.vala \
src/engine/imap/StatusResponse.vala \
src/engine/imap/ServerData.vala \
src/engine/imap/ServerDataType.vala \
src/engine/imap/FetchDataType.vala \
src/engine/imap/Status.vala \
src/engine/imap/CommandResponse.vala \
src/engine/imap/FetchResults.vala \
src/engine/imap/FetchDataDecoder.vala \
src/engine/imap/MessageData.vala \
src/engine/imap/Serializable.vala \
src/engine/imap/Serializer.vala \
src/engine/imap/Deserializer.vala \
src/engine/imap/Error.vala \
src/engine/imap/decoders/FetchDataDecoder.vala \
src/engine/imap/decoders/FetchResults.vala \
src/engine/imap/decoders/NoopResults.vala \
src/engine/rfc822/MailboxAddress.vala \
src/engine/rfc822/MessageData.vala \
src/engine/util/String.vala \
@ -51,7 +53,10 @@ LSMBOX_SRC := \
READMAIL_SRC := \
src/tests/readmail.vala
ALL_SRC := $(ENGINE_SRC) $(CONSOLE_SRC) $(SYNTAX_SRC) $(LSMBOX_SRC) $(READMAIL_SRC)
WATCHMBOX_SRC := \
src/tests/watchmbox.vala
ALL_SRC := $(ENGINE_SRC) $(CONSOLE_SRC) $(SYNTAX_SRC) $(LSMBOX_SRC) $(READMAIL_SRC) $(WATCHMBOX_SRC)
EXTERNAL_PKGS := \
gio-2.0 \
@ -86,3 +91,8 @@ readmail: $(ENGINE_SRC) $(READMAIL_SRC) Makefile
$(ENGINE_SRC) $(READMAIL_SRC) \
-o $@
watchmbox: $(ENGINE_SRC) $(WATCHMBOX_SRC) Makefile
$(VALAC) --save-temps -g $(foreach pkg,$(EXTERNAL_PKGS),--pkg=$(pkg)) \
$(ENGINE_SRC) $(WATCHMBOX_SRC) \
-o $@

View file

@ -354,9 +354,9 @@ class ImapConsole : Gtk.Window {
status("Fetching %s".printf(args[0]));
Geary.Imap.FetchDataItem[] data_items = new Geary.Imap.FetchDataItem[0];
Geary.Imap.FetchDataType[] data_items = new Geary.Imap.FetchDataType[0];
for (int ctr = 1; ctr < args.length; ctr++)
data_items += Geary.Imap.FetchDataItem.decode(args[ctr]);
data_items += Geary.Imap.FetchDataType.decode(args[ctr]);
cx.post(new Geary.Imap.FetchCommand(cx.generate_tag(), args[0], data_items), on_fetch);
}

View file

@ -5,6 +5,10 @@
*/
public class Geary.Imap.ClientSession : Object, Geary.Account {
// 30 min keepalive required to maintain session; back off by 30 sec for breathing room
public const int MIN_KEEPALIVE_SEC = (30 * 60) - 30;
public const int DEFAULT_KEEPALIVE_SEC = 60;
// Need this because delegates with targets cannot be stored in ADTs.
private class CommandCallback {
public SourceFunc callback;
@ -149,6 +153,7 @@ public class Geary.Imap.ClientSession : Object, Geary.Account {
private Gee.Queue<CommandCallback> cb_queue = new Gee.LinkedList<CommandCallback>();
private Gee.Queue<CommandResponse> cmd_response_queue = new Gee.LinkedList<CommandResponse>();
private CommandResponse current_cmd_response = new CommandResponse();
private uint keepalive_id = 0;
// state used only during connect and disconnect
private bool awaiting_connect_response = false;
@ -156,6 +161,18 @@ public class Geary.Imap.ClientSession : Object, Geary.Account {
private AsyncParams? connect_params = null;
private AsyncParams? disconnect_params = null;
public virtual signal void unsolicited_expunged(MessageNumber msg) {
}
public virtual signal void unsolicited_exists(int exists) {
}
public virtual signal void unsolicitied_recent(int recent) {
}
public virtual signal void unsolicited_flags(FetchResults flags) {
}
public ClientSession(string server, uint default_port) {
this.server = server;
this.default_port = default_port;
@ -422,6 +439,75 @@ public class Geary.Imap.ClientSession : Object, Geary.Account {
return State.NOAUTH;
}
//
// keepalives (nop idling to keep the session alive and to periodically receive notifications
// of changes)
//
/**
* Returns true if keepalives are activated, false if already enabled.
*/
public bool enable_keepalives(int seconds = DEFAULT_KEEPALIVE_SEC) {
if (keepalive_id != 0)
return false;
keepalive_id = Timeout.add_seconds(seconds, on_keepalive);
return true;
}
/**
* Returns true if keepalives are disactivated, false if already disabled.
*/
public bool disable_keepalives() {
if (keepalive_id == 0)
return false;
Source.remove(keepalive_id);
keepalive_id = 0;
return true;
}
private bool on_keepalive() {
send_command_async.begin(new NoopCommand(generate_tag()), null, on_keepalive_completed);
return true;
}
private void on_keepalive_completed(Object? source, AsyncResult result) {
NoopResults results;
try {
results = NoopResults.decode(send_command_async.end(result));
} catch (Error err) {
message("Keepalive error: %s", err.message);
return;
}
if (results.status_response.status != Status.OK) {
debug("Keepalive failed: %s", results.status_response.to_string());
return;
}
if (results.expunged != null) {
foreach (MessageNumber msg in results.expunged)
unsolicited_expunged(msg);
}
if (results.has_exists())
unsolicited_exists(results.exists);
if (results.has_recent())
unsolicitied_recent(results.recent);
if (results.flags != null) {
foreach (FetchResults flags in results.flags)
unsolicited_flags(flags);
}
}
//
// send commands
//
@ -739,7 +825,9 @@ public class Geary.Imap.ClientSession : Object, Geary.Account {
}
private uint on_ignored_transition(uint state, uint event) {
#if VERBOSE_SESSION
debug("Ignored transition: %s@%s", fsm.get_event_string(event), fsm.get_state_string(state));
#endif
return state;
}
@ -813,15 +901,21 @@ public class Geary.Imap.ClientSession : Object, Geary.Account {
//
private void on_network_connected() {
#if VERBOSE_SESSION
debug("Connected to %s", server);
#endif
}
private void on_network_disconnected() {
#if VERBOSE_SESSION
debug("Disconnected from %s", server);
#endif
}
private void on_network_sent_command(Command cmd) {
#if VERBOSE_SESSION
debug("Sent command %s", cmd.to_string());
#endif
}
private void on_received_status_response(StatusResponse status_response) {

View file

@ -76,3 +76,24 @@ public class Geary.Imap.CloseCommand : Command {
}
}
public class Geary.Imap.FetchCommand : Command {
public const string NAME = "fetch";
public FetchCommand(Tag tag, string msg_span, FetchDataType[] data_items) {
base (tag, NAME);
add(new StringParameter(msg_span));
assert(data_items.length > 0);
if (data_items.length == 1) {
add(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());
add(data_item_list);
}
}
}

View file

@ -5,7 +5,7 @@
*/
// TODO: Support body[section]<partial> and body.peek[section]<partial> forms
public enum Geary.Imap.FetchDataItem {
public enum Geary.Imap.FetchDataType {
UID,
FLAGS,
INTERNALDATE,
@ -66,7 +66,7 @@ public enum Geary.Imap.FetchDataItem {
}
}
public static FetchDataItem decode(string value) throws ImapError {
public static FetchDataType decode(string value) throws ImapError {
switch (value.down()) {
case "uid":
return UID;
@ -116,7 +116,7 @@ public enum Geary.Imap.FetchDataItem {
return new StringParameter(to_string());
}
public static FetchDataItem from_parameter(StringParameter strparam) throws ImapError {
public static FetchDataType from_parameter(StringParameter strparam) throws ImapError {
return decode(strparam.value);
}
@ -152,24 +152,3 @@ public enum Geary.Imap.FetchDataItem {
}
}
public class Geary.Imap.FetchCommand : Command {
public const string NAME = "fetch";
public FetchCommand(Tag tag, string msg_span, FetchDataItem[] data_items) {
base (tag, NAME);
add(new StringParameter(msg_span));
assert(data_items.length > 0);
if (data_items.length == 1) {
add(data_items[0].to_parameter());
} else {
ListParameter data_item_list = new ListParameter(this);
foreach (FetchDataItem data_item in data_items)
data_item_list.add(data_item.to_parameter());
add(data_item_list);
}
}
}

View file

@ -32,7 +32,7 @@ private class Geary.Imap.MessageStreamImpl : Object, Geary.MessageStream {
public async Gee.List<Message>? read(Cancellable? cancellable = null) throws Error {
CommandResponse resp = yield sess.send_command_async(new FetchCommand(sess.generate_tag(),
span, { FetchDataItem.ENVELOPE }), cancellable);
span, { FetchDataType.ENVELOPE }), cancellable);
if (resp.status_response.status != Status.OK)
throw new ImapError.SERVER_ERROR(resp.status_response.text);
@ -41,7 +41,7 @@ private class Geary.Imap.MessageStreamImpl : Object, Geary.MessageStream {
FetchResults[] results = FetchResults.decode(resp);
foreach (FetchResults res in results) {
Envelope envelope = (Envelope) res.get_data(FetchDataItem.ENVELOPE);
Envelope envelope = (Envelope) res.get_data(FetchDataType.ENVELOPE);
msgs.add(new Message(res.msg_num, envelope.from, envelope.subject, envelope.sent));
}

View file

@ -26,6 +26,12 @@ public class Geary.Imap.UID : Geary.Common.IntMessageData, Geary.Imap.MessageDat
}
}
public class Geary.Imap.MessageNumber : Geary.Common.IntMessageData, Geary.Imap.MessageData {
public MessageNumber(int value) {
base (value);
}
}
public class Geary.Imap.Flag {
public static Flag ANSWERED = new Flag("\\answered");
public static Flag DELETED = new Flag("\\deleted");

View file

@ -0,0 +1,101 @@
/* 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 enum Geary.Imap.ServerDataType {
CAPABILITY,
EXISTS,
EXPUNGE,
FETCH,
FLAGS,
LIST,
LSUB,
RECENT,
SEARCH,
STATUS;
public string to_string() {
switch (this) {
case CAPABILITY:
return "capability";
case EXISTS:
return "exists";
case EXPUNGE:
return "expunge";
case FETCH:
return "fetch";
case FLAGS:
return "flags";
case LIST:
return "list";
case LSUB:
return "lsub";
case RECENT:
return "recent";
case SEARCH:
return "search";
case STATUS:
return "status";
default:
assert_not_reached();
}
}
public static ServerDataType decode(string value) throws ImapError {
switch (value.down()) {
case "capability":
return CAPABILITY;
case "exists":
return EXISTS;
case "expunge":
return EXPUNGE;
case "fetch":
return FETCH;
case "flags":
return FLAGS;
case "list":
return LIST;
case "lsub":
return LSUB;
case "recent":
return RECENT;
case "search":
return SEARCH;
case "status":
return STATUS;
default:
throw new ImapError.PARSE_ERROR("\"%s\" is not a valid server data type", value);
}
}
public StringParameter to_parameter() {
return new StringParameter(to_string());
}
public static ServerDataType from_parameter(StringParameter param) throws ImapError {
return decode(param.value);
}
}

View file

@ -9,17 +9,17 @@
* While they can be used standalone, they're intended to be used by FetchResults to process
* a CommandResponse.
*
* Note that FetchDataDecoders are keyed off of FetchDataItem; new implementations should add
* themselves to FetchDataItem.get_decoder().
* Note that FetchDataDecoders are keyed off of FetchDataType; new implementations should add
* themselves to FetchDataType.get_decoder().
*
* In the future FetchDataDecoder may be used to decode MessageData stored in other formats, such
* as in a database.
*/
public abstract class Geary.Imap.FetchDataDecoder {
public FetchDataItem data_item { get; private set; }
public FetchDataType data_item { get; private set; }
public FetchDataDecoder(FetchDataItem data_item) {
public FetchDataDecoder(FetchDataType data_item) {
this.data_item = data_item;
}
@ -57,7 +57,7 @@ public abstract class Geary.Imap.FetchDataDecoder {
public class Geary.Imap.UIDDecoder : Geary.Imap.FetchDataDecoder {
public UIDDecoder() {
base (FetchDataItem.UID);
base (FetchDataType.UID);
}
protected override MessageData decode_string(StringParameter stringp) throws ImapError {
@ -67,7 +67,7 @@ public class Geary.Imap.UIDDecoder : Geary.Imap.FetchDataDecoder {
public class Geary.Imap.FlagsDecoder : Geary.Imap.FetchDataDecoder {
public FlagsDecoder() {
base (FetchDataItem.FLAGS);
base (FetchDataType.FLAGS);
}
protected override MessageData decode_list(ListParameter listp) throws ImapError {
@ -81,7 +81,7 @@ public class Geary.Imap.FlagsDecoder : Geary.Imap.FetchDataDecoder {
public class Geary.Imap.InternalDateDecoder : Geary.Imap.FetchDataDecoder {
public InternalDateDecoder() {
base (FetchDataItem.INTERNALDATE);
base (FetchDataType.INTERNALDATE);
}
protected override MessageData decode_string(StringParameter stringp) throws ImapError {
@ -91,7 +91,7 @@ public class Geary.Imap.InternalDateDecoder : Geary.Imap.FetchDataDecoder {
public class Geary.Imap.RFC822SizeDecoder : Geary.Imap.FetchDataDecoder {
public RFC822SizeDecoder() {
base (FetchDataItem.RFC822_SIZE);
base (FetchDataType.RFC822_SIZE);
}
protected override MessageData decode_string(StringParameter stringp) throws ImapError {
@ -101,7 +101,7 @@ public class Geary.Imap.RFC822SizeDecoder : Geary.Imap.FetchDataDecoder {
public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder {
public EnvelopeDecoder() {
base (FetchDataItem.ENVELOPE);
base (FetchDataType.ENVELOPE);
}
// TODO: This doesn't handle group lists (see Johnson, p.268)
@ -149,7 +149,7 @@ public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder {
public class Geary.Imap.RFC822HeaderDecoder : Geary.Imap.FetchDataDecoder {
public RFC822HeaderDecoder() {
base (FetchDataItem.RFC822_HEADER);
base (FetchDataType.RFC822_HEADER);
}
protected override MessageData decode_literal(LiteralParameter literalp) throws ImapError {
@ -159,7 +159,7 @@ public class Geary.Imap.RFC822HeaderDecoder : Geary.Imap.FetchDataDecoder {
public class Geary.Imap.RFC822TextDecoder : Geary.Imap.FetchDataDecoder {
public RFC822TextDecoder() {
base (FetchDataItem.RFC822_TEXT);
base (FetchDataType.RFC822_TEXT);
}
protected override MessageData decode_literal(LiteralParameter literalp) throws ImapError {
@ -169,7 +169,7 @@ public class Geary.Imap.RFC822TextDecoder : Geary.Imap.FetchDataDecoder {
public class Geary.Imap.RFC822FullDecoder : Geary.Imap.FetchDataDecoder {
public RFC822FullDecoder() {
base (FetchDataItem.RFC822);
base (FetchDataType.RFC822);
}
protected override MessageData decode_literal(LiteralParameter literalp) throws ImapError {

View file

@ -15,13 +15,13 @@
public class Geary.Imap.FetchResults {
public int msg_num { get; private set; }
private Gee.Map<FetchDataItem, MessageData> map = new Gee.HashMap<FetchDataItem, MessageData>();
private Gee.Map<FetchDataType, MessageData> map = new Gee.HashMap<FetchDataType, MessageData>();
public FetchResults(int msg_num) {
this.msg_num = msg_num;
}
private static FetchResults decode_data(ServerData data) throws ImapError {
public static FetchResults decode_data(ServerData data) throws ImapError {
StringParameter msg_num = (StringParameter) data.get_as(1, typeof(StringParameter));
StringParameter cmd = (StringParameter) data.get_as(2, typeof(StringParameter));
ListParameter list = (ListParameter) data.get_as(3, typeof(ListParameter));
@ -38,7 +38,7 @@ public class Geary.Imap.FetchResults {
// and the structured data itself
for (int ctr = 0; ctr < list.get_count(); ctr += 2) {
StringParameter data_item_param = (StringParameter) list.get_as(ctr, typeof(StringParameter));
FetchDataItem data_item = FetchDataItem.decode(data_item_param.value);
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",
@ -54,6 +54,8 @@ public class Geary.Imap.FetchResults {
}
public static FetchResults[] decode(CommandResponse response) throws ImapError {
assert(response.is_sealed());
FetchResults[] array = new FetchResults[0];
foreach (ServerData data in response.server_data)
array += decode_data(data);
@ -61,11 +63,11 @@ public class Geary.Imap.FetchResults {
return array;
}
public void set_data(FetchDataItem data_item, MessageData primitive) {
public void set_data(FetchDataType data_item, MessageData primitive) {
map.set(data_item, primitive);
}
public MessageData? get_data(FetchDataItem data_item) {
public MessageData? get_data(FetchDataType data_item) {
return map.get(data_item);
}

View file

@ -0,0 +1,80 @@
/* 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.NoopResults {
public StatusResponse status_response { get; private set; }
public Gee.List<MessageNumber>? expunged { get; private set; }
/**
* -1 if "exists" result not returned by server.
*/
public int exists { get; private set; }
public Gee.List<FetchResults>? flags { get; private set; }
/**
* -1 if "recent" result not returned by server.
*/
public int recent { get; private set; }
public NoopResults(StatusResponse status_response, Gee.List<MessageNumber>? expunged, int exists,
Gee.List<FetchResults>? flags, int recent) {
this.status_response = status_response;
this.expunged = expunged;
this.exists = exists;
this.flags = flags;
this.recent = recent;
}
public static NoopResults decode(CommandResponse response) throws ImapError {
assert(response.is_sealed());
Gee.List<MessageNumber> expunged = new Gee.ArrayList<MessageNumber>();
Gee.List<FetchResults> flags = new Gee.ArrayList<FetchResults>();
int exists = -1;
int recent = -1;
foreach (ServerData data in response.server_data) {
try {
int ordinal = data.get_as_string(1).as_int().clamp(-1, int.MAX);
ServerDataType type = ServerDataType.from_parameter(data.get_as_string(2));
switch (type) {
case ServerDataType.EXPUNGE:
expunged.add(new MessageNumber(ordinal));
break;
case ServerDataType.EXISTS:
exists = ordinal;
break;
case ServerDataType.RECENT:
recent = ordinal;
break;
case ServerDataType.FETCH:
flags.add(FetchResults.decode_data(data));
break;
default:
message("NOOP server data type \"%s\" unrecognized", type.to_string());
break;
}
} catch (ImapError ierr) {
message("NOOP decode error for \"%s\": %s", data.to_string(), ierr.message);
}
}
return new NoopResults(response.status_response, (expunged.size > 0) ? expunged : null,
exists, (flags.size > 0) ? flags : null, recent);
}
public bool has_exists() {
return exists >= 0;
}
public bool has_recent() {
return recent >= 0;
}
}

View file

@ -18,13 +18,13 @@ async void async_start() {
yield sess.examine_async(mailbox);
Geary.Imap.FetchCommand fetch = new Geary.Imap.FetchCommand(sess.generate_tag(),
"%d".printf(msg_num), { Geary.Imap.FetchDataItem.RFC822 });
"%d".printf(msg_num), { Geary.Imap.FetchDataType.RFC822 });
Geary.Imap.CommandResponse resp = yield sess.send_command_async(fetch);
Geary.Imap.FetchResults[] results = Geary.Imap.FetchResults.decode(resp);
assert(results.length == 1);
Geary.RFC822.Full? full =
results[0].get_data(Geary.Imap.FetchDataItem.RFC822) as Geary.RFC822.Full;
results[0].get_data(Geary.Imap.FetchDataType.RFC822) as Geary.RFC822.Full;
assert(full != null);
DataInputStream dins = new DataInputStream(full.buffer.get_input_stream());

62
src/tests/watchmbox.vala Normal file
View file

@ -0,0 +1,62 @@
/* 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.
*/
MainLoop? main_loop = null;
Geary.Imap.ClientSession? sess = null;
string? user = null;
string? pass = null;
string? mailbox = null;
void on_exists(int exists) {
stdout.printf("EXISTS: %d\n", exists);
}
void on_expunged(Geary.Imap.MessageNumber expunged) {
stdout.printf("EXPUNGED: %d\n", expunged.value);
}
void on_recent(int recent) {
stdout.printf("RECENT: %d\n", recent);
}
async void async_start() {
try {
yield sess.connect_async();
yield sess.login_async(user, pass);
yield sess.examine_async(mailbox);
sess.unsolicited_exists.connect(on_exists);
sess.unsolicited_expunged.connect(on_expunged);
sess.unsolicitied_recent.connect(on_recent);
sess.enable_keepalives(5);
} catch (Error err) {
debug("Error: %s", err.message);
}
}
int main(string[] args) {
if (args.length < 4) {
stderr.printf("usage: watchmbox <user> <pass> <mailbox>\n");
return 1;
}
main_loop = new MainLoop();
user = args[1];
pass = args[2];
mailbox = args[3];
sess = new Geary.Imap.ClientSession("imap.gmail.com", 993);
async_start.begin();
stdout.printf("Watching %s, Ctrl+C to exit...\n", mailbox);
main_loop.run();
return 0;
}