From 6433ebfa5b47907796878f4c64f773e71734f3ae Mon Sep 17 00:00:00 2001 From: Jim Nelson Date: Thu, 14 Apr 2011 01:15:05 +0000 Subject: [PATCH] Further development building the layers of IMAP decode, connectivity, and session management. First-stab implementation of a preliminary Engine API. Tons of work to go and tons of clean-up to make what's here more robust and more efficient. --- Makefile | 23 +++- src/console/main.vala | 96 +++++++++++++-- src/engine/Engine.vala | 15 +++ src/engine/Interfaces.vala | 18 +++ src/engine/Message.vala | 24 ++++ src/engine/imap/ClientConnection.vala | 79 ++++++------ src/engine/imap/ClientSession.vala | 168 ++++++++++++++++++++++++-- src/engine/imap/Command.vala | 2 +- src/engine/imap/CommandResponse.vala | 46 +++++++ src/engine/imap/Commands.vala | 28 ++--- src/engine/imap/Deserializer.vala | 53 ++++---- src/engine/imap/Error.vala | 7 +- src/engine/imap/FetchCommand.vala | 143 ++++++++++++++++++++++ src/engine/imap/Mailbox.vala | 70 +++++++++++ src/engine/imap/Parameter.vala | 77 +++++------- src/engine/imap/Response.vala | 49 ++++++++ src/engine/imap/ResponseCode.vala | 22 ++++ src/engine/imap/Serializer.vala | 10 -- src/engine/imap/ServerData.vala | 16 +++ src/engine/imap/Status.vala | 70 +++++++++++ src/engine/imap/StatusResponse.vala | 51 ++++++++ src/engine/imap/Tag.vala | 31 ++++- src/engine/state/Machine.vala | 2 +- src/tests/lsmbox.vala | 64 ++++++++++ 24 files changed, 997 insertions(+), 167 deletions(-) create mode 100644 src/engine/Engine.vala create mode 100644 src/engine/Interfaces.vala create mode 100644 src/engine/Message.vala create mode 100644 src/engine/imap/CommandResponse.vala create mode 100644 src/engine/imap/FetchCommand.vala create mode 100644 src/engine/imap/Mailbox.vala create mode 100644 src/engine/imap/Response.vala create mode 100644 src/engine/imap/ResponseCode.vala create mode 100644 src/engine/imap/ServerData.vala create mode 100644 src/engine/imap/Status.vala create mode 100644 src/engine/imap/StatusResponse.vala create mode 100644 src/tests/lsmbox.vala diff --git a/Makefile b/Makefile index 89f1d2bc..24f3a68f 100644 --- a/Makefile +++ b/Makefile @@ -3,18 +3,29 @@ BUILD_ROOT = 1 VALAC := valac -APPS := console syntax +APPS := console syntax lsmbox ENGINE_SRC := \ + src/engine/Engine.vala \ + src/engine/Interfaces.vala \ + src/engine/Message.vala \ src/engine/state/Machine.vala \ src/engine/state/MachineDescriptor.vala \ src/engine/state/Mapping.vala \ src/engine/imap/ClientConnection.vala \ src/engine/imap/ClientSession.vala \ + src/engine/imap/Mailbox.vala \ src/engine/imap/Parameter.vala \ 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/Response.vala \ + src/engine/imap/StatusResponse.vala \ + src/engine/imap/ServerData.vala \ + src/engine/imap/Status.vala \ + src/engine/imap/CommandResponse.vala \ src/engine/imap/Serializable.vala \ src/engine/imap/Serializer.vala \ src/engine/imap/Deserializer.vala \ @@ -27,7 +38,10 @@ CONSOLE_SRC := \ SYNTAX_SRC := \ src/tests/syntax.vala -ALL_SRC := $(ENGINE_SRC) $(CONSOLE_SRC) $(SYNTAX_SRC) +LSMBOX_SRC := \ + src/tests/lsmbox.vala + +ALL_SRC := $(ENGINE_SRC) $(CONSOLE_SRC) $(SYNTAX_SRC) $(LSMBOX_SRC) EXTERNAL_PKGS := \ gio-2.0 \ @@ -52,3 +66,8 @@ syntax: $(ENGINE_SRC) $(SYNTAX_SRC) Makefile $(ENGINE_SRC) $(SYNTAX_SRC) \ -o $@ +lsmbox: $(ENGINE_SRC) $(LSMBOX_SRC) Makefile + $(VALAC) --save-temps -g $(foreach pkg,$(EXTERNAL_PKGS),--pkg=$(pkg)) \ + $(ENGINE_SRC) $(LSMBOX_SRC) \ + -o $@ + diff --git a/src/console/main.vala b/src/console/main.vala index 9b5b1ff0..d6998121 100644 --- a/src/console/main.vala +++ b/src/console/main.vala @@ -17,7 +17,6 @@ class ImapConsole : Gtk.Window { private uint statusbar_ctx = 0; private uint statusbar_msg_id = 0; - private Geary.Imap.ClientSession session = new Geary.Imap.ClientSession(); private Geary.Imap.ClientConnection? cx = null; public ImapConsole() { @@ -72,6 +71,25 @@ class ImapConsole : Gtk.Window { status(err.message); } + private static string[] cmdnames = { + "noop", + "nop", + "capabililties", + "caps", + "connect", + "disconnect", + "login", + "logout", + "bye", + "list", + "examine", + "fetch", + "help", + "exit", + "quit", + "gmail" + }; + private void exec(string input) { string[] lines = input.strip().split(";"); foreach (string line in lines) { @@ -90,6 +108,8 @@ class ImapConsole : Gtk.Window { clear_status(); + // TODO: Need to break out the command delegates into their own objects with the + // human command-names and usage and exec()'s and such; this isn't a long-term approach try { switch (cmd) { case "noop": @@ -128,6 +148,15 @@ class ImapConsole : Gtk.Window { examine(cmd, args); break; + case "fetch": + fetch(cmd, args); + break; + + case "help": + foreach (string cmdname in cmdnames) + print_console_line(cmdname); + break; + case "exit": case "quit": quit(cmd, args); @@ -164,7 +193,7 @@ class ImapConsole : Gtk.Window { private void capabilities(string cmd, string[] args) throws Error { check_connected(cmd, args, 0, null); - cx.send_async.begin(new Geary.Imap.CapabilityCommand(session), Priority.DEFAULT, null, + cx.send_async.begin(new Geary.Imap.CapabilityCommand(cx.generate_tag()), Priority.DEFAULT, null, on_capabilities); } @@ -180,7 +209,7 @@ class ImapConsole : Gtk.Window { private void noop(string cmd, string[] args) throws Error { check_connected(cmd, args, 0, null); - cx.send_async.begin(new Geary.Imap.NoopCommand(session), Priority.DEFAULT, null, + cx.send_async.begin(new Geary.Imap.NoopCommand(cx.generate_tag()), Priority.DEFAULT, null, on_noop); } @@ -211,9 +240,12 @@ class ImapConsole : Gtk.Window { status("Connected"); cx.sent_command.connect(on_sent_command); - cx.received_response.connect(on_received_response); - cx.xon(); + cx.received_status_response.connect(on_received_status_response); + cx.received_server_data.connect(on_received_server_data); + cx.received_bad_response.connect(on_received_bad_response); + // start transmission and reception + cx.xon(); } catch (Error err) { cx = null; @@ -234,6 +266,10 @@ class ImapConsole : Gtk.Window { status("Disconnected"); cx.sent_command.disconnect(on_sent_command); + cx.received_status_response.disconnect(on_received_status_response); + cx.received_server_data.connect(on_received_server_data); + cx.received_bad_response.disconnect(on_received_bad_response); + cx = null; } catch (Error err) { exception(err); @@ -244,12 +280,13 @@ class ImapConsole : Gtk.Window { check_connected(cmd, args, 2, "user pass"); status("Logging in..."); - cx.post(new Geary.Imap.LoginCommand(session, args[0], args[1]), on_logged_in); + cx.post(new Geary.Imap.LoginCommand(cx.generate_tag(), args[0], args[1]), on_logged_in); } private void on_logged_in(Object? source, AsyncResult result) { try { cx.finish_post(result); + status("Login completed"); } catch (Error err) { exception(err); } @@ -259,7 +296,7 @@ class ImapConsole : Gtk.Window { check_connected(cmd, args, 0, null); status("Logging out..."); - cx.post(new Geary.Imap.LogoutCommand(session), on_logout); + cx.post(new Geary.Imap.LogoutCommand(cx.generate_tag()), on_logout); } private void on_logout(Object? source, AsyncResult result) { @@ -275,7 +312,7 @@ class ImapConsole : Gtk.Window { check_connected(cmd, args, 2, " "); status("Listing..."); - cx.post(new Geary.Imap.ListCommand.wildcarded(session, args[0], args[1]), on_list); + cx.post(new Geary.Imap.ListCommand.wildcarded(cx.generate_tag(), args[0], args[1]), on_list); } private void on_list(Object? source, AsyncResult result) { @@ -291,7 +328,7 @@ class ImapConsole : Gtk.Window { check_connected(cmd, args, 1, ""); status("Opening %s read-only".printf(args[0])); - cx.post(new Geary.Imap.ExamineCommand(session, args[0]), on_examine); + cx.post(new Geary.Imap.ExamineCommand(cx.generate_tag(), args[0]), on_examine); } private void on_examine(Object? source, AsyncResult result) { @@ -303,19 +340,56 @@ class ImapConsole : Gtk.Window { } } + private void fetch(string cmd, string[] args) throws Error { + check_connected(cmd, args, 2, " "); + + status("Fetching %s".printf(args[0])); + cx.post(new Geary.Imap.FetchCommand(cx.generate_tag(), args[0], + { Geary.Imap.FetchDataItem.decode(args[1]) }), on_fetch); + } + + private void on_fetch(Object? source, AsyncResult result) { + try { + cx.finish_post(result); + status("Fetched"); + } catch (Error err) { + exception(err); + } + } + private void quit(string cmd, string[] args) throws Error { Gtk.main_quit(); } + private void print_console_line(string text) { + append_to_console("[C] "); + append_to_console(text); + append_to_console("\n"); + } + private void on_sent_command(Geary.Imap.Command cmd) { append_to_console("[L] "); append_to_console(cmd.to_string()); append_to_console("\n"); } - private void on_received_response(Geary.Imap.RootParameters params) { + private void on_received_status_response(Geary.Imap.StatusResponse status_response) { append_to_console("[R] "); - append_to_console(params.to_string()); + append_to_console(status_response.to_string()); + append_to_console("\n"); + } + + private void on_received_server_data(Geary.Imap.ServerData server_data) { + append_to_console("[D] "); + append_to_console(server_data.to_string()); + append_to_console("\n"); + } + + private void on_received_bad_response(Geary.Imap.RootParameters root, Geary.ImapError err) { + append_to_console("[E] "); + append_to_console(err.message); + append_to_console(": "); + append_to_console(root.to_string()); append_to_console("\n"); } diff --git a/src/engine/Engine.vala b/src/engine/Engine.vala new file mode 100644 index 00000000..1e9304a1 --- /dev/null +++ b/src/engine/Engine.vala @@ -0,0 +1,15 @@ +/* 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.Engine : Object { + public static async Account? login(string server, string user, string pass) throws Error { + Imap.ClientSession account = new Imap.ClientSession(server, Imap.ClientConnection.DEFAULT_PORT_TLS); + yield account.connect_async(user, pass); + + return account; + } +} + diff --git a/src/engine/Interfaces.vala b/src/engine/Interfaces.vala new file mode 100644 index 00000000..97801629 --- /dev/null +++ b/src/engine/Interfaces.vala @@ -0,0 +1,18 @@ +/* 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 interface Geary.Account : Object { + public abstract async Folder open(string name, Cancellable? cancellable = null) throws Error; +} + +public interface Geary.Folder : Object { + public abstract MessageStream? read(int low, int count); +} + +public interface Geary.MessageStream : Object { + public abstract async Gee.List? read(Cancellable? cancellable = null) throws Error; +} + diff --git a/src/engine/Message.vala b/src/engine/Message.vala new file mode 100644 index 00000000..62527f22 --- /dev/null +++ b/src/engine/Message.vala @@ -0,0 +1,24 @@ +/* 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.Message { + public int msg_num { get; private set; } + public string from { get; private set; } + public string subject { get; private set; } + public string sent { get; private set; } + + public Message(int msg_num, string from, string subject, string sent) { + this.msg_num = msg_num; + this.from = from; + this.subject = subject; + this.sent = sent; + } + + public string to_string() { + return "[%d] %s: %s (%s)".printf(msg_num, from, subject, sent); + } +} + diff --git a/src/engine/imap/ClientConnection.vala b/src/engine/imap/ClientConnection.vala index b81ae16a..ae6f94d9 100644 --- a/src/engine/imap/ClientConnection.vala +++ b/src/engine/imap/ClientConnection.vala @@ -18,6 +18,8 @@ public class Geary.Imap.ClientConnection { private bool flow_controlled = true; private Deserializer des = new Deserializer(); private uint8[] block_buffer = new uint8[4096]; + private int tag_counter = 0; + private char tag_prefix = 'a'; public virtual signal void connected() { } @@ -31,7 +33,13 @@ public class Geary.Imap.ClientConnection { public virtual signal void sent_command(Command cmd) { } - public virtual signal void received_response(RootParameters params) { + public virtual signal void received_status_response(StatusResponse status_response) { + } + + public virtual signal void received_server_data(ServerData server_data) { + } + + public virtual signal void received_bad_response(RootParameters root, ImapError err) { } public virtual signal void receive_failed(Error err) { @@ -47,23 +55,24 @@ public class Geary.Imap.ClientConnection { des.parameters_ready.connect(on_parameters_ready); } - private void on_parameters_ready(RootParameters params) { - received_response(params); + ~ClientConnection() { + // TODO: Close connection as gracefully as possible } - /* - public void connect(Cancellable? cancellable = null) throws Error { - if (cx != null) - throw new IOError.EXISTS("Already connected to %s", to_string()); + // Generates a unique tag for the IMAP connection in the form of "<000-999>". + public Tag generate_tag() { + // watch for odometer rollover + if (++tag_counter >= 1000) { + tag_counter = 0; + if (tag_prefix == 'z') + tag_prefix = 'a'; + else + tag_prefix++; + } - cx = socket_client.connect_to_host(host_specifier, default_port, cancellable); - iouts = new Imap.OutputStream(cx.output_stream); - dins = new DataInputStream(cx.input_stream); - dins.set_newline_type(DataStreamNewlineType.CR_LF); - - connected(); + // TODO This could be optimized, but we'll leave it for now. + return new Tag("%c%03d".printf(tag_prefix, tag_counter)); } - */ public async void connect_async(Cancellable? cancellable = null) throws Error { if (cx != null) @@ -76,23 +85,6 @@ public class Geary.Imap.ClientConnection { connected(); } - /* - public void disconnect(Cancellable? cancellable = null) throws Error { - if (cx == null) - return; - - dins.close(cancellable); - iouts.close(cancellable); - cx.close(cancellable); - - dins = null; - iouts = null; - cx = null; - - disconnected(); - } - */ - public async void disconnect_async(Cancellable? cancellable = null) throws Error { if (cx == null) @@ -167,6 +159,20 @@ public class Geary.Imap.ClientConnection { next_deserialize_step(); } + private void on_parameters_ready(RootParameters root) { + try { + bool is_status_response; + ServerResponse response = ServerResponse.from_server(root, out is_status_response); + + if (is_status_response) + received_status_response((StatusResponse) response); + else + received_server_data((ServerData) response); + } catch (ImapError err) { + received_bad_response(root, err); + } + } + public void xoff() throws Error { check_for_connection(); @@ -180,17 +186,6 @@ public class Geary.Imap.ClientConnection { ins_cancellable = new Cancellable(); } - /* - public void send(Command command, Cancellable? cancellable = null) throws Error { - if (cx == null) - throw new IOError.CLOSED("Not connected to %s", to_string()); - - command.serialize(iouts, cancellable); - - sent_command(command); - } - */ - /** * Convenience method for send_async.begin(). */ diff --git a/src/engine/imap/ClientSession.vala b/src/engine/imap/ClientSession.vala index 74f05a11..a3d4a88d 100644 --- a/src/engine/imap/ClientSession.vala +++ b/src/engine/imap/ClientSession.vala @@ -4,22 +4,164 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Imap.ClientSession { - private int tag_counter = 0; - private char tag_prefix = 'a'; +public class Geary.Imap.ClientSession : Object, Geary.Account { + // Need this because delegates with targets cannot be stored in ADTs. + private class CommandCallback { + public SourceFunc callback; + + public CommandCallback(SourceFunc callback) { + this.callback = callback; + } + } - // Generates a unique tag for the IMAP session in the form of "<000-999>". - public string generate_tag_value() { - // watch for odometer rollover - if (++tag_counter >= 1000) { - tag_counter = 0; - if (tag_prefix == 'z') - tag_prefix = 'a'; - else - tag_prefix++; + private string server; + private uint default_port; + private ClientConnection? cx = null; + private Mailbox? current_mailbox = null; + private Gee.Queue cb_queue = new Gee.LinkedList(); + private Gee.Queue cmd_response_queue = new Gee.LinkedList(); + private CommandResponse current_cmd_response = new CommandResponse(); + private bool awaiting_connect_response = false; + private ServerData? connect_response = null; + + public ClientSession(string server, uint default_port) { + this.server = server; + this.default_port = default_port; + } + + public Tag? generate_tag() { + return (cx != null) ? cx.generate_tag() : null; + } + + public async void connect_async(string user, string pass, Cancellable? cancellable = null) + throws Error { + if (cx != null) + return; + + cx = new ClientConnection(server, ClientConnection.DEFAULT_PORT_TLS); + cx.connected.connect(on_connected); + cx.disconnected.connect(on_disconnected); + cx.sent_command.connect(on_sent_command); + cx.received_status_response.connect(on_received_status_response); + cx.received_server_data.connect(on_received_server_data); + cx.received_bad_response.connect(on_received_bad_response); + cx.receive_failed.connect(on_receive_failed); + + yield cx.connect_async(cancellable); + + // start receiving traffic from the server + cx.xon(); + + // wait for the initial OK response from the server + cb_queue.offer(new CommandCallback(connect_async.callback)); + awaiting_connect_response = true; + yield; + + assert(connect_response != null); + Status status = Status.from_parameter( + (StringParameter) connect_response.get_as(1, typeof(StringParameter))); + if (status != Status.OK) + throw new ImapError.SERVER_ERROR("Unable to connect: %s", connect_response.to_string()); + + // issue login command + yield send_command_async(new LoginCommand(cx.generate_tag(), user, pass), cancellable); + } + + public async void disconnect_async(string user, string pass, Cancellable? cancellable = null) + throws Error { + if (cx == null) + return; + + CommandResponse response = yield send_command_async(new LogoutCommand(cx.generate_tag()), + cancellable); + if (response.status_response.status != Status.OK) + message("Logout to %s failed: %s", server, response.status_response.to_string()); + + yield cx.disconnect_async(cancellable); + + cx = null; + } + + public async CommandResponse send_command_async(Command cmd, Cancellable? cancellable = null) + throws Error { + if (cx == null) + throw new ImapError.NOT_CONNECTED("Not connected to %s", server); + + yield cx.send_async(cmd, Priority.DEFAULT, cancellable); + + cb_queue.offer(new CommandCallback(send_command_async.callback)); + yield; + + CommandResponse? cmd_response = cmd_response_queue.poll(); + assert(cmd_response != null); + assert(cmd_response.is_sealed()); + assert(cmd_response.status_response.tag.equals(cmd.tag)); + + return cmd_response; + } + + public async Geary.Folder open(string name, Cancellable? cancellable = null) throws Error { + if (cx == null) + throw new ImapError.NOT_CONNECTED("Not connected to %s", server); + + assert(current_mailbox == null); + + yield send_command_async(new ExamineCommand(cx.generate_tag(), name), cancellable); + current_mailbox = new Mailbox(name, this); + + return current_mailbox; + } + + private void on_connected() { + debug("Connected to %s", server); + } + + private void on_disconnected() { + debug("Disconnected from %s", server); + } + + private void on_sent_command(Command cmd) { + debug("Sent command %s", cmd.to_string()); + } + + private void on_received_status_response(StatusResponse status_response) { + assert(!current_cmd_response.is_sealed()); + current_cmd_response.seal(status_response); + assert(current_cmd_response.is_sealed()); + + cmd_response_queue.offer(current_cmd_response); + current_cmd_response = new CommandResponse(); + + CommandCallback? cmd_callback = cb_queue.poll(); + assert(cmd_callback != null); + + Idle.add(cmd_callback.callback); + } + + private void on_received_server_data(ServerData server_data) { + // The first response from the server is an untagged status response, which is considered + // ServerData in our model. This captures that and treats it as such. + if (awaiting_connect_response) { + awaiting_connect_response = false; + connect_response = server_data; + + CommandCallback? cmd_callback = cb_queue.poll(); + assert(cmd_callback != null); + + Idle.add(cmd_callback.callback); + + return; } - return "%c%03d".printf(tag_prefix, tag_counter); + current_cmd_response.add_server_data(server_data); + } + + private void on_received_bad_response(RootParameters root, ImapError err) { + debug("Received bad response %s: %s", root.to_string(), err.message); + } + + private void on_receive_failed(Error err) { + debug("Receive failed: %s", err.message); } } diff --git a/src/engine/imap/Command.vala b/src/engine/imap/Command.vala index 2ee4f029..e8570e45 100644 --- a/src/engine/imap/Command.vala +++ b/src/engine/imap/Command.vala @@ -9,7 +9,7 @@ public class Geary.Imap.Command : RootParameters { public string name { get; private set; } public string[]? args { get; private set; } - public Command(Tag tag, string name, string[]? args = null) requires (!tag.is_untagged()) { + public Command(Tag tag, string name, string[]? args = null) requires (tag.is_tagged()) { this.tag = tag; this.name = name; this.args = args; diff --git a/src/engine/imap/CommandResponse.vala b/src/engine/imap/CommandResponse.vala new file mode 100644 index 00000000..def993f3 --- /dev/null +++ b/src/engine/imap/CommandResponse.vala @@ -0,0 +1,46 @@ +/* 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.CommandResponse { + public Gee.List server_data { get; private set; } + public StatusResponse? status_response { get; private set; } + + public CommandResponse() { + server_data = new Gee.ArrayList(); + } + + public void add_server_data(ServerData data) { + assert(!is_sealed()); + + server_data.add(data); + } + + public void seal(StatusResponse status_response) { + assert(!is_sealed()); + + this.status_response = status_response; + } + + public bool is_sealed() { + return (status_response != null); + } + + public string to_string() { + StringBuilder builder = new StringBuilder(); + + foreach (ServerData data in server_data) + builder.append("%s\n".printf(data.to_string())); + + if (status_response != null) + builder.append(status_response.to_string()); + + if (!is_sealed()) + builder.append("(incomplete command response)"); + + return builder.str; + } +} + diff --git a/src/engine/imap/Commands.vala b/src/engine/imap/Commands.vala index 206a3476..a9dd2dfb 100644 --- a/src/engine/imap/Commands.vala +++ b/src/engine/imap/Commands.vala @@ -7,24 +7,24 @@ public class Geary.Imap.CapabilityCommand : Command { public const string NAME = "capability"; - public CapabilityCommand(ClientSession session) { - base (new Tag.generated(session), NAME); + public CapabilityCommand(Tag tag) { + base (tag, NAME); } } public class Geary.Imap.NoopCommand : Command { public const string NAME = "noop"; - public NoopCommand(ClientSession session) { - base (new Tag.generated(session), NAME); + public NoopCommand(Tag tag) { + base (tag, NAME); } } public class Geary.Imap.LoginCommand : Command { public const string NAME = "login"; - public LoginCommand(ClientSession session, string user, string pass) { - base (new Tag.generated(session), NAME, { user, pass }); + public LoginCommand(Tag tag, string user, string pass) { + base (tag, NAME, { user, pass }); } public override string to_string() { @@ -35,28 +35,28 @@ public class Geary.Imap.LoginCommand : Command { public class Geary.Imap.LogoutCommand : Command { public const string NAME = "logout"; - public LogoutCommand(ClientSession session) { - base (new Tag.generated(session), NAME); + public LogoutCommand(Tag tag) { + base (tag, NAME); } } public class Geary.Imap.ListCommand : Command { public const string NAME = "list"; - public ListCommand(ClientSession session, string mailbox) { - base (new Tag.generated(session), NAME, { "", mailbox }); + public ListCommand(Tag tag, string mailbox) { + base (tag, NAME, { "", mailbox }); } - public ListCommand.wildcarded(ClientSession session, string reference, string mailbox) { - base (new Tag.generated(session), NAME, { reference, mailbox }); + public ListCommand.wildcarded(Tag tag, string reference, string mailbox) { + base (tag, NAME, { reference, mailbox }); } } public class Geary.Imap.ExamineCommand : Command { public const string NAME = "examine"; - public ExamineCommand(ClientSession session, string mailbox) { - base (new Tag.generated(session), NAME, { mailbox }); + public ExamineCommand(Tag tag, string mailbox) { + base (tag, NAME, { mailbox }); } } diff --git a/src/engine/imap/Deserializer.vala b/src/engine/imap/Deserializer.vala index 94f8ad1e..ce8824a4 100644 --- a/src/engine/imap/Deserializer.vala +++ b/src/engine/imap/Deserializer.vala @@ -148,7 +148,7 @@ public class Geary.Imap.Deserializer { public Mode get_mode() { switch (fsm.get_state()) { case State.LITERAL_DATA: - return Mode.LINE; + return Mode.BLOCK; case State.FAILED: return Mode.FAILED; @@ -184,6 +184,10 @@ public class Geary.Imap.Deserializer { current_string = null; } + private void clear_string_parameter() { + current_string = null; + } + private void save_literal_parameter() { if (current_literal == null) return; @@ -196,8 +200,9 @@ public class Geary.Imap.Deserializer { current.add(param); } - private void push() { - ListParameter child = new ListParameter(current); + // ListParameter's parent *must* be current + private void push(ListParameter child) { + assert(child.get_parent() == current); current.add(child); current = child; @@ -206,7 +211,7 @@ public class Geary.Imap.Deserializer { private State pop() { ListParameter? parent = current.get_parent(); if (parent == null) { - warning("Attempt to close unopened list"); + warning("Attempt to close unopened list/response code"); return State.FAILED; } @@ -224,7 +229,9 @@ public class Geary.Imap.Deserializer { } if (!is_current_string_empty() || current_literal != null || literal_length_remaining > 0) { - warning("Unfinished parameter"); + warning("Unfinished parameter: string=%s literal=%s %ld remaining", + (!is_current_string_empty()).to_string(), (current_literal != null).to_string(), + literal_length_remaining); return State.FAILED; } @@ -245,6 +252,13 @@ public class Geary.Imap.Deserializer { // look for opening characters to special parameter formats, otherwise jump to atom // handler (i.e. don't drop this character in the case of atoms) switch (*((unichar *) user)) { + case '[': + // open response code + ResponseCode response_code = new ResponseCode(current); + push(response_code); + + return State.START_PARAM; + case '{': return State.LITERAL; @@ -253,12 +267,14 @@ public class Geary.Imap.Deserializer { case '(': // open list - push(); + ListParameter list = new ListParameter(current); + push(list); return State.START_PARAM; case ')': - // close list + case ']': + // close list or response code return pop(); default: @@ -275,12 +291,8 @@ public class Geary.Imap.Deserializer { unichar ch = *((unichar *) user); - // drop everything above 0x7F - if (ch > 0x7F) - return state; - - // drop control characters - if (ch.iscntrl()) + // drop everything above 0x7F and control characters + if (ch > 0x7F || ch.iscntrl()) return state; // tags and atoms have different special characters @@ -300,8 +312,9 @@ public class Geary.Imap.Deserializer { return State.START_PARAM; } - // close-parens after an atom indicates end-of-list - if (state == State.ATOM && ch == ')') { + // close-parens/close-square-bracket after an atom indicates end-of-list/end-of-response + // code + if (state == State.ATOM && (ch == ')' || ch == ']')) { save_string_parameter(); return pop(); @@ -322,12 +335,8 @@ public class Geary.Imap.Deserializer { private uint on_quoted_char(uint state, uint event, void *user) { unichar ch = *((unichar *) user); - // drop anything above 0x7F - if (ch > 0x7F) - return State.QUOTED; - - // drop NUL, CR, and LF - if (ch == '\0' || ch == '\r' || ch == '\n') + // drop anything above 0x7F, NUL, CR, and LF + if (ch > 0x7F || ch == '\0' || ch == '\r' || ch == '\n') return State.QUOTED; // look for escaped characters @@ -377,6 +386,8 @@ public class Geary.Imap.Deserializer { return State.FAILED; } + clear_string_parameter(); + return State.LITERAL_DATA_BEGIN; } diff --git a/src/engine/imap/Error.vala b/src/engine/imap/Error.vala index 20faa844..9ed78d08 100644 --- a/src/engine/imap/Error.vala +++ b/src/engine/imap/Error.vala @@ -4,7 +4,10 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -errordomain Geary.ImapError { - PARSE_ERROR; +public errordomain Geary.ImapError { + PARSE_ERROR, + TYPE_ERROR, + SERVER_ERROR, + NOT_CONNECTED } diff --git a/src/engine/imap/FetchCommand.vala b/src/engine/imap/FetchCommand.vala new file mode 100644 index 00000000..c32baf59 --- /dev/null +++ b/src/engine/imap/FetchCommand.vala @@ -0,0 +1,143 @@ +/* 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.FetchDataItem { + UID, + FLAGS, + INTERNALDATE, + ENVELOPE, + BODYSTRUCTURE, + BODY, + RFC822, + RFC822_HEADER, + RFC822_SIZE, + RFC822_TEXT, + FAST, + ALL, + FULL; + + public string to_string() { + switch (this) { + case UID: + return "uid"; + + case FLAGS: + return "flags"; + + case INTERNALDATE: + return "internaldate"; + + case ENVELOPE: + return "envelope"; + + case BODYSTRUCTURE: + return "bodystructure"; + + case BODY: + return "body"; + + case RFC822: + return "rfc822"; + + case RFC822_HEADER: + return "rfc822.header"; + + case RFC822_SIZE: + return "rfc822.size"; + + case RFC822_TEXT: + return "rfc822.text"; + + case FAST: + return "fast"; + + case ALL: + return "all"; + + case FULL: + return "full"; + + default: + assert_not_reached(); + } + } + + public static FetchDataItem decode(string value) throws ImapError { + switch (value.down()) { + case "uid": + return UID; + + case "flags": + return FLAGS; + + case "internaldate": + return INTERNALDATE; + + case "envelope": + return ENVELOPE; + + case "bodystructure": + return BODYSTRUCTURE; + + case "body": + return BODY; + + case "rfc822": + return RFC822; + + case "rfc822.header": + return RFC822_HEADER; + + case "rfc822.size": + return RFC822_SIZE; + + case "rfc822.text": + return RFC822_TEXT; + + case "fast": + return FAST; + + case "all": + return ALL; + + case "full": + return FULL; + + default: + throw new ImapError.PARSE_ERROR("\"%s\" is not a valid fetch-command data item", value); + } + } + + public StringParameter to_parameter() { + return new StringParameter(to_string()); + } + + public static FetchDataItem from_parameter(StringParameter strparam) throws ImapError { + return decode(strparam.value); + } +} + +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); + } + } +} + diff --git a/src/engine/imap/Mailbox.vala b/src/engine/imap/Mailbox.vala new file mode 100644 index 00000000..7899455e --- /dev/null +++ b/src/engine/imap/Mailbox.vala @@ -0,0 +1,70 @@ +/* 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.Mailbox : Object, Geary.Folder { + private string name; + private ClientSession sess; + + internal Mailbox(string name, ClientSession sess) { + this.name = name; + this.sess = sess; + } + + public MessageStream? read(int low, int count) { + return new MessageStreamImpl(sess, low, count); + } +} + +private class Geary.Imap.MessageStreamImpl : Object, Geary.MessageStream { + private ClientSession sess; + private string span; + + public MessageStreamImpl(ClientSession sess, int low, int count) { + assert(count > 0); + + this.sess = sess; + span = (count > 1) ? "%d:%d".printf(low, low + count - 1) : "%d".printf(low); + } + + public async Gee.List? read(Cancellable? cancellable = null) throws Error { + CommandResponse resp = yield sess.send_command_async(new FetchCommand(sess.generate_tag(), + span, { FetchDataItem.ENVELOPE }), cancellable); + + if (resp.status_response.status != Status.OK) + throw new ImapError.SERVER_ERROR(resp.status_response.text); + + Gee.List msgs = new Gee.ArrayList(); + foreach (ServerData data in resp.server_data) { + StringParameter? label = data.get(2) as StringParameter; + if (label == null || label.value.down() != "fetch") { + debug("Not fetch data: %s", (label == null) ? "(null)" : label.value); + continue; + } + + StringParameter msg_num = (StringParameter) data.get_as(1, typeof(StringParameter)); + ListParameter envelope = (ListParameter) data.get_as(3, typeof(ListParameter)); + + ListParameter fields = (ListParameter) envelope.get_as(1, typeof(ListParameter)); + + StringParameter date = (StringParameter) fields.get_as(0, typeof(StringParameter)); + StringParameter subject = (StringParameter) fields.get_as(1, typeof(StringParameter)); + + ListParameter from_fields = (ListParameter) fields.get_as(3, typeof(ListParameter)); + ListParameter first_from = (ListParameter) from_fields.get_as(0, typeof(ListParameter)); + StringParameter from_name = (StringParameter) first_from.get_as(0, typeof(StringParameter)); + StringParameter from_mailbox = (StringParameter) first_from.get_as(2, typeof(StringParameter)); + StringParameter from_domain = (StringParameter) first_from.get_as(3, typeof(StringParameter)); + + Message msg = new Message(int.parse(msg_num.value), + "%s <%s@%s>".printf(from_name.value, from_mailbox.value, from_domain.value), + subject.value, date.value); + msgs.add(msg); + } + + return msgs; + } +} + diff --git a/src/engine/imap/Parameter.vala b/src/engine/imap/Parameter.vala index 407a5289..0b4f88ee 100644 --- a/src/engine/imap/Parameter.vala +++ b/src/engine/imap/Parameter.vala @@ -4,9 +4,11 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public abstract class Geary.Imap.Parameter : Serializable { +public abstract class Geary.Imap.Parameter : Object, Serializable { public abstract void serialize(Serializer ser) throws Error; + // to_string() returns a representation of the Parameter suitable for logging and debugging, + // but should not be relied upon for wire or persistent representation. public abstract string to_string(); } @@ -93,50 +95,48 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter { return list.size; } + public new Parameter? get(int index) { + return list.get(index); + } + + public Parameter get_as(int index, Type type) throws ImapError { + assert(type.is_a(typeof(Parameter))); + + if (index >= list.size) + throw new ImapError.TYPE_ERROR("No parameter at index %d", index); + + Parameter param = list.get(index); + if (!param.get_type().is_a(type)) + throw new ImapError.TYPE_ERROR("Parameter %d is not of type %s", index, type.name()); + + return param; + } + public Gee.List get_all() { return list.read_only_view; } - /* - public Parameter? get_next(ref int index, Type type, bool optional) throws ImapError { - assert(type.is_a(Parameter)); - - if (index >= list.size) { - if (!optional) - throw new ImapError.PARSE_ERROR; - - return null; - } - - Parameter param = list.get(index); - if (!(typeof(param).is_a(type)) { - if (!optional) - throw new ImapError.PARSE_ERROR; - - return null; - } - - index++; - - return param; + // This replaces all existing parameters with those from the supplied list + public void copy(ListParameter src) { + list.clear(); + list.add_all(src.get_all()); } - */ protected string stringize_list() { - string str = ""; + StringBuilder builder = new StringBuilder(); int length = list.size; for (int ctr = 0; ctr < length; ctr++) { - str += list[ctr].to_string(); + builder.append(list[ctr].to_string()); if (ctr < (length - 1)) - str += " "; + builder.append_c(' '); } - return str; + return builder.str; } public override string to_string() { - return "%d:(%s)".printf(list.size, stringize_list()); + return "(%s)".printf(stringize_list()); } protected void serialize_list(Serializer ser) throws Error { @@ -160,24 +160,11 @@ public class Geary.Imap.RootParameters : Geary.Imap.ListParameter { base (null, initial); } - /* - public bool is_status_response() { - if (get_count() < 2) - return false; + public RootParameters.clone(RootParameters root) { + base (null); - StringParameter? strparam = get_all().get(1) as StringParameter; - if (strparam == null) - return false; - - try { - Status.decode(strparam.value); - } catch (Error err) { - return false; - } - - return true; + base.copy(root); } - */ public override string to_string() { return stringize_list(); diff --git a/src/engine/imap/Response.vala b/src/engine/imap/Response.vala new file mode 100644 index 00000000..2223895e --- /dev/null +++ b/src/engine/imap/Response.vala @@ -0,0 +1,49 @@ +/* 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 abstract class Geary.Imap.ServerResponse : RootParameters { + public Tag tag { get; private set; } + + public ServerResponse(Tag tag) { + this.tag = tag; + } + + public ServerResponse.reconstitute(RootParameters root) throws ImapError { + base.clone(root); + + tag = new Tag.from_parameter((StringParameter) get_as(0, typeof(StringParameter))); + } + + // Returns true if the RootParameters represents a StatusResponse, otherwise they should be + // treated as ServerData. + public static ServerResponse from_server(RootParameters root, out bool is_status_response) + throws ImapError { + // must be at least two parameters: a tag and a status or a value + if (root.get_count() < 2) { + throw new ImapError.TYPE_ERROR("Too few parameters (%d) for server response", + root.get_count()); + } + + Tag tag = new Tag.from_parameter((StringParameter) root.get_as(0, typeof(StringParameter))); + if (tag.is_tagged()) { + // Attempt to decode second parameter for predefined status codes (piggyback on + // Status.decode's exception if this is invalid) + StringParameter? statusparam = root.get(1) as StringParameter; + if (statusparam != null) + Status.decode(statusparam.value); + + // tagged and has proper status, so it's a status response + is_status_response = true; + + return new StatusResponse.reconstitute(root); + } + + is_status_response = false; + + return new ServerData.reconstitute(root); + } +} + diff --git a/src/engine/imap/ResponseCode.vala b/src/engine/imap/ResponseCode.vala new file mode 100644 index 00000000..d25ba7cf --- /dev/null +++ b/src/engine/imap/ResponseCode.vala @@ -0,0 +1,22 @@ +/* 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.ResponseCode : Geary.Imap.ListParameter { + public ResponseCode(ListParameter parent, Parameter? initial = null) { + base (parent, initial); + } + + public override string to_string() { + return "[%s]".printf(stringize_list()); + } + + public override void serialize(Serializer ser) throws Error { + ser.push_string("["); + serialize_list(ser); + ser.push_string("]"); + } +} + diff --git a/src/engine/imap/Serializer.vala b/src/engine/imap/Serializer.vala index f3110c00..e89055a3 100644 --- a/src/engine/imap/Serializer.vala +++ b/src/engine/imap/Serializer.vala @@ -25,16 +25,6 @@ public class Geary.Imap.Serializer { return get_content_length() > 0; } - // TODO: Remove - public void push_nil() throws Error { - douts.put_string("nil", null); - } - - // TODO: Remove - public void push_token(string str) throws Error { - douts.put_string(str, null); - } - public void push_string(string str) throws Error { douts.put_string(str, null); } diff --git a/src/engine/imap/ServerData.vala b/src/engine/imap/ServerData.vala new file mode 100644 index 00000000..93914bb0 --- /dev/null +++ b/src/engine/imap/ServerData.vala @@ -0,0 +1,16 @@ +/* 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.ServerData : ServerResponse { + public ServerData(Tag tag) { + base (tag); + } + + public ServerData.reconstitute(RootParameters root) throws ImapError { + base.reconstitute(root); + } +} + diff --git a/src/engine/imap/Status.vala b/src/engine/imap/Status.vala new file mode 100644 index 00000000..18b99f27 --- /dev/null +++ b/src/engine/imap/Status.vala @@ -0,0 +1,70 @@ +/* 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.Status { + OK, + NO, + BAD, + PREAUTH, + BYE; + + public string to_string() { + switch (this) { + case OK: + return "ok"; + + case NO: + return "no"; + + case BAD: + return "bad"; + + case PREAUTH: + return "preauth"; + + case BYE: + return "bye"; + + default: + assert_not_reached(); + } + } + + public static Status decode(string value) throws ImapError { + switch (value.down()) { + case "ok": + return OK; + + case "no": + return NO; + + case "bad": + return BAD; + + case "preauth": + return PREAUTH; + + case "bye": + return BYE; + + default: + throw new ImapError.PARSE_ERROR("Unrecognized status response \"%s\"", value); + } + } + + public static Status from_parameter(StringParameter strparam) throws ImapError { + return decode(strparam.value); + } + + public Parameter to_parameter() { + return new StringParameter(to_string()); + } + + public void serialize(Serializer ser) throws Error { + ser.push_string(to_string()); + } +} + diff --git a/src/engine/imap/StatusResponse.vala b/src/engine/imap/StatusResponse.vala new file mode 100644 index 00000000..16f80416 --- /dev/null +++ b/src/engine/imap/StatusResponse.vala @@ -0,0 +1,51 @@ +/* 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.StatusResponse : ServerResponse { + public Status status { get; private set; } + public ResponseCode? response_code { get; private set; } + public string? text { get; private set; } + + public StatusResponse(Tag tag, Status status, ResponseCode? response_code, string? text) { + base (tag); + + this.status = status; + this.response_code = response_code; + this.text = text; + + add(status.to_parameter()); + if (response_code != null) + add(response_code); + if (text != null) + add(new StringParameter(text)); + } + + public StatusResponse.reconstitute(RootParameters root) throws ImapError { + base.reconstitute(root); + + status = Status.from_parameter((StringParameter) get_as(1, typeof(StringParameter))); + response_code = get(2) as ResponseCode; + text = (response_code != null) ? flatten_to_text(3) : flatten_to_text(2); + } + + private string? flatten_to_text(int start_index) throws ImapError { + StringBuilder builder = new StringBuilder(); + + while (start_index < get_count()) { + StringParameter? strparam = get(start_index) as StringParameter; + if (strparam != null) { + builder.append(strparam.value); + if (start_index < (get_count() - 1)) + builder.append_c(' '); + } + + start_index++; + } + + return !is_empty_string(builder.str) ? builder.str : null; + } +} + diff --git a/src/engine/imap/Tag.vala b/src/engine/imap/Tag.vala index 46c60526..43b3b15a 100644 --- a/src/engine/imap/Tag.vala +++ b/src/engine/imap/Tag.vala @@ -5,16 +5,37 @@ */ public class Geary.Imap.Tag : StringParameter { - public Tag.generated(ClientSession session) { - base (session.generate_tag_value()); - } + public const string UNTAGGED_VALUE = "*"; + + private static Tag? untagged = null; public Tag(string value) { base (value); } - public bool is_untagged() { - return value == "*"; + public Tag.from_parameter(StringParameter strparam) { + base (strparam.value); + } + + public static Tag get_untagged() { + if (untagged == null) + untagged = new Tag(UNTAGGED_VALUE); + + return untagged; + } + + public bool is_tagged() { + return value != UNTAGGED_VALUE; + } + + public bool equals(Tag? tag) { + if (this == tag) + return true; + + if (tag == null) + return false; + + return (this.value == tag.value); } } diff --git a/src/engine/state/Machine.vala b/src/engine/state/Machine.vala index 12395dec..1436b85a 100644 --- a/src/engine/state/Machine.vala +++ b/src/engine/state/Machine.vala @@ -59,7 +59,7 @@ public class Geary.State.Machine { assert(event < descriptor.event_count); assert(state < descriptor.state_count); - Mapping? mapping = transitions[state, event]; + unowned Mapping? mapping = transitions[state, event]; Transition transition = (mapping != null) ? mapping.transition : default_transition; if (transition == null) { diff --git a/src/tests/lsmbox.vala b/src/tests/lsmbox.vala new file mode 100644 index 00000000..9103732a --- /dev/null +++ b/src/tests/lsmbox.vala @@ -0,0 +1,64 @@ +/* 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; +int start = 0; +int count = 0; + +async void async_start() { + try { + yield sess.connect_async(user, pass); + + Geary.Folder folder = yield sess.open(mailbox); + Geary.MessageStream? mstream = folder.read(start, count); + + bool ok = false; + if (mstream != null) { + Gee.List? msgs = yield mstream.read(); + if (msgs != null && msgs.size > 0) { + foreach (Geary.Message msg in msgs) + stdout.printf("%s\n", msg.to_string()); + + ok = true; + } + } + + if (!ok) + debug("Unable to examine mailbox %s", mailbox); + } catch (Error err) { + debug("Error: %s", err.message); + } + + main_loop.quit(); +} + +int main(string[] args) { + if (args.length < 6) { + stderr.printf("usage: lsmbox \n"); + + return 1; + } + + main_loop = new MainLoop(); + + user = args[1]; + pass = args[2]; + mailbox = args[3]; + start = int.parse(args[4]); + count = int.parse(args[5]); + + sess = new Geary.Imap.ClientSession("imap.gmail.com", 993); + async_start.begin(); + + main_loop.run(); + + return 0; +} +