diff --git a/Mailnag/common/utils.py b/Mailnag/common/utils.py index 1717679..9e5e71e 100644 --- a/Mailnag/common/utils.py +++ b/Mailnag/common/utils.py @@ -3,7 +3,7 @@ # # utils.py # -# Copyright 2011, 2012 Patrick Ulbrich +# Copyright 2011 - 2013 Patrick Ulbrich # Copyright 2007 Marco Ferragina # # This program is free software; you can redistribute it and/or modify @@ -23,10 +23,7 @@ # import xdg.BaseDirectory as base -from gi.repository import Gst, Gio -import threading import os -import time import urllib2 from common.dist_cfg import PACKAGE_NAME @@ -56,19 +53,6 @@ def set_procname(newname): libc.prctl(15, byref(buff), 0, 0, 0) -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 - - # check for internet connection def is_online(): try: @@ -76,41 +60,3 @@ def is_online(): return True except: return False - - -class _GstPlayThread(threading.Thread): - def __init__(self, ply): - self.ply = ply - threading.Thread.__init__(self) - - - def run(self): - def on_eos(bus, msg): -# print "EOS" # debug - self.ply.set_state(Gst.State.NULL) - return True - - bus = self.ply.get_bus() - bus.add_signal_watch() - bus.connect('message::eos', on_eos) - - self.ply.set_state(Gst.State.PLAYING) - - -_gst_initialized = False - -def gstplay(filename): - global _gst_initialized - if not _gst_initialized: - Gst.init(None) - _gst_initialized = True - - try: - cwd = os.getcwd() - location = os.path.join(cwd, filename) - ply = Gst.ElementFactory.make("playbin", "player") - ply.set_property("uri", "file://" + location) - pt = _GstPlayThread(ply) - pt.start() - except: - pass diff --git a/Mailnag/daemon/dbusservice.py b/Mailnag/daemon/dbusservice.py new file mode 100644 index 0000000..80fdb3c --- /dev/null +++ b/Mailnag/daemon/dbusservice.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# dbusservice.py +# +# Copyright 2013 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 dbus +import dbus.service + +# DBUS server that exports mailnag signals end methods +class DBUSService(dbus.service.Object): + def __init__(self): + self._mails = [] + bus_name = dbus.service.BusName('mailnag.MailnagService', bus = dbus.SessionBus()) + dbus.service.Object.__init__(self, bus_name, '/mailnag/MailnagService') + + + def set_mails(self, mails): + self._mails = mails + + + @dbus.service.signal(dbus_interface = 'mailnag.MailnagService', signature = 'u') + def MailsAdded(self, new_count): + pass + + + @dbus.service.signal(dbus_interface = 'mailnag.MailnagService', signature = 'u') + def MailsRemoved(self, new_count): + pass + + + @dbus.service.method(dbus_interface = 'mailnag.MailnagService', out_signature = 'aa{sv}') + def GetMails(self): + mails = [] + for m in self._mails: + d = {} + d['datetime'] = m.datetime # int32 (i) + d['subject'] = m.subject # string (s) + d['sender'] = m.sender # string (s) + d['id'] = m.id # string (s) + + mails.append(d) + + return mails + + + @dbus.service.method(dbus_interface = 'mailnag.MailnagService', out_signature = 'u') + def GetMailCount(self): + return len(self._mails) diff --git a/Mailnag/daemon/mail.py b/Mailnag/daemon/mail.py index 218af41..2fe59de 100644 --- a/Mailnag/daemon/mail.py +++ b/Mailnag/daemon/mail.py @@ -3,7 +3,7 @@ # # mail.py # -# Copyright 2011, 2012 Patrick Ulbrich +# Copyright 2011 - 2013 Patrick Ulbrich # Copyright 2011 Ralf Hersel # # This program is free software; you can redistribute it and/or modify @@ -23,10 +23,9 @@ # class Mail: - def __init__(self, seconds, subject, sender, datetime, id, account_id): - self.seconds = seconds + def __init__(self, datetime, subject, sender, id, account_id): + self.datetime = datetime self.subject = subject self.sender = sender - self.datetime = datetime self.id = id self.account_id = account_id diff --git a/Mailnag/daemon/mailchecker.py b/Mailnag/daemon/mailchecker.py index fa06717..6e2fe15 100644 --- a/Mailnag/daemon/mailchecker.py +++ b/Mailnag/daemon/mailchecker.py @@ -3,7 +3,7 @@ # # mailchecker.py # -# Copyright 2011, 2012 Patrick Ulbrich +# Copyright 2011 - 2013 Patrick Ulbrich # Copyright 2011 Ralf Hersel # # This program is free software; you can redistribute it and/or modify @@ -22,39 +22,34 @@ # MA 02110-1301, USA. # -from gi.repository import Notify, Gio import threading import sys import subprocess import os import time -from common.utils import get_data_file, gstplay, is_online, get_default_mail_reader +from common.utils import get_data_file, is_online from common.i18n import _ from daemon.reminder import Reminder from daemon.mailsyncer import MailSyncer from daemon.pid import Pid class MailChecker: - def __init__(self, cfg): - self.MAIL_LIST_LIMIT = 10 # prevent flooding of the messaging tray + def __init__(self, cfg, dbusservice): self._firstcheck = True; # first check after startup self._mailcheck_lock = threading.Lock() - self._mail_list = [] self._mailsyncer = MailSyncer(cfg) self._reminder = Reminder() + self._dbusservice = dbusservice self._pid = Pid() self._cfg = cfg - self._gsettings = Gio.Settings.new('org.gnome.shell') - # dict that tracks all notifications that need to be closed - self._notifications = {} self._reminder.load() - # initialize Notification - Notify.init("Mailnag") def check(self, accounts): + # make sure multiple threads (idler and polling thread) + # don't check for mails simultaneously. with self._mailcheck_lock: print 'Checking %s email account(s) at: %s' % (len(accounts), time.asctime()) # kill all zombies @@ -64,47 +59,37 @@ class MailChecker: print 'Error: No internet connection' return - self._mail_list = self._mailsyncer.sync(accounts) - + all_mails = self._mailsyncer.sync(accounts) unseen_mails = [] - new_mails = [] + new_mail_count = 0 script_data = "" script_data_mailcount = 0 - for mail in self._mail_list: + for mail in all_mails: if self._reminder.contains(mail.id): # mail was fetched before if self._reminder.unseen(mail.id): # mail was not marked as seen unseen_mails.append(mail) if self._firstcheck: - new_mails.append(mail) + new_mail_count += 1 else: # mail is fetched the first time unseen_mails.append(mail) - new_mails.append(mail) + new_mail_count += 1 script_data += ' "<%s> %s"' % (mail.sender, mail.subject) script_data_mailcount += 1 script_data = str(script_data_mailcount) + script_data - - if len(self._mail_list) == 0: - # no mails (e.g. email client has been launched) -> close notifications - for n in self._notifications.itervalues(): - n.close() - self._notifications = {} - elif len(new_mails) > 0: - if self._cfg.get('general', 'notification_mode') == '1': - self._notify_summary(unseen_mails) - else: - self._notify_single(new_mails) - - # play sound if it is enabled in mailnags settings and - # gnome-shell notifications aren't disabled - if (self._cfg.get('general', 'playsound') == '1') and \ - (self._gsettings.get_int('saved-session-presence') != 2): - gstplay(get_data_file(self._cfg.get('general', 'soundfile'))) - - self._reminder.save(self._mail_list) + + self._dbusservice.set_mails(unseen_mails) + # TODO : signal MailsRemoved if not all mails have been removed + # (i. e. if mailcount has been decreased) + if len(all_mails) == 0: + self._dbusservice.MailsRemoved(0) + elif new_mail_count > 0: + self._dbusservice.MailsAdded(new_mail_count) + + self._reminder.save(all_mails) # process user scripts self._run_user_scripts("on_mail_check", script_data) # write stdout to log file @@ -113,72 +98,6 @@ class MailChecker: return - - def dispose(self): - for n in self._notifications.itervalues(): - n.close() - - - def _notify_summary(self, unseen_mails): - summary = "" - body = "" - - if len(self._notifications) == 0: - self._notifications['0'] = self._get_notification(" ", None, None) # empty string will emit a gtk warning - - ubound = len(unseen_mails) if len(unseen_mails) <= self.MAIL_LIST_LIMIT else self.MAIL_LIST_LIMIT - - for i in range(ubound): - body += unseen_mails[i].sender + ":\n" + unseen_mails[i].subject + "\n\n" - - if len(unseen_mails) > self.MAIL_LIST_LIMIT: - body += "" + _("(and {0} more)").format(str(len(unseen_mails) - self.MAIL_LIST_LIMIT)) + "" - - if len(unseen_mails) > 1: # multiple new emails - summary = _("You have {0} new mails.").format(str(len(unseen_mails))) - else: - summary = _("You have a new mail.") - - self._notifications['0'].update(summary, body, "mail-unread") - self._notifications['0'].show() - - - def _notify_single(self, new_mails): - for mail in new_mails: - n = self._get_notification(mail.sender, mail.subject, "mail-unread") - notification_id = str(id(n)) - n.add_action("mark-as-read", _("Mark as read"), self._notification_action_handler, (mail, notification_id), None) - n.show() - self._notifications[notification_id] = n - - - def _get_notification(self, summary, body, icon): - n = Notify.Notification.new(summary, body, icon) - n.set_category("email") - n.add_action("default", "default", self._notification_action_handler, None, None) - - return n - - - def _notification_action_handler(self, n, action, user_data): - with self._mailcheck_lock: - if action == "default": - mailclient = get_default_mail_reader() - if mailclient != None: - self._pid.append(subprocess.Popen(mailclient)) - - # clicking the notification bubble has closed all notifications - # so clear the reference array as well. - self._notifications = {} - - elif action == "mark-as-read": - self._reminder.set_to_seen(user_data[0].id) - self._reminder.save(self._mail_list) - - # clicking the action has closed the notification - # so remove its reference - del self._notifications[user_data[1]] - def _run_user_scripts(self, event, data): if event == "on_mail_check": diff --git a/Mailnag/daemon/mails.py b/Mailnag/daemon/mails.py index 64b3e1c..9828f2b 100644 --- a/Mailnag/daemon/mails.py +++ b/Mailnag/daemon/mails.py @@ -3,7 +3,7 @@ # # mails.py # -# Copyright 2011, 2012 Patrick Ulbrich +# Copyright 2011 - 2013 Patrick Ulbrich # Copyright 2011 Leighton Earl # Copyright 2011 Ralf Hersel # @@ -97,16 +97,14 @@ class Mails: try: try: # get date and format it - datetime, seconds = self._format_header('date', msg['Date']) + datetime = self._format_header('date', msg['Date']) except KeyError: print "KeyError exception for key 'Date' in message." # debug - datetime, seconds = self._format_header('date', msg['date']) + datetime = self._format_header('date', msg['date']) except: print "Could not get date from IMAP message." # debug - # take current time as "2010.12.31 13:57:04" - datetime = time.strftime('%Y.%m.%d %X') # current time to seconds - seconds = time.time() + datetime = time.time() try: try: # get subject and format it @@ -130,8 +128,8 @@ class Mails: # prevent duplicates caused by Gmail labels if id not in mail_ids: if not (filter_enabled and self._in_filter(sender + subject)): # check filter - mail_list.append(Mail(seconds, subject, \ - sender, datetime, id, acc.get_id())) + mail_list.append(Mail(datetime, subject, \ + sender, id, acc.get_id())) mail_ids.append(id) # don't close IMAP idle connections @@ -171,16 +169,14 @@ class Mails: try: try: # get date and format it - datetime, seconds = self._format_header('date', msg['Date']) + datetime = self._format_header('date', msg['Date']) except KeyError: print "KeyError exception for key 'Date' in message." # debug - datetime, seconds = self._format_header('date', msg['date']) + datetime = self._format_header('date', msg['date']) except: print "Could not get date from POP message." # debug - # take current time as "2010.12.31 13:57:04" - datetime = time.strftime('%Y.%m.%d %X') # current time to seconds - seconds = time.time() + datetime = time.time() try: try: # get subject and format it @@ -206,8 +202,8 @@ class Mails: id = acc.user + uidl.split(' ')[2] if not (filter_enabled and self._in_filter(sender + subject)): # check filter - mail_list.append(Mail(seconds, subject, sender, \ - datetime, id, acc.get_id())) + mail_list.append(Mail(datetime, subject, sender, \ + id, acc.get_id())) # disconnect from Email-Server srv.quit() @@ -246,7 +242,7 @@ class Mails: def sort_mails(mail_list, sort_order): sort_list = [] for mail in mail_list: - sort_list.append([mail.seconds, mail]) + sort_list.append([mail.datetime, mail]) # sort asc sort_list.sort() if sort_order == 'desc': @@ -286,18 +282,12 @@ class Mails: # make a 10-tupel (UTC) parsed_date = email.utils.parsedate_tz(content) # convert 10-tupel to seconds incl. timezone shift - seconds = email.utils.mktime_tz(parsed_date) - # convert seconds to tupel - tupel = time.localtime(seconds) - # convert tupel to string - datetime = time.strftime('%Y.%m.%d %X', tupel) + datetime = email.utils.mktime_tz(parsed_date) except: print 'Error: cannot format date.' - # take current time as "2010.12.31 13:57:04" - datetime = time.strftime('%Y.%m.%d %X') # current time to seconds - seconds = time.time() - return datetime, seconds + datetime = time.time() + return datetime if field == 'subject': try: diff --git a/Mailnag/mailnag.py b/Mailnag/mailnag.py index f617009..4b4decb 100644 --- a/Mailnag/mailnag.py +++ b/Mailnag/mailnag.py @@ -3,7 +3,7 @@ # # mailnag.py # -# Copyright 2011, 2012 Patrick Ulbrich +# Copyright 2011 - 2013 Patrick Ulbrich # Copyright 2011 Leighton Earl # Copyright 2011 Ralf Hersel # @@ -25,6 +25,8 @@ import os from gi.repository import GObject, GLib +from dbus.mainloop.glib import DBusGMainLoop +import threading import time import signal import traceback @@ -33,12 +35,14 @@ from common.config import read_cfg, cfg_exists, cfg_folder from common.utils import set_procname, is_online from common.accountlist import AccountList from daemon.mailchecker import MailChecker +from daemon.dbusservice import DBUSService from daemon.idlers import Idlers mainloop = None -mailchecker = None idlers = None - +start_thread = None +poll_thread = None +poll_thread_stop = threading.Event() def read_config(): if not cfg_exists(): @@ -60,30 +64,6 @@ def delete_pid(): os.remove(pid_file) -# Workaround: -# sometimes gnomeshell's notification server (org.freedesktop.Notifications implementation) -# doesn't seem to be up immediately upon session start, so prevent Mailnag from crashing -# by checking if the org.freedesktop.Notifications DBUS interface is available yet. -# See https://github.com/pulb/mailnag/issues/48 -def wait_for_notification_server(): - import dbus - bus = dbus.SessionBus() - - while True: - try: - notify = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications') - iface = dbus.Interface(notify, 'org.freedesktop.Notifications') - inf = iface.GetServerInformation() - - if inf[0] == u'gnome-shell': - break - except: - pass - - print 'Waiting for GNOME-Shell notification server...' - time.sleep(5) - - def wait_for_inet_connection(): if not is_online(): print 'Waiting for internet connection...' @@ -93,12 +73,16 @@ def wait_for_inet_connection(): def cleanup(): # clean up resources - if mailchecker != None: - mailchecker.dispose() - if idlers != None: idlers.dispose() + if (start_thread != None) and (start_thread.is_alive()): + start_thread.join() + + if (poll_thread != None) and (poll_thread.is_alive()): + poll_thread_stop.set() + poll_thread.join() + delete_pid() @@ -108,12 +92,12 @@ def sig_handler(signum, frame): def main(): - global mainloop, mailchecker, idlers + global mainloop, start_thread set_procname("mailnag") GObject.threads_init() - + DBusGMainLoop(set_as_default = True) signal.signal(signal.SIGTERM, sig_handler) try: @@ -125,13 +109,32 @@ def main(): print 'Error: Cannot find configuration file. Please run mailnag_config first.' exit(1) - wait_for_notification_server() wait_for_inet_connection() + + dbusservice = DBUSService() + + # start checking for mails asynchronously + start_thread = threading.Thread(target = start, args = (cfg, dbusservice,)) + start_thread.start() + # start mainloop for DBus communication + mainloop = GObject.MainLoop() + mainloop.run() + + except KeyboardInterrupt: + pass # ctrl+c pressed + finally: + cleanup() + + +def start(cfg, dbusservice): + global poll_thread, idlers + + try: accounts = AccountList() accounts.load_from_cfg(cfg, enabled_only = True) - mailchecker = MailChecker(cfg) + mailchecker = MailChecker(cfg, dbusservice) # immediate check, check *all* accounts try: @@ -141,20 +144,27 @@ def main(): idle_accounts = filter(lambda acc: acc.imap and acc.idle, accounts) non_idle_accounts = filter(lambda acc: (not acc.imap) or (acc.imap and not acc.idle), accounts) - + # start polling thread for POP3 accounts and # IMAP accounts without idle support if len(non_idle_accounts) > 0: + check_interval = int(cfg.get('general', 'check_interval')) + def poll_func(): try: - mailchecker.check(non_idle_accounts) + while (True): + poll_thread_stop.wait(timeout = 60.0 * check_interval) + if poll_thread_stop.is_set(): + break + + mailchecker.check(non_idle_accounts) except: traceback.print_exc() - return True - - check_interval = int(cfg.get('general', 'check_interval')) - GObject.timeout_add_seconds(60 * check_interval, poll_func) + poll_thread = threading.Thread(target = poll_func) + poll_thread.start() + + # start idler threads for IMAP accounts with idle support if len(idle_accounts) > 0: def sync_func(account): @@ -162,16 +172,13 @@ def main(): mailchecker.check([account]) except: traceback.print_exc() - + + idlers = Idlers(idle_accounts, sync_func) idlers.run() + except: + traceback.print_exc() + mainloop.quit() - mainloop = GObject.MainLoop() - mainloop.run() - except KeyboardInterrupt: - pass # ctrl+c pressed - finally: - cleanup() - if __name__ == '__main__': main()