Merge branch 'wip/713006-better-error-reporting'. Fixes Bug 713006.

This commit is contained in:
Michael James Gratton 2017-11-19 18:35:06 +11:00
commit d1d8d6411e
23 changed files with 1492 additions and 378 deletions

View file

@ -46,6 +46,7 @@
* webkit2gtk-4.0
* gcr-3
* enchant
* libunwind
* messaging-menu (optional; enables support for Ubuntu Unity
messaging menu)
* unity (optional; enables support for Ubuntu Unity launcher)
@ -67,7 +68,7 @@
desktop-file-utils gnome-doc-utils libcanberra-devel libgee-devel \
glib2-devel gmime-devel gtk3-devel libnotify-devel sqlite-devel \
webkitgtk4-devel libsecret-devel libxml2-devel vala-tools \
gcr-devel enchant-devel
gcr-devel enchant-devel libunwind-devel
* Installing dependencies on Ubuntu/Debian
@ -86,7 +87,8 @@
cmake desktop-file-utils gnome-doc-utils libcanberra-dev \
libgee-0.8-dev libglib2.0-dev libgmime-2.6-dev libgtk-3-dev \
libsecret-1-dev libxml2-dev libnotify-dev libsqlite3-dev \
libwebkit2gtk-4.0-dev libgcr-3-dev libenchant-dev
libwebkit2gtk-4.0-dev libgcr-3-dev libenchant-dev \
libunwind-dev
And for Ubuntu Unity integration:

View file

@ -0,0 +1,91 @@
/*
* Based on version from Sentry-GLib: https://github.com/arteymix/sentry-glib
* Courtesy of Guillaume Poirier-Morency <guillaumepoiriermorency@gmail.com>
*
* Copyright (C) 1996 X Consortium
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy,
* modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Except as contained in this notice, the name of the X Consortium
* shall not be used in advertising or otherwise to promote the sale,
* use or other dealings in this Software without prior written
* authorization from the X Consortium.
*
* X Window System is a trademark of X Consortium, Inc.
*/
[CCode (cprefix = "UNW_", lower_case_cprefix = "unw_", cheader_filename = "libunwind.h")]
namespace Unwind
{
[CCode (cname = "unw_context_t")]
public struct Context
{
[CCode (cname = "unw_getcontext")]
public Context ();
}
[CCode (cname = "unw_proc_info_t")]
public struct ProcInfo
{
void* start_ip;
void* end_ip;
void* lsda;
void* handler;
void* gp;
long flags;
int format;
}
[CCode (cname = "unw_frame_regnum_t")]
public enum Reg
{
IP,
SP,
EH
}
[CCode (cname = "unw_cursor_t", cprefix = "unw_")]
public struct Cursor
{
public Cursor.local (Context ctx);
public int get_proc_info (out ProcInfo pip);
public int get_proc_name (uint8[] bufp, out long offp = null);
public int get_reg (Reg reg, out void* valp);
public int step ();
}
[CCode (cname = "unw_error_t", cprefix = "UNW_E", has_type_id = false)]
public enum Error
{
SUCCESS,
UNSPEC,
NOMEM,
BADREG,
READONLYREG,
STOPUNWIND,
INVALIDIP,
BADFRAME,
INVAL,
BADVERSION,
NOINFO
}
}

2
debian/control vendored
View file

@ -23,6 +23,7 @@ Build-Depends: debhelper (>= 8),
gnome-doc-utils,
libgcr-3-dev (>= 3.10.1),
libenchant-dev (>= 1.6.0)
libunwind8-dev (>= 1.1)
Standards-Version: 3.8.3
Homepage: https://wiki.gnome.org/Apps/Geary
@ -45,6 +46,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends},
libgcr-base-3-1 (>= 3.10.1),
libgcr-ui-3-1 (>= 3.10.1),
libenchant1c2a (>= 1.6.0)
libunwind8 (>= 1.1)
Description: Email application
Geary is an email application built around conversations, for the
GNOME 3 desktop. It allows you to read, find and send email with a

View file

@ -31,6 +31,7 @@ src/client/components/folder-popover.vala
src/client/components/icon-factory.vala
src/client/components/main-toolbar.vala
src/client/components/main-window.vala
src/client/components/main-window-info-bar.vala
src/client/components/monitored-progress-bar.vala
src/client/components/monitored-spinner.vala
src/client/components/search-bar.vala
@ -128,6 +129,7 @@ src/engine/api/geary-folder.vala
src/engine/api/geary-logging.vala
src/engine/api/geary-named-flag.vala
src/engine/api/geary-named-flags.vala
src/engine/api/geary-problem-report.vala
src/engine/api/geary-progress-monitor.vala
src/engine/api/geary-revokable.vala
src/engine/api/geary-search-folder.vala
@ -414,6 +416,7 @@ ui/login.glade
ui/main-toolbar.ui
ui/main-toolbar-menus.ui
ui/main-window.ui
ui/main-window-info-bar.ui
ui/password-dialog.glade
ui/preferences-dialog.ui
ui/remove_confirm.glade

View file

@ -37,6 +37,7 @@ engine/api/geary-folder-supports-remove.vala
engine/api/geary-logging.vala
engine/api/geary-named-flag.vala
engine/api/geary-named-flags.vala
engine/api/geary-problem-report.vala
engine/api/geary-progress-monitor.vala
engine/api/geary-revokable.vala
engine/api/geary-search-folder.vala
@ -345,6 +346,7 @@ client/components/folder-popover.vala
client/components/icon-factory.vala
client/components/main-toolbar.vala
client/components/main-window.vala
client/components/main-window-info-bar.vala
client/components/monitored-progress-bar.vala
client/components/monitored-spinner.vala
client/components/search-bar.vala
@ -513,6 +515,7 @@ pkg_check_modules(DEPS REQUIRED
webkit2gtk-web-extension-4.0>=${TARGET_WEBKIT}
javascriptcoregtk-4.0>=${TARGET_WEBKIT}
enchant>=1.6
libunwind-generic>=1.1
${EXTRA_CLIENT_PKG_CONFIG}
)
@ -530,6 +533,7 @@ set(ENGINE_PACKAGES
gio-2.0
glib-2.0
gmime-2.6
libunwind
javascriptcore-4.0
libxml-2.0
posix

View file

@ -869,44 +869,72 @@ public class GearyController : Geary.BaseObject {
debug("Error updating stored passwords: %s", e.message);
}
}
private void on_report_problem(Geary.Account account, Geary.Account.Problem problem, Error? err) {
debug("Reported problem: %s Error: %s", problem.to_string(), err != null ? err.message : "(N/A)");
switch (problem) {
case Geary.Account.Problem.DATABASE_FAILURE:
case Geary.Account.Problem.HOST_UNREACHABLE:
case Geary.Account.Problem.NETWORK_UNAVAILABLE:
// TODO
break;
case Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED:
case Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED:
// At this point, we've prompted them for the password and
// they've hit cancel, so there's not much for us to do here.
close_account(account);
break;
case Geary.Account.Problem.SEND_EMAIL_DELIVERY_FAILURE:
handle_outbox_failure(StatusBar.Message.OUTBOX_SEND_FAILURE);
break;
private void report_problem(Geary.ProblemReport report) {
debug("Problem reported: %s", report.to_string());
case Geary.Account.Problem.SEND_EMAIL_SAVE_FAILED:
if (!(report.error is IOError.CANCELLED)) {
if (report.problem_type == Geary.ProblemType.SEND_EMAIL_SAVE_FAILED) {
handle_outbox_failure(StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED);
break;
case Geary.Account.Problem.CONNECTION_FAILURE:
ErrorDialog dialog = new ErrorDialog(main_window,
_("Error connecting to the server"),
_("Geary encountered an error while connecting to the server. Please try again in a few moments."));
dialog.run();
break;
default:
assert_not_reached();
} else {
MainWindowInfoBar info_bar = new MainWindowInfoBar.for_problem(report);
info_bar.retry.connect(on_retry_problem);
this.main_window.show_infobar(info_bar);
}
}
}
private void on_retry_problem(MainWindowInfoBar info_bar) {
Geary.ServiceProblemReport? service_report =
info_bar.report as Geary.ServiceProblemReport;
Error retry_err = null;
if (service_report != null) {
Geary.Account? account = null;
try {
account = this.application.engine.get_account_instance(
service_report.account
);
} catch (Error err) {
debug("Error getting account for error retry: %s", err.message);
}
if (account != null && account.is_open()) {
switch (service_report.service_type) {
case Geary.Service.IMAP:
account.start_incoming_client.begin((obj, ret) => {
try {
account.start_incoming_client.end(ret);
} catch (Error err) {
retry_err = err;
}
});
break;
case Geary.Service.SMTP:
account.start_outgoing_client.begin((obj, ret) => {
try {
account.start_outgoing_client.end(ret);
} catch (Error err) {
retry_err = err;
}
});
break;
}
if (retry_err != null) {
report_problem(
new Geary.ServiceProblemReport(
Geary.ProblemType.GENERIC_ERROR,
service_report.account,
service_report.service_type,
retry_err
)
);
}
}
}
}
private void handle_outbox_failure(StatusBar.Message message) {
bool activate_message = false;
try {
@ -949,7 +977,11 @@ public class GearyController : Geary.BaseObject {
}
}
}
private void on_report_problem(Geary.Account account, Geary.ProblemReport problem) {
report_problem(problem);
}
private void on_account_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
if (folder.special_folder_type == Geary.SpecialFolderType.OUTBOX) {
main_window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SEND_FAILURE);
@ -2820,5 +2852,5 @@ public class GearyController : Geary.BaseObject {
});
}
}
}
}

View file

@ -0,0 +1,324 @@
/*
* Copyright 2017 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Displays application-wide or important account-related messages.
*/
[GtkTemplate (ui = "/org/gnome/Geary/main-window-info-bar.ui")]
public class MainWindowInfoBar : Gtk.InfoBar {
private enum ResponseType { COPY, DETAILS, RETRY; }
/** If reporting a problem, returns the problem report else null. */
public Geary.ProblemReport? report { get; private set; default = null; }
/** Emitted when the user clicks the Retry button, if any. */
public signal void retry();
[GtkChild]
private Gtk.Label title;
[GtkChild]
private Gtk.Label description;
[GtkChild]
private Gtk.Grid problem_details;
[GtkChild]
private Gtk.TextView detail_text;
public MainWindowInfoBar.for_problem(Geary.ProblemReport report) {
Gtk.MessageType type = Gtk.MessageType.WARNING;
string title = "";
string descr = "";
string? retry = null;
bool show_generic = false;
bool show_close = false;
if (report is Geary.ServiceProblemReport) {
Geary.ServiceProblemReport service_report = (Geary.ServiceProblemReport) report;
Geary.Endpoint endpoint = service_report.endpoint;
string account = service_report.account.display_name;
string server = endpoint.remote_address.hostname;
if (report.problem_type == Geary.ProblemType.CONNECTION_ERROR &&
service_report.service_type == Geary.Service.IMAP) {
// Translators: String substitution is the account name
title = _("Problem connecting to incoming server for %s".printf(account));
// Translators: String substitution is the server name
descr = _("Could not connect to %s, check your Internet access and the server name and try again").printf(server);
retry = _("Retry connecting now");
} else if (report.problem_type == Geary.ProblemType.CONNECTION_ERROR &&
service_report.service_type == Geary.Service.SMTP) {
// Translators: String substitution is the account name
title = _("Problem connecting to outgoing server for %s".printf(account));
// Translators: String substitution is the server name
descr = _("Could not connect to %s, check your Internet access and the server name and try again").printf(server);
retry = _("Try reconnecting now");
retry = _("Retry connecting now");
} else if (report.problem_type == Geary.ProblemType.NETWORK_ERROR &&
service_report.service_type == Geary.Service.IMAP) {
// Translators: String substitution is the account name
title = _("Problem with connection to incoming server for %s").printf(account);
// Translators: String substitution is the server name
descr = _("Network error talking to %s, check your Internet access try again").printf(server);
retry = _("Try reconnecting");
} else if (report.problem_type == Geary.ProblemType.NETWORK_ERROR &&
service_report.service_type == Geary.Service.SMTP) {
// Translators: String substitution is the account name
title = _("Problem with connection to outgoing server for %s").printf(account);
// Translators: String substitution is the server name
descr = _("Network error talking to %s, check your Internet access try again").printf(server);
retry = _("Try reconnecting");
} else if (report.problem_type == Geary.ProblemType.SERVER_ERROR &&
service_report.service_type == Geary.Service.IMAP) {
// Translators: String substitution is the account name
title = _("Problem communicating with incoming server for %s").printf(account);
// Translators: String substitution is the server name
descr = _("Geary did not understand a message from %s or vice versa, please file a bug report").printf(server);
retry = _("Try reconnecting");
} else if (report.problem_type == Geary.ProblemType.SERVER_ERROR &&
service_report.service_type == Geary.Service.SMTP) {
title = _("Problem communicating with outgoing mail server");
// Translators: First string substitution is the server
// name, second is the account name
descr = _("Could now communicate with %s for %s, server name and try again in a moment").printf(server, account);
retry = _("Try reconnecting");
} else if (report.problem_type == Geary.ProblemType.LOGIN_FAILED &&
service_report.service_type == Geary.Service.IMAP) {
// Translators: String substitution is the account name
title = _("Incoming mail server password required for %s").printf(account);
descr = _("Messages cannot be received without the correct password.");
retry = _("Retry receiving email, you will be prompted for a password");
} else if (report.problem_type == Geary.ProblemType.LOGIN_FAILED &&
service_report.service_type == Geary.Service.SMTP) {
// Translators: String substitution is the account name
title = _("Outgoing mail server password required for %s").printf(account);
descr = _("Messages cannot be sent without the correct password.");
retry = _("Retry sending queued messages, you will be prompted for a password");
} else if (report.problem_type == Geary.ProblemType.GENERIC_ERROR &&
service_report.service_type == Geary.Service.IMAP) {
// Translators: String substitution is the account name
title = _("A problem occurred checking mail for %s").printf(account);
descr = _("Something went wrong, please file a bug report if the problem persists");
retry = _("Try reconnecting");
} else if (report.problem_type == Geary.ProblemType.GENERIC_ERROR &&
service_report.service_type == Geary.Service.SMTP) {
// Translators: String substitution is the account name
title = _("A problem occurred sending mail for %s").printf(account);
descr = _("Something went wrong, please file a bug report if the problem persists");
retry = _("Retry sending queued messages");
} else {
debug("Un-handled service problem report: %s".printf(report.to_string()));
show_generic = true;
}
} else if (report is Geary.AccountProblemReport) {
Geary.AccountProblemReport account_report = (Geary.AccountProblemReport) report;
string account = account_report.account.display_name;
if (report.problem_type == Geary.ProblemType.DATABASE_FAILURE) {
type = Gtk.MessageType.ERROR;
title = _("A database problem has occurred");
// Translators: String substitution is the account name
descr = _("Messages for %s must be downloaded again.").printf(account);
show_close = true;
} else {
debug("Un-handled account problem report: %s".printf(report.to_string()));
show_generic = true;
}
} else {
debug("Un-handled generic problem report: %s".printf(report.to_string()));
show_generic = true;
}
if (show_generic) {
title = _("Geary has encountered a problem");
descr = _("Please check the technical details and report the problem if it persists.");
show_close = true;
}
this(type, title, descr, show_close);
this.report = report;
if (this.report.error != null) {
Gtk.Button details = add_button(_("_Details"), ResponseType.DETAILS);
details.tooltip_text = _("View technical details about the error");
}
if (retry != null) {
Gtk.Button retry_btn = add_button(_("_Retry"), ResponseType.RETRY);
retry_btn.tooltip_text = retry;
}
}
protected MainWindowInfoBar(Gtk.MessageType type,
string title,
string description,
bool show_close) {
this.message_type = type;
this.title.label = title;
// Set the label and tooltip for the description in case it is
// long enough to be ellipsized
this.description.label = description;
this.description.tooltip_text = description;
this.show_close_button = show_close;
}
private string format_details() {
Geary.ServiceProblemReport? service_report = this.report as Geary.ServiceProblemReport;
Geary.AccountProblemReport? account_report = this.report as Geary.AccountProblemReport;
StringBuilder details = new StringBuilder();
details.append_printf(
"Geary version: %s\n",
GearyApplication.VERSION
);
details.append_printf(
"GTK version: %u.%u.%u\n",
Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()
);
details.append_printf(
"Desktop: %s\n",
Environment.get_variable("XDG_CURRENT_DESKTOP") ?? "Unknown"
);
details.append_printf(
"Problem type: %s\n",
this.report.problem_type.to_string()
);
if (account_report != null) {
details.append_printf(
"Account type: %s\n",
account_report.account.service_provider.to_string()
);
}
if (service_report != null) {
details.append_printf(
"Service type: %s\n",
service_report.service_type.to_string()
);
details.append_printf(
"Endpoint: %s\n",
service_report.endpoint.to_string()
);
}
if (this.report.error == null) {
details.append("No error reported");
} else {
details.append_printf("Error type: %s\n", this.report.format_error_type());
details.append_printf("Message: %s\n", this.report.error.message);
}
if (this.report.backtrace != null) {
details.append("Back trace:\n");
foreach (Geary.ProblemReport.StackFrame frame in this.report.backtrace) {
details.append_printf(" - %s\n", frame.to_string());
}
}
return details.str;
}
private void show_details() {
this.detail_text.buffer.text = format_details();
// Would love to construct the dialog in Builder, but we to
// construct the dialog manually since we can't adjust the
// Headerbar setting afterwards. If the user re-clicks on the
// Details button to re-show it, a whole bunch of GTK
// criticals are spewed and the dialog appears b0rked, so just
// do it from scratch ever time anyway.
bool use_header = Gtk.Settings.get_default().gtk_dialogs_use_header;
Gtk.DialogFlags flags = Gtk.DialogFlags.MODAL;
if (use_header) {
flags |= Gtk.DialogFlags.USE_HEADER_BAR;
}
Gtk.Dialog dialog = new Gtk.Dialog.with_buttons(
_("Details"), // same as the button
get_toplevel() as Gtk.Window,
flags,
null
);
dialog.set_default_size(600, -1);
dialog.get_content_area().add(this.problem_details);
Gtk.HeaderBar? header_bar = dialog.get_header_bar() as Gtk.HeaderBar;
use_header = (header_bar != null);
if (use_header) {
header_bar.show_close_button = true;
} else {
dialog.add_button(_("_Close"), Gtk.ResponseType.CLOSE);
}
Gtk.Widget copy = dialog.add_button(
_("Copy to Clipboard"), ResponseType.COPY
);
copy.tooltip_text =
_("Copy technical details to clipboard for pasting into an email or bug report");
dialog.set_default_response(ResponseType.COPY);
dialog.response.connect(on_details_response);
dialog.show();
copy.grab_focus();
}
private void copy_details() {
get_clipboard(Gdk.SELECTION_CLIPBOARD).set_text(format_details(), -1);
}
[GtkCallback]
private void on_info_bar_response(int response) {
switch(response) {
case ResponseType.DETAILS:
show_details();
break;
case ResponseType.RETRY:
retry();
this.hide();
break;
default:
this.hide();
break;
}
}
[GtkCallback]
private void on_hide() {
this.parent.remove(this);
}
private void on_details_response(Gtk.Dialog dialog, int response) {
switch(response) {
case ResponseType.COPY:
copy_details();
break;
default:
// fml
dialog.get_content_area().remove(this.problem_details);
dialog.hide();
break;
}
}
}

View file

@ -50,6 +50,12 @@ public class MainWindow : Gtk.ApplicationWindow {
[GtkChild]
private Gtk.ScrolledWindow conversation_list_scrolled;
// This is a frame so users can use F6/Shift-F6 to get to it
[GtkChild]
private Gtk.Frame info_bar_frame;
[GtkChild]
private Gtk.Grid info_bar_container;
/** Fired when the shift key is pressed or released. */
public signal void on_shift_key(bool pressed);
@ -72,6 +78,11 @@ public class MainWindow : Gtk.ApplicationWindow {
on_change_orientation();
}
public void show_infobar(MainWindowInfoBar info_bar) {
this.info_bar_container.add(info_bar);
this.info_bar_frame.show();
}
private void load_config(Configuration config) {
// This code both loads AND saves the pane positions with live updating. This is more
// resilient against crashes because the value in dconf changes *immediately*, and
@ -422,4 +433,14 @@ public class MainWindow : Gtk.ApplicationWindow {
}
return Gdk.EVENT_STOP;
}
[GtkCallback]
private void on_info_bar_container_remove() {
// Ensure the info bar frame is hidden when the last info bar
// is removed from the container.
if (this.info_bar_container.get_children().length() == 0) {
this.info_bar_frame.hide();
}
}
}

View file

@ -21,17 +21,11 @@
*/
public abstract class Geary.Account : BaseObject {
public enum Problem {
CONNECTION_FAILURE,
DATABASE_FAILURE,
HOST_UNREACHABLE,
NETWORK_UNAVAILABLE,
RECV_EMAIL_LOGIN_FAILED,
SEND_EMAIL_DELIVERY_FAILURE,
SEND_EMAIL_ERROR,
SEND_EMAIL_LOGIN_FAILED,
SEND_EMAIL_SAVE_FAILED,
}
/** Number of times to attempt re-authentication. */
internal const uint AUTH_ATTEMPTS_MAX = 3;
public Geary.AccountInformation information { get; protected set; }
@ -46,8 +40,8 @@ public abstract class Geary.Account : BaseObject {
public signal void closed();
public signal void email_sent(Geary.RFC822.Message rfc822);
public signal void report_problem(Geary.Account.Problem problem, Error? err);
public signal void report_problem(Geary.ProblemReport problem);
public signal void contacts_loaded();
@ -131,64 +125,7 @@ public abstract class Geary.Account : BaseObject {
this.name = name;
this.information = information;
}
protected virtual void notify_folders_available_unavailable(Gee.List<Geary.Folder>? available,
Gee.List<Geary.Folder>? unavailable) {
folders_available_unavailable(available, unavailable);
}
protected virtual void notify_folders_added_removed(Gee.List<Geary.Folder>? added,
Gee.List<Geary.Folder>? removed) {
folders_added_removed(added, removed);
}
protected virtual void notify_folders_contents_altered(Gee.Collection<Geary.Folder> altered) {
folders_contents_altered(altered);
}
protected virtual void notify_email_appended(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_appended(folder, ids);
}
protected virtual void notify_email_inserted(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_inserted(folder, ids);
}
protected virtual void notify_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_removed(folder, ids);
}
protected virtual void notify_email_locally_complete(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids) {
email_locally_complete(folder, ids);
}
protected virtual void notify_email_discovered(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids) {
email_discovered(folder, ids);
}
protected virtual void notify_email_flags_changed(Geary.Folder folder,
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> flag_map) {
email_flags_changed(folder, flag_map);
}
protected virtual void notify_opened() {
opened();
}
protected virtual void notify_closed() {
closed();
}
protected virtual void notify_email_sent(RFC822.Message message) {
email_sent(message);
}
protected virtual void notify_report_problem(Geary.Account.Problem problem, Error? err) {
report_problem(problem, err);
}
/**
* A utility method to sort a Gee.Collection of {@link Folder}s by their {@link FolderPath}s
* to ensure they comport with {@link folders_available_unavailable} and
@ -244,7 +181,27 @@ public abstract class Geary.Account : BaseObject {
* Unlike most methods in Account, this should only be called when the Account is closed.
*/
public abstract async void rebuild_async(Cancellable? cancellable = null) throws Error;
/**
* Starts delivery of messages to the outgoing server.
*
* Outgoing delivery will be started by default when the account
* is opened. This method is mostly useful when re-starting it
* after an error has occurred.
*/
public abstract async void start_outgoing_client()
throws Error;
/**
* Starts receiving messages from the incoming server.
*
* The incoming client will be started by default when the account
* is opened. This method is mostly useful when re-starting it
* after an error has occurred.
*/
public abstract async void start_incoming_client()
throws Error;
/**
* Lists all the currently-available folders found under the parent path
* unless it's null, in which case it lists all the root folders. If the
@ -395,5 +352,86 @@ public abstract class Geary.Account : BaseObject {
public virtual string to_string() {
return name;
}
}
/** Fires a {@link opened}} signal. */
protected virtual void notify_opened() {
opened();
}
/** Fires a {@link closed}} signal. */
protected virtual void notify_closed() {
closed();
}
/** Fires a {@link folders_available_unavailable}} signal. */
protected virtual void notify_folders_available_unavailable(Gee.List<Geary.Folder>? available,
Gee.List<Geary.Folder>? unavailable) {
folders_available_unavailable(available, unavailable);
}
/** Fires a {@link folders_added_removed}} signal. */
protected virtual void notify_folders_added_removed(Gee.List<Geary.Folder>? added,
Gee.List<Geary.Folder>? removed) {
folders_added_removed(added, removed);
}
/** Fires a {@link folders_contents_altered}} signal. */
protected virtual void notify_folders_contents_altered(Gee.Collection<Geary.Folder> altered) {
folders_contents_altered(altered);
}
/** Fires a {@link email_appended}} signal. */
protected virtual void notify_email_appended(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_appended(folder, ids);
}
/** Fires a {@link email_inserted}} signal. */
protected virtual void notify_email_inserted(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_inserted(folder, ids);
}
/** Fires a {@link email_removed}} signal. */
protected virtual void notify_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_removed(folder, ids);
}
/** Fires a {@link email_locally_complete}} signal. */
protected virtual void notify_email_locally_complete(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids) {
email_locally_complete(folder, ids);
}
/** Fires a {@link email_discovered}} signal. */
protected virtual void notify_email_discovered(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids) {
email_discovered(folder, ids);
}
/** Fires a {@link email_flags_changed}} signal. */
protected virtual void notify_email_flags_changed(Geary.Folder folder,
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> flag_map) {
email_flags_changed(folder, flag_map);
}
protected virtual void notify_email_sent(RFC822.Message message) {
email_sent(message);
}
/** Fires a {@link report_problem}} signal for this account. */
protected virtual void notify_report_problem(ProblemReport report) {
report_problem(report);
}
/**
* Fires a {@link report_problem}} signal for this account.
*/
protected virtual void notify_account_problem(ProblemType type, Error? err) {
report_problem(new AccountProblemReport(type, this.information, err));
}
/** Fires a {@link report_problem}} signal for a service for this account. */
protected virtual void notify_service_problem(ProblemType type, Service service_type, Error? err) {
report_problem(new ServiceProblemReport(type, this.information, service_type, err));
}
}

View file

@ -127,7 +127,7 @@ public class Geary.Endpoint : BaseObject {
this.remote_address = new NetworkAddress(host_specifier, default_port);
this.flags = flags;
this.timeout_sec = timeout_sec;
this.connectivity = new ConnectivityManager(this);
this.connectivity = new ConnectivityManager(this.remote_address);
}
private SocketClient get_socket_client() {

View file

@ -552,12 +552,11 @@ public abstract class Geary.Folder : BaseObject {
*/
public abstract async Geary.Email fetch_email_async(Geary.EmailIdentifier email_id,
Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable = null) throws Error;
/**
* Used for debugging. Should not be used for user-visible labels.
*/
public virtual string to_string() {
return "%s:%s".printf(account.to_string(), path.to_string());
return "%s:%s".printf(this.account.information.id, this.path.to_string());
}
}

View file

@ -0,0 +1,222 @@
/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2017 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/** Describes available problem types. */
public enum Geary.ProblemType {
/** Indicates an engine problem not covered by one of the other types. */
GENERIC_ERROR,
/** Indicates an error opening, using or closing the account database. */
DATABASE_FAILURE,
/** Indicates a problem establishing a connection. */
CONNECTION_ERROR,
/** Indicates a problem caused by a network operation. */
NETWORK_ERROR,
/** Indicates a non-network related server error. */
SERVER_ERROR,
/** Indicates credentials supplied for authentication were rejected. */
LOGIN_FAILED,
/** Indicates an outgoing message was sent, but not saved. */
SEND_EMAIL_SAVE_FAILED;
/** Determines the appropriate problem type for an IOError. */
public static ProblemType for_ioerror(IOError error) {
if (error is IOError.CONNECTION_REFUSED ||
error is IOError.HOST_NOT_FOUND ||
error is IOError.HOST_UNREACHABLE ||
error is IOError.NETWORK_UNREACHABLE) {
return ProblemType.CONNECTION_ERROR;
}
if (error is IOError.CONNECTION_CLOSED ||
error is IOError.NOT_CONNECTED) {
return ProblemType.NETWORK_ERROR;
}
return ProblemType.GENERIC_ERROR;
}
}
/**
* Describes a error that the engine encountered, for reporting to the client.
*/
public class Geary.ProblemReport : Object {
/**
* Represents an individual stack frame in a call back-trace.
*/
public class StackFrame {
/** Name of the function being called. */
public string name = "unknown";
internal StackFrame(Unwind.Cursor frame) {
uint8 proc_name[256];
int ret = -frame.get_proc_name(proc_name);
if (ret == Unwind.Error.SUCCESS ||
ret == Unwind.Error.NOMEM) {
this.name = (string) proc_name;
}
}
public string to_string() {
return this.name;
}
}
/** Describes the type of being reported. */
public ProblemType problem_type { get; private set; }
/** The exception caused the problem, if any. */
public Error? error { get; private set; default = null; }
/** A back trace from when the problem report was constructed. */
public Gee.List<StackFrame>? backtrace = null;
public ProblemReport(ProblemType type, Error? error) {
this.problem_type = type;
this.error = error;
if (error != null) {
// Some kind of exception occurred, so build a trace. This
// is far from perfect, but at least we will know where it
// was getting caught.
this.backtrace = new Gee.LinkedList<StackFrame>();
Unwind.Context trace = Unwind.Context();
Unwind.Cursor cursor = Unwind.Cursor.local(trace);
// This misses the first frame, but that's this
// constructor call, so we don't really care.
while (cursor.step() != 0) {
this.backtrace.add(new StackFrame(cursor));
}
}
}
/** Returns a string representation of the report, for debugging only. */
public string to_string() {
return "%s: %s".printf(
this.problem_type.to_string(),
format_full_error() ?? "no error reported"
);
}
/** Returns a string representation of the error type, for debugging. */
public string? format_error_type() {
string type = null;
if (this.error != null) {
const string QUARK_SUFFIX = "-quark";
string ugly_domain = this.error.domain.to_string();
if (ugly_domain.has_suffix(QUARK_SUFFIX)) {
ugly_domain = ugly_domain.substring(
0, ugly_domain.length - QUARK_SUFFIX.length
);
}
StringBuilder nice_domain = new StringBuilder();
string separator = (ugly_domain.index_of("_") != -1) ? "_" : "-";
foreach (string part in ugly_domain.split(separator)) {
if (part.length > 0) {
if (part == "io") {
nice_domain.append("IO");
} else {
nice_domain.append(part.up(1));
nice_domain.append(part.substring(1));
}
}
}
type = "%s %i".printf(nice_domain.str, this.error.code);
}
return type;
}
/** Returns a string representation of the complete error, for debugging. */
public string? format_full_error() {
string error = null;
if (this.error != null) {
error = String.is_empty(this.error.message)
? "%s: no message specified".printf(format_error_type())
: "%s: \"%s\"".printf(format_error_type(), this.error.message);
}
return error;
}
}
/**
* Describes an account-related error that the engine encountered.
*/
public class Geary.AccountProblemReport : ProblemReport {
/** The account related to the problem report. */
public AccountInformation account { get; private set; }
public AccountProblemReport(ProblemType type, AccountInformation account, Error? error) {
base(type, error);
this.account = account;
}
/** Returns a string representation of the report, for debugging only. */
public new string to_string() {
return "%s: %s".printf(this.account.id, base.to_string());
}
}
/**
* Describes a service-related error that the engine encountered.
*/
public class Geary.ServiceProblemReport : AccountProblemReport {
/** The service related to the problem report. */
public Service service_type { get; private set; }
/** The endpoint for the report's service type. */
public Endpoint endpoint {
owned get {
return (this.service_type == Service.IMAP)
? this.account.get_imap_endpoint()
: this.account.get_smtp_endpoint();
}
}
public ServiceProblemReport(ProblemType type, AccountInformation account, Service service_type, Error? error) {
base(type, account, error);
this.service_type = service_type;
}
/** Returns a string representation of the report, for debugging only. */
public new string to_string() {
return "%s: %s: %s: %s".printf(
this.account.id,
this.service_type.to_string(),
this.problem_type.to_string(),
format_full_error() ?? "no error reported"
);
}
}

View file

@ -233,11 +233,10 @@ private class Geary.ImapDB.Account : BaseObject {
public Account(Geary.AccountInformation account_information) {
this.account_information = account_information;
contact_store = new ImapEngine.ContactStore(this);
name = "IMAP database account for %s".printf(account_information.imap_credentials.user);
this.contact_store = new ImapEngine.ContactStore(this);
this.name = account_information.id + ":db";
}
private void check_open() throws Error {
if (db == null)
throw new EngineError.OPEN_REQUIRED("Database not open");

View file

@ -25,9 +25,6 @@ private class Geary.SmtpOutboxFolder :
// loaded, connections to settle, pigs to fly, etc.
private const uint START_TIMEOUT = 4;
// Number of times to retry sending after auth failures
private const uint AUTH_ERROR_MAX_RETRY = 3;
private class OutboxRow {
public int64 id;
@ -95,7 +92,7 @@ private class Geary.SmtpOutboxFolder :
public signal void email_sent(Geary.RFC822.Message rfc822);
/** Fired if a user-notifiable problem occurs. */
public signal void report_problem(Geary.Account.Problem problem, Error? err);
public signal void report_problem(ProblemReport report);
// Requires the Database from the get-go because it runs a background task that access it
@ -113,9 +110,84 @@ private class Geary.SmtpOutboxFolder :
);
}
// create_email_async() requires the Outbox be open according to contract, but enqueuing emails
// for background delivery can happen at any time, so this is the mechanism to do so.
// email_count is the number of emails in the Outbox after enqueueing the message.
/**
* Starts delivery of messages in the outbox.
*/
public async void start_postman_async() {
debug("Starting outbox postman with %u messages queued", this.outbox_queue.size);
if (this.queue_cancellable != null) {
debug("Postman already started, not starting another");
return;
}
Cancellable cancellable = this.queue_cancellable = new Cancellable();
uint send_retry_seconds = MIN_SEND_RETRY_INTERVAL_SEC;
// Start the send queue.
while (!cancellable.is_cancelled()) {
// yield until a message is ready
OutboxRow? row = null;
bool row_handled = false;
try {
row = yield this.outbox_queue.recv_async(cancellable);
row_handled = yield postman_send(row, cancellable);
} catch (SmtpError err) {
ProblemType problem = ProblemType.GENERIC_ERROR;
if (err is SmtpError.AUTHENTICATION_FAILED) {
problem = ProblemType.LOGIN_FAILED;
} else if (err is SmtpError.STARTTLS_FAILED) {
problem = ProblemType.CONNECTION_ERROR;
} else if (err is SmtpError.NOT_CONNECTED) {
problem = ProblemType.NETWORK_ERROR;
} else if (err is SmtpError.PARSE_ERROR ||
err is SmtpError.SERVER_ERROR ||
err is SmtpError.NOT_SUPPORTED) {
problem = ProblemType.SERVER_ERROR;
}
notify_report_problem(problem, err);
cancellable.cancel();
} catch (IOError err) {
notify_report_problem(ProblemType.for_ioerror(err), err);
cancellable.cancel();
} catch (Error err) {
notify_report_problem(ProblemType.GENERIC_ERROR, err);
cancellable.cancel();
}
if (row_handled) {
// send was good, reset nap length
send_retry_seconds = MIN_SEND_RETRY_INTERVAL_SEC;
} else {
// send was bad, try sending again later
if (row != null) {
this.outbox_queue.send(row);
}
if (!cancellable.is_cancelled()) {
debug("Outbox napping for %u seconds...", send_retry_seconds);
// Take a brief nap before continuing to allow
// connection problems to resolve.
yield Geary.Scheduler.sleep_async(send_retry_seconds);
send_retry_seconds = Geary.Numeric.uint_ceiling(
send_retry_seconds * 2,
MAX_SEND_RETRY_INTERVAL_SEC
);
}
}
}
this.queue_cancellable = null;
debug("Exiting outbox postman");
}
/**
* Queues a message in the outbox for delivery.
*
* This should be used instead of {@link create_email_async()},
* since that requires the Outbox be open according to contract,
* but enqueuing emails for background delivery can happen at any
* time, so this is the mechanism to do so.
*/
public async SmtpOutboxEmailIdentifier enqueue_email_async(Geary.RFC822.Message rfc822,
Cancellable? cancellable) throws Error {
debug("Queuing message for sending: %s",
@ -385,64 +457,6 @@ private class Geary.SmtpOutboxFolder :
return row_to_email(row);
}
private async void start_postman_async() {
debug("Starting outbox postman with %u messages queued", this.outbox_queue.size);
if (this.queue_cancellable != null) {
debug("Postman already started, not starting another");
return;
}
Cancellable cancellable = this.queue_cancellable = new Cancellable();
uint send_retry_seconds = MIN_SEND_RETRY_INTERVAL_SEC;
// Start the send queue.
while (!cancellable.is_cancelled()) {
// yield until a message is ready
OutboxRow? row = null;
try {
row = yield this.outbox_queue.recv_async(cancellable);
if (yield postman_send(row, cancellable)) {
// send was good, reset nap length
send_retry_seconds = MIN_SEND_RETRY_INTERVAL_SEC;
} else {
// send was bad, try sending again later
this.outbox_queue.send(row);
if (!cancellable.is_cancelled()) {
debug("Outbox napping for %u seconds...", send_retry_seconds);
// Take a brief nap before continuing to allow
// connection problems to resolve.
yield Geary.Scheduler.sleep_async(send_retry_seconds);
send_retry_seconds = Geary.Numeric.uint_ceiling(
send_retry_seconds * 2,
MAX_SEND_RETRY_INTERVAL_SEC
);
}
}
} catch (Error err) {
// A hard error occurred. This will cause the postman
// to exit but we still want to re-queue the row in
// case it restarts.
if (row != null) {
this.outbox_queue.send(row);
}
if (!(err is IOError.CANCELLED)) {
debug("Outbox postman error: %s", err.message);
if (err is SmtpError.AUTHENTICATION_FAILED) {
report_problem(Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED, err);
} else {
report_problem(Geary.Account.Problem.SEND_EMAIL_ERROR, err);
}
}
// Get out of here
cancellable.cancel();
}
}
this.queue_cancellable = null;
debug("Exiting outbox postman");
}
// Returns true if row was successfully processed, else false
private async bool postman_send(OutboxRow row, Cancellable cancellable)
throws Error {
@ -481,8 +495,8 @@ private class Geary.SmtpOutboxFolder :
// We immediately retry auth errors after the prompting
// the user, but if they get it wrong enough times or
// cancel we have no choice other than to stop the postman
int attempts = 0;
while (!mail_sent && ++attempts <= AUTH_ERROR_MAX_RETRY) {
uint attempts = 0;
while (!mail_sent && ++attempts <= Geary.Account.AUTH_ATTEMPTS_MAX) {
try {
debug("Outbox postman: Sending \"%s\" (ID:%s)...",
message_subject(message), row.outbox_id.to_string());
@ -491,8 +505,14 @@ private class Geary.SmtpOutboxFolder :
} catch (Error send_err) {
debug("Outbox postman send error: %s", send_err.message);
if (send_err is SmtpError.AUTHENTICATION_FAILED) {
// At this point we may already have a password in memory -- but it's incorrect.
// Delete the current password, prompt the user for a new one, and try again.
if (attempts == Geary.Account.AUTH_ATTEMPTS_MAX) {
throw send_err;
}
// At this point we may already have a
// password in memory -- but it's incorrect.
// Delete the current password, prompt the
// user for a new one, and try again.
bool user_confirmed = false;
try {
user_confirmed = yield account.fetch_passwords_async(
@ -503,18 +523,19 @@ private class Geary.SmtpOutboxFolder :
}
if (!user_confirmed) {
// The user cancelled hence they don't
// want to be prompted again, so report it
// and bail out.
// The user cancelled and hence they don't
// want to be prompted again, so bail out.
throw send_err;
}
} else if (send_err is TlsError) {
// up to application to be aware of problem via Geary.Engine, but do nap and
// try later
// up to application to be aware of problem
// via Geary.Engine, but do nap and try later
debug("TLS connection warnings connecting to %s, user must confirm connection to continue",
this.smtp_endpoint.to_string());
break;
} else {
report_problem(Geary.Account.Problem.SEND_EMAIL_DELIVERY_FAILURE, send_err);
// not much else we can do - just bail out
throw send_err;
}
}
}
@ -544,7 +565,7 @@ private class Geary.SmtpOutboxFolder :
yield save_sent_mail_async(message, cancellable);
} catch (Error err) {
debug("Outbox postman: Error saving sent mail: %s", err.message);
report_problem(Geary.Account.Problem.SEND_EMAIL_SAVE_FAILED, err);
notify_report_problem(ProblemType.SEND_EMAIL_SAVE_FAILED, err);
return false;
}
}
@ -856,10 +877,15 @@ private class Geary.SmtpOutboxFolder :
return stmt.exec_get_modified(cancellable) > 0;
}
private void notify_report_problem(ProblemType problem, Error? err) {
report_problem(new ServiceProblemReport(problem, this.account.information, Service.SMTP, err));
}
private void on_account_opened() {
this.fill_outbox_queue.begin();
this.smtp_endpoint.connectivity.notify["is-reachable"].connect(on_reachable_changed);
if (this.smtp_endpoint.connectivity.is_reachable) {
this.smtp_endpoint.connectivity.address_error_reported.connect(on_connectivity_error);
if (this.smtp_endpoint.connectivity.is_reachable.is_certain()) {
this.start_timer.start();
} else {
this.smtp_endpoint.connectivity.check_reachable.begin();
@ -869,18 +895,23 @@ private class Geary.SmtpOutboxFolder :
private void on_account_closed() {
this.stop_postman();
this.smtp_endpoint.connectivity.notify["is-reachable"].disconnect(on_reachable_changed);
this.smtp_endpoint.connectivity.address_error_reported.disconnect(on_connectivity_error);
}
private void on_reachable_changed() {
if (this.smtp_endpoint.connectivity.is_reachable) {
if (this.smtp_endpoint.connectivity.is_reachable.is_certain()) {
if (this.queue_cancellable == null) {
this.start_timer.start();
}
} else {
this.start_timer.reset();
if (this.queue_cancellable != null) {
stop_postman();
}
stop_postman();
}
}
private void on_connectivity_error(Error error) {
stop_postman();
notify_report_problem(ProblemType.CONNECTION_ERROR, error);
}
}

View file

@ -29,7 +29,6 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
= new Gee.HashMap<FolderPath, uint>();
private Gee.HashSet<Geary.Folder> in_refresh_unseen = new Gee.HashSet<Geary.Folder>();
private AccountSynchronizer sync;
private bool awaiting_credentials = false;
private Cancellable? enumerate_folder_cancellable = null;
private TimeoutManager refresh_folder_timer;
@ -43,10 +42,9 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
this.remote = remote;
this.remote.ready.connect(on_remote_ready);
this.remote.report_problem.connect(notify_report_problem);
this.local = local;
this.remote.login_failed.connect(on_login_failed);
this.local.contacts_loaded.connect(() => { contacts_loaded(); });
this.local.email_sent.connect(on_email_sent);
@ -264,7 +262,31 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
message("%s: Rebuild complete", to_string());
}
/**
* This starts the outbox postman running.
*/
public override async void start_outgoing_client()
throws Error {
check_open();
this.local.outbox.start_postman_async.begin();
}
/**
* This closes then reopens the IMAP account.
*/
public override async void start_incoming_client()
throws Error {
check_open();
try {
yield this.remote.close_async();
} catch (Error err) {
debug("Ignoring error closing IMAP account for restart: %s", err.message);
}
yield this.remote.open_async();
}
// Subclasses should implement this to return their flavor of a MinimalFolder with the
// appropriate interfaces attached. The returned folder should have its SpecialFolderType
// set using either the properties from the local folder or its path.
@ -425,10 +447,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
successful = true;
}
} catch (ImapError err) {
notify_account_problem(ProblemType.SERVER_ERROR, err);
} catch (IOError err) {
notify_account_problem(ProblemType.for_ioerror(err), err);
} catch (Error err) {
if (!(err is IOError.CANCELLED)) {
report_problem(Geary.Account.Problem.CONNECTION_FAILURE, err);
}
notify_account_problem(ProblemType.GENERIC_ERROR, err);
}
this.enumerate_folder_cancellable = null;
@ -935,44 +959,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
return yield local.get_containing_folders_async(ids, cancellable);
}
private void on_login_failed(Geary.Credentials? credentials, Geary.Imap.StatusResponse? response) {
if (awaiting_credentials)
return; // We're already asking for the password.
awaiting_credentials = true;
do_login_failed_async.begin(credentials, response, () => { awaiting_credentials = false; });
}
private void on_remote_ready() {
this.enumerate_folders_async.begin();
}
private async void do_login_failed_async(Geary.Credentials? credentials, Geary.Imap.StatusResponse? response) {
bool reask_password = true;
try {
reask_password = (
response == null ||
response.response_code == null ||
response.response_code.get_response_code_type().value != Geary.Imap.ResponseCodeType.UNAVAILABLE
);
} catch (ImapError ierr) {
debug("Unable to parse ResponseCode %s: %s", response.response_code.to_string(),
ierr.message);
}
// login can fail due to an invalid password hence we should re-ask it
// but it can also fail due to server inaccessibility, for instance "[UNAVAILABLE] /
// Maximum number of connections from user+IP exceeded". In that case, resetting password seems unneeded.
if (reask_password) {
try {
if (yield information.fetch_passwords_async(ServiceFlag.IMAP, true))
return;
} catch (Error e) {
debug("Error prompting for IMAP password: %s", e.message);
}
notify_report_problem(Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED, null);
} else {
notify_report_problem(Geary.Account.Problem.CONNECTION_FAILURE, null);
}
}
}

View file

@ -33,8 +33,9 @@ private class Geary.Imap.Account : BaseObject {
public bool is_ready { get { return this.session_mgr.is_ready; } }
private string name;
private AccountInformation account_information;
private AccountInformation account;
private ClientSessionManager session_mgr;
private uint authentication_failures = 0;
private ClientSession? account_session = null;
private Nonblocking.Mutex account_session_mutex = new Nonblocking.Mutex();
private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex();
@ -43,6 +44,7 @@ private class Geary.Imap.Account : BaseObject {
private Gee.List<StatusData>? status_collector = null;
private Gee.List<ServerData>? server_data_collector = null;
/**
* Fired after opening when the account has a working connection.
*
@ -51,27 +53,31 @@ private class Geary.Imap.Account : BaseObject {
*/
public signal void ready();
public signal void login_failed(Geary.Credentials? cred, StatusResponse? response);
/** Fired if a user-notifiable problem occurs. */
public signal void report_problem(ProblemReport report);
public Account(Geary.AccountInformation account_information) {
name = "IMAP Account for %s".printf(account_information.imap_credentials.to_string());
this.account_information = account_information;
this.session_mgr = new ClientSessionManager(account_information);
public Account(Geary.AccountInformation account) {
this.name = account.id + ":imap";
this.account = account;
this.session_mgr = new ClientSessionManager(account);
this.session_mgr.ready.connect(on_session_ready);
this.session_mgr.connection_failed.connect(on_connection_failed);
this.session_mgr.login_failed.connect(on_login_failed);
}
private void check_open() throws Error {
if (!is_open)
throw new EngineError.OPEN_REQUIRED("Imap.Account not open");
}
public async void open_async(Cancellable? cancellable = null) throws Error {
if (is_open)
throw new EngineError.ALREADY_OPEN("Imap.Account already open");
// Reset this so we start trying to authenticate again
this.authentication_failures = 0;
// This will cause the session manager to open at least one
// connection. We can't attempt to claim one straight away
// since we might not be online.
yield session_mgr.open_async(cancellable);
is_open = true;
}
@ -569,22 +575,27 @@ private class Geary.Imap.Account : BaseObject {
return responses;
}
private void check_open() throws Error {
if (!is_open)
throw new EngineError.OPEN_REQUIRED("Imap.Account not open");
}
private inline Imap.Folder newUnselectableFolder(FolderPath path, MailboxAttributes attrs) {
return new Imap.Folder(
path, new Imap.FolderProperties(0, 0, 0, null, null, attrs), this.session_mgr
);
}
private void notify_report_problem(ProblemType problem, Error? err) {
report_problem(new ServiceProblemReport(problem, this.account, Service.IMAP, err));
}
[NoReturn]
private void throw_not_found(Geary.FolderPath? path) throws EngineError {
throw new EngineError.NOT_FOUND("Folder %s not found on %s",
(path != null) ? path.to_string() : "root", session_mgr.to_string());
}
private void on_login_failed(StatusResponse? response) {
login_failed(account_information.imap_credentials, response);
}
private void on_list_data(MailboxInformation mailbox_info) {
if (list_collector != null)
list_collector.add(mailbox_info);
@ -605,10 +616,75 @@ private class Geary.Imap.Account : BaseObject {
}
private void on_session_ready() {
// Now have a valid session, so credentials must be good
this.authentication_failures = 0;
ready();
}
private void on_connection_failed(Error error) {
// There was an error connecting to the IMAP host
this.authentication_failures = 0;
if (error is ImapError.UNAUTHENTICATED) {
// This is effectively a login failure
on_login_failed(null);
} else {
notify_report_problem(ProblemType.CONNECTION_ERROR, error);
}
}
private void on_login_failed(Geary.Imap.StatusResponse? response) {
this.authentication_failures++;
if (this.authentication_failures >= Geary.Account.AUTH_ATTEMPTS_MAX) {
// We have tried auth too many times, so bail out
notify_report_problem(ProblemType.LOGIN_FAILED, null);
} else {
// login can fail due to an invalid password hence we
// should re-ask it but it can also fail due to server
// inaccessibility, for instance "[UNAVAILABLE] / Maximum
// number of connections from user+IP exceeded". In that
// case, resetting password seems unneeded.
bool reask_password = false;
Error? login_error = null;
try {
reask_password = (
response == null ||
response.response_code == null ||
response.response_code.get_response_code_type().value != Geary.Imap.ResponseCodeType.UNAVAILABLE
);
} catch (ImapError err) {
login_error = err;
debug("Unable to parse ResponseCode %s: %s", response.response_code.to_string(),
err.message);
}
if (!reask_password) {
// Either the server was unavailable, or we were unable to
// parse the login response. Either way, indicate a
// non-login error.
notify_report_problem(ProblemType.SERVER_ERROR, login_error);
} else {
// Now, we should ask the user for their password
this.account.fetch_passwords_async.begin(
ServiceFlag.IMAP, true,
(obj, ret) => {
try {
if (this.account.fetch_passwords_async.end(ret)) {
// Have a new password, so try that
this.session_mgr.credentials_updated();
} else {
// User cancelled, so indicate a login problem
notify_report_problem(ProblemType.LOGIN_FAILED, null);
}
} catch (Error err) {
notify_report_problem(ProblemType.GENERIC_ERROR, err);
}
});
}
}
}
public string to_string() {
return name;
}
}

View file

@ -1,16 +1,15 @@
/*
* Copyright 2017 Michael Gratton <mike@vee.net>
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2017 Michael Gratton <mike@vee.net>
*
* 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.ClientSessionManager : BaseObject {
private const int DEFAULT_MIN_POOL_SIZE = 1;
private const int POOL_START_TIMEOUT_SEC = 4;
private const int POOL_RETRY_MIN_TIMEOUT_SEC = 1;
private const int POOL_RETRY_MAX_TIMEOUT_SEC = 10;
private const int POOL_STOP_TIMEOUT_SEC = 1;
/** Determines if the manager has been opened. */
@ -69,7 +68,6 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
private bool untrusted_host = false;
private TimeoutManager pool_start;
private TimeoutManager pool_retry;
private TimeoutManager pool_stop;
/**
@ -81,12 +79,15 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
*/
public signal void ready();
/** Fired when a network or non-auth error occurs opening a session. */
public signal void connection_failed(Error err);
/** Fired when an authentication error occurs opening a session. */
public signal void login_failed(StatusResponse? response);
public ClientSessionManager(AccountInformation account_information) {
this.account_information = account_information;
this.account_information.notify["imap-credentials"].connect(on_imap_credentials_notified);
// NOTE: This works because AccountInformation guarantees the IMAP endpoint not to change
// for the lifetime of the AccountInformation object; if this ever changes, will need to
@ -100,11 +101,6 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
() => { this.adjust_session_pool.begin(); }
);
this.pool_retry = new TimeoutManager.seconds(
POOL_RETRY_MIN_TIMEOUT_SEC,
() => { this.adjust_session_pool.begin(); }
);
this.pool_stop = new TimeoutManager.seconds(
POOL_STOP_TIMEOUT_SEC,
() => { this.force_disconnect_all.begin(); }
@ -115,7 +111,6 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
if (is_open)
warning("Destroying opened ClientSessionManager");
this.account_information.notify["imap-credentials"].disconnect(on_imap_credentials_notified);
this.endpoint.untrusted_host.disconnect(on_imap_untrusted_host);
this.endpoint.notify[Endpoint.PROP_TRUST_UNTRUSTED_HOST].disconnect(on_imap_trust_untrusted_host);
}
@ -124,10 +119,12 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
if (is_open)
throw new EngineError.ALREADY_OPEN("ClientSessionManager already open");
is_open = true;
this.is_open = true;
this.authentication_failed = false;
this.endpoint.connectivity.notify["is-reachable"].connect(on_connectivity_change);
if (this.endpoint.connectivity.is_reachable) {
this.endpoint.connectivity.address_error_reported.connect(on_connectivity_error);
if (this.endpoint.connectivity.is_reachable.is_certain()) {
this.adjust_session_pool.begin();
} else {
this.endpoint.connectivity.check_reachable.begin();
@ -142,10 +139,10 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
this.is_ready = false;
this.pool_start.reset();
this.pool_retry.reset();
this.pool_stop.reset();
this.endpoint.connectivity.notify["is-reachable"].disconnect(on_connectivity_change);
this.endpoint.connectivity.address_error_reported.disconnect(on_connectivity_error);
// to avoid locking down the sessions table while scheduling disconnects, make a copy
// and work off of that
@ -172,14 +169,20 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
break;
}
}
private void on_imap_credentials_notified() {
authentication_failed = false;
if (is_open)
adjust_session_pool.begin();
/**
* Informs the manager that the account's IMAP credentials have changed.
*
* This will reset the manager's authentication state and if open,
* attempt to open a connection to the server.
*/
public void credentials_updated() {
this.authentication_failed = false;
if (this.is_open) {
this.adjust_session_pool.begin();
}
}
private void check_open() throws Error {
if (!is_open)
throw new EngineError.OPEN_REQUIRED("ClientSessionManager is not open");
@ -203,9 +206,18 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
&& this.is_open
&& !this.authentication_failed
&& !this.untrusted_host
&& this.endpoint.connectivity.is_reachable) {
pending_sessions++;
create_new_authorized_session.begin(null, on_created_new_authorized_session);
&& this.endpoint.connectivity.is_reachable.is_certain()) {
this.pending_sessions++;
create_new_authorized_session.begin(
null,
(obj, res) => {
this.pending_sessions--;
try {
this.create_new_authorized_session.end(res);
} catch (Error err) {
connection_failed(err);
}
});
}
try {
@ -215,24 +227,6 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
}
}
private void on_created_new_authorized_session(Object? source, AsyncResult result) {
this.pending_sessions--;
try {
this.create_new_authorized_session.end(result);
this.pool_retry.reset();
} catch (Error err) {
debug("Unable to create authorized session to %s: %s", endpoint.to_string(), err.message);
// try again after a slight delay and bump up delay
this.pool_retry.start();
this.pool_retry.interval = (this.pool_retry.interval * 2).clamp(
POOL_RETRY_MIN_TIMEOUT_SEC,
POOL_RETRY_MAX_TIMEOUT_SEC
);
}
}
private async ClientSession create_new_authorized_session(Cancellable? cancellable) throws Error {
if (authentication_failed)
throw new ImapError.UNAUTHENTICATED("Invalid ClientSessionManager credentials");
@ -240,9 +234,9 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
if (untrusted_host)
throw new ImapError.UNAVAILABLE("Untrusted host %s", endpoint.to_string());
if (!this.endpoint.connectivity.is_reachable)
if (!this.endpoint.connectivity.is_reachable.is_certain())
throw new ImapError.UNAVAILABLE("Host at %s is unreachable", endpoint.to_string());
ClientSession new_session = new ClientSession(endpoint);
// add session to pool before launching all the connect activity so error cases can properly
@ -296,9 +290,6 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
ready();
}
// reset delay
this.pool_retry.interval = POOL_RETRY_MIN_TIMEOUT_SEC;
// do this after logging in
new_session.enable_keepalives(selected_keepalive_sec, unselected_keepalive_sec,
selected_with_idle_keepalive_sec);
@ -557,7 +548,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
}
private void on_connectivity_change() {
bool is_reachable = this.endpoint.connectivity.is_reachable;
bool is_reachable = this.endpoint.connectivity.is_reachable.is_certain();
if (is_reachable) {
this.pool_start.start();
this.pool_stop.reset();
@ -567,7 +558,13 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
this.pool_start.reset();
this.pool_stop.start();
}
this.pool_retry.reset();
}
private void on_connectivity_error(Error error) {
this.is_ready = false;
this.pool_start.reset();
this.pool_stop.start();
connection_failed(error);
}
/**

View file

@ -4,14 +4,33 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public errordomain Geary.SmtpError {
PARSE_ERROR,
STARTTLS_FAILED,
AUTHENTICATION_FAILED,
SERVER_ERROR,
ALREADY_CONNECTED,
NOT_CONNECTED,
REQUIRED_FIELD,
NOT_SUPPORTED
}
/**
* Thrown when an error occurs communicating with a SMTP server.
*/
public errordomain Geary.SmtpError {
/** The client already has a connection to the server. */
ALREADY_CONNECTED,
/** The credentials presented for authentication were rejected. */
AUTHENTICATION_FAILED,
/** The client does not have a connection to the server. */
NOT_CONNECTED,
/** The server does not support an SMTP feature required by the engine. */
NOT_SUPPORTED,
/** A response from the server could not be parsed. */
PARSE_ERROR,
/** A message could not be sent because a field required by SMTP was missing. */
REQUIRED_FIELD,
/** The server reported an error. */
SERVER_ERROR,
/** Establishing STARTTLS was attempted, but failed. */
STARTTLS_FAILED
}

View file

@ -6,13 +6,13 @@
*/
/**
* Keeps track of network connectivity changes for an endpoint.
* Keeps track of network connectivity changes for a network address.
*
* This class is a convenience API for the GIO NetworkMonitor. Since
* when connecting and disconnecting from a network, multiple
* network-changed signals may be sent, this class coalesces these as
* best as possible so the rest of the engine is only notified once
* when an endpoint becomes reachable, and once when it becomes
* when an address becomes reachable, and once when it becomes
* unreachable.
*
* Note this class is not thread safe and should only be invoked from
@ -20,15 +20,22 @@
*/
public class Geary.ConnectivityManager : BaseObject {
private const uint CHECK_QUIESCENCE_MS = 60 * 1000;
/** Determines if the managed endpoint is currently reachable. */
public bool is_reachable { get; private set; default = false; }
/** The address being monitored. */
public NetworkAddress address { get; private set; default = null; }
// Weak to avoid a circular ref with the endpoint
private weak Endpoint endpoint;
/** Determines if the managed address is currently reachable. */
public Trillian is_reachable { get; private set; default = Geary.Trillian.UNKNOWN; }
/**
* Determines if a the address's network address name is valid.
*
* This will become certain if the address becomes reachable, and
* will become impossible if a fatal address error is reported.
*/
public Trillian is_valid { get; private set; default = Geary.Trillian.UNKNOWN; }
private NetworkMonitor monitor;
@ -41,10 +48,21 @@ public class Geary.ConnectivityManager : BaseObject {
/**
* Constructs a new manager for a specific endpoint.
* Fired when a fatal error was reported checking the address.
*
* This is typically caused by an an authoritative DNS name not
* found error, but may be anything else that indicates that the
* address will be unusable as-is without some kind of user or
* server administrator intervention.
*/
public ConnectivityManager(Endpoint endpoint) {
this.endpoint = endpoint;
public signal void address_error_reported(Error error);
/**
* Constructs a new manager for a specific address.
*/
public ConnectivityManager(NetworkAddress address) {
this.address = address;
this.monitor = NetworkMonitor.get_default();
this.monitor.network_changed.connect(on_network_changed);
@ -59,7 +77,7 @@ public class Geary.ConnectivityManager : BaseObject {
}
/**
* Starts checking if the manager's endpoint is reachable.
* Starts checking if the manager's address is reachable.
*
* This will cancel any existing check, and start a new one
* running, updating the `is_reachable` property on completion.
@ -76,13 +94,12 @@ public class Geary.ConnectivityManager : BaseObject {
Cancellable cancellable = new Cancellable();
this.existing_check = cancellable;
string endpoint = this.endpoint.to_string();
bool is_reachable = this.is_reachable;
string endpoint = to_address_string();
bool is_reachable = false;
try {
debug("Checking if %s reachable...", endpoint);
is_reachable = yield this.monitor.can_reach_async(
this.endpoint.remote_address,
cancellable
this.address, cancellable
);
this.next_check = get_real_time() + CHECK_QUIESCENCE_MS;
} catch (Error err) {
@ -95,11 +112,19 @@ public class Geary.ConnectivityManager : BaseObject {
is_reachable = true;
debug("Assuming %s is reachable, despite network unavailability",
endpoint);
} else if (err is ResolverError.TEMPORARY_FAILURE) {
// This often happens when networking is coming back
// online, may because the interface is up but has not
// been assigned an address yet? Since we should get
// another network change when the interface is
// configured, just ignore it.
debug("Ignoring: %s", err.message);
} else if (!(err is IOError.CANCELLED)) {
// Service is unreachable
debug("Error checking %s reachable, treating as unreachable: %s",
endpoint, err.message);
is_reachable = false;
endpoint, err.message);
set_invalid();
address_error_reported(err);
}
} finally {
if (!cancellable.is_cancelled()) {
@ -137,7 +162,7 @@ public class Geary.ConnectivityManager : BaseObject {
//
// Otherwise, schedule a delayed check to work around the
// issue in Bug 776042.
if (!this.is_reachable ||
if (this.is_reachable.is_uncertain() ||
this.existing_check != null ||
this.next_check <= get_real_time()) {
this.check_reachable.begin();
@ -153,13 +178,38 @@ public class Geary.ConnectivityManager : BaseObject {
private inline void set_reachable(bool reachable) {
// Coalesce changes to is_reachable, since Vala <= 0.34 always
// fires notify signals on set, even if the value doesn't
// change. 0.36 fixes that, so pull this out when we can
// change. 0.36 fixes that, so pull this test out when we can
// depend on that as a minimum.
if (this.is_reachable != reachable) {
if ((reachable && !this.is_reachable.is_certain()) ||
(!reachable && !this.is_reachable.is_impossible())) {
debug("Host %s became %s",
this.endpoint.to_string(), reachable ? "reachable" : "unreachable");
this.is_reachable = reachable;
this.address.to_string(), reachable ? "reachable" : "unreachable");
this.is_reachable = reachable ? Trillian.TRUE : Trillian.FALSE;
}
// We only work out if the name is valid (or becomes valid
// again) if the address becomes reachable.
if (reachable && this.is_valid.is_uncertain()) {
this.is_valid = Trillian.TRUE;
}
}
private inline void set_invalid() {
// Coalesce changes to is_reachable, since Vala <= 0.34 always
// fires notify signals on set, even if the value doesn't
// change. 0.36 fixes that, so pull this method out when we can
// depend on that as a minimum.
if (this.is_valid != Trillian.FALSE) {
this.is_valid = Trillian.FALSE;
}
}
private inline string to_address_string() {
// Unlikely to be the case, but if IPv6 format it nicely
return (this.address.hostname.index_of(":") == -1)
? "%s:%u".printf(this.address.hostname, this.address.port)
: "[%s]:%u".printf(this.address.hostname, this.address.port);
}
}

View file

@ -30,6 +30,7 @@ set(RESOURCE_LIST
STRIPBLANKS "main-toolbar.ui"
STRIPBLANKS "main-toolbar-menus.ui"
STRIPBLANKS "main-window.ui"
STRIPBLANKS "main-window-info-bar.ui"
STRIPBLANKS "password-dialog.glade"
STRIPBLANKS "preferences-dialog.ui"
STRIPBLANKS "remove_confirm.glade"

View file

@ -6,6 +6,8 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/* MainWindow */
.geary-folder-frame > border {
border-left-width: 0;
border-top-width: 0;
@ -39,6 +41,14 @@
border-top-left-radius: 0px;
}
/* MainWindowInfoBarSet */
.geary-info-bar-frame > border {
border-top-width: 0;
border-left-width: 0;
border-right-width: 0;
}
/* FolderPopover */
row.geary-folder-popover-list-row {

153
ui/main-window-info-bar.ui Normal file
View file

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="MainWindowInfoBar" parent="GtkInfoBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<signal name="hide" handler="on_hide" after="yes" swapped="no"/>
<signal name="response" handler="on_info_bar_response" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="title">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label">Title</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="description">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label">Description.</property>
<property name="ellipsize">end</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="sigh"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</template>
<object class="GtkGrid" id="problem_details">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">18</property>
<property name="margin_right">18</property>
<property name="margin_top">18</property>
<property name="margin_bottom">18</property>
<property name="row_spacing">6</property>
<property name="column_spacing">12</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">start</property>
<property name="valign">baseline</property>
<property name="margin_bottom">12</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">If the problem is serious or persists, please copy and send these details to the &lt;a href="https://wiki.gnome.org/Apps/Geary/Contact"&gt;mailing list&lt;/a&gt; or file a &lt;a href="https://wiki.gnome.org/Apps/Geary/ReportingABug"&gt;new bug report&lt;/a&gt;.</property>
<property name="use_markup">True</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">baseline</property>
<property name="label" translatable="yes">Details:</property>
<property name="track_visited_links">False</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="width_request">600</property>
<property name="height_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="detail_text">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="editable">False</property>
<property name="wrap_mode">word</property>
<property name="left_margin">6</property>
<property name="right_margin">6</property>
<property name="top_margin">6</property>
<property name="bottom_margin">6</property>
<property name="cursor_visible">False</property>
<property name="monospace">True</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,69 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.14"/>
<requires lib="gtk+" version="3.20"/>
<template class="MainWindow" parent="GtkApplicationWindow">
<property name="visible">False</property>
<property name="show_menubar">False</property>
<property name="name">GearyMainWindow</property>
<property name="can_focus">False</property>
<property name="events">GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_FOCUS_CHANGE_MASK | GDK_STRUCTURE_MASK</property>
<signal name="delete_event" handler="on_delete_event"/>
<signal name="key_release_event" handler="on_key_release_event"/>
<signal name="focus_in_event" handler="on_focus_event"/>
<property name="show_menubar">False</property>
<signal name="delete-event" handler="on_delete_event" swapped="no"/>
<signal name="focus-in-event" handler="on_focus_event" swapped="no"/>
<signal name="key-release-event" handler="on_key_release_event" swapped="no"/>
<child>
<object class="GtkBox" id="main_layout">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">0</property>
<child>
<object class="GtkPaned" id="conversations_paned">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">horizontal</property>
<child>
<object class="GtkBox" id="search_bar_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">0</property>
<style>
<class name="sidebar"/>
</style>
<child>
<object class="GtkPaned" id="folder_paned">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">horizontal</property>
<style>
<class name="geary-sidebar-pane-separator"/>
</style>
<child>
<object class="GtkBox" id="folder_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">0</property>
<child>
<object class="GtkFrame" id="folder_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkScrolledWindow" id="folder_list_scrolled">
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hscrollbar_policy">never</property>
</object>
</child>
<style>
<class name="geary-folder-frame"/>
</style>
<child>
<object class="GtkScrolledWindow" id="folder_list_scrolled">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="width_request">100</property>
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
@ -77,28 +68,27 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">0</property>
<child>
<object class="GtkFrame" id="conversation_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkScrolledWindow" id="conversation_list_scrolled">
<property name="width_request">250</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<style>
<class name="geary-conversation-frame"/>
</style>
<child>
<object class="GtkScrolledWindow" id="conversation_list_scrolled">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="width_request">250</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
@ -107,13 +97,20 @@
<property name="shrink">False</property>
</packing>
</child>
<style>
<class name="geary-sidebar-pane-separator"/>
</style>
</object>
<packing>
<property name="pack_type">end</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="sidebar"/>
</style>
</object>
<packing>
<property name="resize">False</property>
@ -122,9 +119,64 @@
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="info_bar_frame">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="info_bar_container">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<signal name="remove" handler="on_info_bar_container_remove" swapped="no"/>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
<style>
<class name="geary-info-bar-frame"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>