2 from ctypes import c_int, c_char_p, c_void_p, c_uint64
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
5 from datetime import date
7 class Database(object):
8 """Represents a notmuch database (wraps notmuch_database_t)
10 .. note:: Do note that as soon as we tear down this object, all underlying
11 derived objects such as queries, threads, messages, tags etc will
12 be freed by the underlying library as well. Accessing these objects
13 will lead to segfaults and other unexpected behavior.
15 We implement reference counting, so that parent objects can be
16 automatically freed when they are not needed anymore, for example::
18 db = Database('path',create=True)
19 msgs = Query(db,'from:myself').search_messages()
21 This returns a :class:`Messages` which internally contains
22 a reference to the parent :class:`Query` object. Otherwise
23 the Query() would be immediately freed, taking our *msgs*
26 In this case, the above Query() object will be
27 automatically freed whenever we delete all derived objects,
28 ie in our case: `del (msgs)` would also delete the parent
29 Query (but not the parent Database() as that is still
30 referenced from the variable *db* in which it is stored.
32 Pretty much the same is valid for all other objects in the hierarchy,
33 such as :class:`Query`, :class:`Messages`, :class:`Message`,
36 MODE = Enum(['READ_ONLY','READ_WRITE'])
37 """Constants: Mode in which to open the database"""
40 """Class attribute to cache user's default database"""
42 """notmuch_database_get_path (notmuch_database_t *database)"""
43 _get_path = nmlib.notmuch_database_get_path
44 _get_path.restype = c_char_p
46 """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
47 _open = nmlib.notmuch_database_open
48 _open.restype = c_void_p
50 """ notmuch_database_find_message """
51 _find_message = nmlib.notmuch_database_find_message
52 _find_message.restype = c_void_p
54 """notmuch_database_get_all_tags (notmuch_database_t *database)"""
55 _get_all_tags = nmlib.notmuch_database_get_all_tags
56 _get_all_tags.restype = c_void_p
58 """ notmuch_database_create(const char *path):"""
59 _create = nmlib.notmuch_database_create
60 _create.restype = c_void_p
62 def __init__(self, path=None, create=False, mode= MODE.READ_ONLY):
63 """If *path* is *None*, we will try to read a users notmuch
64 configuration and use his default database. If *create* is `True`,
65 the database will always be created in
66 :attr:`MODE.READ_WRITE` mode as creating an empty
67 database for reading only does not make a great deal of sense.
69 :param path: Directory to open/create the database in (see
70 above for behavior if `None`)
71 :type path: `str` or `None`
72 :param create: False to open an existing, True to create a new
75 :param mdoe: Mode to open a database in. Always
76 :attr:`MODE`.READ_WRITE when creating a new one.
77 :type mode: :attr:`MODE`
79 :exception: :exc:`NotmuchError` in case of failure.
83 # no path specified. use a user's default database
84 if Database._std_db_path is None:
85 #the following line throws a NotmuchError if it fails
86 Database._std_db_path = self._get_user_default_db()
87 path = Database._std_db_path
90 self.open(path, status)
94 def create(self, path):
95 """Creates a new notmuch database
97 This function wraps *notmuch_database_create(...)* and creates
98 a new notmuch database at *path*. It will always return a database in
99 :attr:`MODE`.READ_WRITE mode as creating an empty database
100 for reading only does not make a great deal of sense.
102 :param path: A directory in which we should create the database.
105 :exception: :exc:`NotmuchError` in case of any failure
106 (after printing an error message on stderr).
108 if self._db is not None:
110 message="Cannot create db, this Database() already has an open one.")
112 res = Database._create(path, MODE.READ_WRITE)
116 message="Could not create the specified database")
119 def open(self, path, status= MODE.READ_ONLY):
120 """calls notmuch_database_open
122 :returns: Raises :exc:`notmuch.NotmuchError` in case
123 of any failure (after printing an error message on stderr).
126 res = Database._open(path, status)
130 message="Could not open the specified database")
134 """notmuch_database_get_path (notmuch_database_t *database); """
135 return Database._get_path(self._db)
137 def find_message(self, msgid):
138 """notmuch_database_find_message
140 :param msgid: The message id
142 :returns: :class:`Message` or `None` if no message is found or if an
143 out-of-memory situation occurs.
144 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
145 the database was not intitialized.
148 raise NotmuchError(STATUS.NOT_INITIALIZED)
149 msg_p = Database._find_message(self._db, msgid)
152 return Message(msg_p, self)
154 def get_all_tags(self):
155 """Returns :class:`Tags` with a list of all tags found in the database
157 :returns: :class:`Tags` object or raises :exc:`NotmuchError` with
158 STATUS.NULL_POINTER on error
161 raise NotmuchError(STATUS.NOT_INITIALIZED)
163 tags_p = Database._get_all_tags (self._db)
165 raise NotmuchError(STATUS.NULL_POINTER)
166 return Tags(tags_p, self)
169 return "'Notmuch DB " + self.get_path() + "'"
172 """Close and free the notmuch database if needed"""
173 if self._db is not None:
174 logging.debug("Freeing the database now")
175 nmlib.notmuch_database_close(self._db)
177 def _get_user_default_db(self):
178 """ Reads a user's notmuch config and returns his db location
180 Throws a NotmuchError if it cannot find it"""
181 from ConfigParser import SafeConfigParser
183 config = SafeConfigParser()
184 config.read(os.path.expanduser('~/.notmuch-config'))
185 if not config.has_option('database','path'):
186 raise NotmuchError(message=
187 "No DB path specified and no user default found")
188 return config.get('database','path')
192 """Property returning a pointer to the notmuch_database_t or `None`.
194 This should normally not be needed by a user."""
197 #------------------------------------------------------------------------------
199 """ Wrapper around a notmuch_query_t
201 Do note that as soon as we tear down this object, all derived
202 threads, and messages will be freed as well.
205 SORT_OLDEST_FIRST = 0
206 SORT_NEWEST_FIRST = 1
209 """notmuch_query_create"""
210 _create = nmlib.notmuch_query_create
211 _create.restype = c_void_p
213 """notmuch_query_search_messages"""
214 _search_messages = nmlib.notmuch_query_search_messages
215 _search_messages.restype = c_void_p
217 def __init__(self, db, querystr):
221 self.create(db, querystr)
223 def create(self, db, querystr):
224 """db is a Database() and querystr a string
226 raises NotmuchError STATUS.NOT_INITIALIZED if db is not inited and
227 STATUS.NULL_POINTER if the query creation failed (too little memory)
230 raise NotmuchError(STATUS.NOT_INITIALIZED)
231 # create reference to parent db to keep it alive
234 # create query, return None if too little mem available
235 query_p = Query._create(db.db_p, querystr)
237 NotmuchError(STATUS.NULL_POINTER)
238 self._query = query_p
240 def set_sort(self, sort):
241 """notmuch_query_set_sort
243 :param sort: one of Query.SORT_OLDEST_FIRST|SORT_NEWEST_FIRST|SORT_MESSAGE_ID
244 :returns: Nothing, but raises NotmuchError if query is not inited
246 if self._query is None:
247 raise NotmuchError(STATUS.NOT_INITIALIZED)
249 nmlib.notmuch_query_set_sort(self._query, sort)
251 def search_messages(self):
252 """notmuch_query_search_messages
253 Returns Messages() or a raises a NotmuchError()
255 if self._query is None:
256 raise NotmuchError(STATUS.NOT_INITIALIZED)
258 msgs_p = Query._search_messages(self._query)
261 NotmuchError(STATUS.NULL_POINTER)
263 return Messages(msgs_p,self)
267 """Close and free the Query"""
268 if self._query is not None:
269 logging.debug("Freeing the Query now")
270 nmlib.notmuch_query_destroy (self._query)
272 #------------------------------------------------------------------------------
274 """Wrapper around notmuch_tags_t"""
277 _get = nmlib.notmuch_tags_get
278 _get.restype = c_char_p
280 def __init__(self, tags_p, parent=None):
282 msg_p is a pointer to an notmuch_message_t Structure. If it is None,
283 we will raise an NotmuchError(STATUS.NULL_POINTER).
285 Is passed the parent these tags are derived from, and saves a
286 reference to it, so we can automatically delete the db object
287 once all derived objects are dead.
289 Tags() provides an iterator over all contained tags. However, you will
290 only be able to iterate over the Tags once, because the underlying C
291 function only allows iterating once.
292 #TODO: make the iterator work more than once and cache the tags in
296 NotmuchError(STATUS.NULL_POINTER)
299 #save reference to parent object so we keep it alive
300 self._parent = parent
301 logging.debug("Inited Tags derived from %s" %(repr(parent)))
304 """ Make Tags an iterator """
308 if self._tags is None:
309 raise NotmuchError(STATUS.NOT_INITIALIZED)
311 if not nmlib.notmuch_tags_valid(self._tags):
315 tag = Tags._get (self._tags)
316 nmlib.notmuch_tags_move_to_next(self._tags)
320 """str() of Tags() is a space separated list of tags
322 This iterates over the list of Tags and will therefore 'exhaust' Tags()
324 return " ".join(self)
327 """Close and free the notmuch tags"""
328 if self._tags is not None:
329 logging.debug("Freeing the Tags now")
330 nmlib.notmuch_tags_destroy (self._tags)
333 #------------------------------------------------------------------------------
334 class Messages(object):
335 """Wrapper around notmuch_messages_t"""
338 _get = nmlib.notmuch_messages_get
339 _get.restype = c_void_p
341 _collect_tags = nmlib.notmuch_messages_collect_tags
342 _collect_tags.restype = c_void_p
344 def __init__(self, msgs_p, parent=None):
346 msg_p is a pointer to an notmuch_messages_t Structure. If it is None,
347 we will raise an NotmuchError(STATUS.NULL_POINTER).
349 If passed the parent query this Messages() is derived from, it saves a
350 reference to it, so we can automatically delete the parent query object
351 once all derived objects are dead.
353 Messages() provides an iterator over all contained Message()s.
354 However, you will only be able to iterate over it once,
355 because the underlying C function only allows iterating once.
356 #TODO: make the iterator work more than once and cache the tags in
360 NotmuchError(STATUS.NULL_POINTER)
363 #store parent, so we keep them alive as long as self is alive
364 self._parent = parent
365 logging.debug("Inited Messages derived from %s" %(str(parent)))
367 def collect_tags(self):
368 """ return the Tags() belonging to the messages
370 Do note that collect_tags will iterate over the messages and
371 therefore will not allow further iterationsl
372 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
374 if self._msgs is None:
375 raise NotmuchError(STATUS.NOT_INITIALIZED)
377 # collect all tags (returns NULL on error)
378 tags_p = Messages._collect_tags (self._msgs)
379 #reset _msgs as we iterated over it and can do so only once
383 raise NotmuchError(STATUS.NULL_POINTER)
384 return Tags(tags_p, self)
387 """ Make Messages an iterator """
391 if self._msgs is None:
392 raise NotmuchError(STATUS.NOT_INITIALIZED)
394 if not nmlib.notmuch_messages_valid(self._msgs):
398 msg = Message(Messages._get (self._msgs), self)
399 nmlib.notmuch_messages_move_to_next(self._msgs)
403 """ Returns the number of contained messages
405 :note: As this iterates over the messages, we will not be able to
406 iterate over them again (as in retrieve them)!
408 if self._msgs is None:
409 raise NotmuchError(STATUS.NOT_INITIALIZED)
412 while nmlib.notmuch_messages_valid(self._msgs):
413 nmlib.notmuch_messages_move_to_next(self._msgs)
421 """Close and free the notmuch Messages"""
422 if self._msgs is not None:
423 logging.debug("Freeing the Messages now")
424 nmlib.notmuch_messages_destroy (self._msgs)
427 #------------------------------------------------------------------------------
428 class Message(object):
429 """Wrapper around notmuch_message_t"""
431 """notmuch_message_get_filename (notmuch_message_t *message)"""
432 _get_filename = nmlib.notmuch_message_get_filename
433 _get_filename.restype = c_char_p
434 """notmuch_message_get_message_id (notmuch_message_t *message)"""
435 _get_message_id = nmlib.notmuch_message_get_message_id
436 _get_message_id.restype = c_char_p
438 """notmuch_message_get_tags (notmuch_message_t *message)"""
439 _get_tags = nmlib.notmuch_message_get_tags
440 _get_tags.restype = c_void_p
442 _get_date = nmlib.notmuch_message_get_date
443 _get_date.restype = c_uint64
445 _get_header = nmlib.notmuch_message_get_header
446 _get_header.restype = c_char_p
448 def __init__(self, msg_p, parent=None):
450 msg_p is a pointer to an notmuch_message_t Structure. If it is None,
451 we will raise an NotmuchError(STATUS.NULL_POINTER).
453 Is a 'parent' object is passed which this message is derived from,
454 we save a reference to it, so we can automatically delete the parent
455 object once all derived objects are dead.
458 NotmuchError(STATUS.NULL_POINTER)
460 #keep reference to parent, so we keep it alive
461 self._parent = parent
462 logging.debug("Inited Message derived from %s" %(str(parent)))
465 def get_message_id(self):
466 """ return the msg id
468 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
470 if self._msg is None:
471 raise NotmuchError(STATUS.NOT_INITIALIZED)
472 return Message._get_message_id(self._msg)
475 """returns time_t of the message date
477 For the original textual representation of the Date header from the
478 message call notmuch_message_get_header() with a header value of
480 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
482 if self._msg is None:
483 raise NotmuchError(STATUS.NOT_INITIALIZED)
484 return Message._get_date(self._msg)
486 def get_header(self, header):
487 """ TODO document me"""
488 if self._msg is None:
489 raise NotmuchError(STATUS.NOT_INITIALIZED)
491 #Returns NULL if any error occurs.
492 header = Message._get_header (self._msg, header)
494 raise NotmuchError(STATUS.NULL_POINTER)
497 def get_filename(self):
498 """ return the msg filename
500 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
502 if self._msg is None:
503 raise NotmuchError(STATUS.NOT_INITIALIZED)
504 return Message._get_filename(self._msg)
507 """ return the msg tags
509 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
510 Raises NotmuchError(STATUS.NULL_POINTER) on error.
512 if self._msg is None:
513 raise NotmuchError(STATUS.NOT_INITIALIZED)
515 tags_p = Message._get_tags(self._msg)
517 raise NotmuchError(STATUS.NULL_POINTER)
518 return Tags(tags_p, self)
521 """A message() is represented by a 1-line summary"""
523 msg['from'] = self.get_header('from')
524 msg['tags'] = str(self.get_tags())
525 msg['date'] = date.fromtimestamp(self.get_date())
526 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
528 def format_as_text(self):
529 """ Output like notmuch show """
533 """Close and free the notmuch Message"""
534 if self._msg is not None:
535 logging.debug("Freeing the Message now")
536 nmlib.notmuch_message_destroy (self._msg)