Email client ho!

This commit is contained in:
Jim Nelson 2011-04-11 23:16:21 +00:00
commit e3cab0804b
17 changed files with 1705 additions and 0 deletions

54
Makefile Normal file
View file

@ -0,0 +1,54 @@
PROGRAM = geary
BUILD_ROOT = 1
VALAC := valac
APPS := console syntax
ENGINE_SRC := \
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/Parameter.vala \
src/engine/imap/Tag.vala \
src/engine/imap/Command.vala \
src/engine/imap/Commands.vala \
src/engine/imap/Serializable.vala \
src/engine/imap/Serializer.vala \
src/engine/imap/Deserializer.vala \
src/engine/imap/Error.vala \
src/engine/util/string.vala
CONSOLE_SRC := \
src/console/main.vala
SYNTAX_SRC := \
src/tests/syntax.vala
ALL_SRC := $(ENGINE_SRC) $(CONSOLE_SRC) $(SYNTAX_SRC)
EXTERNAL_PKGS := \
gio-2.0 \
gee-1.0 \
gtk+-2.0
.PHONY: all
all: $(APPS)
.PHONY: clean
clean:
rm -f $(ALL_SRC:.vala=.c)
rm -f $(APPS)
console: $(ENGINE_SRC) $(CONSOLE_SRC) Makefile
$(VALAC) --save-temps -g $(foreach pkg,$(EXTERNAL_PKGS),--pkg=$(pkg)) \
$(ENGINE_SRC) $(CONSOLE_SRC) \
-o $@
syntax: $(ENGINE_SRC) $(SYNTAX_SRC) Makefile
$(VALAC) --save-temps -g $(foreach pkg,$(EXTERNAL_PKGS),--pkg=$(pkg)) \
$(ENGINE_SRC) $(SYNTAX_SRC) \
-o $@

337
src/console/main.vala Normal file
View file

@ -0,0 +1,337 @@
/* 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.
*/
errordomain CommandException {
USAGE,
STATE
}
class ImapConsole : Gtk.Window {
private Gtk.TextView console = new Gtk.TextView();
private Gtk.Entry cmdline = new Gtk.Entry();
private Gtk.Statusbar statusbar = new Gtk.Statusbar();
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() {
title = "IMAP Console";
destroy.connect(() => { Gtk.main_quit(); });
set_default_size(800, 600);
Gtk.VBox layout = new Gtk.VBox(false, 4);
console.editable = false;
Gtk.ScrolledWindow scrolled_console = new Gtk.ScrolledWindow(null, null);
scrolled_console.add(console);
scrolled_console.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
layout.pack_start(scrolled_console, true, true, 0);
cmdline.activate.connect(on_activate);
layout.pack_start(cmdline, false, false, 0);
statusbar_ctx = statusbar.get_context_id("status");
statusbar.has_resize_grip = true;
layout.pack_end(statusbar, false, false, 0);
add(layout);
cmdline.grab_focus();
}
private void on_activate() {
exec(cmdline.buffer.text);
cmdline.buffer.delete_text(0, -1);
}
private void clear_status() {
if (statusbar_msg_id == 0)
return;
statusbar.remove(statusbar_ctx, statusbar_msg_id);
statusbar_msg_id = 0;
}
private void status(string text) {
clear_status();
string msg = text;
if (!msg.has_suffix(".") && !msg.has_prefix("usage"))
msg += ".";
statusbar_msg_id = statusbar.push(statusbar_ctx, msg);
}
private void exception(Error err) {
status(err.message);
}
private void exec(string input) {
string[] lines = input.strip().split(";");
foreach (string line in lines) {
string[] tokens = line.strip().split(" ");
if (tokens.length == 0)
continue;
string cmd = tokens[0].strip().down();
string[] args = new string[0];
for (int ctr = 1; ctr < tokens.length; ctr++) {
string arg = tokens[ctr].strip();
if (!is_empty_string(arg))
args += arg;
}
clear_status();
try {
switch (cmd) {
case "noop":
case "nop":
noop(cmd, args);
break;
case "capabilities":
case "caps":
capabilities(cmd, args);
break;
case "connect":
connect_cmd(cmd, args);
break;
case "disconnect":
disconnect_cmd(cmd, args);
break;
case "login":
login(cmd, args);
break;
case "logout":
case "bye":
case "kthxbye":
logout(cmd, args);
break;
case "list":
list(cmd, args);
break;
case "examine":
examine(cmd, args);
break;
case "exit":
case "quit":
quit(cmd, args);
break;
case "gmail":
string[] fake_args = new string[1];
fake_args[0] = "imap.gmail.com:993";
connect_cmd("connect", fake_args);
break;
default:
status("Unknown command \"%s\"".printf(cmd));
break;
}
} catch (Error ce) {
status(ce.message);
}
}
}
private void check_args(string cmd, string[] args, int count, string? usage) throws CommandException {
if (args.length != count)
throw new CommandException.USAGE("usage: %s %s", cmd, usage != null ? usage : "");
}
private void check_connected(string cmd, string[] args, int count, string? usage) throws CommandException {
if (cx == null)
throw new CommandException.STATE("'connect' required");
check_args(cmd, args, count, usage);
}
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,
on_capabilities);
}
private void on_capabilities(Object? source, AsyncResult result) {
try {
cx.send_async.end(result);
status("Success");
} catch (Error err) {
exception(err);
}
}
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,
on_noop);
}
private void on_noop(Object? source, AsyncResult result) {
try {
cx.send_async.end(result);
status("Success");
} catch (Error err) {
exception(err);
}
}
private void connect_cmd(string cmd, string[] args) throws Error {
if (cx != null)
throw new CommandException.STATE("'logout' required");
check_args(cmd, args, 1, "hostname[:port]");
cx = new Geary.Imap.ClientConnection(args[0], Geary.Imap.ClientConnection.DEFAULT_PORT);
status("Connecting to %s...".printf(args[0]));
cx.connect_async.begin(null, on_connected);
}
private void on_connected(Object? source, AsyncResult result) {
try {
cx.connect_async.end(result);
status("Connected");
cx.sent_command.connect(on_sent_command);
cx.received_response.connect(on_received_response);
cx.xon();
} catch (Error err) {
cx = null;
exception(err);
}
}
private void disconnect_cmd(string cmd, string[] args) throws Error {
check_connected(cmd, args, 0, null);
status("Disconnecting...");
cx.disconnect_async.begin(null, on_disconnected);
}
private void on_disconnected(Object? source, AsyncResult result) {
try {
cx.disconnect_async.end(result);
status("Disconnected");
cx.sent_command.disconnect(on_sent_command);
cx = null;
} catch (Error err) {
exception(err);
}
}
private void login(string cmd, string[] args) throws Error {
check_connected(cmd, args, 2, "user pass");
status("Logging in...");
cx.post(new Geary.Imap.LoginCommand(session, args[0], args[1]), on_logged_in);
}
private void on_logged_in(Object? source, AsyncResult result) {
try {
cx.finish_post(result);
} catch (Error err) {
exception(err);
}
}
private void logout(string cmd, string[] args) throws Error {
check_connected(cmd, args, 0, null);
status("Logging out...");
cx.post(new Geary.Imap.LogoutCommand(session), on_logout);
}
private void on_logout(Object? source, AsyncResult result) {
try {
cx.finish_post(result);
status("Logged out");
} catch (Error err) {
exception(err);
}
}
private void list(string cmd, string[] args) throws Error {
check_connected(cmd, args, 2, "<reference> <mailbox>");
status("Listing...");
cx.post(new Geary.Imap.ListCommand.wildcarded(session, args[0], args[1]), on_list);
}
private void on_list(Object? source, AsyncResult result) {
try {
cx.finish_post(result);
status("Listed");
} catch (Error err) {
exception(err);
}
}
private void examine(string cmd, string[] args) throws Error {
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);
}
private void on_examine(Object? source, AsyncResult result) {
try {
cx.finish_post(result);
status("Opened read-only");
} catch (Error err) {
exception(err);
}
}
private void quit(string cmd, string[] args) throws Error {
Gtk.main_quit();
}
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) {
append_to_console("[R] ");
append_to_console(params.to_string());
append_to_console("\n");
}
private void append_to_console(string text) {
Gtk.TextIter iter;
console.buffer.get_iter_at_offset(out iter, -1);
console.buffer.insert(iter, text, -1);
}
}
void main(string[] args) {
Gtk.init(ref args);
ImapConsole console = new ImapConsole();
console.show_all();
Gtk.main();
}

View file

@ -0,0 +1,264 @@
/* 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.ClientConnection {
public const uint16 DEFAULT_PORT = 143;
public const uint16 DEFAULT_PORT_TLS = 993;
private string host_specifier;
private uint16 default_port;
private SocketClient socket_client = new SocketClient();
private SocketConnection? cx = null;
private DataInputStream? dins = null;
private int ins_priority = Priority.DEFAULT;
private Cancellable ins_cancellable = new Cancellable();
private bool flow_controlled = true;
private Deserializer des = new Deserializer();
private uint8[] block_buffer = new uint8[4096];
public virtual signal void connected() {
}
public virtual signal void disconnected() {
}
public virtual signal void flow_control(bool xon) {
}
public virtual signal void sent_command(Command cmd) {
}
public virtual signal void received_response(RootParameters params) {
}
public virtual signal void receive_failed(Error err) {
}
public ClientConnection(string host_specifier, uint16 default_port) {
this.host_specifier = host_specifier;
this.default_port = default_port;
socket_client.set_tls(true);
socket_client.set_tls_validation_flags(TlsCertificateFlags.UNKNOWN_CA);
des.parameters_ready.connect(on_parameters_ready);
}
private void on_parameters_ready(RootParameters params) {
received_response(params);
}
/*
public void connect(Cancellable? cancellable = null) throws Error {
if (cx != null)
throw new IOError.EXISTS("Already connected to %s", to_string());
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();
}
*/
public async void connect_async(Cancellable? cancellable = null) throws Error {
if (cx != null)
throw new IOError.EXISTS("Already connected to %s", to_string());
cx = yield socket_client.connect_to_host_async(host_specifier, default_port, cancellable);
dins = new DataInputStream(cx.input_stream);
dins.set_newline_type(DataStreamNewlineType.CR_LF);
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)
return;
yield cx.close_async(Priority.DEFAULT, cancellable);
cx = null;
dins = null;
disconnected();
}
public void xon(int priority = Priority.DEFAULT) throws Error {
check_for_connection();
if (!flow_controlled)
return;
flow_controlled = false;
ins_priority = priority;
next_deserialize_step();
flow_control(true);
}
private void next_deserialize_step() {
switch (des.get_mode()) {
case Deserializer.Mode.LINE:
dins.read_line_async.begin(ins_priority, ins_cancellable, on_read_line);
break;
case Deserializer.Mode.BLOCK:
long count = long.min(block_buffer.length, des.get_max_data_length());
dins.read_async.begin(block_buffer[0:count], ins_priority, ins_cancellable,
on_read_block);
break;
default:
error("Failed");
}
}
private void on_read_line(Object? source, AsyncResult result) {
try {
string line = dins.read_line_async.end(result);
des.push_line(line);
} catch (Error err) {
if (!(err is IOError.CANCELLED))
receive_failed(err);
return;
}
if (!flow_controlled)
next_deserialize_step();
}
private void on_read_block(Object? source, AsyncResult result) {
try {
ssize_t read = dins.read_async.end(result);
des.push_data(block_buffer[0:read]);
} catch (Error err) {
if (!(err is IOError.CANCELLED))
receive_failed(err);
return;
}
if (!flow_controlled)
next_deserialize_step();
}
public void xoff() throws Error {
check_for_connection();
if (flow_controlled)
return;
// turn off the spigot
// TODO: Don't cancel the read, merely don't post the next window
flow_controlled = true;
ins_cancellable.cancel();
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().
*/
public void post(Command cmd, AsyncReadyCallback cb, int priority = Priority.DEFAULT,
Cancellable? cancellable = null) {
send_async.begin(cmd, priority, cancellable, cb);
}
/**
* Convenience method for sync_async.end(). This is largely provided for symmetry with
* post_send().
*/
public void finish_post(AsyncResult result) throws Error {
send_async.end(result);
}
public async void send_async(Command cmd, int priority = Priority.DEFAULT,
Cancellable? cancellable = null) throws Error {
check_for_connection();
Serializer ser = new Serializer();
cmd.serialize(ser);
assert(ser.has_content());
yield write_all_async(ser, priority, cancellable);
sent_command(cmd);
}
public async void send_multiple_async(Gee.List<Command> cmds, int priority = Priority.DEFAULT,
Cancellable? cancellable = null) throws Error {
if (cmds.size == 0)
return;
check_for_connection();
Serializer ser = new Serializer();
foreach (Command cmd in cmds)
cmd.serialize(ser);
assert(ser.has_content());
yield write_all_async(ser, priority, cancellable);
// Variable named due to this bug: https://bugzilla.gnome.org/show_bug.cgi?id=596861
foreach (Command cmd2 in cmds)
sent_command(cmd2);
}
// Can't pass the raw buffer due to this bug: https://bugzilla.gnome.org/show_bug.cgi?id=639054
private async void write_all_async(Serializer ser, int priority, Cancellable? cancellable)
throws Error {
ssize_t index = 0;
size_t length = ser.get_content_length();
while (index < length) {
index += yield cx.output_stream.write_async(ser.get_content()[index:length],
priority, cancellable);
if (index < length)
debug("PARTIAL WRITE TO %s: %lu/%lu bytes", to_string(), index, length);
}
}
private void check_for_connection() throws Error {
if (cx == null)
throw new IOError.CLOSED("Not connected to %s", to_string());
}
public string to_string() {
return "%s:%ud".printf(host_specifier, default_port);
}
}

View file

@ -0,0 +1,25 @@
/* 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.ClientSession {
private int tag_counter = 0;
private char tag_prefix = 'a';
// 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++;
}
return "%c%03d".printf(tag_prefix, tag_counter);
}
}

View file

@ -0,0 +1,25 @@
/* 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.Command : RootParameters {
public Tag tag { get; private set; }
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()) {
this.tag = tag;
this.name = name;
this.args = args;
add(tag);
add(new StringParameter(name));
if (args != null) {
foreach (string arg in args)
add(new StringParameter(arg));
}
}
}

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.
*/
public class Geary.Imap.CapabilityCommand : Command {
public const string NAME = "capability";
public CapabilityCommand(ClientSession session) {
base (new Tag.generated(session), NAME);
}
}
public class Geary.Imap.NoopCommand : Command {
public const string NAME = "noop";
public NoopCommand(ClientSession session) {
base (new Tag.generated(session), 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 override string to_string() {
return "%s %s <user> <pass>".printf(tag.to_string(), name);
}
}
public class Geary.Imap.LogoutCommand : Command {
public const string NAME = "logout";
public LogoutCommand(ClientSession session) {
base (new Tag.generated(session), 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.wildcarded(ClientSession session, string reference, string mailbox) {
base (new Tag.generated(session), 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 });
}
}

View file

@ -0,0 +1,417 @@
/* 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.Deserializer {
public enum Mode {
LINE,
BLOCK,
FAILED
}
private enum State {
TAG,
START_PARAM,
ATOM,
QUOTED,
QUOTED_ESCAPE,
LITERAL,
LITERAL_DATA_BEGIN,
LITERAL_DATA,
FAILED,
COUNT
}
private static string state_to_string(uint state) {
return ((State) state).to_string();
}
private enum Event {
CHAR,
EOL,
DATA,
COUNT
}
private static string event_to_string(uint event) {
return ((Event) event).to_string();
}
// Atom specials includes space and close-parens, but those are handled in particular ways while
// in the ATOM state, so they're not included here. Also note that while documentation
// indicates that the backslash cannot be used in an atom, they *are* used for message flags
// and thus must be special-cased in the code.
private static unichar[] atom_specials = {
'(', '{', '%', '*', '\"'
};
// Tag specials are like atom specials but include the continuation character ('+'). Like atom
// specials, the space is treated in a particular way, but unlike atom, the close-parens
// character is not. Also, the star character is allowed, although technically only correct
// in the context of a status response; it's the responsibility of higher layers to catch this.
private static unichar[] tag_specials = {
'(', ')', '{', '%', '\"', '\\', '+'
};
private struct LiteralData {
public unowned uint8[] data;
public LiteralData(owned uint8[] data) {
this.data = data;
}
}
private static Geary.State.MachineDescriptor machine_desc = new Geary.State.MachineDescriptor(
"Geary.Imap.Deserializer", State.TAG, State.COUNT, Event.COUNT,
state_to_string, event_to_string);
private Geary.State.Machine fsm;
private ListParameter current;
private RootParameters root = new RootParameters();
private StringBuilder? current_string = null;
private LiteralParameter? current_literal = null;
private long literal_length_remaining = 0;
public signal void parameters_ready(RootParameters root);
public signal void failed();
public Deserializer() {
current = root;
Geary.State.Mapping[] mappings = {
new Geary.State.Mapping(State.TAG, Event.CHAR, on_tag_or_atom_char),
new Geary.State.Mapping(State.START_PARAM, Event.CHAR, on_first_param_char),
new Geary.State.Mapping(State.START_PARAM, Event.EOL, on_eol),
new Geary.State.Mapping(State.ATOM, Event.CHAR, on_tag_or_atom_char),
new Geary.State.Mapping(State.ATOM, Event.EOL, on_atom_eol),
new Geary.State.Mapping(State.QUOTED, Event.CHAR, on_quoted_char),
new Geary.State.Mapping(State.QUOTED_ESCAPE, Event.CHAR, on_quoted_escape_char),
new Geary.State.Mapping(State.LITERAL, Event.CHAR, on_literal_char),
new Geary.State.Mapping(State.LITERAL_DATA_BEGIN, Event.EOL, on_literal_data_begin_eol),
new Geary.State.Mapping(State.LITERAL_DATA, Event.DATA, on_literal_data)
};
fsm = new Geary.State.Machine(machine_desc, mappings, on_bad_transition);
}
// Push a line (without the CRLF!).
public Mode push_line(string line) {
assert(get_mode() == Mode.LINE);
int index = 0;
unichar ch;
while (line.get_next_char(ref index, out ch)) {
if (fsm.issue(Event.CHAR, &ch) == State.FAILED) {
failed();
return Mode.FAILED;
}
}
if (fsm.issue(Event.EOL) == State.FAILED) {
failed();
return Mode.FAILED;
}
return get_mode();
}
public long get_max_data_length() {
return literal_length_remaining;
}
// Push a block of literal data
public Mode push_data(uint8[] data) {
assert(get_mode() == Mode.BLOCK);
LiteralData literal_data = LiteralData(data);
if (fsm.issue(Event.DATA, &literal_data) == State.FAILED) {
failed();
return Mode.FAILED;
}
return get_mode();
}
public Mode get_mode() {
switch (fsm.get_state()) {
case State.LITERAL_DATA:
return Mode.LINE;
case State.FAILED:
return Mode.FAILED;
default:
return Mode.LINE;
}
}
private bool is_current_string_empty() {
return (current_string == null) || is_empty_string(current_string.str);
}
private void append_to_string(unichar ch) {
if (current_string == null)
current_string = new StringBuilder();
current_string.append_unichar(ch);
}
private void append_to_literal(uint8[] data) {
if (current_literal == null)
current_literal = new LiteralParameter(data);
else
current_literal.add(data);
}
private void save_string_parameter() {
if (is_current_string_empty())
return;
save_parameter(new StringParameter(current_string.str));
current_string = null;
}
private void save_literal_parameter() {
if (current_literal == null)
return;
save_parameter(current_literal);
current_literal = null;
}
private void save_parameter(Parameter param) {
current.add(param);
}
private void push() {
ListParameter child = new ListParameter(current);
current.add(child);
current = child;
}
private State pop() {
ListParameter? parent = current.get_parent();
if (parent == null) {
warning("Attempt to close unopened list");
return State.FAILED;
}
current = parent;
return State.START_PARAM;
}
private State flush_params() {
if (current != root) {
warning("Unclosed list in parameters");
return State.FAILED;
}
if (!is_current_string_empty() || current_literal != null || literal_length_remaining > 0) {
warning("Unfinished parameter");
return State.FAILED;
}
parameters_ready(root);
root = new RootParameters();
current = root;
return State.TAG;
}
//
// Transition handlers
//
private uint on_first_param_char(uint state, uint event, void *user) {
// 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 '{':
return State.LITERAL;
case '\"':
return State.QUOTED;
case '(':
// open list
push();
return State.START_PARAM;
case ')':
// close list
return pop();
default:
return on_tag_or_atom_char(State.ATOM, event, user);
}
}
private uint on_eol(uint state, uint event, void *user) {
return flush_params();
}
private uint on_tag_or_atom_char(uint state, uint event, void *user) {
assert(state == State.TAG || state == State.ATOM);
unichar ch = *((unichar *) user);
// drop everything above 0x7F
if (ch > 0x7F)
return state;
// drop control characters
if (ch.iscntrl())
return state;
// tags and atoms have different special characters
if (state == State.TAG && (ch in tag_specials))
return state;
else if (state == State.ATOM && (ch in atom_specials))
return state;
// message flag indicator is only legal at start of atom
if (state == State.ATOM && ch == '\\' && !is_current_string_empty())
return state;
// space indicates end-of-atom or end-of-tag
if (ch == ' ') {
save_string_parameter();
return State.START_PARAM;
}
// close-parens after an atom indicates end-of-list
if (state == State.ATOM && ch == ')') {
save_string_parameter();
return pop();
}
append_to_string(ch);
return state;
}
private uint on_atom_eol(uint state, uint event, void *user) {
// clean up final atom
save_string_parameter();
return flush_params();
}
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')
return State.QUOTED;
// look for escaped characters
if (ch == '\\')
return State.QUOTED_ESCAPE;
// DQUOTE ends quoted string and return to parsing atoms
if (ch == '\"') {
save_string_parameter();
return State.START_PARAM;
}
append_to_string(ch);
return State.QUOTED;
}
private uint on_quoted_escape_char(uint state, uint event, void *user) {
unichar ch = *((unichar *) user);
// only two accepted escaped characters: double-quote and backslash
// everything else dropped on the floor
switch (ch) {
case '\"':
case '\\':
append_to_string(ch);
break;
}
return State.QUOTED;
}
private uint on_literal_char(uint state, uint event, void *user) {
unichar ch = *((unichar *) user);
// if close-bracket, end of literal length field -- next event must be EOL
if (ch == '}') {
// empty literal treated as garbage
if (is_current_string_empty())
return State.FAILED;
literal_length_remaining = long.parse(current_string.str);
if (literal_length_remaining < 0) {
warning("Negative literal data length %ld", literal_length_remaining);
return State.FAILED;
}
return State.LITERAL_DATA_BEGIN;
}
// drop anything non-numeric
if (!ch.isdigit())
return State.LITERAL;
append_to_string(ch);
return State.LITERAL;
}
private uint on_literal_data_begin_eol(uint state, uint event, void *user) {
return State.LITERAL_DATA;
}
private uint on_literal_data(uint state, uint event, void *user) {
LiteralData *literal_data = (LiteralData *) user;
assert(literal_data.data.length <= literal_length_remaining);
literal_length_remaining -= literal_data.data.length;
append_to_literal(literal_data.data);
if (literal_length_remaining > 0)
return State.LITERAL_DATA;
save_literal_parameter();
return State.START_PARAM;
}
private uint on_bad_transition(uint state, uint event, void *user) {
warning("Bad event %s at state %s", event_to_string(event), state_to_string(state));
return State.FAILED;
}
}

View file

@ -0,0 +1,10 @@
/* 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.
*/
errordomain Geary.ImapError {
PARSE_ERROR;
}

View file

@ -0,0 +1,191 @@
/* 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.Parameter : Serializable {
public abstract void serialize(Serializer ser) throws Error;
public abstract string to_string();
}
public class Geary.Imap.StringParameter : Geary.Imap.Parameter {
public string value { get; private set; }
public StringParameter(string value) requires (!is_empty_string(value)) {
this.value = value;
}
public StringParameter.NIL() {
this.value = "nil";
}
public bool is_nil() {
return value.down() == "nil";
}
public override string to_string() {
return value;
}
public override void serialize(Serializer ser) throws Error {
ser.push_string(value);
}
}
public class Geary.Imap.LiteralParameter : Geary.Imap.Parameter {
private MemoryInputStream mins = new MemoryInputStream();
private long size = 0;
public LiteralParameter(uint8[]? initial = null) {
if (initial != null)
add(initial);
}
public void add(uint8[] data) {
if (data.length == 0)
return;
mins.add_data(data, null);
size += data.length;
}
public long get_size() {
return size;
}
public override string to_string() {
return "{literal/%ldb}".printf(size);
}
public override void serialize(Serializer ser) throws Error {
ser.push_string("{%ld}".printf(size));
ser.push_eol();
ser.push_input_stream_literal_data(mins);
// seek to start
mins.seek(0, SeekType.SET);
}
}
public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
private weak ListParameter? parent;
private Gee.List<Parameter> list = new Gee.ArrayList<Parameter>();
public ListParameter(ListParameter? parent, Parameter? initial = null) {
this.parent = parent;
if (initial != null)
add(initial);
}
public ListParameter? get_parent() {
return parent;
}
public void add(Parameter param) {
bool added = list.add(param);
assert(added);
}
public int get_count() {
return list.size;
}
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;
}
*/
protected string stringize_list() {
string str = "";
int length = list.size;
for (int ctr = 0; ctr < length; ctr++) {
str += list[ctr].to_string();
if (ctr < (length - 1))
str += " ";
}
return str;
}
public override string to_string() {
return "%d:(%s)".printf(list.size, stringize_list());
}
protected void serialize_list(Serializer ser) throws Error {
int length = list.size;
for (int ctr = 0; ctr < length; ctr++) {
list[ctr].serialize(ser);
if (ctr < (length - 1))
ser.push_space();
}
}
public override void serialize(Serializer ser) throws Error {
ser.push_string("(");
serialize_list(ser);
ser.push_string(")");
}
}
public class Geary.Imap.RootParameters : Geary.Imap.ListParameter {
public RootParameters(Parameter? initial = null) {
base (null, initial);
}
/*
public bool is_status_response() {
if (get_count() < 2)
return false;
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;
}
*/
public override string to_string() {
return stringize_list();
}
public override void serialize(Serializer ser) throws Error {
serialize_list(ser);
ser.push_eol();
}
}

View file

@ -0,0 +1,10 @@
/* 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.Imap.Serializable {
public abstract void serialize(Serializer ser) throws Error;
}

View file

@ -0,0 +1,60 @@
/* 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.Serializer {
private MemoryOutputStream mouts;
private DataOutputStream douts;
public Serializer() {
mouts = new MemoryOutputStream(null, realloc, free);
douts = new DataOutputStream(mouts);
}
public unowned uint8[] get_content() {
return mouts.get_data();
}
public size_t get_content_length() {
return mouts.get_data_size();
}
public bool has_content() {
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);
}
public void push_space() throws Error {
douts.put_byte(' ', null);
}
public void push_eol() throws Error {
douts.put_string("\r\n", null);
}
public void push_literal_data(uint8[] data) throws Error {
size_t written;
douts.write_all(data, out written);
assert(written == data.length);
}
public void push_input_stream_literal_data(InputStream ins) throws Error {
douts.splice(ins, OutputStreamSpliceFlags.NONE);
}
}

20
src/engine/imap/Tag.vala Normal file
View file

@ -0,0 +1,20 @@
/* 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.Tag : StringParameter {
public Tag.generated(ClientSession session) {
base (session.generate_tag_value());
}
public Tag(string value) {
base (value);
}
public bool is_untagged() {
return value == "*";
}
}

View file

@ -0,0 +1,102 @@
/* 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.State.Machine {
private Geary.State.MachineDescriptor descriptor;
private uint state;
private Mapping[,] transitions;
private Transition? default_transition;
private bool locked = false;
private bool abort_on_no_transition = true;
private bool logging = false;
public Machine(MachineDescriptor descriptor, Mapping[] mappings, Transition? default_transition) {
this.descriptor = descriptor;
this.default_transition = default_transition;
// verify that each state and event in the mappings are valid
foreach (Mapping mapping in mappings) {
assert(mapping.state < descriptor.state_count);
assert(mapping.event < descriptor.event_count);
}
state = descriptor.start_state;
// build a transition map with state/event IDs (i.e. offsets) pointing directly into the
// map
transitions = new Mapping[descriptor.state_count, descriptor.event_count];
for (int ctr = 0; ctr < mappings.length; ctr++) {
Mapping mapping = mappings[ctr];
assert(transitions[mapping.state, mapping.event] == null);
transitions[mapping.state, mapping.event] = mapping;
}
}
public uint get_state() {
return state;
}
public bool get_abort_on_no_transition() {
return abort_on_no_transition;
}
public void set_abort_on_no_transition(bool abort) {
abort_on_no_transition = abort;
}
public void set_logging(bool logging) {
this.logging = logging;
}
public bool get_logging() {
return logging;
}
public uint issue(uint event, void *user = null) {
assert(event < descriptor.event_count);
assert(state < descriptor.state_count);
Mapping? mapping = transitions[state, event];
Transition transition = (mapping != null) ? mapping.transition : default_transition;
if (transition == null) {
string msg = "%s: No transition defined at %s for %s".printf(to_string(),
descriptor.get_state_string(state), descriptor.get_event_string(event));
if (get_abort_on_no_transition())
error(msg);
else
critical(msg);
return state;
}
// guard against reentrancy ... don't want to use a non-reentrant lock because then
// the machine will simply hang; assertion is better to ferret out design flaws
assert(!locked);
locked = true;
uint old_state = state;
state = transition(state, event, user);
assert(state < descriptor.state_count);
assert(locked);
locked = false;
if (get_logging()) {
message("%s: State transition from %s to %s due to event %s", to_string(),
descriptor.get_state_string(old_state), descriptor.get_state_string(state),
descriptor.get_event_string(event));
}
return state;
}
public string to_string() {
return "Machine %s".printf(descriptor.name);
}
}

View file

@ -0,0 +1,39 @@
/* 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 delegate string Geary.State.StateEventToString(uint state_or_event);
public class Geary.State.MachineDescriptor {
public string name { get; private set; }
public uint start_state { get; private set; }
public uint state_count { get; private set; }
public uint event_count { get; private set; }
private StateEventToString? state_to_string;
private StateEventToString? event_to_string;
public MachineDescriptor(string name, uint start_state, uint state_count, uint event_count,
StateEventToString? state_to_string, StateEventToString? event_to_string) {
this.name = name;
this.start_state = start_state;
this.state_count = state_count;
this.event_count = event_count;
this.state_to_string = state_to_string;
this.event_to_string = event_to_string;
// starting state should be valid
assert(start_state < state_count);
}
public string get_state_string(uint state) {
return (state_to_string != null) ? state_to_string(state) : "%s STATE %u".printf(name, state);
}
public string get_event_string(uint event) {
return (event_to_string != null) ? event_to_string(event) : "%s EVENT %u".printf(name, event);
}
}

View file

@ -0,0 +1,28 @@
/* 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 delegate uint Geary.State.Transition(uint state, uint event, void *user);
public class Geary.State.Mapping {
public uint state;
public uint event;
public Transition transition;
public Mapping(uint state, uint event, Transition transition) {
this.state = state;
this.event = event;
this.transition = transition;
}
}
namespace Geary.State {
// A utility Transition for nop transitions (i.e. it merely returns the state passed in).
public uint nop(uint state, uint event, void *user) {
return state;
}
}

View file

@ -0,0 +1,10 @@
/* 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 inline bool is_empty_string(string? str) {
return (str == null || str[0] == 0);
}

51
src/tests/syntax.vala Normal file
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.
*/
void print(int depth, Gee.List<Geary.Imap.Parameter> params) {
string pad = string.nfill(depth * 4, ' ');
int index = 0;
foreach (Geary.Imap.Parameter param in params) {
Geary.Imap.ListParameter? list = param as Geary.Imap.ListParameter;
if (list == null) {
stdout.printf("%s#%02d >%s<\n", pad, index++, param.to_string());
continue;
}
print(depth + 1, list.get_all());
}
}
void on_params_ready(Geary.Imap.RootParameters root) {
print(0, root.get_all());
}
int main(string[] args) {
if (args.length < 2) {
stderr.printf("usage: syntax <imap command>\n");
return 1;
}
Geary.Imap.Deserializer des = new Geary.Imap.Deserializer();
des.parameters_ready.connect(on_params_ready);
// turn argument into single line for deserializer
string line = "";
for (int ctr = 1; ctr < args.length; ctr++) {
line += args[ctr];
if (ctr < (args.length - 1))
line += " ";
}
stdout.printf("INPUT: >%s<\n", line);
Geary.Imap.Deserializer.Mode mode = des.push_line(line);
stdout.printf("INPUT MODE: %s\n", mode.to_string());
return 0;
}