]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/database.py
Merge tag '0.18.2_rc1'
[notmuch] / bindings / python / notmuch / database.py
1 """
2 This file is part of notmuch.
3
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.
8
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
12 for more details.
13
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/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
18 """
19
20 import os
21 import codecs
22 from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER
23 from .compat import SafeConfigParser
24 from .globals import (
25     nmlib,
26     Enum,
27     _str,
28     NotmuchDatabaseP,
29     NotmuchDirectoryP,
30     NotmuchMessageP,
31     NotmuchTagsP,
32 )
33 from .errors import (
34     STATUS,
35     FileError,
36     NotmuchError,
37     NullPointerError,
38     NotInitializedError,
39     ReadOnlyDatabaseError,
40 )
41 from .message import Message
42 from .tag import Tags
43 from .query import Query
44 from .directory import Directory
45
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.
57
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
61     information.
62
63     .. note::
64
65         Any function in this class can and will throw an
66         :exc:`NotInitializedError` if the database was not intitialized
67         properly.
68     """
69     _std_db_path = None
70     """Class attribute to cache user's default database"""
71
72     MODE = Enum(['READ_ONLY', 'READ_WRITE'])
73     """Constants: Mode in which to open the database"""
74
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
79
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
84
85     """notmuch_database_get_version"""
86     _get_version = nmlib.notmuch_database_get_version
87     _get_version.argtypes = [NotmuchDatabaseP]
88     _get_version.restype = c_uint
89
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
94
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
99
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
105
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
111
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
116
117     """notmuch_database_create"""
118     _create = nmlib.notmuch_database_create
119     _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
120     _create.restype = c_uint
121
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`.
128
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.
131
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
136                        database.
137         :type create:  bool
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
142             failure.
143         """
144         self._db = None
145         self.mode = mode
146         if path is None:
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
152
153         if create == False:
154             self.open(path, mode)
155         else:
156             self.create(path)
157
158     _destroy = nmlib.notmuch_database_destroy
159     _destroy.argtypes = [NotmuchDatabaseP]
160     _destroy.restype = c_uint
161
162     def __del__(self):
163         if self._db:
164             status = self._destroy(self._db)
165             if status != STATUS.SUCCESS:
166                 raise NotmuchError(status)
167
168     def _assert_db_is_initialized(self):
169         """Raises :exc:`NotInitializedError` if self._db is `None`"""
170         if not self._db:
171             raise NotInitializedError()
172
173     def create(self, path):
174         """Creates a new notmuch database
175
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.
182
183         :param path: A directory in which we should create the database.
184         :type path: str
185         :raises: :exc:`NotmuchError` in case of any failure
186                     (possibly after printing an error message on stderr).
187         """
188         if self._db:
189             raise NotmuchError(message="Cannot create db, this Database() "
190                                        "already has an open one.")
191
192         db = NotmuchDatabaseP()
193         status = Database._create(_str(path), byref(db))
194
195         if status != STATUS.SUCCESS:
196             raise NotmuchError(status)
197         self._db = db
198         return status
199
200     def open(self, path, mode=0):
201         """Opens an existing database
202
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.
206
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).
211         """
212         db = NotmuchDatabaseP()
213         status = Database._open(_str(path), mode, byref(db))
214
215         if status != STATUS.SUCCESS:
216             raise NotmuchError(status)
217         self._db = db
218         return status
219
220     _close = nmlib.notmuch_database_close
221     _close.argtypes = [NotmuchDatabaseP]
222     _close.restype = c_uint
223
224     def close(self):
225         '''
226         Closes the notmuch database.
227
228         .. warning::
229
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
233             NotmuchError.
234         '''
235         if self._db:
236             status = self._close(self._db)
237             if status != STATUS.SUCCESS:
238                 raise NotmuchError(status)
239
240     def __enter__(self):
241         '''
242         Implements the context manager protocol.
243         '''
244         return self
245
246     def __exit__(self, exc_type, exc_value, traceback):
247         '''
248         Implements the context manager protocol.
249         '''
250         self.close()
251
252     def get_path(self):
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')
256
257     def get_version(self):
258         """Returns the database format version
259
260         :returns: The database version as positive integer
261         """
262         self._assert_db_is_initialized()
263         return Database._get_version(self._db)
264
265     _needs_upgrade = nmlib.notmuch_database_needs_upgrade
266     _needs_upgrade.argtypes = [NotmuchDatabaseP]
267     _needs_upgrade.restype = bool
268
269     def needs_upgrade(self):
270         """Does this database need to be upgraded before writing to it?
271
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.
276
277         :returns: `True` or `False`
278         """
279         self._assert_db_is_initialized()
280         return self._needs_upgrade(self._db)
281
282     def upgrade(self):
283         """Upgrades the current database
284
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.
288
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.
294
295         :TODO: catch exceptions, document return values and etc...
296         """
297         self._assert_db_is_initialized()
298         status = Database._upgrade(self._db, None, None)
299         #TODO: catch exceptions, document return values and etc
300         return status
301
302     _begin_atomic = nmlib.notmuch_database_begin_atomic
303     _begin_atomic.argtypes = [NotmuchDatabaseP]
304     _begin_atomic.restype = c_uint
305
306     def begin_atomic(self):
307         """Begin an atomic database operation
308
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.
314
315         :returns: :attr:`STATUS`.SUCCESS or raises
316         :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
317                     Xapian exception occurred; atomic section not entered.
318
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)
324         return status
325
326     _end_atomic = nmlib.notmuch_database_end_atomic
327     _end_atomic.argtypes = [NotmuchDatabaseP]
328     _end_atomic.restype = c_uint
329
330     def end_atomic(self):
331         """Indicate the end of an atomic database operation
332
333         See :meth:`begin_atomic` for details.
334
335         :returns: :attr:`STATUS`.SUCCESS or raises
336
337         :raises:
338             :exc:`NotmuchError`:
339                 :attr:`STATUS`.XAPIAN_EXCEPTION
340                     A Xapian exception occurred; atomic section not
341                     ended.
342                 :attr:`STATUS`.UNBALANCED_ATOMIC:
343                     end_atomic has been called more times than begin_atomic.
344
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)
350         return status
351
352     def get_directory(self, path):
353         """Returns a :class:`Directory` of path,
354
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.
361         """
362         self._assert_db_is_initialized()
363
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')
371             abs_dirpath = path
372         else:
373             #we got a relative path, make it absolute
374             abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
375
376         dir_p = NotmuchDirectoryP()
377         status = Database._get_directory(self._db, _str(path), byref(dir_p))
378
379         if status != STATUS.SUCCESS:
380             raise NotmuchError(status)
381         if not dir_p:
382             return None
383
384         # return the Directory, init it with the absolute path
385         return Directory(abs_dirpath, dir_p, self)
386
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
391
392     def add_message(self, filename, sync_maildir_flags=False):
393         """Adds a new message to the database
394
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.
399
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
404             file.
405
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`.
412
413         :returns: On success, we return
414
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:
418
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.
425
426         :rtype:   2-tuple(:class:`Message`, :attr:`STATUS`)
427
428         :raises: Raises a :exc:`NotmuchError` with the following meaning.
429               If such an exception occurs, nothing was added to the database.
430
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
436                       message.
437               :attr:`STATUS`.READ_ONLY_DATABASE
438                       Database was opened in read-only mode so no message can
439                       be added.
440         """
441         self._assert_db_is_initialized()
442         msg_p = NotmuchMessageP()
443         status = self._add_message(self._db, _str(filename), byref(msg_p))
444
445         if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
446             raise NotmuchError(status)
447
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()
453         return (msg, status)
454
455     _remove_message = nmlib.notmuch_database_remove_message
456     _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
457     _remove_message.restype = c_uint
458
459     def remove_message(self, filename):
460         """Removes a message (filename) from the given notmuch database
461
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.
468
469         :returns: A :attr:`STATUS` value with the following meaning:
470
471              :attr:`STATUS`.SUCCESS
472                The last filename was removed and the message was removed
473                from the database.
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.
477
478         :raises: Raises a :exc:`NotmuchError` with the following meaning.
479              If such an exception occurs, nothing was removed from the
480              database.
481
482              :attr:`STATUS`.READ_ONLY_DATABASE
483                Database was opened in read-only mode so no message can be
484                removed.
485         """
486         self._assert_db_is_initialized()
487         return self._remove_message(self._db, _str(filename))
488
489     def find_message(self, msgid):
490         """Returns a :class:`Message` as identified by its message ID
491
492         Wraps the underlying *notmuch_database_find_message* function.
493
494         :param msgid: The message ID
495         :type msgid: unicode or str
496         :returns: :class:`Message` or `None` if no message is found.
497         :raises:
498             :exc:`OutOfMemoryError`
499                   If an Out-of-memory occured while constructing the message.
500             :exc:`XapianError`
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.
508         """
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
515
516     def find_message_by_filename(self, filename):
517         """Find a message with the given filename
518
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
522             filename.
523
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
531                  retry.
532         :raises: :exc:`NotInitializedError` if the database was not
533                  intitialized.
534
535         *Added in notmuch 0.9*"""
536         self._assert_db_is_initialized()
537
538         msg_p = NotmuchMessageP()
539         status = Database._find_message_by_filename(self._db, _str(filename),
540                                                     byref(msg_p))
541         if status != STATUS.SUCCESS:
542             raise NotmuchError(status)
543         return msg_p and Message(msg_p, self) or None
544
545     def get_all_tags(self):
546         """Returns :class:`Tags` with a list of all tags found in the database
547
548         :returns: :class:`Tags`
549         :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
550                     on error
551         """
552         self._assert_db_is_initialized()
553         tags_p = Database._get_all_tags(self._db)
554         if not tags_p:
555             raise NullPointerError()
556         return Tags(tags_p, self)
557
558     def create_query(self, querystring):
559         """Returns a :class:`Query` derived from this database
560
561         This is a shorthand method for doing::
562
563           # short version
564           # Automatically frees the Database() when 'q' is deleted
565
566           q  = Database(dbpath).create_query('from:"Biene Maja"')
567
568           # long version, which is functionally equivalent but will keep the
569           # Database in the 'db' variable around after we delete 'q':
570
571           db = Database(dbpath)
572           q  = Query(db,'from:"Biene Maja"')
573
574         This function is a python extension and not in the underlying C API.
575         """
576         return Query(self, querystring)
577
578     def __repr__(self):
579         return "'Notmuch DB " + self.get_path() + "'"
580
581     def _get_user_default_db(self):
582         """ Reads a user's notmuch config and returns his db location
583
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')