]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/database.py
5c62d4572308dac012e05767578e99850c76016a
[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 notmuch.globals import (
24     nmlib,
25     Enum,
26     _str,
27     NotmuchDatabaseP,
28     NotmuchDirectoryP,
29     NotmuchMessageP,
30     NotmuchTagsP,
31 )
32 from .errors import (
33     STATUS,
34     FileError,
35     NotmuchError,
36     NullPointerError,
37     NotInitializedError,
38     ReadOnlyDatabaseError,
39 )
40 from notmuch.message import Message
41 from notmuch.tag import Tags
42 from .query import Query
43 from .directory import Directory
44
45 class Database(object):
46     """The :class:`Database` is the highest-level object that notmuch
47     provides. It references a notmuch database, and can be opened in
48     read-only or read-write mode. A :class:`Query` can be derived from
49     or be applied to a specific database to find messages. Also adding
50     and removing messages to the database happens via this
51     object. Modifications to the database are not atmic by default (see
52     :meth:`begin_atomic`) and once a database has been modified, all
53     other database objects pointing to the same data-base will throw an
54     :exc:`XapianError` as the underlying database has been
55     modified. Close and reopen the database to continue working with it.
56
57     :class:`Database` objects implement the context manager protocol
58     so you can use the :keyword:`with` statement to ensure that the
59     database is properly closed. See :meth:`close` for more
60     information.
61
62     .. note::
63
64         Any function in this class can and will throw an
65         :exc:`NotInitializedError` if the database was not intitialized
66         properly.
67
68     .. note::
69
70         Do remember that as soon as we tear down (e.g. via `del db`) this
71         object, all underlying derived objects such as queries, threads,
72         messages, tags etc will be freed by the underlying library as well.
73         Accessing these objects will lead to segfaults and other unexpected
74         behavior. See above for more details.
75     """
76     _std_db_path = None
77     """Class attribute to cache user's default database"""
78
79     MODE = Enum(['READ_ONLY', 'READ_WRITE'])
80     """Constants: Mode in which to open the database"""
81
82     """notmuch_database_get_directory"""
83     _get_directory = nmlib.notmuch_database_get_directory
84     _get_directory.argtypes = [NotmuchDatabaseP, c_char_p]
85     _get_directory.restype = NotmuchDirectoryP
86
87     """notmuch_database_get_path"""
88     _get_path = nmlib.notmuch_database_get_path
89     _get_path.argtypes = [NotmuchDatabaseP]
90     _get_path.restype = c_char_p
91
92     """notmuch_database_get_version"""
93     _get_version = nmlib.notmuch_database_get_version
94     _get_version.argtypes = [NotmuchDatabaseP]
95     _get_version.restype = c_uint
96
97     """notmuch_database_open"""
98     _open = nmlib.notmuch_database_open
99     _open.argtypes = [c_char_p, c_uint]
100     _open.restype = NotmuchDatabaseP
101
102     """notmuch_database_upgrade"""
103     _upgrade = nmlib.notmuch_database_upgrade
104     _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
105     _upgrade.restype = c_uint
106
107     """ notmuch_database_find_message"""
108     _find_message = nmlib.notmuch_database_find_message
109     _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
110                               POINTER(NotmuchMessageP)]
111     _find_message.restype = c_uint
112
113     """notmuch_database_find_message_by_filename"""
114     _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
115     _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
116                                           POINTER(NotmuchMessageP)]
117     _find_message_by_filename.restype = c_uint
118
119     """notmuch_database_get_all_tags"""
120     _get_all_tags = nmlib.notmuch_database_get_all_tags
121     _get_all_tags.argtypes = [NotmuchDatabaseP]
122     _get_all_tags.restype = NotmuchTagsP
123
124     """notmuch_database_create"""
125     _create = nmlib.notmuch_database_create
126     _create.argtypes = [c_char_p]
127     _create.restype = NotmuchDatabaseP
128
129     def __init__(self, path = None, create = False,
130                  mode = MODE.READ_ONLY):
131         """If *path* is `None`, we will try to read a users notmuch
132         configuration and use his configured database. The location of the
133         configuration file can be specified through the environment variable
134         *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
135
136         If *create* is `True`, the database will always be created in
137         :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
138
139         :param path:   Directory to open/create the database in (see
140                        above for behavior if `None`)
141         :type path:    `str` or `None`
142         :param create: Pass `False` to open an existing, `True` to create a new
143                        database.
144         :type create:  bool
145         :param mode:   Mode to open a database in. Is always
146                        :attr:`MODE`.READ_WRITE when creating a new one.
147         :type mode:    :attr:`MODE`
148         :raises: :exc:`NotmuchError` or derived exception in case of
149             failure.
150         """
151         self._db = None
152         self.mode = mode
153         if path is None:
154             # no path specified. use a user's default database
155             if Database._std_db_path is None:
156                 #the following line throws a NotmuchError if it fails
157                 Database._std_db_path = self._get_user_default_db()
158             path = Database._std_db_path
159
160         if create == False:
161             self.open(path, mode)
162         else:
163             self.create(path)
164
165     _destroy = nmlib.notmuch_database_destroy
166     _destroy.argtypes = [NotmuchDatabaseP]
167     _destroy.restype = None
168
169     def __del__(self):
170         if self._db:
171             self._destroy(self._db)
172
173     def _assert_db_is_initialized(self):
174         """Raises :exc:`NotInitializedError` if self._db is `None`"""
175         if not self._db:
176             raise NotInitializedError()
177
178     def create(self, path):
179         """Creates a new notmuch database
180
181         This function is used by __init__() and usually does not need
182         to be called directly. It wraps the underlying
183         *notmuch_database_create* function and creates a new notmuch
184         database at *path*. It will always return a database in :attr:`MODE`
185         .READ_WRITE mode as creating an empty database for
186         reading only does not make a great deal of sense.
187
188         :param path: A directory in which we should create the database.
189         :type path: str
190         :raises: :exc:`NotmuchError` in case of any failure
191                     (possibly after printing an error message on stderr).
192         """
193         if self._db:
194             raise NotmuchError(message="Cannot create db, this Database() "
195                                        "already has an open one.")
196
197         res = Database._create(_str(path), Database.MODE.READ_WRITE)
198
199         if not res:
200             raise NotmuchError(
201                 message="Could not create the specified database")
202         self._db = res
203
204     def open(self, path, mode=0):
205         """Opens an existing database
206
207         This function is used by __init__() and usually does not need
208         to be called directly. It wraps the underlying
209         *notmuch_database_open* function.
210
211         :param status: Open the database in read-only or read-write mode
212         :type status:  :attr:`MODE`
213         :raises: Raises :exc:`NotmuchError` in case of any failure
214                     (possibly after printing an error message on stderr).
215         """
216         res = Database._open(_str(path), mode)
217
218         if not res:
219             raise NotmuchError(message="Could not open the specified database")
220         self._db = res
221
222     _close = nmlib.notmuch_database_close
223     _close.argtypes = [NotmuchDatabaseP]
224     _close.restype = None
225
226     def close(self):
227         '''
228         Closes the notmuch database.
229
230         .. warning::
231
232             This function closes the notmuch database. From that point
233             on every method invoked on any object ever derived from
234             the closed database may cease to function and raise a
235             NotmuchError.
236         '''
237         if self._db:
238             self._close(self._db)
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         (creating it if it does not exist(?))
355
356         :param path: An unicode string containing the path relative to the path
357               of database (see :meth:`get_path`), or else should be an absolute
358               path with initial components that match the path of 'database'.
359         :returns: :class:`Directory` or raises an exception.
360         :raises: :exc:`FileError` if path is not relative database or absolute
361                  with initial components same as database.
362         :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
363                  opened in read-write mode
364         """
365         self._assert_db_is_initialized()
366
367         # work around libnotmuch calling exit(3), see
368         # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
369         # TODO: remove once this issue is resolved
370         if self.mode != Database.MODE.READ_WRITE:
371             raise ReadOnlyDatabaseError('The database has to be opened in '
372                                         'read-write mode for get_directory')
373
374         # sanity checking if path is valid, and make path absolute
375         if path and path[0] == os.sep:
376             # we got an absolute path
377             if not path.startswith(self.get_path()):
378                 # but its initial components are not equal to the db path
379                 raise FileError('Database().get_directory() called '
380                                 'with a wrong absolute path')
381             abs_dirpath = path
382         else:
383             #we got a relative path, make it absolute
384             abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
385
386         dir_p = Database._get_directory(self._db, _str(path))
387
388         # return the Directory, init it with the absolute path
389         return Directory(abs_dirpath, dir_p, self)
390
391     _add_message = nmlib.notmuch_database_add_message
392     _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
393                              POINTER(NotmuchMessageP)]
394     _add_message.restype = c_uint
395
396     def add_message(self, filename, sync_maildir_flags=False):
397         """Adds a new message to the database
398
399         :param filename: should be a path relative to the path of the
400             open database (see :meth:`get_path`), or else should be an
401             absolute filename with initial components that match the
402             path of the database.
403
404             The file should be a single mail message (not a
405             multi-message mbox) that is expected to remain at its
406             current location, since the notmuch database will reference
407             the filename, and will not copy the entire contents of the
408             file.
409
410         :param sync_maildir_flags: If the message contains Maildir
411             flags, we will -depending on the notmuch configuration- sync
412             those tags to initial notmuch tags, if set to `True`. It is
413             `False` by default to remain consistent with the libnotmuch
414             API. You might want to look into the underlying method
415             :meth:`Message.maildir_flags_to_tags`.
416
417         :returns: On success, we return
418
419            1) a :class:`Message` object that can be used for things
420               such as adding tags to the just-added message.
421            2) one of the following :attr:`STATUS` values:
422
423               :attr:`STATUS`.SUCCESS
424                   Message successfully added to database.
425               :attr:`STATUS`.DUPLICATE_MESSAGE_ID
426                   Message has the same message ID as another message already
427                   in the database. The new filename was successfully added
428                   to the list of the filenames for the existing message.
429
430         :rtype:   2-tuple(:class:`Message`, :attr:`STATUS`)
431
432         :raises: Raises a :exc:`NotmuchError` with the following meaning.
433               If such an exception occurs, nothing was added to the database.
434
435               :attr:`STATUS`.FILE_ERROR
436                       An error occurred trying to open the file, (such as
437                       permission denied, or file not found, etc.).
438               :attr:`STATUS`.FILE_NOT_EMAIL
439                       The contents of filename don't look like an email
440                       message.
441               :attr:`STATUS`.READ_ONLY_DATABASE
442                       Database was opened in read-only mode so no message can
443                       be added.
444         """
445         self._assert_db_is_initialized()
446         msg_p = NotmuchMessageP()
447         status = self._add_message(self._db, _str(filename), byref(msg_p))
448
449         if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
450             raise NotmuchError(status)
451
452         #construct Message() and return
453         msg = Message(msg_p, self)
454         #automatic sync initial tags from Maildir flags
455         if sync_maildir_flags:
456             msg.maildir_flags_to_tags()
457         return (msg, status)
458
459     _remove_message = nmlib.notmuch_database_remove_message
460     _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
461     _remove_message.restype = c_uint
462
463     def remove_message(self, filename):
464         """Removes a message (filename) from the given notmuch database
465
466         Note that only this particular filename association is removed from
467         the database. If the same message (as determined by the message ID)
468         is still available via other filenames, then the message will
469         persist in the database for those filenames. When the last filename
470         is removed for a particular message, the database content for that
471         message will be entirely removed.
472
473         :returns: A :attr:`STATUS` value with the following meaning:
474
475              :attr:`STATUS`.SUCCESS
476                The last filename was removed and the message was removed
477                from the database.
478              :attr:`STATUS`.DUPLICATE_MESSAGE_ID
479                This filename was removed but the message persists in the
480                database with at least one other filename.
481
482         :raises: Raises a :exc:`NotmuchError` with the following meaning.
483              If such an exception occurs, nothing was removed from the
484              database.
485
486              :attr:`STATUS`.READ_ONLY_DATABASE
487                Database was opened in read-only mode so no message can be
488                removed.
489         """
490         self._assert_db_is_initialized()
491         return self._remove_message(self._db, _str(filename))
492
493     def find_message(self, msgid):
494         """Returns a :class:`Message` as identified by its message ID
495
496         Wraps the underlying *notmuch_database_find_message* function.
497
498         :param msgid: The message ID
499         :type msgid: unicode or str
500         :returns: :class:`Message` or `None` if no message is found.
501         :raises:
502             :exc:`OutOfMemoryError`
503                   If an Out-of-memory occured while constructing the message.
504             :exc:`XapianError`
505                   In case of a Xapian Exception. These exceptions
506                   include "Database modified" situations, e.g. when the
507                   notmuch database has been modified by another program
508                   in the meantime. In this case, you should close and
509                   reopen the database and retry.
510             :exc:`NotInitializedError` if
511                     the database was not intitialized.
512         """
513         self._assert_db_is_initialized()
514         msg_p = NotmuchMessageP()
515         status = Database._find_message(self._db, _str(msgid), byref(msg_p))
516         if status != STATUS.SUCCESS:
517             raise NotmuchError(status)
518         return msg_p and Message(msg_p, self) or None
519
520     def find_message_by_filename(self, filename):
521         """Find a message with the given filename
522
523         :returns: If the database contains a message with the given
524             filename, then a class:`Message:` is returned.  This
525             function returns None if no message is found with the given
526             filename.
527
528         :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
529                  constructing the message.
530         :raises: :exc:`XapianError` in case of a Xapian Exception.
531                  These exceptions include "Database modified"
532                  situations, e.g. when the notmuch database has been
533                  modified by another program in the meantime. In this
534                  case, you should close and reopen the database and
535                  retry.
536         :raises: :exc:`NotInitializedError` if the database was not
537                  intitialized.
538         :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
539                  opened in read-write mode
540
541         *Added in notmuch 0.9*"""
542         self._assert_db_is_initialized()
543
544         # work around libnotmuch calling exit(3), see
545         # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
546         # TODO: remove once this issue is resolved
547         if self.mode != Database.MODE.READ_WRITE:
548             raise ReadOnlyDatabaseError('The database has to be opened in '
549                                         'read-write mode for get_directory')
550
551         msg_p = NotmuchMessageP()
552         status = Database._find_message_by_filename(self._db, _str(filename),
553                                                     byref(msg_p))
554         if status != STATUS.SUCCESS:
555             raise NotmuchError(status)
556         return msg_p and Message(msg_p, self) or None
557
558     def get_all_tags(self):
559         """Returns :class:`Tags` with a list of all tags found in the database
560
561         :returns: :class:`Tags`
562         :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
563                     on error
564         """
565         self._assert_db_is_initialized()
566         tags_p = Database._get_all_tags(self._db)
567         if tags_p == None:
568             raise NullPointerError()
569         return Tags(tags_p, self)
570
571     def create_query(self, querystring):
572         """Returns a :class:`Query` derived from this database
573
574         This is a shorthand method for doing::
575
576           # short version
577           # Automatically frees the Database() when 'q' is deleted
578
579           q  = Database(dbpath).create_query('from:"Biene Maja"')
580
581           # long version, which is functionally equivalent but will keep the
582           # Database in the 'db' variable around after we delete 'q':
583
584           db = Database(dbpath)
585           q  = Query(db,'from:"Biene Maja"')
586
587         This function is a python extension and not in the underlying C API.
588         """
589         return Query(self, querystring)
590
591     def __repr__(self):
592         return "'Notmuch DB " + self.get_path() + "'"
593
594     def _get_user_default_db(self):
595         """ Reads a user's notmuch config and returns his db location
596
597         Throws a NotmuchError if it cannot find it"""
598         try:
599             # python3.x
600             from configparser import SafeConfigParser
601         except ImportError:
602             # python2.x
603             from ConfigParser import SafeConfigParser
604
605         config = SafeConfigParser()
606         conf_f = os.getenv('NOTMUCH_CONFIG',
607                            os.path.expanduser('~/.notmuch-config'))
608         config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
609         if not config.has_option('database', 'path'):
610             raise NotmuchError(message="No DB path specified"
611                                        " and no user default found")
612         return config.get('database', 'path')
613
614     @property
615     def db_p(self):
616         """Property returning a pointer to `notmuch_database_t` or `None`
617
618         This should normally not be needed by a user (and is not yet
619         guaranteed to remain stable in future versions).
620         """
621         return self._db