""" This file is part of alot. Alot 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 3 of the License, or (at your option) any later version. Alot 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 notmuch. If not, see . Copyright (C) 2011 Patrick Totzke """ from notmuch import Database, NotmuchError from datetime import datetime from collections import deque from message import Message from settings import notmuchconfig as config DB_ENC = 'utf-8' class DatabaseError(Exception): pass class DatabaseROError(DatabaseError): pass class DatabaseLockedError(DatabaseError): pass class DBManager: """ keeps track of your index parameters, can create notmuch.Query objects from its Database on demand and implements a bunch of database specific functions. """ def __init__(self, path=None, ro=False): """ :param path: absolute path to the notmuch index :type path: str :param ro: open the index in read-only mode :type ro: boolean """ self.ro = ro self.path = path self.writequeue = deque([]) def flush(self): """ tries to flush all queued write commands to the index. :exception: :exc:`DatabaseROError` if db is opened in read-only mode :exception: :exc:`DatabaseLockedError` if db is locked """ if self.ro: raise DatabaseROError() if self.writequeue: try: mode = Database.MODE.READ_WRITE db = Database(path=self.path, mode=mode) except NotmuchError: raise DatabaseLockedError() while self.writequeue: cmd, querystring, tags, sync = self.writequeue.popleft() query = db.create_query(querystring) for msg in query.search_messages(): msg.freeze() if cmd == 'tag': for tag in tags: msg.add_tag(tag.encode(DB_ENC), sync_maildir_flags=sync) if cmd == 'set': msg.remove_all_tags() for tag in tags: msg.add_tag(tag.encode(DB_ENC), sync_maildir_flags=sync) elif cmd == 'untag': for tag in tags: msg.remove_tag(tag.encode(DB_ENC), sync_maildir_flags=sync) msg.thaw() def tag(self, querystring, tags, remove_rest=False): """ add tags to all matching messages. Raises :exc:`DatabaseROError` if in read only mode. :param querystring: notmuch search string :type querystring: str :param tags: a list of tags to be added :type tags: list of str :param remove_rest: remove tags from matching messages before tagging :type remove_rest: boolean :exception: :exc:`NotmuchError` """ if self.ro: raise DatabaseROError() sync_maildir_flags = config.getboolean('maildir', 'synchronize_flags') if remove_rest: self.writequeue.append(('set', querystring, tags, sync_maildir_flags)) else: self.writequeue.append(('tag', querystring, tags, sync_maildir_flags)) def untag(self, querystring, tags): """ add tags to all matching messages. Raises :exc:`DatabaseROError` if in read only mode. :param querystring: notmuch search string :type querystring: str :param tags: a list of tags to be added :type tags: list of str :exception: :exc:`NotmuchError` """ if self.ro: raise DatabaseROError() sync_maildir_flags = config.getboolean('maildir', 'synchronize_flags') self.writequeue.append(('untag', querystring, tags, sync_maildir_flags)) def count_messages(self, querystring): """returns number of messages that match querystring""" return self.query(querystring).count_messages() def search_thread_ids(self, querystring): """returns the ids of all threads that match the querystring This copies! all integer thread ids into an new list.""" threads = self.query(querystring).search_threads() return [thread.get_thread_id() for thread in threads] def get_thread(self, tid): """returns the thread with given id as alot.db.Thread object""" query = self.query('thread:' + tid) #TODO raise exceptions here in 01 try: return Thread(self, query.search_threads().next()) except: return None def get_message(self, mid): """returns the message with given id as alot.message.Message object""" mode = Database.MODE.READ_ONLY db = Database(path=self.path, mode=mode) msg = db.find_message(mid) return Message(self, msg) def get_all_tags(self): """returns all tags as list of strings""" db = Database(path=self.path) return [t for t in db.get_all_tags()] def query(self, querystring): """creates notmuch.Query objects on demand :param querystring: The query string to use for the lookup :type query: str. :returns: notmuch.Query -- the query object. """ mode = Database.MODE.READ_ONLY db = Database(path=self.path, mode=mode) return db.create_query(querystring) class Thread: def __init__(self, dbman, thread): """ :param dbman: db manager that is used for further lookups :type dbman: alot.db.DBManager :param msg: the wrapped thread :type msg: notmuch.database.Thread """ self._dbman = dbman self._id = thread.get_thread_id() self.refresh(thread) def refresh(self, thread=None): if not thread: query = self._dbman.query('thread:' + self._id) thread = query.search_threads().next() self._total_messages = thread.get_total_messages() self._authors = thread.get_authors() self._subject = thread.get_subject() ts = thread.get_oldest_date() self._oldest_date = datetime.fromtimestamp(ts) self._newest_date = datetime.fromtimestamp(thread.get_newest_date()) 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): """returns tags attached to this thread as list of strings""" l = list(self._tags) l.sort() return l def add_tags(self, tags): """adds tags to all messages in this thread :param tags: tags to add :type tags: list of str """ newtags = set(tags).difference(self._tags) if newtags: self._dbman.tag('thread:' + self._id, newtags) self._tags = self._tags.union(newtags) def remove_tags(self, tags): """remove tags from all messages in this thread :param tags: tags to remove :type tags: list of str """ rmtags = set(tags).intersection(self._tags) if rmtags: self._dbman.untag('thread:' + self._id, tags) self._tags = self._tags.difference(rmtags) def set_tags(self, tags): """set tags of all messages in this thread. This removes all tags and attaches the given ones in one step. :param tags: tags to add :type tags: list of str """ if tags != self._tags: self._dbman.tag('thread:' + self._id, tags, remove_rest=True) self._tags = set(tags) def get_authors(self): # TODO: make this return a list of strings """returns all authors in this thread""" return self._authors def get_subject(self): """returns this threads subject""" return self._subject def get_toplevel_messages(self): """returns all toplevel messages as list of :class:`Message`""" if not self._messages: self.get_messages() return self._toplevel_messages def get_messages(self): """returns all messages in this thread :returns: dict mapping all contained :class:`Message`s to a list of their respective children. """ if not self._messages: 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 :param msg: the parent message, must be contained in thread :type msg: alot.sb.Message """ 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 datetime""" return self._newest_date def get_oldest_date(self): """returns date header of oldest message in this thread as datetime""" return self._oldest_date def get_total_messages(self): """returns number of contained messages""" return self._total_messages