diff --git a/Makefile b/Makefile index 67f822ed..19090c7f 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +# Only geary is built by default. Use "make all" to build command-line tools. + PROGRAM = geary BUILD_ROOT = 1 @@ -16,6 +18,7 @@ ENGINE_SRC := \ src/engine/common/MessageData.vala \ src/engine/imap/ClientConnection.vala \ src/engine/imap/ClientSession.vala \ + src/engine/imap/DataFormat.vala \ src/engine/imap/Mailbox.vala \ src/engine/imap/Parameter.vala \ src/engine/imap/Tag.vala \ @@ -34,9 +37,11 @@ ENGINE_SRC := \ src/engine/imap/Serializer.vala \ src/engine/imap/Deserializer.vala \ src/engine/imap/Error.vala \ + src/engine/imap/Flag.vala \ src/engine/imap/decoders/FetchDataDecoder.vala \ src/engine/imap/decoders/FetchResults.vala \ src/engine/imap/decoders/NoopResults.vala \ + src/engine/imap/decoders/ListResults.vala \ src/engine/rfc822/MailboxAddress.vala \ src/engine/rfc822/MessageData.vala \ src/engine/util/String.vala \ @@ -49,6 +54,8 @@ CLIENT_SRC := \ src/client/ui/MainWindow.vala \ src/client/ui/MessageListView.vala \ src/client/ui/MessageListStore.vala \ + src/client/ui/FolderListView.vala \ + src/client/ui/FolderListStore.vala \ src/client/util/Intl.vala \ src/client/util/Date.vala @@ -80,6 +87,11 @@ EXTERNAL_PKGS := \ VAPI_FILES := \ vapi/gmime-2.4.vapi +geary: $(ENGINE_SRC) $(CLIENT_SRC) Makefile $(VAPI_FILES) + $(VALAC) $(VALAFLAGS) $(foreach pkg,$(EXTERNAL_PKGS),--pkg=$(pkg)) \ + $(ENGINE_SRC) $(CLIENT_SRC) \ + -o $@ + .PHONY: all all: $(APPS) @@ -88,11 +100,6 @@ clean: rm -f $(ALL_SRC:.vala=.c) rm -f $(APPS) -geary: $(ENGINE_SRC) $(CLIENT_SRC) Makefile $(VAPI_FILES) - $(VALAC) $(VALAFLAGS) $(foreach pkg,$(EXTERNAL_PKGS),--pkg=$(pkg)) \ - $(ENGINE_SRC) $(CLIENT_SRC) \ - -o $@ - console: $(ENGINE_SRC) $(CONSOLE_SRC) Makefile $(VALAC) $(VALAFLAGS) $(foreach pkg,$(EXTERNAL_PKGS),--pkg=$(pkg)) \ $(ENGINE_SRC) $(CONSOLE_SRC) \ diff --git a/src/client/ui/FolderListStore.vala b/src/client/ui/FolderListStore.vala new file mode 100644 index 00000000..b420d684 --- /dev/null +++ b/src/client/ui/FolderListStore.vala @@ -0,0 +1,55 @@ +/* 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 FolderListStore : Gtk.TreeStore { + public enum Column { + NAME, + N_COLUMNS; + + public static Column[] all() { + return { NAME }; + } + + public static Type[] get_types() { + return { + typeof (string) + }; + } + + public string to_string() { + switch (this) { + case NAME: + return _("Name"); + + default: + assert_not_reached(); + } + } + } + + public FolderListStore() { + set_column_types(Column.get_types()); + } + + public void add_folder(string folder) { + Gtk.TreeIter iter; + append(out iter, null); + + set(iter, Column.NAME, folder); + } + + public string? get_folder_at(Gtk.TreePath path) { + Gtk.TreeIter iter; + if (!get_iter(out iter, path)) + return null; + + string folder; + get(iter, 0, out folder); + + return folder; + } +} + diff --git a/src/client/ui/FolderListView.vala b/src/client/ui/FolderListView.vala new file mode 100644 index 00000000..38915ecd --- /dev/null +++ b/src/client/ui/FolderListView.vala @@ -0,0 +1,34 @@ +/* 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 FolderListView : Gtk.TreeView { + public signal void folder_selected(string folder); + + public FolderListView(FolderListStore store) { + set_model(store); + + Gtk.CellRendererText name_renderer = new Gtk.CellRendererText(); + Gtk.TreeViewColumn name_column = new Gtk.TreeViewColumn.with_attributes( + FolderListStore.Column.NAME.to_string(), name_renderer, "text", FolderListStore.Column.NAME); + append_column(name_column); + + get_selection().changed.connect(on_selection_changed); + } + + private FolderListStore get_store() { + return (FolderListStore) get_model(); + } + + private void on_selection_changed() { + Gtk.TreeModel model; + Gtk.TreePath path = get_selection().get_selected_rows(out model).nth_data(0); + + string? folder = get_store().get_folder_at(path); + + folder_selected(folder); + } +} + diff --git a/src/client/ui/MainWindow.vala b/src/client/ui/MainWindow.vala index bcc793ee..53a21c41 100644 --- a/src/client/ui/MainWindow.vala +++ b/src/client/ui/MainWindow.vala @@ -21,8 +21,11 @@ public class MainWindow : Gtk.Window { private MessageListStore message_list_store = new MessageListStore(); private MessageListView message_list_view; + private FolderListStore folder_list_store = new FolderListStore(); + private FolderListView folder_list_view; private Gtk.UIManager ui = new Gtk.UIManager(); private Geary.Engine? engine = null; + private Geary.Account? account = null; public MainWindow() { title = GearyApplication.PROGRAM_NAME; @@ -42,6 +45,9 @@ public class MainWindow : Gtk.Window { message_list_view = new MessageListView(message_list_store); + folder_list_view = new FolderListView(folder_list_store); + folder_list_view.folder_selected.connect(on_folder_selected); + create_layout(); } @@ -53,20 +59,17 @@ public class MainWindow : Gtk.Window { private async void do_login(string user, string pass) { try { - Geary.Account? account = yield engine.login("imap.gmail.com", user, pass); + account = yield engine.login("imap.gmail.com", user, pass); if (account == null) error("Unable to login"); - Geary.Folder folder = yield account.open("inbox"); - - Geary.MessageStream? msg_stream = folder.read(1, 100); - if (msg_stream == null) - error("Unable to read from folder"); - - Gee.List? msgs = yield msg_stream.read(); - if (msgs != null && msgs.size > 0) { - foreach (Geary.Message msg in msgs) - message_list_store.append_message(msg); + Gee.Collection? folders = yield account.list("/"); + if (folders != null) { + debug("%d folders found", folders.size); + foreach (string folder in folders) + folder_list_store.add_folder(folder); + } else { + debug("no folders"); } } catch (Error err) { error("%s", err.message); @@ -115,11 +118,23 @@ public class MainWindow : Gtk.Window { // main menu main_layout.pack_start(ui.get_widget("/MenuBar"), false, false, 0); + // three-pane display: folder list on left, message list on right, separated with grippable + // pane + Gtk.HPaned paned = new Gtk.HPaned(); + + // folder list + Gtk.ScrolledWindow folder_list_scrolled = new Gtk.ScrolledWindow(null, null); + folder_list_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); + folder_list_scrolled.add_with_viewport(folder_list_view); + paned.pack1(folder_list_scrolled, false, false); + // message list Gtk.ScrolledWindow message_list_scrolled = new Gtk.ScrolledWindow(null, null); message_list_scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); message_list_scrolled.add_with_viewport(message_list_view); - main_layout.pack_end(message_list_scrolled, true, true, 0); + paned.pack2(message_list_scrolled, true, false); + + main_layout.pack_end(paned, true, true, 0); add(main_layout); } @@ -139,5 +154,35 @@ public class MainWindow : Gtk.Window { "website-label", GearyApplication.WEBSITE_LABEL ); } + + private void on_folder_selected(string folder) { + do_select_folder.begin(folder, on_select_folder_completed); + } + + private async void do_select_folder(string folder_name) throws Error { + message_list_store.clear(); + + Geary.Folder folder = yield account.open(folder_name); + + Geary.MessageStream? msg_stream = folder.read(1, 100); + if (msg_stream == null) + error("Unable to read from folder"); + + Gee.List? msgs = yield msg_stream.read(); + if (msgs != null && msgs.size > 0) { + foreach (Geary.Message msg in msgs) + message_list_store.append_message(msg); + } + + yield folder.close(); + } + + private void on_select_folder_completed(Object? source, AsyncResult result) { + try { + do_select_folder.end(result); + } catch (Error err) { + debug("Unable to select folder: %s", err.message); + } + } } diff --git a/src/console/main.vala b/src/console/main.vala index f92eb084..f869fc07 100644 --- a/src/console/main.vala +++ b/src/console/main.vala @@ -10,6 +10,8 @@ errordomain CommandException { } class ImapConsole : Gtk.Window { + private static const int KEEPALIVE_SEC = 60 * 10; + private Gtk.TextView console = new Gtk.TextView(); private Gtk.Entry cmdline = new Gtk.Entry(); private Gtk.Statusbar statusbar = new Gtk.Statusbar(); @@ -82,12 +84,14 @@ class ImapConsole : Gtk.Window { "logout", "bye", "list", + "xlist", "examine", "fetch", "help", "exit", "quit", - "gmail" + "gmail", + "keepalive" }; private void exec(string input) { @@ -144,6 +148,10 @@ class ImapConsole : Gtk.Window { list(cmd, args); break; + case "xlist": + xlist(cmd, args); + break; + case "examine": examine(cmd, args); break; @@ -168,6 +176,10 @@ class ImapConsole : Gtk.Window { connect_cmd("connect", fake_args); break; + case "keepalive": + keepalive(cmd, args); + break; + default: status("Unknown command \"%s\"".printf(cmd)); break; @@ -333,6 +345,14 @@ class ImapConsole : Gtk.Window { } } + private void xlist(string cmd, string[] args) throws Error { + check_connected(cmd, args, 2, " "); + + status("Xlisting..."); + cx.post(new Geary.Imap.XListCommand.wildcarded(cx.generate_tag(), args[0], args[1]), + on_list); + } + private void examine(string cmd, string[] args) throws Error { check_connected(cmd, args, 1, ""); @@ -374,6 +394,35 @@ class ImapConsole : Gtk.Window { Gtk.main_quit(); } + private bool keepalive_on = false; + + private void keepalive(string cmd, string[] args) throws Error { + if (keepalive_on) { + status("Keepalive already active."); + + return; + } + + check_connected(cmd, args, 0, null); + + keepalive_on = true; + Timeout.add_seconds(KEEPALIVE_SEC, on_keepalive); + + status("Keepalive on."); + } + + private bool on_keepalive() { + try { + noop("noop", new string[0]); + } catch (Error err) { + status("Keepalive failed, halted: %s".printf(err.message)); + + keepalive_on = false; + } + + return keepalive_on; + } + private void print_console_line(string text) { append_to_console("[C] "); append_to_console(text); diff --git a/src/engine/Interfaces.vala b/src/engine/Interfaces.vala index 97801629..8a5dddba 100644 --- a/src/engine/Interfaces.vala +++ b/src/engine/Interfaces.vala @@ -5,11 +5,16 @@ */ public interface Geary.Account : Object { - public abstract async Folder open(string name, Cancellable? cancellable = null) throws Error; + public abstract async Gee.Collection list(string parent, Cancellable? cancellable = null) + throws Error; + + public abstract async Folder open(string folder, Cancellable? cancellable = null) throws Error; } public interface Geary.Folder : Object { public abstract MessageStream? read(int low, int count); + + public abstract async void close(Cancellable? cancellable = null) throws Error; } public interface Geary.MessageStream : Object { diff --git a/src/engine/imap/ClientSession.vala b/src/engine/imap/ClientSession.vala index 03665f8f..536ec9e4 100644 --- a/src/engine/imap/ClientSession.vala +++ b/src/engine/imap/ClientSession.vala @@ -644,8 +644,6 @@ public class Geary.Imap.ClientSession : Object, Geary.Account { if (params.err != null) throw params.err; - - debug("Closed mailbox"); } private uint on_close_mailbox(uint state, uint event, void *user, Object? object) { @@ -666,6 +664,8 @@ public class Geary.Imap.ClientSession : Object, Geary.Account { } private uint on_closed_mailbox(uint state, uint event) { + current_mailbox = null; + return State.AUTHORIZED; } @@ -864,7 +864,7 @@ public class Geary.Imap.ClientSession : Object, Geary.Account { return new AsyncCommandResponse(cmd_response, user, null); } - private void generic_issue_command_completed(AsyncResult result, Event ok_event, Event error_event) { + private bool generic_issue_command_completed(AsyncResult result, Event ok_event, Event error_event) { AsyncCommandResponse async_response = issue_command_async.end(result); assert(async_response.user != null); @@ -873,14 +873,33 @@ public class Geary.Imap.ClientSession : Object, Geary.Account { params.cmd_response = async_response.cmd_response; params.err = async_response.err; + bool success; if (async_response.err != null) { fsm.issue(Event.SEND_ERROR, null, null, async_response.err); + success = false; } else { issue_status(async_response.cmd_response.status_response.status, ok_event, error_event, params); + success = true; } Idle.add(params.cb); + + return success; + } + + // + // Geary.Account + // + + public async Gee.Collection list(string parent, Cancellable? cancellable = null) throws Error { + string specifier = String.is_empty(parent) ? "/" : parent; + specifier += (specifier.has_suffix("/")) ? "%" : "/%"; + + ListResults results = ListResults.decode(yield send_command_async( + new ListCommand(generate_tag(), specifier), cancellable)); + + return results.get_names(); } public async Geary.Folder open(string mailbox, Cancellable? cancellable = null) throws Error { diff --git a/src/engine/imap/Commands.vala b/src/engine/imap/Commands.vala index 27c331a1..1b6aad74 100644 --- a/src/engine/imap/Commands.vala +++ b/src/engine/imap/Commands.vala @@ -52,6 +52,18 @@ public class Geary.Imap.ListCommand : Command { } } +public class Geary.Imap.XListCommand : Command { + public const string NAME = "xlist"; + + public XListCommand(Tag tag, string mailbox) { + base (tag, NAME, { "", mailbox }); + } + + public XListCommand.wildcarded(Tag tag, string reference, string mailbox) { + base (tag, NAME, { reference, mailbox }); + } +} + public class Geary.Imap.ExamineCommand : Command { public const string NAME = "examine"; diff --git a/src/engine/imap/DataFormat.vala b/src/engine/imap/DataFormat.vala new file mode 100644 index 00000000..8664789a --- /dev/null +++ b/src/engine/imap/DataFormat.vala @@ -0,0 +1,129 @@ +/* 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. + */ + +namespace Geary.Imap.DataFormat { + +private const unichar[] ATOM_SPECIALS = { + '(', ')', '{', ' ', '%', '*', '\"' +}; + +private const unichar[] TAG_SPECIALS = { + '(', ')', '{', '%', '\"', '\\', '+' +}; + +public enum Quoting { + REQUIRED, + OPTIONAL, + UNALLOWED +} + +private bool is_special_char(unichar ch, unichar[] ar, string? exceptions) { + if (ch > 0x7F || ch.iscntrl()) + return true; + + if (ch in ar) + return (exceptions != null) ? exceptions.index_of_char(ch) < 0 : true; + + return false; +} + +/** + * Returns true if the character is considered an atom special. 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 by the caller. + */ +public inline bool is_atom_special(unichar ch, string? exceptions = null) { + return is_special_char(ch, ATOM_SPECIALS, exceptions); +} + +/** + * Tag specials are like atom specials but include the continuation character ('+'). Also, the + * star character is allowed, although technically only correct in the context of a status response; + * it's the responsibility of the caller to catch this. + */ +public bool is_tag_special(unichar ch, string? exceptions = null) { + return is_special_char(ch, TAG_SPECIALS, exceptions); +} + +/** + * Returns Quoting to indicate if the string must be quoted before sent on the wire, of if it + * must be sent as a literal. + */ +public Quoting is_quoting_required(string str) { + if (String.is_empty(str)) + return Quoting.REQUIRED; + + int index = 0; + unichar ch; + while (str.get_next_char(ref index, out ch)) { + if (ch > 0x7F) + return Quoting.UNALLOWED; + + switch (ch) { + case '\n': + case '\r': + case '\0': + return Quoting.UNALLOWED; + + case '"': + case '\\': + return Quoting.REQUIRED; + + default: + if (is_atom_special(ch)) + return Quoting.REQUIRED; + break; + } + } + + return Quoting.OPTIONAL; +} + +/** + * Converts the supplied string to a quoted string and returns whether or not the quoted format + * is required on the wire. If Quoting.UNALLOWED is returned, the only way to represent the string + * is with a literal. + */ +public Quoting convert_to_quoted(string str, out string quoted) { + Quoting requirement = String.is_empty(str) ? Quoting.REQUIRED : Quoting.OPTIONAL; + quoted = ""; + + StringBuilder builder = new StringBuilder("\""); + int index = 0; + unichar ch; + while (str.get_next_char(ref index, out ch)) { + if (ch > 0x7F) + return Quoting.UNALLOWED; + + switch (ch) { + case '\n': + case '\r': + case '\0': + return Quoting.UNALLOWED; + + case '"': + case '\\': + requirement = Quoting.REQUIRED; + builder.append_c('\\'); + builder.append_unichar(ch); + break; + + default: + if (is_atom_special(ch)) + requirement = Quoting.REQUIRED; + + builder.append_unichar(ch); + break; + } + } + + quoted = builder.append_c('"').str; + + return requirement; +} + +} + diff --git a/src/engine/imap/Deserializer.vala b/src/engine/imap/Deserializer.vala index 98a2a83d..324913ee 100644 --- a/src/engine/imap/Deserializer.vala +++ b/src/engine/imap/Deserializer.vala @@ -53,22 +53,6 @@ public class Geary.Imap.Deserializer { 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 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); @@ -385,14 +369,12 @@ public class Geary.Imap.Deserializer { unichar ch = *((unichar *) user); - // drop everything above 0x7F and control characters - if (ch > 0x7F || ch.iscntrl()) + // Atom specials includes space and close-parens, but those are handled in particular ways + // while in the ATOM state, so they're excluded here. Like atom specials, the space is + // treated in a particular way for tags, but unlike atom, the close-parens character is not. + if (state == State.TAG && DataFormat.is_tag_special(ch, " ")) 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)) + else if (state == State.ATOM && DataFormat.is_atom_special(ch, " )")) return state; // message flag indicator is only legal at start of atom diff --git a/src/engine/imap/FetchDataType.vala b/src/engine/imap/FetchDataType.vala index f140dcc0..d610fcee 100644 --- a/src/engine/imap/FetchDataType.vala +++ b/src/engine/imap/FetchDataType.vala @@ -126,7 +126,7 @@ public enum Geary.Imap.FetchDataType { return new UIDDecoder(); case FLAGS: - return new FlagsDecoder(); + return new MessageFlagsDecoder(); case ENVELOPE: return new EnvelopeDecoder(); diff --git a/src/engine/imap/Flag.vala b/src/engine/imap/Flag.vala new file mode 100644 index 00000000..e99ee4ff --- /dev/null +++ b/src/engine/imap/Flag.vala @@ -0,0 +1,63 @@ +/* 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.Flag { + public string value { get; private set; } + + public Flag(string value) { + this.value = value; + } + + public bool is_system() { + return value[0] == '\\'; + } + + public bool equals_string(string value) { + return this.value.down() == value.down(); + } + + public bool equals(Flag flag) { + return (flag == this) ? true : flag.equals_string(value); + } + + public string to_string() { + return value; + } + + public static uint hash_func(void *flag) { + return str_hash(((Flag *) flag)->value); + } + + public static bool equal_func(void *a, void *b) { + return ((Flag *) a)->equals((Flag *) b); + } +} + +public class Geary.Imap.MessageFlag : Geary.Imap.Flag { + public static MessageFlag ANSWERED = new MessageFlag("\\answered"); + public static MessageFlag DELETED = new MessageFlag("\\deleted"); + public static MessageFlag DRAFT = new MessageFlag("\\draft"); + public static MessageFlag FLAGGED = new MessageFlag("\\flagged"); + public static MessageFlag RECENT = new MessageFlag("\\recent"); + public static MessageFlag SEEN = new MessageFlag("\\seen"); + + public MessageFlag(string value) { + base (value); + } +} + +public class Geary.Imap.MailboxAttribute : Geary.Imap.Flag { + public static MailboxAttribute NO_INFERIORS = new MailboxAttribute("\\noinferiors"); + public static MailboxAttribute NO_SELECT = new MailboxAttribute("\\noselect"); + public static MailboxAttribute MARKED = new MailboxAttribute("\\marked"); + public static MailboxAttribute UNMARKED = new MailboxAttribute("\\unmarked"); + public static MailboxAttribute HAS_NO_CHILDREN = new MailboxAttribute("\\hasnochildren"); + + public MailboxAttribute(string value) { + base (value); + } +} + diff --git a/src/engine/imap/Mailbox.vala b/src/engine/imap/Mailbox.vala index b9b2c757..bfe2af1a 100644 --- a/src/engine/imap/Mailbox.vala +++ b/src/engine/imap/Mailbox.vala @@ -8,15 +8,25 @@ public class Geary.Imap.Mailbox : Object, Geary.Folder { public string name { get; private set; } private ClientSession sess; + private bool is_closed = false; internal Mailbox(string name, ClientSession sess) { this.name = name; this.sess = sess; } + ~Mailbox() { + assert(is_closed); + } + public MessageStream? read(int low, int count) { return new MessageStreamImpl(sess, low, count); } + + public async void close(Cancellable? cancellable = null) throws Error { + yield sess.close_mailbox_async(cancellable); + is_closed = true; + } } private class Geary.Imap.MessageStreamImpl : Object, Geary.MessageStream { diff --git a/src/engine/imap/MessageData.vala b/src/engine/imap/MessageData.vala index e792b326..76e8d9e5 100644 --- a/src/engine/imap/MessageData.vala +++ b/src/engine/imap/MessageData.vala @@ -32,46 +32,7 @@ public class Geary.Imap.MessageNumber : Geary.Common.IntMessageData, Geary.Imap. } } -public class Geary.Imap.Flag { - public static Flag ANSWERED = new Flag("\\answered"); - public static Flag DELETED = new Flag("\\deleted"); - public static Flag DRAFT = new Flag("\\draft"); - public static Flag FLAGGED = new Flag("\\flagged"); - public static Flag RECENT = new Flag("\\recent"); - public static Flag SEEN = new Flag("\\seen"); - - public string value { get; private set; } - - public Flag(string value) { - this.value = value; - } - - public bool is_system() { - return value[0] == '\\'; - } - - public bool has_value(string value) { - return this.value.down() == value.down(); - } - - public bool equals(Flag flag) { - return (flag == this) ? true : flag.has_value(value); - } - - public string to_string() { - return value; - } - - public static uint hash_func(void *flag) { - return str_hash(((Flag *) flag)->value); - } - - public static bool equal_func(void *a, void *b) { - return ((Flag *) a)->equals((Flag *) b); - } -} - -public class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.MessageData { +public abstract class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.MessageData { private Gee.Set list; public Flags(Gee.Collection flags) { @@ -100,6 +61,18 @@ public class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.MessageData } } +public class Geary.Imap.MessageFlags : Geary.Imap.Flags { + public MessageFlags(Gee.Collection flags) { + base (flags); + } +} + +public class Geary.Imap.MailboxAttributes : Geary.Imap.Flags { + public MailboxAttributes(Gee.Collection attrs) { + base (attrs); + } +} + public class Geary.Imap.InternalDate : Geary.RFC822.Date, Geary.Imap.MessageData { public InternalDate(string iso8601) throws ImapError { base (iso8601); diff --git a/src/engine/imap/Parameter.vala b/src/engine/imap/Parameter.vala index 3ef84436..e8407e7f 100644 --- a/src/engine/imap/Parameter.vala +++ b/src/engine/imap/Parameter.vala @@ -50,7 +50,7 @@ public class Geary.Imap.StringParameter : Geary.Imap.Parameter { } } - public StringParameter(string value) requires (!String.is_empty(value)) { + public StringParameter(string value) { this.value = value; } diff --git a/src/engine/imap/Serializer.vala b/src/engine/imap/Serializer.vala index 11e3ad02..fc2ee4f0 100644 --- a/src/engine/imap/Serializer.vala +++ b/src/engine/imap/Serializer.vala @@ -35,7 +35,26 @@ public class Geary.Imap.Serializer { } public void push_string(string str) throws Error { - douts.put_string(str, null); + // see if need to convert to quoted string, only emitting it if required + switch (DataFormat.is_quoting_required(str)) { + case DataFormat.Quoting.OPTIONAL: + douts.put_string(str); + break; + + case DataFormat.Quoting.REQUIRED: + string quoted; + DataFormat.Quoting requirement = DataFormat.convert_to_quoted(str, out quoted); + debug("str=%s quoted=%s", str, quoted); + assert(requirement == DataFormat.Quoting.REQUIRED); + + douts.put_string(quoted); + break; + + case DataFormat.Quoting.UNALLOWED: + default: + // TODO: Not handled currently + assert_not_reached(); + } } public void push_space() throws Error { diff --git a/src/engine/imap/decoders/FetchDataDecoder.vala b/src/engine/imap/decoders/FetchDataDecoder.vala index 7c856886..38b47351 100644 --- a/src/engine/imap/decoders/FetchDataDecoder.vala +++ b/src/engine/imap/decoders/FetchDataDecoder.vala @@ -66,17 +66,17 @@ public class Geary.Imap.UIDDecoder : Geary.Imap.FetchDataDecoder { } } -public class Geary.Imap.FlagsDecoder : Geary.Imap.FetchDataDecoder { - public FlagsDecoder() { +public class Geary.Imap.MessageFlagsDecoder : Geary.Imap.FetchDataDecoder { + public MessageFlagsDecoder() { base (FetchDataType.FLAGS); } protected override MessageData decode_list(ListParameter listp) throws ImapError { Gee.List flags = new Gee.ArrayList(); for (int ctr = 0; ctr < listp.get_count(); ctr++) - flags.add(new Flag(listp.get_as_string(ctr).value)); + flags.add(new MessageFlag(listp.get_as_string(ctr).value)); - return new Flags(flags); + return new MessageFlags(flags); } } diff --git a/src/engine/imap/decoders/FetchResults.vala b/src/engine/imap/decoders/FetchResults.vala index 682807ce..82ec776c 100644 --- a/src/engine/imap/decoders/FetchResults.vala +++ b/src/engine/imap/decoders/FetchResults.vala @@ -27,7 +27,7 @@ public class Geary.Imap.FetchResults { ListParameter list = data.get_as_list(3); // verify this is a FETCH response - if (!cmd.equals_ci("fetch")) { + if (!cmd.equals_ci(FetchCommand.NAME)) { throw new ImapError.TYPE_ERROR("Unable to decode fetch response \"%s\": Not marked as fetch response", data.to_string()); } diff --git a/src/engine/imap/decoders/ListResults.vala b/src/engine/imap/decoders/ListResults.vala new file mode 100644 index 00000000..5660c1e0 --- /dev/null +++ b/src/engine/imap/decoders/ListResults.vala @@ -0,0 +1,79 @@ +/* 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.FolderDetail { + public string name { get; private set; } + public string delim { get; private set; } + public MailboxAttributes attrs { get; private set; } + + public FolderDetail(string name, string delim, MailboxAttributes attrs) { + this.name = name; + this.delim = delim; + this.attrs = attrs; + } +} + +public class Geary.Imap.ListResults { + private Gee.HashMap map = new Gee.HashMap(); + + private ListResults(Gee.Collection details) { + foreach (FolderDetail detail in details) + map.set(detail.name, detail); + } + + public static ListResults decode(CommandResponse response) { + assert(response.is_sealed()); + + Gee.List details = new Gee.ArrayList(); + foreach (ServerData data in response.server_data) { + try { + StringParameter cmd = data.get_as_string(1); + ListParameter attrs = data.get_as_list(2); + StringParameter delim = data.get_as_string(3); + StringParameter mailbox = data.get_as_string(4); + + if (!cmd.equals_ci(ListCommand.NAME) && !cmd.equals_ci(XListCommand.NAME)) { + debug("Bad list response \"%s\": Not marked as list or xlist response", + data.to_string()); + + continue; + } + + Gee.Collection list = new Gee.ArrayList(); + foreach (Parameter attr in attrs.get_all()) { + StringParameter? stringp = attr as StringParameter; + if (stringp == null) { + debug("Bad list attribute \"%s\": Attribute not a string value", + data.to_string()); + + continue; + } + + list.add(new MailboxAttribute(stringp.value)); + } + + details.add(new FolderDetail(mailbox.value, delim.value, new MailboxAttributes(list))); + } catch (ImapError ierr) { + debug("Unable to decode \"%s\": %s", data.to_string(), ierr.message); + } + } + + return new ListResults(details); + } + + public Gee.Collection get_names() { + return map.keys; + } + + public Gee.Collection get_all() { + return map.values; + } + + public FolderDetail? get_detail(string name) { + return map.get(name); + } +} +