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 = None
164 self._destroy(self._db)
166 def _assert_db_is_initialized(self):
167 """Raises :exc:`NotInitializedError` if self._db is `None`"""
169 raise NotInitializedError()
171 def create(self, path):
172 """Creates a new notmuch database
174 This function is used by __init__() and usually does not need
175 to be called directly. It wraps the underlying
176 *notmuch_database_create* function and creates a new notmuch
177 database at *path*. It will always return a database in :attr:`MODE`
178 .READ_WRITE mode as creating an empty database for
179 reading only does not make a great deal of sense.
181 :param path: A directory in which we should create the database.
183 :raises: :exc:`NotmuchError` in case of any failure
184 (possibly after printing an error message on stderr).
187 raise NotmuchError(message="Cannot create db, this Database() "
188 "already has an open one.")
190 db = NotmuchDatabaseP()
191 status = Database._create(_str(path), Database.MODE.READ_WRITE, byref(db))
193 if status != STATUS.SUCCESS:
194 raise NotmuchError(status)
198 def open(self, path, mode=0):
199 """Opens an existing database
201 This function is used by __init__() and usually does not need
202 to be called directly. It wraps the underlying
203 *notmuch_database_open* function.
205 :param status: Open the database in read-only or read-write mode
206 :type status: :attr:`MODE`
207 :raises: Raises :exc:`NotmuchError` in case of any failure
208 (possibly after printing an error message on stderr).
210 db = NotmuchDatabaseP()
211 status = Database._open(_str(path), mode, byref(db))
213 if status != STATUS.SUCCESS:
214 raise NotmuchError(status)
218 _close = nmlib.notmuch_database_close
219 _close.argtypes = [NotmuchDatabaseP]
220 _close.restype = None
224 Closes the notmuch database.
228 This function closes the notmuch database. From that point
229 on every method invoked on any object ever derived from
230 the closed database may cease to function and raise a
234 self._close(self._db)
238 Implements the context manager protocol.
242 def __exit__(self, exc_type, exc_value, traceback):
244 Implements the context manager protocol.
249 """Returns the file path of an open database"""
250 self._assert_db_is_initialized()
251 return Database._get_path(self._db).decode('utf-8')
253 def get_version(self):
254 """Returns the database format version
256 :returns: The database version as positive integer
258 self._assert_db_is_initialized()
259 return Database._get_version(self._db)
261 _needs_upgrade = nmlib.notmuch_database_needs_upgrade
262 _needs_upgrade.argtypes = [NotmuchDatabaseP]
263 _needs_upgrade.restype = bool
265 def needs_upgrade(self):
266 """Does this database need to be upgraded before writing to it?
268 If this function returns `True` then no functions that modify the
269 database (:meth:`add_message`,
270 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
271 etc.) will work unless :meth:`upgrade` is called successfully first.
273 :returns: `True` or `False`
275 self._assert_db_is_initialized()
276 return self._needs_upgrade(self._db)
279 """Upgrades the current database
281 After opening a database in read-write mode, the client should
282 check if an upgrade is needed (notmuch_database_needs_upgrade) and
283 if so, upgrade with this function before making any modifications.
285 NOT IMPLEMENTED: The optional progress_notify callback can be
286 used by the caller to provide progress indication to the
287 user. If non-NULL it will be called periodically with
288 'progress' as a floating-point value in the range of [0.0..1.0]
289 indicating the progress made so far in the upgrade process.
291 :TODO: catch exceptions, document return values and etc...
293 self._assert_db_is_initialized()
294 status = Database._upgrade(self._db, None, None)
295 #TODO: catch exceptions, document return values and etc
298 _begin_atomic = nmlib.notmuch_database_begin_atomic
299 _begin_atomic.argtypes = [NotmuchDatabaseP]
300 _begin_atomic.restype = c_uint
302 def begin_atomic(self):
303 """Begin an atomic database operation
305 Any modifications performed between a successful
306 :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
307 the database atomically. Note that, unlike a typical database
308 transaction, this only ensures atomicity, not durability;
309 neither begin nor end necessarily flush modifications to disk.
311 :returns: :attr:`STATUS`.SUCCESS or raises
312 :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
313 Xapian exception occurred; atomic section not entered.
315 *Added in notmuch 0.9*"""
316 self._assert_db_is_initialized()
317 status = self._begin_atomic(self._db)
318 if status != STATUS.SUCCESS:
319 raise NotmuchError(status)
322 _end_atomic = nmlib.notmuch_database_end_atomic
323 _end_atomic.argtypes = [NotmuchDatabaseP]
324 _end_atomic.restype = c_uint
326 def end_atomic(self):
327 """Indicate the end of an atomic database operation
329 See :meth:`begin_atomic` for details.
331 :returns: :attr:`STATUS`.SUCCESS or raises
335 :attr:`STATUS`.XAPIAN_EXCEPTION
336 A Xapian exception occurred; atomic section not
338 :attr:`STATUS`.UNBALANCED_ATOMIC:
339 end_atomic has been called more times than begin_atomic.
341 *Added in notmuch 0.9*"""
342 self._assert_db_is_initialized()
343 status = self._end_atomic(self._db)
344 if status != STATUS.SUCCESS:
345 raise NotmuchError(status)
348 def get_directory(self, path):
349 """Returns a :class:`Directory` of path,
350 (creating it if it does not exist(?))
352 :param path: An unicode string containing the path relative to the path
353 of database (see :meth:`get_path`), or else should be an absolute
354 path with initial components that match the path of 'database'.
355 :returns: :class:`Directory` or raises an exception.
356 :raises: :exc:`FileError` if path is not relative database or absolute
357 with initial components same as database.
358 :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
359 opened in read-write mode
361 self._assert_db_is_initialized()
363 # sanity checking if path is valid, and make path absolute
364 if path and path[0] == os.sep:
365 # we got an absolute path
366 if not path.startswith(self.get_path()):
367 # but its initial components are not equal to the db path
368 raise FileError('Database().get_directory() called '
369 'with a wrong absolute path')
372 #we got a relative path, make it absolute
373 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
375 dir_p = NotmuchDirectoryP()
376 status = Database._get_directory(self._db, _str(path), byref(dir_p))
378 if status != STATUS.SUCCESS:
379 raise NotmuchError(status)
383 # return the Directory, init it with the absolute path
384 return Directory(abs_dirpath, dir_p, self)
386 _add_message = nmlib.notmuch_database_add_message
387 _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
388 POINTER(NotmuchMessageP)]
389 _add_message.restype = c_uint
391 def add_message(self, filename, sync_maildir_flags=False):
392 """Adds a new message to the database
394 :param filename: should be a path relative to the path of the
395 open database (see :meth:`get_path`), or else should be an
396 absolute filename with initial components that match the
397 path of the database.
399 The file should be a single mail message (not a
400 multi-message mbox) that is expected to remain at its
401 current location, since the notmuch database will reference
402 the filename, and will not copy the entire contents of the
405 :param sync_maildir_flags: If the message contains Maildir
406 flags, we will -depending on the notmuch configuration- sync
407 those tags to initial notmuch tags, if set to `True`. It is
408 `False` by default to remain consistent with the libnotmuch
409 API. You might want to look into the underlying method
410 :meth:`Message.maildir_flags_to_tags`.
412 :returns: On success, we return
414 1) a :class:`Message` object that can be used for things
415 such as adding tags to the just-added message.
416 2) one of the following :attr:`STATUS` values:
418 :attr:`STATUS`.SUCCESS
419 Message successfully added to database.
420 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
421 Message has the same message ID as another message already
422 in the database. The new filename was successfully added
423 to the list of the filenames for the existing message.
425 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
427 :raises: Raises a :exc:`NotmuchError` with the following meaning.
428 If such an exception occurs, nothing was added to the database.
430 :attr:`STATUS`.FILE_ERROR
431 An error occurred trying to open the file, (such as
432 permission denied, or file not found, etc.).
433 :attr:`STATUS`.FILE_NOT_EMAIL
434 The contents of filename don't look like an email
436 :attr:`STATUS`.READ_ONLY_DATABASE
437 Database was opened in read-only mode so no message can
440 self._assert_db_is_initialized()
441 msg_p = NotmuchMessageP()
442 status = self._add_message(self._db, _str(filename), byref(msg_p))
444 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
445 raise NotmuchError(status)
447 #construct Message() and return
448 msg = Message(msg_p, self)
449 #automatic sync initial tags from Maildir flags
450 if sync_maildir_flags:
451 msg.maildir_flags_to_tags()
454 _remove_message = nmlib.notmuch_database_remove_message
455 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
456 _remove_message.restype = c_uint
458 def remove_message(self, filename):
459 """Removes a message (filename) from the given notmuch database
461 Note that only this particular filename association is removed from
462 the database. If the same message (as determined by the message ID)
463 is still available via other filenames, then the message will
464 persist in the database for those filenames. When the last filename
465 is removed for a particular message, the database content for that
466 message will be entirely removed.
468 :returns: A :attr:`STATUS` value with the following meaning:
470 :attr:`STATUS`.SUCCESS
471 The last filename was removed and the message was removed
473 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
474 This filename was removed but the message persists in the
475 database with at least one other filename.
477 :raises: Raises a :exc:`NotmuchError` with the following meaning.
478 If such an exception occurs, nothing was removed from the
481 :attr:`STATUS`.READ_ONLY_DATABASE
482 Database was opened in read-only mode so no message can be
485 self._assert_db_is_initialized()
486 return self._remove_message(self._db, _str(filename))
488 def find_message(self, msgid):
489 """Returns a :class:`Message` as identified by its message ID
491 Wraps the underlying *notmuch_database_find_message* function.
493 :param msgid: The message ID
494 :type msgid: unicode or str
495 :returns: :class:`Message` or `None` if no message is found.
497 :exc:`OutOfMemoryError`
498 If an Out-of-memory occured while constructing the message.
500 In case of a Xapian Exception. These exceptions
501 include "Database modified" situations, e.g. when the
502 notmuch database has been modified by another program
503 in the meantime. In this case, you should close and
504 reopen the database and retry.
505 :exc:`NotInitializedError` if
506 the database was not intitialized.
508 self._assert_db_is_initialized()
509 msg_p = NotmuchMessageP()
510 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
511 if status != STATUS.SUCCESS:
512 raise NotmuchError(status)
513 return msg_p and Message(msg_p, self) or None
515 def find_message_by_filename(self, filename):
516 """Find a message with the given filename
518 :returns: If the database contains a message with the given
519 filename, then a class:`Message:` is returned. This
520 function returns None if no message is found with the given
523 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
524 constructing the message.
525 :raises: :exc:`XapianError` in case of a Xapian Exception.
526 These exceptions include "Database modified"
527 situations, e.g. when the notmuch database has been
528 modified by another program in the meantime. In this
529 case, you should close and reopen the database and
531 :raises: :exc:`NotInitializedError` if the database was not
533 :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
534 opened in read-write mode
536 *Added in notmuch 0.9*"""
537 self._assert_db_is_initialized()
539 # work around libnotmuch calling exit(3), see
540 # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
541 # TODO: remove once this issue is resolved
542 if self.mode != Database.MODE.READ_WRITE:
543 raise ReadOnlyDatabaseError('The database has to be opened in '
544 'read-write mode for get_directory')
546 msg_p = NotmuchMessageP()
547 status = Database._find_message_by_filename(self._db, _str(filename),
549 if status != STATUS.SUCCESS:
550 raise NotmuchError(status)
551 return msg_p and Message(msg_p, self) or None
553 def get_all_tags(self):
554 """Returns :class:`Tags` with a list of all tags found in the database
556 :returns: :class:`Tags`
557 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
560 self._assert_db_is_initialized()
561 tags_p = Database._get_all_tags(self._db)
563 raise NullPointerError()
564 return Tags(tags_p, self)
566 def create_query(self, querystring):
567 """Returns a :class:`Query` derived from this database
569 This is a shorthand method for doing::
572 # Automatically frees the Database() when 'q' is deleted
574 q = Database(dbpath).create_query('from:"Biene Maja"')
576 # long version, which is functionally equivalent but will keep the
577 # Database in the 'db' variable around after we delete 'q':
579 db = Database(dbpath)
580 q = Query(db,'from:"Biene Maja"')
582 This function is a python extension and not in the underlying C API.
584 return Query(self, querystring)
587 return "'Notmuch DB " + self.get_path() + "'"
589 def _get_user_default_db(self):
590 """ Reads a user's notmuch config and returns his db location
592 Throws a NotmuchError if it cannot find it"""
593 config = SafeConfigParser()
594 conf_f = os.getenv('NOTMUCH_CONFIG',
595 os.path.expanduser('~/.notmuch-config'))
596 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
597 if not config.has_option('database', 'path'):
598 raise NotmuchError(message="No DB path specified"
599 " and no user default found")
600 return config.get('database', 'path')
604 """Property returning a pointer to `notmuch_database_t` or `None`
606 This should normally not be needed by a user (and is not yet
607 guaranteed to remain stable in future versions).