]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/database.py
321b9611ac2cf83af5a752040c87afba5087944a
[notmuch] / cnotmuch / database.py
1 import ctypes, os
2 from ctypes import c_int, c_char_p, c_void_p, c_uint, byref
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
4 from cnotmuch.thread import Threads
5 from cnotmuch.message import Messages
6 from cnotmuch.tag import Tags
7
8 class Database(object):
9     """Represents a notmuch database (wraps notmuch_database_t)
10
11     .. note:: Do remember that as soon as we tear down this object,
12            all underlying derived objects such as queries, threads,
13            messages, tags etc will be freed by the underlying library
14            as well. Accessing these objects will lead to segfaults and
15            other unexpected behavior. See above for more details.
16     """
17     _std_db_path = None
18     """Class attribute to cache user's default database"""
19
20     MODE = Enum(['READ_ONLY','READ_WRITE'])
21     """Constants: Mode in which to open the database"""
22
23     """notmuch_database_get_path (notmuch_database_t *database)"""
24     _get_path = nmlib.notmuch_database_get_path
25     _get_path.restype = c_char_p
26
27     """notmuch_database_get_version"""
28     _get_version = nmlib.notmuch_database_get_version
29     _get_version.restype = c_uint
30
31     """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
32     _open = nmlib.notmuch_database_open 
33     _open.restype = c_void_p
34
35     """ notmuch_database_find_message """
36     _find_message = nmlib.notmuch_database_find_message
37     _find_message.restype = c_void_p
38
39     """notmuch_database_get_all_tags (notmuch_database_t *database)"""
40     _get_all_tags = nmlib.notmuch_database_get_all_tags
41     _get_all_tags.restype = c_void_p
42
43     """ notmuch_database_create(const char *path):"""
44     _create = nmlib.notmuch_database_create
45     _create.restype = c_void_p
46
47     def __init__(self, path=None, create=False, mode= 0):
48         """If *path* is *None*, we will try to read a users notmuch 
49         configuration and use his configured database. The location of the 
50         configuration file can be specified through the environment variable
51         *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
52
53         If *create* is `True`, the database will always be created in
54         :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
55
56         :param path:   Directory to open/create the database in (see
57                        above for behavior if `None`)
58         :type path:    `str` or `None`
59         :param create: Pass `False` to open an existing, `True` to create a new
60                        database.  
61         :type create:  bool
62         :param mode:   Mode to open a database in. Is always 
63                        :attr:`MODE`.READ_WRITE when creating a new one.
64         :type mode:    :attr:`MODE`
65         :returns:      Nothing
66         :exception:    :exc:`NotmuchError` in case of failure.
67         """
68         self._db = None
69         if path is None:
70             # no path specified. use a user's default database
71             if Database._std_db_path is None:
72                 #the following line throws a NotmuchError if it fails
73                 Database._std_db_path = self._get_user_default_db()
74             path = Database._std_db_path
75
76         if create == False:
77             self.open(path, mode)
78         else:
79             self.create(path)
80
81     def _verify_initialized_db(self):
82         """Raises a NotmuchError in case self._db is still None"""
83         if self._db is None:
84             raise NotmuchError(STATUS.NOT_INITIALIZED)            
85
86     def create(self, path):
87         """Creates a new notmuch database
88
89         This function is used by __init__() and usually does not need
90         to be called directly. It wraps the underlying
91         *notmuch_database_create* function and creates a new notmuch
92         database at *path*. It will always return a database in
93         :attr:`MODE`.READ_WRITE mode as creating an empty database for
94         reading only does not make a great deal of sense.
95
96         :param path: A directory in which we should create the database.
97         :type path: str
98         :returns: Nothing
99         :exception: :exc:`NotmuchError` in case of any failure
100                     (after printing an error message on stderr).
101         """
102         if self._db is not None:
103             raise NotmuchError(
104             message="Cannot create db, this Database() already has an open one.")
105
106         res = Database._create(path, Database.MODE.READ_WRITE)
107
108         if res is None:
109             raise NotmuchError(
110                 message="Could not create the specified database")
111         self._db = res
112
113     def open(self, path, mode= 0): 
114         """Opens an existing database
115
116         This function is used by __init__() and usually does not need
117         to be called directly. It wraps the underlying
118         *notmuch_database_open* function.
119
120         :param status: Open the database in read-only or read-write mode
121         :type status:  :attr:`MODE` 
122         :returns: Nothing
123         :exception: Raises :exc:`NotmuchError` in case
124                     of any failure (after printing an error message on stderr).
125         """
126
127         res = Database._open(path, mode)
128
129         if res is None:
130             raise NotmuchError(
131                 message="Could not open the specified database")
132         self._db = res
133
134     def get_path(self):
135         """Returns the file path of an open database
136
137         Wraps notmuch_database_get_path"""
138         # Raise a NotmuchError if not initialized
139         self._verify_initialized_db()
140
141         return Database._get_path(self._db)
142
143     def get_version(self):
144         """Returns the database format version
145
146         :returns: The database version as positive integer
147         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
148                     the database was not intitialized.
149         """
150         # Raise a NotmuchError if not initialized
151         self._verify_initialized_db()
152
153         return Database._get_version (self._db)
154
155     def needs_upgrade(self):
156         """Does this database need to be upgraded before writing to it?
157
158         If this function returns True then no functions that modify the
159         database (:meth:`add_message`, :meth:`add_tag`,
160         :meth:`Directory.set_mtime`, etc.) will work unless :meth:`upgrade` 
161         is called successfully first.
162
163         :returns: `True` or `False`
164         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
165                     the database was not intitialized.
166         """
167         # Raise a NotmuchError if not initialized
168         self._verify_initialized_db()
169
170         return notmuch_database_needs_upgrade(self.db) 
171
172     def add_message(self, filename):
173         """Adds a new message to the database
174
175         `filename` should be a path relative to the path of the open
176         database (see :meth:`get_path`), or else should be an absolute
177         filename with initial components that match the path of the
178         database.
179
180         The file should be a single mail message (not a multi-message mbox)
181         that is expected to remain at its current location, since the
182         notmuch database will reference the filename, and will not copy the
183         entire contents of the file.
184
185         :returns: On success, we return 
186
187            1) a :class:`Message` object that can be used for things
188               such as adding tags to the just-added message.
189            2) one of the following STATUS values:
190
191               STATUS.SUCCESS
192                   Message successfully added to database.
193               STATUS.DUPLICATE_MESSAGE_ID
194                   Message has the same message ID as another message already
195                   in the database. The new filename was successfully added
196                   to the message in the database.
197
198         :rtype:   2-tuple(:class:`Message`, STATUS)
199
200         :exception: Raises a :exc:`NotmuchError` with the following meaning.
201               If such an exception occurs, nothing was added to the database.
202
203               STATUS.FILE_ERROR
204                       An error occurred trying to open the file, (such as 
205                       permission denied, or file not found, etc.).
206               STATUS.FILE_NOT_EMAIL
207                       The contents of filename don't look like an email message.
208               STATUS.READ_ONLY_DATABASE
209                       Database was opened in read-only mode so no message can
210                       be added.
211               STATUS.NOT_INITIALIZED
212                       The database has not been initialized.
213         """
214         # Raise a NotmuchError if not initialized
215         self._verify_initialized_db()
216
217         msg_p = c_void_p()
218         status = nmlib.notmuch_database_add_message(self._db,
219                                                   filename,
220                                                   byref(msg_p))
221  
222         if not status in [STATUS.SUCCESS,STATUS.DUPLICATE_MESSAGE_ID]:
223             raise NotmuchError(status)
224
225         #construct Message() and return
226         msg = Message(msg_p, self)
227         return (msg, status)
228
229     def remove_message(self, filename):
230         """Removes a message from the given notmuch database
231
232         Note that only this particular filename association is removed from
233         the database. If the same message (as determined by the message ID)
234         is still available via other filenames, then the message will
235         persist in the database for those filenames. When the last filename
236         is removed for a particular message, the database content for that
237         message will be entirely removed.
238
239         :returns: A STATUS.* value with the following meaning:
240
241              STATUS.SUCCESS
242                The last filename was removed and the message was removed 
243                from the database.
244              STATUS.DUPLICATE_MESSAGE_ID
245                This filename was removed but the message persists in the 
246                database with at least one other filename.
247
248         :exception: Raises a :exc:`NotmuchError` with the following meaning.
249              If such an exception occurs, nothing was removed from the database.
250
251              STATUS.READ_ONLY_DATABASE
252                Database was opened in read-only mode so no message can be 
253                removed.
254              STATUS.NOT_INITIALIZED
255                The database has not been initialized.
256         """
257         # Raise a NotmuchError if not initialized
258         self._verify_initialized_db()
259
260         status = nmlib.notmuch_database_remove_message(self._db,
261                                                        filename)
262
263     def find_message(self, msgid):
264         """Returns a :class:`Message` as identified by its message ID
265
266         Wraps the underlying *notmuch_database_find_message* function.
267
268         :param msgid: The message ID
269         :type msgid: string
270         :returns: :class:`Message` or `None` if no message is found or if an
271                   out-of-memory situation occurs.
272         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
273                   the database was not intitialized.
274         """
275         # Raise a NotmuchError if not initialized
276         self._verify_initialized_db()
277
278         msg_p = Database._find_message(self._db, msgid)
279         if msg_p is None:
280             return None
281         return Message(msg_p, self)
282
283     def get_all_tags(self):
284         """Returns :class:`Tags` with a list of all tags found in the database
285
286         :returns: :class:`Tags`
287         :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
288         """
289         # Raise a NotmuchError if not initialized
290         self._verify_initialized_db()
291
292         tags_p = Database._get_all_tags (self._db)
293         if tags_p == None:
294             raise NotmuchError(STATUS.NULL_POINTER)
295         return Tags(tags_p, self)
296
297     def create_query(self, querystring):
298         """Returns a :class:`Query` derived from this database
299
300         This is a shorthand method for doing::
301
302           # short version
303           # Automatically frees the Database() when 'q' is deleted
304
305           q  = Database(dbpath).create_query('from:"Biene Maja"')
306
307           # long version, which is functionally equivalent but will keep the
308           # Database in the 'db' variable around after we delete 'q':
309
310           db = Database(dbpath)
311           q  = Query(db,'from:"Biene Maja"')
312
313         This function is a python extension and not in the underlying C API.
314         """
315         # Raise a NotmuchError if not initialized
316         self._verify_initialized_db()
317
318         return Query(self, querystring)
319
320     def __repr__(self):
321         return "'Notmuch DB " + self.get_path() + "'"
322
323     def __del__(self):
324         """Close and free the notmuch database if needed"""
325         if self._db is not None:
326             nmlib.notmuch_database_close(self._db)
327
328     def _get_user_default_db(self):
329         """ Reads a user's notmuch config and returns his db location
330
331         Throws a NotmuchError if it cannot find it"""
332         from ConfigParser import SafeConfigParser
333         config = SafeConfigParser()
334         conf_f = os.getenv('NOTMUCH_CONFIG',
335                            os.path.expanduser('~/.notmuch-config'))
336         config.read(conf_f)
337         if not config.has_option('database','path'):
338             raise NotmuchError(message=
339                                "No DB path specified and no user default found")
340         return config.get('database','path')
341
342     @property
343     def db_p(self):
344         """Property returning a pointer to `notmuch_database_t` or `None`
345
346         This should normally not be needed by a user (and is not yet
347         guaranteed to remain stable in future versions).
348         """
349         return self._db
350
351 #------------------------------------------------------------------------------
352 class Query(object):
353     """Represents a search query on an opened :class:`Database`.
354
355     A query selects and filters a subset of messages from the notmuch
356     database we derive from.
357
358     Query() provides an instance attribute :attr:`sort`, which
359     contains the sort order (if specified via :meth:`set_sort`) or
360     `None`.
361
362     Technically, it wraps the underlying *notmuch_query_t* struct.
363
364     .. note:: Do remember that as soon as we tear down this object,
365            all underlying derived objects such as threads,
366            messages, tags etc will be freed by the underlying library
367            as well. Accessing these objects will lead to segfaults and
368            other unexpected behavior. See above for more details.
369     """
370     # constants
371     SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
372     """Constants: Sort order in which to return results"""
373
374     """notmuch_query_create"""
375     _create = nmlib.notmuch_query_create
376     _create.restype = c_void_p
377
378     """notmuch_query_search_threads"""
379     _search_threads = nmlib.notmuch_query_search_threads
380     _search_threads.restype = c_void_p
381
382     """notmuch_query_search_messages"""
383     _search_messages = nmlib.notmuch_query_search_messages
384     _search_messages.restype = c_void_p
385
386
387     """notmuch_query_count_messages"""
388     _count_messages = nmlib.notmuch_query_count_messages
389     _count_messages.restype = c_uint
390
391     def __init__(self, db, querystr):
392         """
393         :param db: An open database which we derive the Query from.
394         :type db: :class:`Database`
395         :param querystr: The query string for the message.
396         :type querystr: str
397         """
398         self._db = None
399         self._query = None
400         self.sort = None
401         self.create(db, querystr)
402
403     def create(self, db, querystr):
404         """Creates a new query derived from a Database
405
406         This function is utilized by __init__() and usually does not need to 
407         be called directly.
408
409         :param db: Database to create the query from.
410         :type db: :class:`Database`
411         :param querystr: The query string
412         :type querystr: str
413         :returns: Nothing
414         :exception: :exc:`NotmuchError`
415
416                       * STATUS.NOT_INITIALIZED if db is not inited
417                       * STATUS.NULL_POINTER if the query creation failed 
418                         (too little memory)
419         """
420         if db.db_p is None:
421             raise NotmuchError(STATUS.NOT_INITIALIZED)            
422         # create reference to parent db to keep it alive
423         self._db = db
424         
425         # create query, return None if too little mem available
426         query_p = Query._create(db.db_p, querystr)
427         if query_p is None:
428             NotmuchError(STATUS.NULL_POINTER)
429         self._query = query_p
430
431     def set_sort(self, sort):
432         """Set the sort order future results will be delivered in
433
434         Wraps the underlying *notmuch_query_set_sort* function.
435
436         :param sort: Sort order (see :attr:`Query.SORT`)
437         :returns: Nothing
438         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not 
439                     been initialized.
440         """
441         if self._query is None:
442             raise NotmuchError(STATUS.NOT_INITIALIZED)
443
444         self.sort = sort
445         nmlib.notmuch_query_set_sort(self._query, sort)
446
447     def search_threads(self):
448         """Execute a query for threads
449
450         Execute a query for threads, returning a :class:`Threads` iterator.
451         The returned threads are owned by the query and as such, will only be 
452         valid until the Query is deleted.
453
454         Technically, it wraps the underlying
455         *notmuch_query_search_threads* function.
456
457         :returns: :class:`Threads`
458         :exception: :exc:`NotmuchError`
459
460                       * STATUS.NOT_INITIALIZED if query is not inited
461                       * STATUS.NULL_POINTER if search_threads failed 
462         """
463         if self._query is None:
464             raise NotmuchError(STATUS.NOT_INITIALIZED)            
465
466         threads_p = Query._search_threads(self._query)
467
468         if threads_p is None:
469             NotmuchError(STATUS.NULL_POINTER)
470
471         return Threads(threads_p,self)
472
473     def search_messages(self):
474         """Filter messages according to the query and return
475         :class:`Messages` in the defined sort order
476
477         Technically, it wraps the underlying
478         *notmuch_query_search_messages* function.
479
480         :returns: :class:`Messages`
481         :exception: :exc:`NotmuchError`
482
483                       * STATUS.NOT_INITIALIZED if query is not inited
484                       * STATUS.NULL_POINTER if search_messages failed 
485         """
486         if self._query is None:
487             raise NotmuchError(STATUS.NOT_INITIALIZED)            
488
489         msgs_p = Query._search_messages(self._query)
490
491         if msgs_p is None:
492             NotmuchError(STATUS.NULL_POINTER)
493
494         return Messages(msgs_p,self)
495
496     def count_messages(self):
497         """Estimate the number of messages matching the query
498
499         This function performs a search and returns Xapian's best
500         guess as to the number of matching messages. It is much faster
501         than performing :meth:`search_messages` and counting the
502         result with `len()` (although it always returned the same
503         result in my tests). Technically, it wraps the underlying
504         *notmuch_query_count_messages* function.
505
506         :returns: :class:`Messages`
507         :exception: :exc:`NotmuchError`
508
509                       * STATUS.NOT_INITIALIZED if query is not inited
510         """
511         if self._query is None:
512             raise NotmuchError(STATUS.NOT_INITIALIZED)            
513
514         return Query._count_messages(self._query)
515
516     def __del__(self):
517         """Close and free the Query"""
518         if self._query is not None:
519             nmlib.notmuch_query_destroy (self._query)