diff --git a/data/io.elementary.mail-daemon.desktop.in b/data/io.elementary.mail-daemon.desktop.in
new file mode 100644
index 000000000..463a422bd
--- /dev/null
+++ b/data/io.elementary.mail-daemon.desktop.in
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Name=Mail Daemon
+Comment=Send and receive mail
+Exec=io.elementary.mail --background
+Icon=io.elementary.mail
+Keywords=Email;E-mail;Mail;
+Terminal=false
+Type=Application
+NoDisplay=true
+X-GNOME-AutoRestart=true
+X-GNOME-Autostart-Delay=5
+X-GNOME-Autostart-Phase=Applications
diff --git a/data/io.elementary.mail.appdata.xml.in b/data/io.elementary.mail.appdata.xml.in
index 29d722bf0..6f25b2542 100644
--- a/data/io.elementary.mail.appdata.xml.in
+++ b/data/io.elementary.mail.appdata.xml.in
@@ -22,6 +22,15 @@
io.elementary.mail
+
+
+ Improvements:
+
+ - Send a notification when new messages arrive
+ - Updated translations
+
+
+
Fixes:
diff --git a/data/meson.build b/data/meson.build
index 9a51996ea..3d7a47905 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -13,6 +13,14 @@ foreach i : icon_sizes
)
endforeach
+daemon_desktop_config = configuration_data()
+configure_file(
+ input: meson.project_name() + '-daemon.desktop.in',
+ output: meson.project_name() + '-daemon.desktop',
+ configuration: daemon_desktop_config,
+ install_dir: join_paths(get_option('sysconfdir'), 'xdg', 'autostart')
+)
+
install_data(
meson.project_name() + '.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0', 'schemas')
diff --git a/src/Application.vala b/src/Application.vala
index a486b1e48..dc9dc16c2 100644
--- a/src/Application.vala
+++ b/src/Application.vala
@@ -19,7 +19,13 @@
*/
public class Mail.Application : Gtk.Application {
+ const OptionEntry[] OPTIONS = {
+ { "background", 'b', 0, OptionArg.NONE, out run_in_background, "Run the Application in background", null},
+ { null }
+ };
+
public static GLib.Settings settings;
+ public static bool run_in_background;
private MainWindow? main_window = null;
@@ -40,6 +46,8 @@ public class Mail.Application : Gtk.Application {
Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
Intl.textdomain (GETTEXT_PACKAGE);
+ add_main_option_entries (OPTIONS);
+
var quit_action = new SimpleAction ("quit", null);
quit_action.activate.connect (() => {
if (main_window != null) {
@@ -81,6 +89,13 @@ public class Mail.Application : Gtk.Application {
}
public override void activate () {
+ if (run_in_background) {
+ run_in_background = false;
+ new InboxMonitor ().start.begin ();
+ hold ();
+ return;
+ }
+
if (main_window == null) {
Gtk.IconTheme.get_default ().add_resource_path ("/io/elementary/mail");
@@ -117,6 +132,8 @@ public class Mail.Application : Gtk.Application {
css_provider.load_from_resource ("io/elementary/mail/application.css");
Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
}
+
+ main_window.present ();
}
}
diff --git a/src/Backend/Session.vala b/src/Backend/Session.vala
index 30a5eb69f..9e19d2a2a 100644
--- a/src/Backend/Session.vala
+++ b/src/Backend/Session.vala
@@ -36,7 +36,7 @@ public class Mail.Backend.Session : Camel.Session {
public signal void account_added (Mail.Backend.Account account);
public signal void account_removed ();
- private Session () {
+ public Session () {
Object (user_data_dir: Path.build_filename (E.get_user_data_dir (), "mail"), user_cache_dir: Path.build_filename (E.get_user_cache_dir (), "mail"));
}
diff --git a/src/InboxMonitor.vala b/src/InboxMonitor.vala
new file mode 100644
index 000000000..99d3f2c86
--- /dev/null
+++ b/src/InboxMonitor.vala
@@ -0,0 +1,239 @@
+/*
+* Copyright 2021 elementary, Inc. (https://elementary.io)
+*
+* This program is free software; you can redistribute it and/or
+* modify it under the terms of the GNU General Public
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public
+* License along with this program; if not, write to the
+* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+* Boston, MA 02110-1301 USA
+*/
+
+public class Mail.InboxMonitor : GLib.Object {
+
+ private NetworkMonitor network_monitor;
+ private Mail.Backend.Session session;
+ private HashTable inbox_folders;
+ private HashTable synchronize_timeout_ids;
+ private E.SourceRegistry registry;
+
+ construct {
+ inbox_folders = new HashTable (E.Source.hash, E.Source.equal);
+ synchronize_timeout_ids = new HashTable (E.Source.hash, E.Source.equal);
+
+ network_monitor = GLib.NetworkMonitor.get_default ();
+ session = new Mail.Backend.Session ();
+ }
+
+ public async void start () {
+ yield session.start ();
+ try {
+ registry = yield new E.SourceRegistry (null);
+
+ } catch (Error e) {
+ critical ("Error starting inbox monitor: %s", e.message);
+ return;
+ }
+
+ var sources = registry.list_sources (E.SOURCE_EXTENSION_MAIL_ACCOUNT);
+ foreach (var source in sources) {
+ add_source (source);
+ }
+
+ registry.source_added.connect (add_source);
+ registry.source_removed.connect (remove_source);
+
+ registry.source_changed.connect ((source) => {
+ remove_source (source);
+ add_source (source);
+ });
+ }
+
+ private void add_source (E.Source source) {
+ if (!source.has_extension (E.SOURCE_EXTENSION_MAIL_ACCOUNT)) {
+ return;
+ }
+ unowned string uid = source.get_uid ();
+ unowned string display_name = source.get_display_name ();
+
+ if (uid == "vfolder") {
+ debug ("[%s] Is a vfolder. Ignoring it…", display_name);
+ return;
+ }
+
+ unowned var extension = (E.SourceMailAccount) source.get_extension (E.SOURCE_EXTENSION_MAIL_ACCOUNT);
+ if (extension.backend_name == "mbox") {
+ debug ("[%s] Is a local inbox. Ignoring it…", display_name);
+ return;
+ }
+
+ Camel.Store? store = null;
+ try {
+ store = (Camel.Store) session.add_service (uid, extension.backend_name, Camel.ProviderType.STORE);
+ } catch (Error e) {
+ warning ("[%s] Error adding service: %s", display_name, e.message);
+ }
+
+ if (store != null) {
+ try {
+ var folder = store.get_inbox_folder_sync (null);
+
+ if (folder != null) {
+ var inbox_folder = store.get_folder_sync (folder.full_name, Camel.StoreGetFolderFlags.NONE, null);
+
+ if (inbox_folder != null) {
+ inbox_folder.changed.connect ((change_info) => {
+ inbox_folder_changed (source, change_info);
+ });
+ inbox_folders.insert (source, inbox_folder);
+
+ uint refresh_interval_in_minutes = 15;
+ if (source.has_extension (E.SOURCE_EXTENSION_REFRESH)) {
+ unowned var refresh_extension = (E.SourceRefresh) source.get_extension (E.SOURCE_EXTENSION_REFRESH);
+
+ if (!refresh_extension.enabled) {
+ refresh_interval_in_minutes = 0;
+
+ } else if (refresh_extension.interval_minutes > 0) {
+ refresh_interval_in_minutes = refresh_extension.interval_minutes;
+ }
+ }
+
+ if (refresh_interval_in_minutes > 0) {
+ debug ("[%s] Checking inbox for new mail every %u minutes…", display_name, refresh_interval_in_minutes);
+ var refresh_timeout_id = GLib.Timeout.add_seconds (refresh_interval_in_minutes * 60, () => {
+ inbox_folder_synchronize_sync.begin (source);
+ return GLib.Source.CONTINUE;
+ });
+ synchronize_timeout_ids.insert (source, refresh_timeout_id);
+
+ inbox_folder_synchronize_sync.begin (source);
+
+ } else {
+ debug ("[%s] Automatically checking inbox for new mail is disabled.", display_name);
+ }
+ }
+
+ } else {
+ debug ("[%s] Inbox folder not found. Can't automatically check for new messages.", display_name);
+ }
+
+ } catch (Error e) {
+ warning ("[%s] Error getting inbox folder: %s", display_name, e.message);
+ }
+
+ } else {
+ debug ("[%s] No store available.", display_name);
+ }
+ }
+
+ private void remove_source (E.Source source) {
+ if (!source.has_extension (E.SOURCE_EXTENSION_MAIL_ACCOUNT)) {
+ return;
+ }
+ debug ("[%s] Removing…", source.display_name);
+
+ bool timeout_id_exists;
+ var timeout_id = synchronize_timeout_ids.take (source, out timeout_id_exists);
+ if (timeout_id_exists) {
+ GLib.Source.remove (timeout_id);
+ }
+
+ bool exists;
+ var inbox_folder = inbox_folders.take (source, out exists);
+ if (exists) {
+ session.remove_service (inbox_folder.parent_store);
+ }
+ }
+
+ private async void inbox_folder_synchronize_sync (E.Source source) {
+ if (!network_monitor.network_available) {
+ debug ("[%s] Network is not avaible. Skipping…", source.display_name);
+ return;
+ }
+
+ var inbox_folder = inbox_folders.get (source);
+ if (inbox_folder != null) {
+ debug ("[%s] Refreshing…", source.display_name);
+
+ try {
+ inbox_folder.refresh_info_sync (null);
+
+ } catch (Error e) {
+ warning ("[%s] Error refreshing: %s", source.display_name, e.message);
+ }
+ }
+ }
+
+ private void inbox_folder_changed (E.Source source, Camel.FolderChangeInfo changes) {
+ var inbox_folder = inbox_folders.get (source);
+ if (inbox_folder == null) {
+ return;
+ }
+
+ unowned var added_uids = changes.get_added_uids ();
+ if (added_uids != null) {
+ var sender_names = new GenericSet (str_hash, str_equal);
+ var unseen_message_infos = new SList ();
+
+ added_uids.foreach ((added_uid) => {
+ var message_info = inbox_folder.get_message_info (added_uid);
+
+ if (!(Camel.MessageFlags.SEEN in message_info.flags)) {
+ unowned string? sender_address;
+ unowned string? sender_name;
+
+ var camel_address = new Camel.InternetAddress ();
+ camel_address.unformat (message_info.from);
+ camel_address.get (0, out sender_name, out sender_address);
+
+ if (sender_name == null) {
+ sender_name = sender_address;
+ }
+
+ sender_names.add (sender_name);
+ unseen_message_infos.append (message_info);
+ }
+ });
+
+ var unseen_message_infos_length = unseen_message_infos.length ();
+ if (unseen_message_infos_length == 1) {
+ var unseen_message_info = unseen_message_infos.nth_data (0);
+
+ var notification = new GLib.Notification (_("%s to %s").printf (sender_names.iterator ().next_value (), inbox_folder.parent_store.display_name));
+ notification.set_body (unseen_message_info.subject);
+ GLib.Application.get_default ().send_notification (unseen_message_info.uid, notification);
+
+ } else if (unseen_message_infos_length > 1) {
+ GLib.Notification notification;
+
+ ///TRANSLATORS: The %s represents the number of new messages translated in your language, e.g. "2 new messages"
+ string messages_count = ngettext ("%u new message", "%u new messages", unseen_message_infos_length).printf (unseen_message_infos_length);
+
+ if (sender_names.length == 1) {
+ var sender_name = sender_names.iterator ().next_value ();
+
+ notification = new GLib.Notification (_("%s to %s").printf (sender_name, inbox_folder.parent_store.display_name));
+ notification.set_body (messages_count);
+
+ } else {
+ notification = new GLib.Notification (inbox_folder.parent_store.display_name);
+
+ ///TRANSLATORS: The first %s represents the number of new messages translated in your language, e.g. "2 new messages"
+ ///The next %s represents the number of senders
+ notification.set_body (ngettext ("%s from %u sender", "%s from %u senders", sender_names.length).printf (messages_count, sender_names.length));
+ }
+
+ GLib.Application.get_default ().send_notification (unseen_message_infos.nth_data (0).uid, notification);
+ }
+ }
+ }
+}
diff --git a/src/meson.build b/src/meson.build
index e949c27a6..99d14e10a 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -2,6 +2,7 @@ vala_files = files(
'Application.vala',
'MainWindow.vala',
'HeaderBar.vala',
+ 'InboxMonitor.vala',
'Utils.vala',
'WebView.vala',
'WelcomeView.vala',