Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notifications #679

Merged
merged 31 commits into from
Sep 9, 2021
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
339f0d2
Basic Daemon Infrastructure
marbetschar Sep 3, 2021
0191f53
Why is folder_changed not fired??
marbetschar Sep 4, 2021
967335d
Sync works, but folder_changed is still not fired
marbetschar Sep 4, 2021
b5ecd18
Merge branch 'master' into notifications
marbetschar Sep 4, 2021
8d61531
Fixed FATAL issue - but now getting SegmentationFault
marbetschar Sep 5, 2021
3544746
Avoid SegmentationFault
marbetschar Sep 6, 2021
1aca398
Check for new mail at configured intervals
marbetschar Sep 6, 2021
333aba0
Merge branch 'master' into notifications
marbetschar Sep 6, 2021
94759aa
Fix Linting Error
marbetschar Sep 6, 2021
f0a5b98
Merge branch 'notifications' of github.com:elementary/mail into notif…
marbetschar Sep 6, 2021
a86c93b
Parsing from address
marbetschar Sep 6, 2021
513c6eb
Still no luck with notification icon
marbetschar Sep 6, 2021
a1b0d4a
Show the mail icon
marbetschar Sep 6, 2021
9bae6da
Fix section header in Notification Center
marbetschar Sep 6, 2021
fa658fc
Use io.elementary.mail as EXEC valueto open it on click
marbetschar Sep 6, 2021
27c95ac
Session: do not use our own Camel.Sasl subclass
marbetschar Sep 6, 2021
7763ef8
Merge branch 'master' into notifications
marbetschar Sep 6, 2021
c0e7e9c
Use two *.desktop files for the daemon
marbetschar Sep 6, 2021
589f032
Merge branch 'notifications' of github.com:elementary/mail into notif…
marbetschar Sep 6, 2021
2ff9af9
Added newlines at end of file
marbetschar Sep 6, 2021
78759f2
One binary for daemon and main app
marbetschar Sep 7, 2021
b4af8b8
Make Linter Happy
marbetschar Sep 7, 2021
67fb3f5
Use dedicated Session instance for daemon
marbetschar Sep 7, 2021
25fe8bf
Make naming a bit more obvious
marbetschar Sep 8, 2021
5583f4e
Made notifications a bit more intelligent
marbetschar Sep 9, 2021
21b2556
Dropped no longer needed stuff from meson
marbetschar Sep 9, 2021
8abbd7e
Get rid of unnecessary variable
marbetschar Sep 9, 2021
aa73d2d
Use ngettext for languages with multiple plural forms
marbetschar Sep 9, 2021
47b7af7
Playing with notification formats
marbetschar Sep 9, 2021
f64201e
Some more changes to notification messages
marbetschar Sep 9, 2021
9d99783
Update io.elementary.mail.appdata.xml.in
danirabbit Sep 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions data/io.elementary.mail-daemon.desktop.in
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions data/io.elementary.mail.appdata.xml.in
Original file line number Diff line number Diff line change
@@ -22,6 +22,15 @@
<binary>io.elementary.mail</binary>
</provides>
<releases>
<release version="6.2.0" date="2021-09-09" urgency="medium">
<description>
<p>Improvements:</p>
<ul>
<li>Send a notification when new messages arrive</li>
<li>Updated translations</li>
</ul>
</description>
</release>
<release version="6.1.1" date="2021-08-30" urgency="medium">
<description>
<p>Fixes:</p>
8 changes: 8 additions & 0 deletions data/meson.build
Original file line number Diff line number Diff line change
@@ -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')
17 changes: 17 additions & 0 deletions src/Application.vala
Original file line number Diff line number Diff line change
@@ -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 ();
}
}

2 changes: 1 addition & 1 deletion src/Backend/Session.vala
Original file line number Diff line number Diff line change
@@ -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"));
}

239 changes: 239 additions & 0 deletions src/InboxMonitor.vala
Original file line number Diff line number Diff line change
@@ -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<E.Source, Camel.Folder> inbox_folders;
private HashTable<E.Source, uint> synchronize_timeout_ids;
private E.SourceRegistry registry;

construct {
inbox_folders = new HashTable<E.Source, Camel.Folder> (E.Source.hash, E.Source.equal);
synchronize_timeout_ids = new HashTable<E.Source, uint> (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<string> (str_hash, str_equal);
var unseen_message_infos = new SList<Camel.MessageInfo> ();

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);
}
}
}
}
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ vala_files = files(
'Application.vala',
'MainWindow.vala',
'HeaderBar.vala',
'InboxMonitor.vala',
'Utils.vala',
'WebView.vala',
'WelcomeView.vala',