]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/database.py
database: add n_d_index_file (deprecates n_d_add_message)
[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 <https://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 )
40 from .message import Message
41 from .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     _std_db_path = None
69     """Class attribute to cache user's default database"""
70
71     MODE = Enum(['READ_ONLY', 'READ_WRITE'])
72     """Constants: Mode in which to open the database"""
73
74     """notmuch_database_get_directory"""
75     _get_directory = nmlib.notmuch_database_get_directory
76     _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)]
77     _get_directory.restype = c_uint
78
79     """notmuch_database_get_path"""
80     _get_path = nmlib.notmuch_database_get_path
81     _get_path.argtypes = [NotmuchDatabaseP]
82     _get_path.restype = c_char_p
83
84     """notmuch_database_get_version"""
85     _get_version = nmlib.notmuch_database_get_version
86     _get_version.argtypes = [NotmuchDatabaseP]
87     _get_version.restype = c_uint
88
89     """notmuch_database_get_revision"""
90     _get_revision = nmlib.notmuch_database_get_revision
91     _get_revision.argtypes = [NotmuchDatabaseP, POINTER(c_char_p)]
92     _get_revision.restype = c_uint
93
94     """notmuch_database_open"""
95     _open = nmlib.notmuch_database_open
96     _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)]
97     _open.restype = c_uint
98
99     """notmuch_database_upgrade"""
100     _upgrade = nmlib.notmuch_database_upgrade
101     _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
102     _upgrade.restype = c_uint
103
104     """ notmuch_database_find_message"""
105     _find_message = nmlib.notmuch_database_find_message
106     _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
107                               POINTER(NotmuchMessageP)]
108     _find_message.restype = c_uint
109
110     """notmuch_database_find_message_by_filename"""
111     _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
112     _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
113                                           POINTER(NotmuchMessageP)]
114     _find_message_by_filename.restype = c_uint
115
116     """notmuch_database_get_all_tags"""
117     _get_all_tags = nmlib.notmuch_database_get_all_tags
118     _get_all_tags.argtypes = [NotmuchDatabaseP]
119     _get_all_tags.restype = NotmuchTagsP
120
121     """notmuch_database_create"""
122     _create = nmlib.notmuch_database_create
123     _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
124     _create.restype = c_uint
125
126     def __init__(self, path = None, create = False,
127                  mode = MODE.READ_ONLY):
128         """If *path* is `None`, we will try to read a users notmuch
129         configuration and use his configured database. The location of the
130         configuration file can be specified through the environment variable
131         *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
132
133         If *create* is `True`, the database will always be created in
134         :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
135
136         :param path:   Directory to open/create the database in (see
137                        above for behavior if `None`)
138         :type path:    `str` or `None`
139         :param create: Pass `False` to open an existing, `True` to create a new
140                        database.
141         :type create:  bool
142         :param mode:   Mode to open a database in. Is always
143                        :attr:`MODE`.READ_WRITE when creating a new one.
144         :type mode:    :attr:`MODE`
145         :raises: :exc:`NotmuchError` or derived exception in case of
146             failure.
147         """
148         self._db = None
149         self.mode = mode
150         if path is None:
151             # no path specified. use a user's default database
152             if Database._std_db_path is None:
153                 #the following line throws a NotmuchError if it fails
154                 Database._std_db_path = self._get_user_default_db()
155             path = Database._std_db_path
156
157         if create == False:
158             self.open(path, mode)
159         else:
160             self.create(path)
161
162     _destroy = nmlib.notmuch_database_destroy
163     _destroy.argtypes = [NotmuchDatabaseP]
164     _destroy.restype = c_uint
165
166     def __del__(self):
167         if self._db:
168             status = self._destroy(self._db)
169             if status != STATUS.SUCCESS:
170                 raise NotmuchError(status)
171
172     def _assert_db_is_initialized(self):
173         """Raises :exc:`NotInitializedError` if self._db is `None`"""
174         if not self._db:
175             raise NotInitializedError()
176
177     def create(self, path):
178         """Creates a new notmuch database
179
180         This function is used by __init__() and usually does not need
181         to be called directly. It wraps the underlying
182         *notmuch_database_create* function and creates a new notmuch
183         database at *path*. It will always return a database in :attr:`MODE`
184         .READ_WRITE mode as creating an empty database for
185         reading only does not make a great deal of sense.
186
187         :param path: A directory in which we should create the database.
188         :type path: str
189         :raises: :exc:`NotmuchError` in case of any failure
190                     (possibly after printing an error message on stderr).
191         """
192         if self._db:
193             raise NotmuchError(message="Cannot create db, this Database() "
194                                        "already has an open one.")
195
196         db = NotmuchDatabaseP()
197         status = Database._create(_str(path), byref(db))
198
199         if status != STATUS.SUCCESS:
200             raise NotmuchError(status)
201         self._db = db
202         return status
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         db = NotmuchDatabaseP()
217         status = Database._open(_str(path), mode, byref(db))
218
219         if status != STATUS.SUCCESS:
220             raise NotmuchError(status)
221         self._db = db
222         return status
223
224     _close = nmlib.notmuch_database_close
225     _close.argtypes = [NotmuchDatabaseP]
226     _close.restype = c_uint
227
228     def close(self):
229         '''
230         Closes the notmuch database.
231
232         .. warning::
233
234             This function closes the notmuch database. From that point
235             on every method invoked on any object ever derived from
236             the closed database may cease to function and raise a
237             NotmuchError.
238         '''
239         if self._db:
240             status = self._close(self._db)
241             if status != STATUS.SUCCESS:
242                 raise NotmuchError(status)
243
244     def __enter__(self):
245         '''
246         Implements the context manager protocol.
247         '''
248         return self
249
250     def __exit__(self, exc_type, exc_value, traceback):
251         '''
252         Implements the context manager protocol.
253         '''
254         self.close()
255
256     def get_path(self):
257         """Returns the file path of an open database"""
258         self._assert_db_is_initialized()
259         return Database._get_path(self._db).decode('utf-8')
260
261     def get_version(self):
262         """Returns the database format version
263
264         :returns: The database version as positive integer
265         """
266         self._assert_db_is_initialized()
267         return Database._get_version(self._db)
268
269     def get_revision (self):
270         """Returns the committed database revison and UUID
271
272         :returns: (revison, uuid) The database revision as a positive integer
273         and the UUID of the database.
274         """
275         self._assert_db_is_initialized()
276         uuid = c_char_p ()
277         revision = Database._get_revision(self._db, byref (uuid))
278         return (revision, uuid.value.decode ('utf-8'))
279
280     _needs_upgrade = nmlib.notmuch_database_needs_upgrade
281     _needs_upgrade.argtypes = [NotmuchDatabaseP]
282     _needs_upgrade.restype = bool
283
284     def needs_upgrade(self):
285         """Does this database need to be upgraded before writing to it?
286
287         If this function returns `True` then no functions that modify the
288         database (:meth:`index_file`,
289         :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
290         etc.) will work unless :meth:`upgrade` is called successfully first.
291
292         :returns: `True` or `False`
293         """
294         self._assert_db_is_initialized()
295         return self._needs_upgrade(self._db)
296
297     def upgrade(self):
298         """Upgrades the current database
299
300         After opening a database in read-write mode, the client should
301         check if an upgrade is needed (notmuch_database_needs_upgrade) and
302         if so, upgrade with this function before making any modifications.
303
304         NOT IMPLEMENTED: The optional progress_notify callback can be
305         used by the caller to provide progress indication to the
306         user. If non-NULL it will be called periodically with
307         'progress' as a floating-point value in the range of [0.0..1.0]
308         indicating the progress made so far in the upgrade process.
309
310         :TODO: catch exceptions, document return values and etc...
311         """
312         self._assert_db_is_initialized()
313         status = Database._upgrade(self._db, None, None)
314         #TODO: catch exceptions, document return values and etc
315         return status
316
317     _begin_atomic = nmlib.notmuch_database_begin_atomic
318     _begin_atomic.argtypes = [NotmuchDatabaseP]
319     _begin_atomic.restype = c_uint
320
321     def begin_atomic(self):
322         """Begin an atomic database operation
323
324         Any modifications performed between a successful
325         :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
326         the database atomically.  Note that, unlike a typical database
327         transaction, this only ensures atomicity, not durability;
328         neither begin nor end necessarily flush modifications to disk.
329
330         :returns: :attr:`STATUS`.SUCCESS or raises
331         :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
332                     Xapian exception occurred; atomic section not entered.
333
334         *Added in notmuch 0.9*"""
335         self._assert_db_is_initialized()
336         status = self._begin_atomic(self._db)
337         if status != STATUS.SUCCESS:
338             raise NotmuchError(status)
339         return status
340
341     _end_atomic = nmlib.notmuch_database_end_atomic
342     _end_atomic.argtypes = [NotmuchDatabaseP]
343     _end_atomic.restype = c_uint
344
345     def end_atomic(self):
346         """Indicate the end of an atomic database operation
347
348         See :meth:`begin_atomic` for details.
349
350         :returns: :attr:`STATUS`.SUCCESS or raises
351
352         :raises:
353             :exc:`NotmuchError`:
354                 :attr:`STATUS`.XAPIAN_EXCEPTION
355                     A Xapian exception occurred; atomic section not
356                     ended.
357                 :attr:`STATUS`.UNBALANCED_ATOMIC:
358                     end_atomic has been called more times than begin_atomic.
359
360         *Added in notmuch 0.9*"""
361         self._assert_db_is_initialized()
362         status = self._end_atomic(self._db)
363         if status != STATUS.SUCCESS:
364             raise NotmuchError(status)
365         return status
366
367     def get_directory(self, path):
368         """Returns a :class:`Directory` of path,
369
370         :param path: An unicode string containing the path relative to the path
371               of database (see :meth:`get_path`), or else should be an absolute
372               path with initial components that match the path of 'database'.
373         :returns: :class:`Directory` or raises an exception.
374         :raises: :exc:`FileError` if path is not relative database or absolute
375                  with initial components same as database.
376         """
377         self._assert_db_is_initialized()
378
379         # sanity checking if path is valid, and make path absolute
380         if path and path[0] == os.sep:
381             # we got an absolute path
382             if not path.startswith(self.get_path()):
383                 # but its initial components are not equal to the db path
384                 raise FileError('Database().get_directory() called '
385                                 'with a wrong absolute path')
386             abs_dirpath = path
387         else:
388             #we got a relative path, make it absolute
389             abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
390
391         dir_p = NotmuchDirectoryP()
392         status = Database._get_directory(self._db, _str(path), byref(dir_p))
393
394         if status != STATUS.SUCCESS:
395             raise NotmuchError(status)
396         if not dir_p:
397             return None
398
399         # return the Directory, init it with the absolute path
400         return Directory(abs_dirpath, dir_p, self)
401
402     _index_file = nmlib.notmuch_database_index_file
403     _index_file.argtypes = [NotmuchDatabaseP, c_char_p,
404                              c_void_p,
405                              POINTER(NotmuchMessageP)]
406     _index_file.restype = c_uint
407
408     def index_file(self, filename, sync_maildir_flags=False):
409         """Adds a new message to the database
410
411         :param filename: should be a path relative to the path of the
412             open database (see :meth:`get_path`), or else should be an
413             absolute filename with initial components that match the
414             path of the database.
415
416             The file should be a single mail message (not a
417             multi-message mbox) that is expected to remain at its
418             current location, since the notmuch database will reference
419             the filename, and will not copy the entire contents of the
420             file.
421
422         :param sync_maildir_flags: If the message contains Maildir
423             flags, we will -depending on the notmuch configuration- sync
424             those tags to initial notmuch tags, if set to `True`. It is
425             `False` by default to remain consistent with the libnotmuch
426             API. You might want to look into the underlying method
427             :meth:`Message.maildir_flags_to_tags`.
428
429         :returns: On success, we return
430
431            1) a :class:`Message` object that can be used for things
432               such as adding tags to the just-added message.
433            2) one of the following :attr:`STATUS` values:
434
435               :attr:`STATUS`.SUCCESS
436                   Message successfully added to database.
437               :attr:`STATUS`.DUPLICATE_MESSAGE_ID
438                   Message has the same message ID as another message already
439                   in the database. The new filename was successfully added
440                   to the list of the filenames for the existing message.
441
442         :rtype:   2-tuple(:class:`Message`, :attr:`STATUS`)
443
444         :raises: Raises a :exc:`NotmuchError` with the following meaning.
445               If such an exception occurs, nothing was added to the database.
446
447               :attr:`STATUS`.FILE_ERROR
448                       An error occurred trying to open the file, (such as
449                       permission denied, or file not found, etc.).
450               :attr:`STATUS`.FILE_NOT_EMAIL
451                       The contents of filename don't look like an email
452                       message.
453               :attr:`STATUS`.READ_ONLY_DATABASE
454                       Database was opened in read-only mode so no message can
455                       be added.
456         """
457         self._assert_db_is_initialized()
458         msg_p = NotmuchMessageP()
459         status = self._index_file(self._db, _str(filename), c_void_p(None), byref(msg_p))
460
461         if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
462             raise NotmuchError(status)
463
464         #construct Message() and return
465         msg = Message(msg_p, self)
466         #automatic sync initial tags from Maildir flags
467         if sync_maildir_flags:
468             msg.maildir_flags_to_tags()
469         return (msg, status)
470
471     def add_message(self, filename, sync_maildir_flags=False):
472         """Deprecated alias for :meth:`index_file`
473         """
474         self.index_file(self, filename, sync_maildir_flags=sync_maildir_flags)
475
476     _remove_message = nmlib.notmuch_database_remove_message
477     _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
478     _remove_message.restype = c_uint
479
480     def remove_message(self, filename):
481         """Removes a message (filename) from the given notmuch database
482
483         Note that only this particular filename association is removed from
484         the database. If the same message (as determined by the message ID)
485         is still available via other filenames, then the message will
486         persist in the database for those filenames. When the last filename
487         is removed for a particular message, the database content for that
488         message will be entirely removed.
489
490         :returns: A :attr:`STATUS` value with the following meaning:
491
492              :attr:`STATUS`.SUCCESS
493                The last filename was removed and the message was removed
494                from the database.
495              :attr:`STATUS`.DUPLICATE_MESSAGE_ID
496                This filename was removed but the message persists in the
497                database with at least one other filename.
498
499         :raises: Raises a :exc:`NotmuchError` with the following meaning.
500              If such an exception occurs, nothing was removed from the
501              database.
502
503              :attr:`STATUS`.READ_ONLY_DATABASE
504                Database was opened in read-only mode so no message can be
505                removed.
506         """
507         self._assert_db_is_initialized()
508         status = self._remove_message(self._db, _str(filename))
509         if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
510             raise NotmuchError(status)
511         return status
512
513     def find_message(self, msgid):
514         """Returns a :class:`Message` as identified by its message ID
515
516         Wraps the underlying *notmuch_database_find_message* function.
517
518         :param msgid: The message ID
519         :type msgid: unicode or str
520         :returns: :class:`Message` or `None` if no message is found.
521         :raises:
522             :exc:`OutOfMemoryError`
523                   If an Out-of-memory occured while constructing the message.
524             :exc:`XapianError`
525                   In case of a Xapian Exception. These exceptions
526                   include "Database modified" situations, e.g. when the
527                   notmuch database has been modified by another program
528                   in the meantime. In this case, you should close and
529                   reopen the database and retry.
530             :exc:`NotInitializedError` if
531                     the database was not intitialized.
532         """
533         self._assert_db_is_initialized()
534         msg_p = NotmuchMessageP()
535         status = Database._find_message(self._db, _str(msgid), byref(msg_p))
536         if status != STATUS.SUCCESS:
537             raise NotmuchError(status)
538         return msg_p and Message(msg_p, self) or None
539
540     def find_message_by_filename(self, filename):
541         """Find a message with the given filename
542
543         :returns: If the database contains a message with the given
544             filename, then a class:`Message:` is returned.  This
545             function returns None if no message is found with the given
546             filename.
547
548         :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
549                  constructing the message.
550         :raises: :exc:`XapianError` in case of a Xapian Exception.
551                  These exceptions include "Database modified"
552                  situations, e.g. when the notmuch database has been
553                  modified by another program in the meantime. In this
554                  case, you should close and reopen the database and
555                  retry.
556         :raises: :exc:`NotInitializedError` if the database was not
557                  intitialized.
558
559         *Added in notmuch 0.9*"""
560         self._assert_db_is_initialized()
561
562         msg_p = NotmuchMessageP()
563         status = Database._find_message_by_filename(self._db, _str(filename),
564                                                     byref(msg_p))
565         if status != STATUS.SUCCESS:
566             raise NotmuchError(status)
567         return msg_p and Message(msg_p, self) or None
568
569     def get_all_tags(self):
570         """Returns :class:`Tags` with a list of all tags found in the database
571
572         :returns: :class:`Tags`
573         :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
574                     on error
575         """
576         self._assert_db_is_initialized()
577         tags_p = Database._get_all_tags(self._db)
578         if not tags_p:
579             raise NullPointerError()
580         return Tags(tags_p, self)
581
582     def create_query(self, querystring):
583         """Returns a :class:`Query` derived from this database
584
585         This is a shorthand method for doing::
586
587           # short version
588           # Automatically frees the Database() when 'q' is deleted
589
590           q  = Database(dbpath).create_query('from:"Biene Maja"')
591
592           # long version, which is functionally equivalent but will keep the
593           # Database in the 'db' variable around after we delete 'q':
594
595           db = Database(dbpath)
596           q  = Query(db,'from:"Biene Maja"')
597
598         This function is a python extension and not in the underlying C API.
599         """
600         return Query(self, querystring)
601
602     """notmuch_database_status_string"""
603     _status_string = nmlib.notmuch_database_status_string
604     _status_string.argtypes = [NotmuchDatabaseP]
605     _status_string.restype = c_char_p
606
607     def status_string(self):
608         """Returns the status string of the database
609
610         This is sometimes used for additional error reporting
611         """
612         self._assert_db_is_initialized()
613         s = Database._status_string(self._db)
614         if s:
615             return s.decode('utf-8', 'ignore')
616         return s
617
618     def __repr__(self):
619         return "'Notmuch DB " + self.get_path() + "'"
620
621     def _get_user_default_db(self):
622         """ Reads a user's notmuch config and returns his db location
623
624         Throws a NotmuchError if it cannot find it"""
625         config = SafeConfigParser()
626         conf_f = os.getenv('NOTMUCH_CONFIG',
627                            os.path.expanduser('~/.notmuch-config'))
628         config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
629         if not config.has_option('database', 'path'):
630             raise NotmuchError(message="No DB path specified"
631                                        " and no user default found")
632         return config.get('database', 'path')