#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # libnotifyplugin.py # # Copyright 2013, 2014 Patrick Ulbrich # # 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 2 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. # import os import dbus import threading from gi.repository import Notify, Gio, Gtk from common.plugins import Plugin, HookTypes from common.i18n import _ from common.subproc import start_subprocess from common.exceptions import InvalidOperationException from daemon.mails import sort_mails NOTIFICATION_MODE_COUNT = '0' NOTIFICATION_MODE_SUMMARY = '1' NOTIFICATION_MODE_SINGLE = '2' plugin_defaults = { 'notification_mode' : NOTIFICATION_MODE_SUMMARY } class LibNotifyPlugin(Plugin): def __init__(self): # prevent flooding of the messaging tray self.MAIL_LIST_LIMIT = 10 # dict that tracks all notifications that need to be closed self._notifications = {} self._initialized = False self._lock = threading.Lock() self._notification_server_wait_event = threading.Event() self._notification_server_ready = False self._is_gnome = False self._mails_added_hook = None self._mails_removed_hook = None def enable(self): self._notification_server_wait_event.clear() self._notification_server_ready = False self._notifications = {} # initialize Notification if not self._initialized: Notify.init("Mailnag") self._is_gnome = (os.environ['GDMSESSION'] == 'gnome') self._initialized = True def mails_added_hook(new_mails, all_mails): self._notify_async(new_mails, all_mails) def mails_removed_hook(remaining_mails): if remaining_mails == 0: # no mails (e.g. email client has been launched) -> close notifications self._close_notifications() self._mails_added_hook = mails_added_hook self._mails_removed_hook = mails_removed_hook controller = self.get_mailnag_controller() hooks = controller.get_hooks() hooks.register_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) hooks.register_hook_func(HookTypes.MAILS_REMOVED, self._mails_removed_hook) def disable(self): controller = self.get_mailnag_controller() hooks = controller.get_hooks() if self._mails_added_hook != None: hooks.unregister_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) self._mails_added_hook = None if self._mails_removed_hook != None: hooks.unregister_hook_func(HookTypes.MAILS_REMOVED, self._mails_removed_hook) self._mails_removed_hook = None # Abort possible notification server wait self._notification_server_wait_event.set() # Close all open notifications # (must be called after _notification_server_wait_event.set() # to prevent a possible deadlock) self._close_notifications() def get_manifest(self): return (_("LibNotify notifications"), _("Shows a popup when new mails arrive."), "1.0", "Patrick Ulbrich ", False) def get_default_config(self): return plugin_defaults def has_config_ui(self): return True def get_config_ui(self): box = Gtk.Box() box.set_spacing(12) box.set_orientation(Gtk.Orientation.VERTICAL) label = Gtk.Label() label.set_markup('%s' % _('Please select a notification mode:')) label.set_alignment(0.0, 0.0) box.pack_start(label, True, True, 0) inner_box = Gtk.Box() inner_box.set_spacing(6) inner_box.set_orientation(Gtk.Orientation.VERTICAL) cb_count = Gtk.RadioButton(label = _('Count of new mails')) inner_box.pack_start(cb_count, True, True, 0) cb_summary = Gtk.RadioButton(label = _('Summary of new mails'), group = cb_count) inner_box.pack_start(cb_summary, True, True, 0) cb_single = Gtk.RadioButton(label = _('One notification per new mail'), group = cb_count) inner_box.pack_start(cb_single, True, True, 0) alignment = Gtk.Alignment() alignment.set_padding(0, 0, 18, 0) alignment.add(inner_box) box.pack_start(alignment, True, True, 0) return box def load_ui_from_config(self, config_ui): config = self.get_config() inner_box = config_ui.get_children()[1].get_child() cb = inner_box.get_children()[int(config['notification_mode'])] cb.set_active(True) def save_ui_to_config(self, config_ui): config = self.get_config() inner_box = config_ui.get_children()[1].get_child() idx = 0 for cb in inner_box.get_children(): if cb.get_active(): config['notification_mode'] = str(idx) break idx += 1 def _notify_async(self, new_mails, all_mails): def thread(): with self._lock: # The desktop session may have started Mailnag # before the libnotify dbus daemon. if not self._notification_server_ready: if not self._wait_for_notification_server(): return self._notification_server_ready = True config = self.get_config() if config['notification_mode'] == NOTIFICATION_MODE_COUNT: self._notify_count(len(all_mails)) elif config['notification_mode'] == NOTIFICATION_MODE_SUMMARY: self._notify_summary(new_mails, all_mails) else: self._notify_single(new_mails) t = threading.Thread(target = thread) t.start() def _notify_summary(self, new_mails, all_mails): summary = "" body = "" # The mail list (all_mails) is sorted by date (mails with most recent # date on top). New mails with no date or older mails that come in # delayed won't be listed on top. So if a mail with no or an older date # arrives, it gives the impression that the top most mail (i.e. the mail # with the most recent date) is re-notified. # To fix that, simply put new mails on top explicitly. mails = new_mails + [m for m in all_mails if m not in new_mails] if len(self._notifications) == 0: self._notifications['0'] = self._get_notification(" ", None, None) # empty string will emit a gtk warning ubound = len(mails) if len(mails) <= self.MAIL_LIST_LIMIT else self.MAIL_LIST_LIMIT for i in range(ubound): if self._is_gnome: body += "%s:\n%s\n\n" % (self._get_sender(mails[i]), mails[i].subject) else: body += "%s - %s\n" % (ellipsize(self._get_sender(mails[i]), 20), ellipsize(mails[i].subject, 20)) if len(mails) > self.MAIL_LIST_LIMIT: if self._is_gnome: body += "%s" % _("(and {0} more)").format(str(len(mails) - self.MAIL_LIST_LIMIT)) else: body += _("(and {0} more)").format(str(len(mails) - self.MAIL_LIST_LIMIT)) if len(mails) > 1: # multiple new emails summary = _("You have {0} new mails.").format(str(len(mails))) else: summary = _("You have a new mail.") self._notifications['0'].update(summary, body, "mail-unread") self._notifications['0'].show() def _notify_single(self, mails): # In single notification mode new mails are # added to the *bottom* of the notification list. mails = sort_mails(mails, sort_desc = False) for mail in mails: n = self._get_notification(self._get_sender(mail), mail.subject, "mail-unread") notification_id = str(id(n)) if self._is_gnome: n.add_action("mark-as-read", _("Mark as read"), self._notification_action_handler, (mail, notification_id)) n.show() self._notifications[notification_id] = n def _notify_count(self, count): if len(self._notifications) == 0: self._notifications['0'] = self._get_notification(" ", None, None) # empty string will emit a gtk warning if count > 1: # multiple new emails summary = _("You have {0} new mails.").format(str(count)) else: summary = _("You have a new mail.") self._notifications['0'].update(summary, None, "mail-unread") self._notifications['0'].show() def _close_notifications(self): with self._lock: for n in self._notifications.itervalues(): n.close() self._notifications = {} def _get_notification(self, summary, body, icon): n = Notify.Notification.new(summary, body, icon) n.set_category("email") if self._is_gnome: n.add_action("default", "default", self._notification_action_handler, None) return n def _wait_for_notification_server(self): bus = dbus.SessionBus() while not bus.name_has_owner('org.freedesktop.Notifications'): self._notification_server_wait_event.wait(5) if self._notification_server_wait_event.is_set(): return False return True def _notification_action_handler(self, n, action, user_data): with self._lock: if action == "default": mailclient = get_default_mail_reader() if mailclient != None: start_subprocess(mailclient) # clicking the notification bubble has closed all notifications # so clear the reference array as well. self._notifications = {} elif action == "mark-as-read": controller = self.get_mailnag_controller() try: controller.mark_mail_as_read(user_data[0].id) except InvalidOperationException: pass # clicking the action has closed the notification # so remove its reference. del self._notifications[user_data[1]] def _get_sender(self, mail): name, addr = mail.sender if len(name) > 0: return name else: return addr def get_default_mail_reader(): mail_reader = None app_info = Gio.AppInfo.get_default_for_type ("x-scheme-handler/mailto", False) if app_info != None: executable = Gio.AppInfo.get_executable(app_info) if (executable != None) and (len(executable) > 0): mail_reader = executable return mail_reader def ellipsize(str, max_len): if max_len < 3: max_len = 3 if len(str) <= max_len: return str else: return str[0:max_len - 3] + '...'