From 657b7525f297c885877281861dba90da5301db25 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 11 Mar 2012 16:06:20 +0000 Subject: refactor db: move into submodule this moves messages into the new submodule alot.db which from now on also contains Threads in a separate file --- alot/db/thread.py | 265 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 alot/db/thread.py (limited to 'alot/db/thread.py') diff --git a/alot/db/thread.py b/alot/db/thread.py new file mode 100644 index 00000000..a13eac0e --- /dev/null +++ b/alot/db/thread.py @@ -0,0 +1,265 @@ +from datetime import datetime + +from message import Message +from alot.settings import settings + + +class Thread(object): + """ + 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.message.Message`. + """ + + 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._id = thread.get_thread_id() + 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() + self._subject = thread.get_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 = set([t for t in thread.get_tags()]) + self._messages = {} # this maps messages to its children + self._toplevel_messages = [] + + def __str__(self): + return "thread:%s: %s" % (self._id, self.get_subject()) + + def get_thread_id(self): + """returns id of this thread""" + return self._id + + 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.get_messages().keys(): + 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 <~alot.db.DBManager>` write queue. + You need to call :meth:`DBManager.flush <~alot.db.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 addr). + + :rtype: list of (str, str) + """ + if self._authors == None: + self._authors = [] + seen = {} + msgs = self.get_messages().keys() + msgs.sort(lambda a, b: cmp(a, b), lambda m: m.get_date()) + for m in msgs: + pair = m.get_author() + if not pair[1] in seen: + seen[pair[1]] = True + self._authors.append(pair) + return self._authors + + def get_authors_string(self, own_addrs=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_addrs: list of own email addresses to replace + :type own_addrs: list of str + :param replace_own: whether or not to actually do replacement + :type replace_own: bool + :rtype: str + """ + if replace_own == None: + replace_own = settings.get('thread_authors_replace_me') + if replace_own: + if own_addrs == None: + own_addrs = settings.get_addresses() + authorslist = [] + for aname, aaddress in self.get_authors(): + if aaddress in own_addrs: + aname = settings.get('thread_authors_me') + if not aname: + aname = aaddress + if not aname in authorslist: + authorslist.append(aname) + return ', '.join(authorslist) + else: + return self._notmuch_authors_string + + def get_subject(self): + """returns subject string""" + return self._subject + + def get_toplevel_messages(self): + """ + returns all toplevel messages contained in this thread. + This are all the messages without a parent message + (identified by 'in-reply-to' or 'references' header. + + :rtype: list of :class:`~alot.message.Message` + """ + if not self._messages: + self.get_messages() + return self._toplevel_messages + + def get_messages(self): + """ + returns all messages in this thread as dict mapping all contained + messages to their direct responses. + + :rtype: dict mapping :class:`~alot.message.Message` to a list of + :class:`~alot.message.Message`. + """ + if not self._messages: # if not already cached + query = self._dbman.query('thread:' + self._id) + thread = query.search_threads().next() + + def accumulate(acc, msg): + M = Message(self._dbman, msg, thread=self) + acc[M] = [] + r = msg.get_replies() + if r is not None: + for m in r: + acc[M].append(accumulate(acc, m)) + return M + + self._messages = {} + for m in thread.get_toplevel_messages(): + self._toplevel_messages.append(accumulate(self._messages, m)) + return self._messages + + def get_replies_to(self, msg): + """ + returns all replies to the given message contained in this thread. + + :param msg: parent message to look up + :type msg: :class:`~alot.message.Message` + :returns: list of :class:`~alot.message.Message` or `None` + """ + mid = msg.get_message_id() + msg_hash = self.get_messages() + for m in msg_hash.keys(): + if m.get_message_id() == mid: + return msg_hash[m] + return None + + def get_newest_date(self): + """ + returns date header of newest message in this thread as + :class:`~datetime.datetime` + """ + return self._newest_date + + def get_oldest_date(self): + """ + returns date header of oldest message in this thread as + :class:`~datetime.datetime` + """ + return self._oldest_date + + def get_total_messages(self): + """returns number of contained messages""" + return self._total_messages + + 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 -- cgit v1.2.3