# Copyright (C) 2011-2012 Patrick Totzke # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file from datetime import datetime from .message import Message from ..settings.const import settings class Thread: """ A wrapper around a notmuch mailthread (:class:`notmuch.database.Thread`) that ensures persistence of the thread: It can be safely read multiple times, its manipulation is done via a :class:`alot.db.DBManager` and it can directly provide contained messages as :class:`~alot.db.message.Message`. """ """ date header of oldest message in this thread as :class:`~datetime.datetime` """ oldest_date = None """ date header of newest message in this thread as :class:`~datetime.datetime` """ newest_date = None """number of contained messages""" total_messages = None """This thread's ID""" id = None """Thread subject""" subject = None """A list of toplevel messages""" toplevel_messages = None """A list of all messages in this thread in depth-first order""" message_list = None """A dict mapping Message-Id strings to Message instances""" messages = None def __init__(self, dbman, thread): """ :param dbman: db manager that is used for further lookups :type dbman: :class:`~alot.db.DBManager` :param thread: the wrapped thread :type thread: :class:`notmuch.database.Thread` """ self._dbman = dbman self._authors = None self.id = thread.threadid self._tags = set() self.toplevel_messages = [] self.message_list = [] self.messages = {} self._refresh(thread) def _refresh(self, thread): """refresh thread metadata from the index""" self.total_messages = len(thread) self._notmuch_authors_string = thread.authors subject_type = settings.get('thread_subject') if subject_type == 'notmuch': subject = thread.subject elif subject_type == 'oldest': try: first_msg = next(thread.toplevel()) subject = first_msg.header('subject') except (StopIteration, LookupError): subject = '' self.subject = subject self._authors = None try: self.oldest_date = datetime.fromtimestamp(thread.first) except ValueError: # year is out of range self.oldest_date = None try: self.newest_date = datetime.fromtimestamp(thread.last) except ValueError: # year is out of range self.newest_date = None self._tags = thread.tags self.messages, self.toplevel_messages, self.message_list = self._gather_messages(thread) def refresh(self): with self._dbman._db_ro() as db: thread = self._dbman._get_notmuch_thread(db, self.id) self._refresh(thread) def _gather_messages(self, thread): msgs = {} msg_tree = [] msg_list = [] def thread_tree_walk(nm_msg, depth, parent): msg = Message(self._dbman, self, nm_msg, depth) msg_list.append(msg) replies = [] for m in nm_msg.replies(): replies.append(thread_tree_walk(m, depth + 1, msg)) msg.replies = replies msg.parent = parent msgs[msg.id] = msg return msg for m in thread.toplevel(): msg_tree.append(thread_tree_walk(m, 0, None)) return msgs, msg_tree, msg_list def __str__(self): return "thread:%s: %s" % (self.id, self.subject) def get_tags(self, intersection=False): """ returns tagsstrings attached to this thread :param intersection: return tags present in all contained messages instead of in at least one (union) :type intersection: bool :rtype: set of str """ tags = set(self._tags) if intersection: for m in self.messages.values(): tags = tags.intersection(set(m.get_tags())) return tags def get_authors(self): """ returns a list of authors (name, addr) of the messages. The authors are ordered by msg date and unique (by name/addr). :rtype: list of (str, str) """ if self._authors is None: # Sort messages with date first (by date ascending), and those # without a date last. msgs = sorted(self.messages.values(), key=lambda m: m.date or datetime.max) orderby = settings.get('thread_authors_order_by') self._authors = [] if orderby == 'latest_message': for m in msgs: pair = m.get_author() if pair in self._authors: self._authors.remove(pair) self._authors.append(pair) else: # i.e. first_message for m in msgs: pair = m.get_author() if pair not in self._authors: self._authors.append(pair) return self._authors def get_authors_string(self, own_accts=None, replace_own=None): """ returns a string of comma-separated authors Depending on settings, it will substitute "me" for author name if address is user's own. :param own_accts: list of own accounts to replace :type own_accts: list of :class:`Account` :param replace_own: whether or not to actually do replacement :type replace_own: bool :rtype: str """ if replace_own is None: replace_own = settings.get('thread_authors_replace_me') if replace_own: if own_accts is None: own_accts = settings.get_accounts() authorslist = [] for aname, aaddress in self.get_authors(): for account in own_accts: if account.matches_address(aaddress): aname = settings.get('thread_authors_me') break if not aname: aname = aaddress if aname not in authorslist: authorslist.append(aname) return ', '.join(authorslist) else: return self._notmuch_authors_string def matches(self, query): """ Check if this thread matches the given notmuch query. :param query: The query to check against :type query: string :returns: True if this thread matches the given query, False otherwise :rtype: bool """ thread_query = 'thread:%s AND (%s)' % (self.id, query) num_matches = self._dbman.count_messages(thread_query) return num_matches > 0 def count_matches(self, query): """ Count the number of messags in this thread that match the given notmuch query. :param query: The query to check against :type query: string """ thread_query = 'thread:%s AND (%s)' % (self.id, query) return self._dbman.count_messages(thread_query)