2 This file is part of notmuch.
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 You should have received a copy of the GNU General Public License
15 along with notmuch. If not, see <http://www.gnu.org/licenses/>.
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
22 from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER
23 from .compat import SafeConfigParser
24 from .globals import (
39 ReadOnlyDatabaseError,
41 from .message import Message
43 from .query import Query
44 from .directory import Directory
46 class Database(object):
47 """The :class:`Database` is the highest-level object that notmuch
48 provides. It references a notmuch database, and can be opened in
49 read-only or read-write mode. A :class:`Query` can be derived from
50 or be applied to a specific database to find messages. Also adding
51 and removing messages to the database happens via this
52 object. Modifications to the database are not atmic by default (see
53 :meth:`begin_atomic`) and once a database has been modified, all
54 other database objects pointing to the same data-base will throw an
55 :exc:`XapianError` as the underlying database has been
56 modified. Close and reopen the database to continue working with it.
58 :class:`Database` objects implement the context manager protocol
59 so you can use the :keyword:`with` statement to ensure that the
60 database is properly closed. See :meth:`close` for more
65 Any function in this class can and will throw an
66 :exc:`NotInitializedError` if the database was not intitialized
70 """Class attribute to cache user's default database"""
72 MODE = Enum(['READ_ONLY', 'READ_WRITE'])
73 """Constants: Mode in which to open the database"""
75 """notmuch_database_get_directory"""
76 _get_directory = nmlib.notmuch_database_get_directory
77 _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)]
78 _get_directory.restype = c_uint
80 """notmuch_database_get_path"""
81 _get_path = nmlib.notmuch_database_get_path
82 _get_path.argtypes = [NotmuchDatabaseP]
83 _get_path.restype = c_char_p
85 """notmuch_database_get_version"""
86 _get_version = nmlib.notmuch_database_get_version
87 _get_version.argtypes = [NotmuchDatabaseP]
88 _get_version.restype = c_uint
90 """notmuch_database_open"""
91 _open = nmlib.notmuch_database_open
92 _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)]
93 _open.restype = c_uint
95 """notmuch_database_upgrade"""
96 _upgrade = nmlib.notmuch_database_upgrade
97 _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
98 _upgrade.restype = c_uint
100 """ notmuch_database_find_message"""
101 _find_message = nmlib.notmuch_database_find_message
102 _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
103 POINTER(NotmuchMessageP)]
104 _find_message.restype = c_uint
106 """notmuch_database_find_message_by_filename"""
107 _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
108 _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
109 POINTER(NotmuchMessageP)]
110 _find_message_by_filename.restype = c_uint
112 """notmuch_database_get_all_tags"""
113 _get_all_tags = nmlib.notmuch_database_get_all_tags
114 _get_all_tags.argtypes = [NotmuchDatabaseP]
115 _get_all_tags.restype = NotmuchTagsP
117 """notmuch_database_create"""
118 _create = nmlib.notmuch_database_create
119 _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
120 _create.restype = c_uint
122 def __init__(self, path = None, create = False,
123 mode = MODE.READ_ONLY):
124 """If *path* is `None`, we will try to read a users notmuch
125 configuration and use his configured database. The location of the
126 configuration file can be specified through the environment variable
127 *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
129 If *create* is `True`, the database will always be created in
130 :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
132 :param path: Directory to open/create the database in (see
133 above for behavior if `None`)
134 :type path: `str` or `None`
135 :param create: Pass `False` to open an existing, `True` to create a new
138 :param mode: Mode to open a database in. Is always
139 :attr:`MODE`.READ_WRITE when creating a new one.
140 :type mode: :attr:`MODE`
141 :raises: :exc:`NotmuchError` or derived exception in case of
147 # no path specified. use a user's default database
148 if Database._std_db_path is None:
149 #the following line throws a NotmuchError if it fails
150 Database._std_db_path = self._get_user_default_db()
151 path = Database._std_db_path
154 self.open(path, mode)
158 _destroy = nmlib.notmuch_database_destroy
159 _destroy.argtypes = [NotmuchDatabaseP]
160 _destroy.restype = c_uint
164 status = self._destroy(self._db)
165 if status != STATUS.SUCCESS:
166 raise NotmuchError(status)
168 def _assert_db_is_initialized(self):
169 """Raises :exc:`NotInitializedError` if self._db is `None`"""
171 raise NotInitializedError()
173 def create(self, path):
174 """Creates a new notmuch database
176 This function is used by __init__() and usually does not need
177 to be called directly. It wraps the underlying
178 *notmuch_database_create* function and creates a new notmuch
179 database at *path*. It will always return a database in :attr:`MODE`
180 .READ_WRITE mode as creating an empty database for
181 reading only does not make a great deal of sense.
183 :param path: A directory in which we should create the database.
185 :raises: :exc:`NotmuchError` in case of any failure
186 (possibly after printing an error message on stderr).
189 raise NotmuchError(message="Cannot create db, this Database() "
190 "already has an open one.")
192 db = NotmuchDatabaseP()
193 status = Database._create(_str(path), byref(db))
195 if status != STATUS.SUCCESS:
196 raise NotmuchError(status)
200 def open(self, path, mode=0):
201 """Opens an existing database
203 This function is used by __init__() and usually does not need
204 to be called directly. It wraps the underlying
205 *notmuch_database_open* function.
207 :param status: Open the database in read-only or read-write mode
208 :type status: :attr:`MODE`
209 :raises: Raises :exc:`NotmuchError` in case of any failure
210 (possibly after printing an error message on stderr).
212 db = NotmuchDatabaseP()
213 status = Database._open(_str(path), mode, byref(db))
215 if status != STATUS.SUCCESS:
216 raise NotmuchError(status)
220 _close = nmlib.notmuch_database_close
221 _close.argtypes = [NotmuchDatabaseP]
222 _close.restype = c_uint
226 Closes the notmuch database.
230 This function closes the notmuch database. From that point
231 on every method invoked on any object ever derived from
232 the closed database may cease to function and raise a
236 status = self._close(self._db)
237 if status != STATUS.SUCCESS:
238 raise NotmuchError(status)
242 Implements the context manager protocol.
246 def __exit__(self, exc_type, exc_value, traceback):
248 Implements the context manager protocol.
253 """Returns the file path of an open database"""
254 self._assert_db_is_initialized()
255 return Database._get_path(self._db).decode('utf-8')
257 def get_version(self):
258 """Returns the database format version
260 :returns: The database version as positive integer
262 self._assert_db_is_initialized()
263 return Database._get_version(self._db)
265 _needs_upgrade = nmlib.notmuch_database_needs_upgrade
266 _needs_upgrade.argtypes = [NotmuchDatabaseP]
267 _needs_upgrade.restype = bool
269 def needs_upgrade(self):
270 """Does this database need to be upgraded before writing to it?
272 If this function returns `True` then no functions that modify the
273 database (:meth:`add_message`,
274 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
275 etc.) will work unless :meth:`upgrade` is called successfully first.
277 :returns: `True` or `False`
279 self._assert_db_is_initialized()
280 return self._needs_upgrade(self._db)
283 """Upgrades the current database
285 After opening a database in read-write mode, the client should
286 check if an upgrade is needed (notmuch_database_needs_upgrade) and
287 if so, upgrade with this function before making any modifications.
289 NOT IMPLEMENTED: The optional progress_notify callback can be
290 used by the caller to provide progress indication to the
291 user. If non-NULL it will be called periodically with
292 'progress' as a floating-point value in the range of [0.0..1.0]
293 indicating the progress made so far in the upgrade process.
295 :TODO: catch exceptions, document return values and etc...
297 self._assert_db_is_initialized()
298 status = Database._upgrade(self._db, None, None)
299 #TODO: catch exceptions, document return values and etc
302 _begin_atomic = nmlib.notmuch_database_begin_atomic
303 _begin_atomic.argtypes = [NotmuchDatabaseP]
304 _begin_atomic.restype = c_uint
306 def begin_atomic(self):
307 """Begin an atomic database operation
309 Any modifications performed between a successful
310 :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
311 the database atomically. Note that, unlike a typical database
312 transaction, this only ensures atomicity, not durability;
313 neither begin nor end necessarily flush modifications to disk.
315 :returns: :attr:`STATUS`.SUCCESS or raises
316 :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
317 Xapian exception occurred; atomic section not entered.
319 *Added in notmuch 0.9*"""
320 self._assert_db_is_initialized()
321 status = self._begin_atomic(self._db)
322 if status != STATUS.SUCCESS:
323 raise NotmuchError(status)
326 _end_atomic = nmlib.notmuch_database_end_atomic
327 _end_atomic.argtypes = [NotmuchDatabaseP]
328 _end_atomic.restype = c_uint
330 def end_atomic(self):
331 """Indicate the end of an atomic database operation
333 See :meth:`begin_atomic` for details.
335 :returns: :attr:`STATUS`.SUCCESS or raises
339 :attr:`STATUS`.XAPIAN_EXCEPTION
340 A Xapian exception occurred; atomic section not
342 :attr:`STATUS`.UNBALANCED_ATOMIC:
343 end_atomic has been called more times than begin_atomic.
345 *Added in notmuch 0.9*"""
346 self._assert_db_is_initialized()
347 status = self._end_atomic(self._db)
348 if status != STATUS.SUCCESS:
349 raise NotmuchError(status)
352 def get_directory(self, path):
353 """Returns a :class:`Directory` of path,
355 :param path: An unicode string containing the path relative to the path
356 of database (see :meth:`get_path`), or else should be an absolute
357 path with initial components that match the path of 'database'.
358 :returns: :class:`Directory` or raises an exception.
359 :raises: :exc:`FileError` if path is not relative database or absolute
360 with initial components same as database.
362 self._assert_db_is_initialized()
364 # sanity checking if path is valid, and make path absolute
365 if path and path[0] == os.sep:
366 # we got an absolute path
367 if not path.startswith(self.get_path()):
368 # but its initial components are not equal to the db path
369 raise FileError('Database().get_directory() called '
370 'with a wrong absolute path')
373 #we got a relative path, make it absolute
374 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
376 dir_p = NotmuchDirectoryP()
377 status = Database._get_directory(self._db, _str(path), byref(dir_p))
379 if status != STATUS.SUCCESS:
380 raise NotmuchError(status)
384 # return the Directory, init it with the absolute path
385 return Directory(abs_dirpath, dir_p, self)
387 _add_message = nmlib.notmuch_database_add_message
388 _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
389 POINTER(NotmuchMessageP)]
390 _add_message.restype = c_uint
392 def add_message(self, filename, sync_maildir_flags=False):
393 """Adds a new message to the database
395 :param filename: should be a path relative to the path of the
396 open database (see :meth:`get_path`), or else should be an
397 absolute filename with initial components that match the
398 path of the database.
400 The file should be a single mail message (not a
401 multi-message mbox) that is expected to remain at its
402 current location, since the notmuch database will reference
403 the filename, and will not copy the entire contents of the
406 :param sync_maildir_flags: If the message contains Maildir
407 flags, we will -depending on the notmuch configuration- sync
408 those tags to initial notmuch tags, if set to `True`. It is
409 `False` by default to remain consistent with the libnotmuch
410 API. You might want to look into the underlying method
411 :meth:`Message.maildir_flags_to_tags`.
413 :returns: On success, we return
415 1) a :class:`Message` object that can be used for things
416 such as adding tags to the just-added message.
417 2) one of the following :attr:`STATUS` values:
419 :attr:`STATUS`.SUCCESS
420 Message successfully added to database.
421 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
422 Message has the same message ID as another message already
423 in the database. The new filename was successfully added
424 to the list of the filenames for the existing message.
426 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
428 :raises: Raises a :exc:`NotmuchError` with the following meaning.
429 If such an exception occurs, nothing was added to the database.
431 :attr:`STATUS`.FILE_ERROR
432 An error occurred trying to open the file, (such as
433 permission denied, or file not found, etc.).
434 :attr:`STATUS`.FILE_NOT_EMAIL
435 The contents of filename don't look like an email
437 :attr:`STATUS`.READ_ONLY_DATABASE
438 Database was opened in read-only mode so no message can
441 self._assert_db_is_initialized()
442 msg_p = NotmuchMessageP()
443 status = self._add_message(self._db, _str(filename), byref(msg_p))
445 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
446 raise NotmuchError(status)
448 #construct Message() and return
449 msg = Message(msg_p, self)
450 #automatic sync initial tags from Maildir flags
451 if sync_maildir_flags:
452 msg.maildir_flags_to_tags()
455 _remove_message = nmlib.notmuch_database_remove_message
456 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
457 _remove_message.restype = c_uint
459 def remove_message(self, filename):
460 """Removes a message (filename) from the given notmuch database
462 Note that only this particular filename association is removed from
463 the database. If the same message (as determined by the message ID)
464 is still available via other filenames, then the message will
465 persist in the database for those filenames. When the last filename
466 is removed for a particular message, the database content for that
467 message will be entirely removed.
469 :returns: A :attr:`STATUS` value with the following meaning:
471 :attr:`STATUS`.SUCCESS
472 The last filename was removed and the message was removed
474 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
475 This filename was removed but the message persists in the
476 database with at least one other filename.
478 :raises: Raises a :exc:`NotmuchError` with the following meaning.
479 If such an exception occurs, nothing was removed from the
482 :attr:`STATUS`.READ_ONLY_DATABASE
483 Database was opened in read-only mode so no message can be
486 self._assert_db_is_initialized()
487 return self._remove_message(self._db, _str(filename))
489 def find_message(self, msgid):
490 """Returns a :class:`Message` as identified by its message ID
492 Wraps the underlying *notmuch_database_find_message* function.
494 :param msgid: The message ID
495 :type msgid: unicode or str
496 :returns: :class:`Message` or `None` if no message is found.
498 :exc:`OutOfMemoryError`
499 If an Out-of-memory occured while constructing the message.
501 In case of a Xapian Exception. These exceptions
502 include "Database modified" situations, e.g. when the
503 notmuch database has been modified by another program
504 in the meantime. In this case, you should close and
505 reopen the database and retry.
506 :exc:`NotInitializedError` if
507 the database was not intitialized.
509 self._assert_db_is_initialized()
510 msg_p = NotmuchMessageP()
511 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
512 if status != STATUS.SUCCESS:
513 raise NotmuchError(status)
514 return msg_p and Message(msg_p, self) or None
516 def find_message_by_filename(self, filename):
517 """Find a message with the given filename
519 :returns: If the database contains a message with the given
520 filename, then a class:`Message:` is returned. This
521 function returns None if no message is found with the given
524 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
525 constructing the message.
526 :raises: :exc:`XapianError` in case of a Xapian Exception.
527 These exceptions include "Database modified"
528 situations, e.g. when the notmuch database has been
529 modified by another program in the meantime. In this
530 case, you should close and reopen the database and
532 :raises: :exc:`NotInitializedError` if the database was not
535 *Added in notmuch 0.9*"""
536 self._assert_db_is_initialized()
538 msg_p = NotmuchMessageP()
539 status = Database._find_message_by_filename(self._db, _str(filename),
541 if status != STATUS.SUCCESS:
542 raise NotmuchError(status)
543 return msg_p and Message(msg_p, self) or None
545 def get_all_tags(self):
546 """Returns :class:`Tags` with a list of all tags found in the database
548 :returns: :class:`Tags`
549 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
552 self._assert_db_is_initialized()
553 tags_p = Database._get_all_tags(self._db)
555 raise NullPointerError()
556 return Tags(tags_p, self)
558 def create_query(self, querystring):
559 """Returns a :class:`Query` derived from this database
561 This is a shorthand method for doing::
564 # Automatically frees the Database() when 'q' is deleted
566 q = Database(dbpath).create_query('from:"Biene Maja"')
568 # long version, which is functionally equivalent but will keep the
569 # Database in the 'db' variable around after we delete 'q':
571 db = Database(dbpath)
572 q = Query(db,'from:"Biene Maja"')
574 This function is a python extension and not in the underlying C API.
576 return Query(self, querystring)
579 return "'Notmuch DB " + self.get_path() + "'"
581 def _get_user_default_db(self):
582 """ Reads a user's notmuch config and returns his db location
584 Throws a NotmuchError if it cannot find it"""
585 config = SafeConfigParser()
586 conf_f = os.getenv('NOTMUCH_CONFIG',
587 os.path.expanduser('~/.notmuch-config'))
588 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
589 if not config.has_option('database', 'path'):
590 raise NotmuchError(message="No DB path specified"
591 " and no user default found")
592 return config.get('database', 'path')