Email client ho!
This commit is contained in:
commit
e3cab0804b
17 changed files with 1705 additions and 0 deletions
54
Makefile
Normal file
54
Makefile
Normal 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
337
src/console/main.vala
Normal 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();
|
||||
}
|
||||
|
||||
264
src/engine/imap/ClientConnection.vala
Normal file
264
src/engine/imap/ClientConnection.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
25
src/engine/imap/ClientSession.vala
Normal file
25
src/engine/imap/ClientSession.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
25
src/engine/imap/Command.vala
Normal file
25
src/engine/imap/Command.vala
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
62
src/engine/imap/Commands.vala
Normal file
62
src/engine/imap/Commands.vala
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
||||
417
src/engine/imap/Deserializer.vala
Normal file
417
src/engine/imap/Deserializer.vala
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
10
src/engine/imap/Error.vala
Normal file
10
src/engine/imap/Error.vala
Normal 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;
|
||||
}
|
||||
|
||||
191
src/engine/imap/Parameter.vala
Normal file
191
src/engine/imap/Parameter.vala
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
10
src/engine/imap/Serializable.vala
Normal file
10
src/engine/imap/Serializable.vala
Normal 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;
|
||||
}
|
||||
|
||||
60
src/engine/imap/Serializer.vala
Normal file
60
src/engine/imap/Serializer.vala
Normal 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
20
src/engine/imap/Tag.vala
Normal 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 == "*";
|
||||
}
|
||||
}
|
||||
|
||||
102
src/engine/state/Machine.vala
Normal file
102
src/engine/state/Machine.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
39
src/engine/state/MachineDescriptor.vala
Normal file
39
src/engine/state/MachineDescriptor.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
28
src/engine/state/Mapping.vala
Normal file
28
src/engine/state/Mapping.vala
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
10
src/engine/util/string.vala
Normal file
10
src/engine/util/string.vala
Normal 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
51
src/tests/syntax.vala
Normal 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;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue