# 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 ids 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.get_thread_id() self._tags = set() self.toplevel_messages = [] self.message_list = [] self.messages = {} self.refresh(thread) def refresh(self, thread=None): """refresh thread metadata from the index""" if not thread: thread = self._dbman._get_notmuch_thread(self.id) self.total_messages = thread.get_total_messages() self._notmuch_authors_string = thread.get_authors() subject_type = settings.get('thread_subject') if subject_type == 'notmuch': subject = thread.get_subject() elif subject_type == 'oldest': try: first_msg = list(thread.get_toplevel_messages())[0] subject = first_msg.get_header('subject') except IndexError: subject = '' self.subject = subject self._authors = None ts = thread.get_oldest_date() try: self.oldest_date = datetime.fromtimestamp(ts) except ValueError: # year is out of range self.oldest_date = None try: timestamp = thread.get_newest_date() self.newest_date = datetime.fromtimestamp(timestamp) except ValueError: # year is out of range self.newest_date = None self._tags = {t for t in thread.get_tags()} self.messages, self.toplevel_messages, self.message_list = self._gather_messages() def _gather_messages(self): query = self._dbman.query('thread:' + self.id) nm_thread = next(query.search_threads()) msgs = {} msg_tree = [] msg_list = [] def thread_tree_walk(nm_msg, depth): msg = Message(self._dbman, self, nm_msg, depth) msg_list.append(msg) replies = [] for m in nm_msg.get_replies(): replies.append(thread_tree_walk(m, depth + 1)) msg.replies = replies msgs[msg.id] = msg return msg for m in nm_thread.get_toplevel_messages(): msg_tree.append(thread_tree_walk(m, 0)) 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(list(self._tags)) if intersection: for m in self.messages.values(): tags = tags.intersection(set(m.get_tags())) return tags def add_tags(self, tags, afterwards=None, remove_rest=False): """ add `tags` to all messages in this thread .. note:: This only adds the requested operation to this objects :class:`DBManager's ` write queue. You need to call :meth:`DBManager.flush ` to actually write out. :param tags: a list of tags to be added :type tags: list of str :param afterwards: callback that gets called after successful application of this tagging operation :type afterwards: callable :param remove_rest: remove all other tags :type remove_rest: bool """ def myafterwards(): if remove_rest: self._tags = set(tags) else: self._tags = self._tags.union(tags) if callable(afterwards): afterwards() self._dbman.tag('thread:' + self.id, tags, afterwards=myafterwards, remove_rest=remove_rest) def remove_tags(self, tags, afterwards=None): """ remove `tags` (list of str) from all messages in this thread .. note:: This only adds the requested operation to this objects :class:`DBManager's ` write queue. You need to call :meth:`DBManager.flush ` to actually write out. :param tags: a list of tags to be added :type tags: list of str :param afterwards: callback that gets called after successful application of this tagging operation :type afterwards: callable """ rmtags = set(tags).intersection(self._tags) if rmtags: def myafterwards(): self._tags = self._tags.difference(tags) if callable(afterwards): afterwards() self._dbman.untag('thread:' + self.id, tags, myafterwards) self._tags = self._tags.difference(rmtags) 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:{tid} AND {subquery}'.format(tid=self.id, subquery=query) num_matches = self._dbman.count_messages(thread_query) return num_matches > 0