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.

This commit is contained in:
Jim Nelson 2011-04-14 01:15:05 +00:00
parent e3cab0804b
commit 6433ebfa5b
24 changed files with 997 additions and 167 deletions

View file

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

View file

@ -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, "<reference> <mailbox>");
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, "<mailbox>");
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, "<message-span> <data-item>");
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");
}

15
src/engine/Engine.vala Normal file
View file

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

View file

@ -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<Message>? read(Cancellable? cancellable = null) throws Error;
}

24
src/engine/Message.vala Normal file
View file

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

View file

@ -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 "<a-z><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().
*/

View file

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

View file

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

View file

@ -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<ServerData> server_data { get; private set; }
public StatusResponse? status_response { get; private set; }
public CommandResponse() {
server_data = new Gee.ArrayList<ServerData>();
}
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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Message>? 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<Message> msgs = new Gee.ArrayList<Message>();
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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

64
src/tests/lsmbox.vala Normal file
View file

@ -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<Geary.Message>? 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 <user> <pass> <mailbox> <start #> <count>\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;
}