added dbus service, removed notifications (sound and popups) from daemon

This commit is contained in:
Patrick Ulbrich
2013-06-10 20:29:19 +02:00
parent 72ba79abe2
commit 7631dabb15
6 changed files with 161 additions and 234 deletions

View File

@@ -3,7 +3,7 @@
#
# utils.py
#
# Copyright 2011, 2012 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2011 - 2013 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2007 Marco Ferragina <marco.ferragina@gmail.com>
#
# 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

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# dbusservice.py
#
# Copyright 2013 Patrick Ulbrich <zulu99@gmx.net>
#
# 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)

View File

@@ -3,7 +3,7 @@
#
# mail.py
#
# Copyright 2011, 2012 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2011 - 2013 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2011 Ralf Hersel <ralf.hersel@gmx.net>
#
# 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

View File

@@ -3,7 +3,7 @@
#
# mailchecker.py
#
# Copyright 2011, 2012 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2011 - 2013 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2011 Ralf Hersel <ralf.hersel@gmx.net>
#
# 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<i>" + unseen_mails[i].subject + "</i>\n\n"
if len(unseen_mails) > self.MAIL_LIST_LIMIT:
body += "<i>" + _("(and {0} more)").format(str(len(unseen_mails) - self.MAIL_LIST_LIMIT)) + "</i>"
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":

View File

@@ -3,7 +3,7 @@
#
# mails.py
#
# Copyright 2011, 2012 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2011 - 2013 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2011 Leighton Earl <leighton.earl@gmx.com>
# Copyright 2011 Ralf Hersel <ralf.hersel@gmx.net>
#
@@ -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:

View File

@@ -3,7 +3,7 @@
#
# mailnag.py
#
# Copyright 2011, 2012 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2011 - 2013 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2011 Leighton Earl <leighton.earl@gmx.com>
# Copyright 2011 Ralf Hersel <ralf.hersel@gmx.net>
#
@@ -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()