From a124d14be80d3afec8a48e0201e87b7d2fed6abd Mon Sep 17 00:00:00 2001 From: Patrick Ulbrich Date: Wed, 4 Nov 2020 20:41:35 +0100 Subject: [PATCH] Some fixes and refactoring of last commit --- AUTHORS | 1 + Mailnag/backends/base.py | 15 ++++++++++++- Mailnag/backends/imap.py | 32 +++++++++++++++++++++++---- Mailnag/backends/local.py | 12 +++++++++- Mailnag/backends/pop3.py | 10 ++++++++- Mailnag/common/accounts.py | 10 ++++++++- Mailnag/common/config.py | 1 - Mailnag/configuration/configwindow.py | 1 + Mailnag/daemon/dbus.py | 4 ++-- Mailnag/daemon/mailchecker.py | 20 ++++++++++++++--- Mailnag/daemon/mailnagdaemon.py | 21 +++++------------- Mailnag/daemon/mails.py | 16 +++++++------- Mailnag/plugins/userscriptplugin.py | 2 +- 13 files changed, 106 insertions(+), 39 deletions(-) diff --git a/AUTHORS b/AUTHORS index 1a222a0..a7afd65 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,6 +14,7 @@ Popper was written by Ralf Hersel . Code, docs and packaging contributors: ====================================== Amin Bandali +Andreas Angerer Balló György Dan Christensen Edwin Smulders diff --git a/Mailnag/backends/base.py b/Mailnag/backends/base.py index 7373403..90fc1ae 100644 --- a/Mailnag/backends/base.py +++ b/Mailnag/backends/base.py @@ -1,3 +1,4 @@ +# Copyright 2020 Patrick Ulbrich # Copyright 2016 Timo Kankare # # This program is free software; you can redistribute it and/or modify @@ -50,7 +51,7 @@ class MailboxBackend(object, metaclass=ABCMeta): @abstractmethod def list_messages(self): """Lists unseen messages from the mailbox for this account. - Yields tuples (folder, message) for every message. + Yields tuples (folder, message, flags) for every message. """ raise NotImplementedError @@ -61,6 +62,18 @@ class MailboxBackend(object, metaclass=ABCMeta): """ raise NotImplementedError + def supports_mark_as_seen(self): + """Returns True if mailbox supports flagging mails as seen.""" + # Default implementation + return False + + @abstractmethod + def mark_as_seen(self, mails): + """Asks mailbox to flag mails in the list as seen. + This may raise an exception if mailbox does not support this action. + """ + raise NotImplementedError + def supports_notifications(self): """Returns True if mailbox supports notifications.""" # Default implementation diff --git a/Mailnag/backends/imap.py b/Mailnag/backends/imap.py index deab175..3e5bf09 100644 --- a/Mailnag/backends/imap.py +++ b/Mailnag/backends/imap.py @@ -1,4 +1,5 @@ -# Copyright 2011 - 2019 Patrick Ulbrich +# Copyright 2011 - 2020 Patrick Ulbrich +# Copyright 2020 Andreas Angerer # Copyright 2016 Timo Kankare # Copyright 2016 Thomas Haider # Copyright 2011 Ralf Hersel @@ -82,7 +83,7 @@ class IMAPMailboxBackend(MailboxBackend): for folder in folder_list: # select IMAP folder - conn.select(f'"{folder}"') + conn.select(f'"{folder}"', readonly = True) try: status, data = conn.uid('SEARCH', None, '(UNSEEN)') # ALL or UNSEEN except: @@ -101,7 +102,7 @@ class IMAPMailboxBackend(MailboxBackend): except: logging.debug("Couldn't get IMAP message.") continue - yield (folder, msg, num.decode("utf-8")) + yield (folder, msg, { 'uid' : num.decode("utf-8"), 'folder' : folder }) def request_folders(self): @@ -128,11 +129,34 @@ class IMAPMailboxBackend(MailboxBackend): return lst + def supports_mark_as_seen(self): + return True + + + def mark_as_seen(self, mails): + # Always create a new connection as an existing one may + # be used for IMAP IDLE. + conn = self._connect() + + try: + last_folder = '' + for m in mails: + if ('uid' in m.flags) and ('folder' in m.flags): + folder = m.flags['folder'] + if folder != last_folder: + conn.select(f'"{folder}"', readonly = False) + last_folder = folder + status, data = conn.uid("STORE", m.flags['uid'], "+FLAGS", "(\Seen)") + finally: + self._disconnect(conn) + + def supports_notifications(self): """Returns True if mailbox supports notifications. IMAP mailbox supports notifications if idle parameter is True""" return self.idle + def notify_next_change(self, callback=None, timeout=None): self._ensure_open() @@ -225,7 +249,7 @@ class IMAPMailboxBackend(MailboxBackend): folder = self.folders[0] else: folder = "INBOX" - conn.select(f'"{folder}"') + conn.select(f'"{folder}"', readonly = True) def _ensure_open(self): if not self.is_open(): diff --git a/Mailnag/backends/local.py b/Mailnag/backends/local.py index c6a685c..2cc490a 100644 --- a/Mailnag/backends/local.py +++ b/Mailnag/backends/local.py @@ -1,3 +1,4 @@ +# Copyright 2020 Patrick Ulbrich # Copyright 2016 Timo Kankare # # This program is free software; you can redistribute it and/or modify @@ -61,7 +62,7 @@ class MBoxBackend(MailboxBackend): try: for msg in mbox: if 'R' not in msg.get_flags(): - yield folder, msg + yield (folder, msg, {}) finally: mbox.close() @@ -71,6 +72,15 @@ class MBoxBackend(MailboxBackend): raise NotImplementedError("mbox does not support folders") + def supports_mark_as_seen(self): + return False + + + def mark_as_seen(self, mails): + # TODO: local mailboxes should support this + raise NotImplementedError + + def notify_next_change(self, callback=None, timeout=None): raise NotImplementedError("mbox does not support notifications") diff --git a/Mailnag/backends/pop3.py b/Mailnag/backends/pop3.py index d17e4d3..39db421 100644 --- a/Mailnag/backends/pop3.py +++ b/Mailnag/backends/pop3.py @@ -117,13 +117,21 @@ class POP3MailboxBackend(MailboxBackend): except: logging.debug("Couldn't get msg from POP message.") continue - yield (folder, msg) + yield (folder, msg, {}) def request_folders(self): raise NotImplementedError("POP3 does not support folders") + def supports_mark_as_seen(self): + return False + + + def mark_as_seen(self, mails): + raise NotImplementedError + + def notify_next_change(self, callback=None, timeout=None): raise NotImplementedError("POP3 does not support notifications") diff --git a/Mailnag/common/accounts.py b/Mailnag/common/accounts.py index 8d55015..8a358f0 100644 --- a/Mailnag/common/accounts.py +++ b/Mailnag/common/accounts.py @@ -1,4 +1,4 @@ -# Copyright 2011 - 2019 Patrick Ulbrich +# Copyright 2011 - 2020 Patrick Ulbrich # Copyright 2016 Thomas Haider # Copyright 2016, 2018 Timo Kankare # Copyright 2011 Ralf Hersel @@ -141,6 +141,14 @@ class Account: return self._get_backend().request_folders() + def supports_mark_as_seen(self): + return self._get_backend().supports_mark_as_seen() + + + def mark_as_seen(self, mails): + self._get_backend().mark_as_seen(mails) + + def get_id(self): """Returns unique id for the account.""" # Assumption: The name of the account is unique. diff --git a/Mailnag/common/config.py b/Mailnag/common/config.py index f638537..fcbdf2d 100644 --- a/Mailnag/common/config.py +++ b/Mailnag/common/config.py @@ -26,7 +26,6 @@ mailnag_defaults = { 'poll_interval' : '10', 'imap_idle_timeout' : '10', 'autostart' : '1', - 'mark_imap_read' : '1', 'connectivity_test' : 'auto', 'enabled_plugins' : 'dbusplugin, soundplugin, libnotifyplugin' } diff --git a/Mailnag/configuration/configwindow.py b/Mailnag/configuration/configwindow.py index 2d51ddc..8ca76e6 100644 --- a/Mailnag/configuration/configwindow.py +++ b/Mailnag/configuration/configwindow.py @@ -278,6 +278,7 @@ class ConfigWindow: aboutdialog.set_license_type(Gtk.License.GPL_2_0) aboutdialog.set_authors([ "Patrick Ulbrich (maintainer)", + "Andreas Angerer", "Balló György", "Dan Christensen", "Edwin Smulders", diff --git a/Mailnag/daemon/dbus.py b/Mailnag/daemon/dbus.py index e420adb..90cb592 100644 --- a/Mailnag/daemon/dbus.py +++ b/Mailnag/daemon/dbus.py @@ -108,9 +108,9 @@ class DBusService(dbus.service.Object): d['subject'] = m.subject # string (s) d['sender_name'] = name # string (s) d['sender_addr'] = addr # string (s) - d['account_name'] = m.account_name # string (s) + d['account_name'] = m.account.name # string (s) d['id'] = m.id # string (s) - d['strID'] = m.strID # string (s) + converted_mails.append(d) return converted_mails diff --git a/Mailnag/daemon/mailchecker.py b/Mailnag/daemon/mailchecker.py index 5a2bd0f..fff9c5b 100644 --- a/Mailnag/daemon/mailchecker.py +++ b/Mailnag/daemon/mailchecker.py @@ -1,4 +1,5 @@ # Copyright 2011 - 2020 Patrick Ulbrich +# Copyright 2020 Andreas Angerer # Copyright 2011 Ralf Hersel # # This program is free software; you can redistribute it and/or modify @@ -36,7 +37,6 @@ class MailChecker: self._conntest = conntest self._dbus_service = dbus_service self._count_on_last_check = 0 - self._all_mails = [] def check(self, accounts): @@ -55,6 +55,7 @@ class MailChecker: all_mails = self._mailsyncer.sync(accounts) unseen_mails = [] new_mails = [] + seen_mails_by_account = {} for mail in all_mails: if self._memorizer.contains(mail.id): # mail was fetched before @@ -62,11 +63,24 @@ class MailChecker: unseen_mails.append(mail) if self._firstcheck: new_mails.append(mail) - + else: + # if the mail account supports tagging mails as seen (e.g. IMAP), + # mark the mail as seen on the server as well. + if mail.account.supports_mark_as_seen(): + if not mail.account in seen_mails_by_account: + seen_mails_by_account[mail.account] = [] + seen_mails_by_account[mail.account].append(mail) else: # mail is fetched the first time unseen_mails.append(mail) new_mails.append(mail) - self._all_mails = all_mails + + # Flag mails to seen on server + for acc, mails in seen_mails_by_account.items(): + try: + acc.mark_as_seen(mails) + except: + logging.warning("Failed to set mails to seen on server (account: '%s').", acc.name) + self._memorizer.sync(all_mails) self._memorizer.save() self._firstcheck = False diff --git a/Mailnag/daemon/mailnagdaemon.py b/Mailnag/daemon/mailnagdaemon.py index d452a3b..87ae300 100644 --- a/Mailnag/daemon/mailnagdaemon.py +++ b/Mailnag/daemon/mailnagdaemon.py @@ -1,5 +1,6 @@ # Copyright 2016 Timo Kankare # Copyright 2014 - 2020 Patrick Ulbrich +# Copyright 2020 Andreas Angerer # # 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 @@ -126,23 +127,11 @@ class MailnagDaemon(MailnagController): # Part of MailnagController interface def mark_mail_as_read(self, mail_id): - mails = self._mailchecker._all_mails - found = False - for mail in mails: - if mail_id == mail.id: - found = True - break - if (not found) or (not bool(int(self._cfg.get('core', 'mark_imap_read')))): - self._ensure_not_disposed() - self._memorizer.set_to_seen(mail_id) - self._memorizer.save() - return - backend = mail.account._get_backend() - if type(backend).__name__ == 'IMAPMailboxBackend': - mailid = mail.strID - conn = backend._conn - status, res = conn.uid("STORE", mailid, "+FLAGS", "(\Seen)") + # Note: ensure_not_disposed() is not really necessary here + # (the memorizer object is available in dispose()), + # but better be consistent with other daemon methods. self._ensure_not_disposed() + self._memorizer.set_to_seen(mail_id) self._memorizer.save() diff --git a/Mailnag/daemon/mails.py b/Mailnag/daemon/mails.py index c144547..9c0228a 100644 --- a/Mailnag/daemon/mails.py +++ b/Mailnag/daemon/mails.py @@ -1,4 +1,5 @@ -# Copyright 2011 - 2019 Patrick Ulbrich +# Copyright 2011 - 2020 Patrick Ulbrich +# Copyright 2020 Andreas Angerer # Copyright 2016, 2018 Timo Kankare # Copyright 2011 Leighton Earl # Copyright 2011 Ralf Hersel @@ -35,15 +36,13 @@ from Mailnag.common.config import cfg_folder # Mail class # class Mail: - def __init__(self, datetime, subject, sender, id, account, strID): + def __init__(self, datetime, subject, sender, id, account, flags): self.datetime = datetime self.subject = subject self.sender = sender self.account = account - self.account_name = account.name - self.account_id = account.get_id() self.id = id - self.strID = strID + self.flags = flags # @@ -68,7 +67,7 @@ class MailCollector: logging.error("Failed to open mailbox for account '%s' (%s)." % (acc.name, ex)) continue - for folder, msg, num in acc.list_messages(): + for folder, msg, flags in acc.list_messages(): sender, subject, datetime, msgid = self._get_header(msg) id = self._get_id(msgid, acc, folder, sender, subject, datetime) @@ -80,7 +79,7 @@ class MailCollector: # Also filter duplicates caused by Gmail labels. if id not in mail_ids: mail_list.append(Mail(datetime, subject, \ - sender, id, acc, num)) + sender, id, acc, flags)) mail_ids[id] = None # leave account with notifications open, so that it can @@ -195,12 +194,13 @@ class MailSyncer: # collect mails from given accounts rcv_lst = MailCollector(self._cfg, accounts).collect_mail(sort = False) + # group received mails by account tmp = {} for acc in accounts: tmp[acc.get_id()] = {} for mail in rcv_lst: - tmp[mail.account_id][mail.id] = mail + tmp[mail.account.get_id()][mail.id] = mail # compare current mails against received mails # and remove those that are gone (probably opened in mail client). diff --git a/Mailnag/plugins/userscriptplugin.py b/Mailnag/plugins/userscriptplugin.py index d4c294c..bf10f91 100644 --- a/Mailnag/plugins/userscriptplugin.py +++ b/Mailnag/plugins/userscriptplugin.py @@ -120,7 +120,7 @@ class UserscriptPlugin(Plugin): sender_name, sender_addr = m.sender if len(sender_addr) == 0: sender_addr = 'UNKNOWN_SENDER' - script_args.append(m.account_name) + script_args.append(m.account.name) script_args.append(sender_addr) script_args.append(m.subject) start_subprocess(script_args)