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 remember that as soon as we tear down this object,
11 all underlying derived objects such as queries, threads,
12 messages, tags etc will be freed by the underlying library
13 as well. Accessing these objects will lead to segfaults and
14 other unexpected behavior. See above for more details.
16 MODE = Enum(['READ_ONLY','READ_WRITE'])
17 """Constants: Mode in which to open the database"""
20 """Class attribute to cache user's default database"""
22 """notmuch_database_get_path (notmuch_database_t *database)"""
23 _get_path = nmlib.notmuch_database_get_path
24 _get_path.restype = c_char_p
26 """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
27 _open = nmlib.notmuch_database_open
28 _open.restype = c_void_p
30 """ notmuch_database_find_message """
31 _find_message = nmlib.notmuch_database_find_message
32 _find_message.restype = c_void_p
34 """notmuch_database_get_all_tags (notmuch_database_t *database)"""
35 _get_all_tags = nmlib.notmuch_database_get_all_tags
36 _get_all_tags.restype = c_void_p
38 """ notmuch_database_create(const char *path):"""
39 _create = nmlib.notmuch_database_create
40 _create.restype = c_void_p
42 def __init__(self, path=None, create=False, mode= MODE.READ_ONLY):
43 """If *path* is *None*, we will try to read a users notmuch
44 configuration and use his default database. If *create* is `True`,
45 the database will always be created in
46 :attr:`MODE`.READ_WRITE mode.
48 :param path: Directory to open/create the database in (see
49 above for behavior if `None`)
50 :type path: `str` or `None`
51 :param create: False to open an existing, True to create a new
54 :param mode: Mode to open a database in. Always
55 :attr:`MODE`.READ_WRITE when creating a new one.
56 :type mode: :attr:`MODE`
58 :exception: :exc:`NotmuchError` in case of failure.
62 # no path specified. use a user's default database
63 if Database._std_db_path is None:
64 #the following line throws a NotmuchError if it fails
65 Database._std_db_path = self._get_user_default_db()
66 path = Database._std_db_path
73 def create(self, path):
74 """Creates a new notmuch database
76 This function is used by __init__() usually does not need
77 to be called directly. It wraps the underlying
78 *notmuch_database_create* function and creates a new notmuch
79 database at *path*. It will always return a database in
80 :attr:`MODE`.READ_WRITE mode as creating an empty database for
81 reading only does not make a great deal of sense.
83 :param path: A directory in which we should create the database.
86 :exception: :exc:`NotmuchError` in case of any failure
87 (after printing an error message on stderr).
89 if self._db is not None:
91 message="Cannot create db, this Database() already has an open one.")
93 res = Database._create(path, MODE.READ_WRITE)
97 message="Could not create the specified database")
100 def open(self, path, mode= MODE.READ_ONLY):
101 """Opens an existing database
103 This function is used by __init__() usually does not need
104 to be called directly. It wraps the underlying
105 *notmuch_database_open* function.
107 :param status: Open the database in read-only or read-write mode
108 :type status: :attr:`MODE`
110 :exception: Raises :exc:`notmuch.NotmuchError` in case
111 of any failure (after printing an error message on stderr).
114 res = Database._open(path, mode)
118 message="Could not open the specified database")
122 """Returns the file path of an open database
124 Wraps notmuch_database_get_path"""
125 return Database._get_path(self._db)
127 def find_message(self, msgid):
128 """Returns a :class:`Message` as identified by its message ID
130 wraps *notmuch_database_find_message*
132 :param msgid: The message id
134 :returns: :class:`Message` or `None` if no message is found or if an
135 out-of-memory situation occurs.
136 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
137 the database was not intitialized.
140 raise NotmuchError(STATUS.NOT_INITIALIZED)
141 msg_p = Database._find_message(self._db, msgid)
144 return Message(msg_p, self)
146 def get_all_tags(self):
147 """Returns :class:`Tags` with a list of all tags found in the database
149 :returns: :class:`Tags` object or raises :exc:`NotmuchError` with
150 STATUS.NULL_POINTER on error
153 raise NotmuchError(STATUS.NOT_INITIALIZED)
155 tags_p = Database._get_all_tags (self._db)
157 raise NotmuchError(STATUS.NULL_POINTER)
158 return Tags(tags_p, self)
161 return "'Notmuch DB " + self.get_path() + "'"
164 """Close and free the notmuch database if needed"""
165 if self._db is not None:
166 logging.debug("Freeing the database now")
167 nmlib.notmuch_database_close(self._db)
169 def _get_user_default_db(self):
170 """ Reads a user's notmuch config and returns his db location
172 Throws a NotmuchError if it cannot find it"""
173 from ConfigParser import SafeConfigParser
175 config = SafeConfigParser()
176 config.read(os.path.expanduser('~/.notmuch-config'))
177 if not config.has_option('database','path'):
178 raise NotmuchError(message=
179 "No DB path specified and no user default found")
180 return config.get('database','path')
184 """Property returning a pointer to the notmuch_database_t or `None`.
186 This should normally not be needed by a user."""
189 #------------------------------------------------------------------------------
191 """ Wrapper around a notmuch_query_t
193 Do note that as soon as we tear down this object, all derived
194 threads, and messages will be freed as well.
197 SORT_OLDEST_FIRST = 0
198 SORT_NEWEST_FIRST = 1
201 """notmuch_query_create"""
202 _create = nmlib.notmuch_query_create
203 _create.restype = c_void_p
205 """notmuch_query_search_messages"""
206 _search_messages = nmlib.notmuch_query_search_messages
207 _search_messages.restype = c_void_p
209 def __init__(self, db, querystr):
213 self.create(db, querystr)
215 def create(self, db, querystr):
216 """db is a Database() and querystr a string
218 raises NotmuchError STATUS.NOT_INITIALIZED if db is not inited and
219 STATUS.NULL_POINTER if the query creation failed (too little memory)
222 raise NotmuchError(STATUS.NOT_INITIALIZED)
223 # create reference to parent db to keep it alive
226 # create query, return None if too little mem available
227 query_p = Query._create(db.db_p, querystr)
229 NotmuchError(STATUS.NULL_POINTER)
230 self._query = query_p
232 def set_sort(self, sort):
233 """notmuch_query_set_sort
235 :param sort: one of Query.SORT_OLDEST_FIRST|SORT_NEWEST_FIRST|SORT_MESSAGE_ID
236 :returns: Nothing, but raises NotmuchError if query is not inited
238 if self._query is None:
239 raise NotmuchError(STATUS.NOT_INITIALIZED)
241 nmlib.notmuch_query_set_sort(self._query, sort)
243 def search_messages(self):
244 """notmuch_query_search_messages
245 Returns Messages() or a raises a NotmuchError()
247 if self._query is None:
248 raise NotmuchError(STATUS.NOT_INITIALIZED)
250 msgs_p = Query._search_messages(self._query)
253 NotmuchError(STATUS.NULL_POINTER)
255 return Messages(msgs_p,self)
259 """Close and free the Query"""
260 if self._query is not None:
261 logging.debug("Freeing the Query now")
262 nmlib.notmuch_query_destroy (self._query)
264 #------------------------------------------------------------------------------
266 """Wrapper around notmuch_tags_t"""
269 _get = nmlib.notmuch_tags_get
270 _get.restype = c_char_p
272 def __init__(self, tags_p, parent=None):
274 msg_p is a pointer to an notmuch_message_t Structure. If it is None,
275 we will raise an NotmuchError(STATUS.NULL_POINTER).
277 Is passed the parent these tags are derived from, and saves a
278 reference to it, so we can automatically delete the db object
279 once all derived objects are dead.
281 Tags() provides an iterator over all contained tags. However, you will
282 only be able to iterate over the Tags once, because the underlying C
283 function only allows iterating once.
284 #TODO: make the iterator work more than once and cache the tags in
288 NotmuchError(STATUS.NULL_POINTER)
291 #save reference to parent object so we keep it alive
292 self._parent = parent
293 logging.debug("Inited Tags derived from %s" %(repr(parent)))
296 """ Make Tags an iterator """
300 if self._tags is None:
301 raise NotmuchError(STATUS.NOT_INITIALIZED)
303 if not nmlib.notmuch_tags_valid(self._tags):
307 tag = Tags._get (self._tags)
308 nmlib.notmuch_tags_move_to_next(self._tags)
312 """str() of Tags() is a space separated list of tags
314 This iterates over the list of Tags and will therefore 'exhaust' Tags()
316 return " ".join(self)
319 """Close and free the notmuch tags"""
320 if self._tags is not None:
321 logging.debug("Freeing the Tags now")
322 nmlib.notmuch_tags_destroy (self._tags)
325 #------------------------------------------------------------------------------
326 class Messages(object):
327 """Wrapper around notmuch_messages_t"""
330 _get = nmlib.notmuch_messages_get
331 _get.restype = c_void_p
333 _collect_tags = nmlib.notmuch_messages_collect_tags
334 _collect_tags.restype = c_void_p
336 def __init__(self, msgs_p, parent=None):
338 msg_p is a pointer to an notmuch_messages_t Structure. If it is None,
339 we will raise an NotmuchError(STATUS.NULL_POINTER).
341 If passed the parent query this Messages() is derived from, it saves a
342 reference to it, so we can automatically delete the parent query object
343 once all derived objects are dead.
345 Messages() provides an iterator over all contained Message()s.
346 However, you will only be able to iterate over it once,
347 because the underlying C function only allows iterating once.
348 #TODO: make the iterator work more than once and cache the tags in
352 NotmuchError(STATUS.NULL_POINTER)
355 #store parent, so we keep them alive as long as self is alive
356 self._parent = parent
357 logging.debug("Inited Messages derived from %s" %(str(parent)))
359 def collect_tags(self):
360 """ return the Tags() belonging to the messages
362 Do note that collect_tags will iterate over the messages and
363 therefore will not allow further iterationsl
364 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
366 if self._msgs is None:
367 raise NotmuchError(STATUS.NOT_INITIALIZED)
369 # collect all tags (returns NULL on error)
370 tags_p = Messages._collect_tags (self._msgs)
371 #reset _msgs as we iterated over it and can do so only once
375 raise NotmuchError(STATUS.NULL_POINTER)
376 return Tags(tags_p, self)
379 """ Make Messages an iterator """
383 if self._msgs is None:
384 raise NotmuchError(STATUS.NOT_INITIALIZED)
386 if not nmlib.notmuch_messages_valid(self._msgs):
390 msg = Message(Messages._get (self._msgs), self)
391 nmlib.notmuch_messages_move_to_next(self._msgs)
395 """ Returns the number of contained messages
397 :note: As this iterates over the messages, we will not be able to
398 iterate over them again (as in retrieve them)!
400 if self._msgs is None:
401 raise NotmuchError(STATUS.NOT_INITIALIZED)
404 while nmlib.notmuch_messages_valid(self._msgs):
405 nmlib.notmuch_messages_move_to_next(self._msgs)
413 """Close and free the notmuch Messages"""
414 if self._msgs is not None:
415 logging.debug("Freeing the Messages now")
416 nmlib.notmuch_messages_destroy (self._msgs)
419 #------------------------------------------------------------------------------
420 class Message(object):
421 """Wrapper around notmuch_message_t"""
423 """notmuch_message_get_filename (notmuch_message_t *message)"""
424 _get_filename = nmlib.notmuch_message_get_filename
425 _get_filename.restype = c_char_p
426 """notmuch_message_get_message_id (notmuch_message_t *message)"""
427 _get_message_id = nmlib.notmuch_message_get_message_id
428 _get_message_id.restype = c_char_p
430 """notmuch_message_get_tags (notmuch_message_t *message)"""
431 _get_tags = nmlib.notmuch_message_get_tags
432 _get_tags.restype = c_void_p
434 _get_date = nmlib.notmuch_message_get_date
435 _get_date.restype = c_uint64
437 _get_header = nmlib.notmuch_message_get_header
438 _get_header.restype = c_char_p
440 def __init__(self, msg_p, parent=None):
442 msg_p is a pointer to an notmuch_message_t Structure. If it is None,
443 we will raise an NotmuchError(STATUS.NULL_POINTER).
445 Is a 'parent' object is passed which this message is derived from,
446 we save a reference to it, so we can automatically delete the parent
447 object once all derived objects are dead.
450 NotmuchError(STATUS.NULL_POINTER)
452 #keep reference to parent, so we keep it alive
453 self._parent = parent
454 logging.debug("Inited Message derived from %s" %(str(parent)))
457 def get_message_id(self):
458 """ return the msg id
460 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
462 if self._msg is None:
463 raise NotmuchError(STATUS.NOT_INITIALIZED)
464 return Message._get_message_id(self._msg)
467 """returns time_t of the message date
469 For the original textual representation of the Date header from the
470 message call notmuch_message_get_header() with a header value of
472 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
474 if self._msg is None:
475 raise NotmuchError(STATUS.NOT_INITIALIZED)
476 return Message._get_date(self._msg)
478 def get_header(self, header):
479 """ TODO document me"""
480 if self._msg is None:
481 raise NotmuchError(STATUS.NOT_INITIALIZED)
483 #Returns NULL if any error occurs.
484 header = Message._get_header (self._msg, header)
486 raise NotmuchError(STATUS.NULL_POINTER)
489 def get_filename(self):
490 """ return the msg filename
492 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
494 if self._msg is None:
495 raise NotmuchError(STATUS.NOT_INITIALIZED)
496 return Message._get_filename(self._msg)
499 """ return the msg tags
501 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
502 Raises NotmuchError(STATUS.NULL_POINTER) on error.
504 if self._msg is None:
505 raise NotmuchError(STATUS.NOT_INITIALIZED)
507 tags_p = Message._get_tags(self._msg)
509 raise NotmuchError(STATUS.NULL_POINTER)
510 return Tags(tags_p, self)
513 """A message() is represented by a 1-line summary"""
515 msg['from'] = self.get_header('from')
516 msg['tags'] = str(self.get_tags())
517 msg['date'] = date.fromtimestamp(self.get_date())
518 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
520 def format_as_text(self):
521 """ Output like notmuch show """
525 """Close and free the notmuch Message"""
526 if self._msg is not None:
527 logging.debug("Freeing the Message now")
528 nmlib.notmuch_message_destroy (self._msg)