]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/database.py
13915fb114aea0d535372241ee4e8433e715132a
[notmuch] / cnotmuch / database.py
1 import ctypes
2 from ctypes import c_int, c_char_p, c_void_p, c_uint64
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
4 import logging
5 from datetime import date
6
7 class Database(object):
8     """Represents a notmuch database (wraps notmuch_database_t)
9
10     .. note:: Do remember that as soon as we tear down this object,
11            all underlying derived objects such as queries, threads,
12            messages, tags etc will be freed by the underlying library
13            as well. Accessing these objects will lead to segfaults and
14            other unexpected behavior. See above for more details.
15     """
16     MODE = Enum(['READ_ONLY','READ_WRITE'])
17     """Constants: Mode in which to open the database"""
18
19     _std_db_path = None
20     """Class attribute to cache user's default database"""
21
22     """notmuch_database_get_path (notmuch_database_t *database)"""
23     _get_path = nmlib.notmuch_database_get_path
24     _get_path.restype = c_char_p
25
26     """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
27     _open = nmlib.notmuch_database_open 
28     _open.restype = c_void_p
29
30     """ notmuch_database_find_message """
31     _find_message = nmlib.notmuch_database_find_message
32     _find_message.restype = c_void_p
33
34     """notmuch_database_get_all_tags (notmuch_database_t *database)"""
35     _get_all_tags = nmlib.notmuch_database_get_all_tags
36     _get_all_tags.restype = c_void_p
37
38     """ notmuch_database_create(const char *path):"""
39     _create = nmlib.notmuch_database_create
40     _create.restype = c_void_p
41
42     def __init__(self, path=None, create=False, mode= MODE.READ_ONLY):
43         """If *path* is *None*, we will try to read a users notmuch
44         configuration and use his default database. If *create* is `True`,
45         the database will always be created in
46         :attr:`MODE`.READ_WRITE mode.
47
48         :param path:   Directory to open/create the database in (see
49                        above for behavior if `None`)
50         :type path:    `str` or `None`
51         :param create: Pass `False` to open an existing, `True` to create a new
52                        database.  
53         :type create:  bool
54         :param mode:   Mode to open a database in. Is always 
55                        :attr:`MODE`.READ_WRITE when creating a new one.
56         :type mode:    :attr:`MODE`
57         :returns:      Nothing
58         :exception:    :exc:`NotmuchError` in case of failure.
59         """
60         self._db = None
61         if path is None:
62             # no path specified. use a user's default database
63             if Database._std_db_path is None:
64                 #the following line throws a NotmuchError if it fails
65                 Database._std_db_path = self._get_user_default_db()
66             path = Database._std_db_path
67
68         if create == False:
69             self.open(path, mode)
70         else:
71             self.create(path)
72
73     def create(self, path):
74         """Creates a new notmuch database
75
76         This function is used by __init__() and usually does not need
77         to be called directly. It wraps the underlying
78         *notmuch_database_create* function and creates a new notmuch
79         database at *path*. It will always return a database in
80         :attr:`MODE`.READ_WRITE mode as creating an empty database for
81         reading only does not make a great deal of sense.
82
83         :param path: A directory in which we should create the database.
84         :type path: str
85         :returns: Nothing
86         :exception: :exc:`NotmuchError` in case of any failure
87                     (after printing an error message on stderr).
88         """
89         if self._db is not None:
90             raise NotmuchError(
91             message="Cannot create db, this Database() already has an open one.")
92
93         res = Database._create(path, MODE.READ_WRITE)
94
95         if res is None:
96             raise NotmuchError(
97                 message="Could not create the specified database")
98         self._db = res
99
100     def open(self, path, mode= MODE.READ_ONLY): 
101         """Opens an existing database
102
103         This function is used by __init__() and usually does not need
104         to be called directly. It wraps the underlying
105         *notmuch_database_open* function.
106
107         :param status: Open the database in read-only or read-write mode
108         :type status:  :attr:`MODE` 
109         :returns: Nothing
110         :exception: Raises :exc:`NotmuchError` in case
111                     of any failure (after printing an error message on stderr).
112         """
113
114         res = Database._open(path, mode)
115
116         if res is None:
117             raise NotmuchError(
118                 message="Could not open the specified database")
119         self._db = res
120
121     def get_path(self):
122         """Returns the file path of an open database
123
124         Wraps notmuch_database_get_path"""
125         return Database._get_path(self._db)
126
127     def find_message(self, msgid):
128         """Returns a :class:`Message` as identified by its message ID
129
130         Wraps the underlying *notmuch_database_find_message* function.
131
132         :param msgid: The message ID
133         :type msgid: string
134         :returns: :class:`Message` or `None` if no message is found or if an
135                   out-of-memory situation occurs.
136         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
137                   the database was not intitialized.
138         """
139         if self._db is None:
140             raise NotmuchError(STATUS.NOT_INITIALIZED)
141         msg_p = Database._find_message(self._db, msgid)
142         if msg_p is None:
143             return None
144         return Message(msg_p, self)
145
146     def get_all_tags(self):
147         """Returns :class:`Tags` with a list of all tags found in the database
148
149         :returns: :class:`Tags`
150         :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
151         """
152         if self._db is None:
153             raise NotmuchError(STATUS.NOT_INITIALIZED)
154
155         tags_p = Database._get_all_tags (self._db)
156         if tags_p == None:
157             raise NotmuchError(STATUS.NULL_POINTER)
158         return Tags(tags_p, self)
159
160     def __repr__(self):
161         return "'Notmuch DB " + self.get_path() + "'"
162
163     def __del__(self):
164         """Close and free the notmuch database if needed"""
165         if self._db is not None:
166             logging.debug("Freeing the database now")
167             nmlib.notmuch_database_close(self._db)
168
169     def _get_user_default_db(self):
170         """ Reads a user's notmuch config and returns his db location
171
172         Throws a NotmuchError if it cannot find it"""
173         from ConfigParser import SafeConfigParser
174         import os.path
175         config = SafeConfigParser()
176         config.read(os.path.expanduser('~/.notmuch-config'))
177         if not config.has_option('database','path'):
178             raise NotmuchError(message=
179                                "No DB path specified and no user default found")
180         return config.get('database','path')
181
182     @property
183     def db_p(self):
184         """Property returning a pointer to the notmuch_database_t or `None`.
185
186         This should normally not be needed by a user."""
187         return self._db
188
189 #------------------------------------------------------------------------------
190 class Query(object):
191     """ Represents a search query on an opened :class:`Database`.
192
193     A query selects and filters a subset of messages from the notmuch
194     database we derive from.
195
196     Technically, it wraps the underlying *notmuch_query_t* struct.
197
198     .. note:: Do remember that as soon as we tear down this object,
199            all underlying derived objects such as queries, threads,
200            messages, tags etc will be freed by the underlying library
201            as well. Accessing these objects will lead to segfaults and
202            other unexpected behavior. See above for more details.
203     """
204     # constants
205     SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
206     """Constants: Sort order in which to return results"""
207
208     """notmuch_query_create"""
209     _create = nmlib.notmuch_query_create
210     _create.restype = c_void_p
211
212     """notmuch_query_search_messages"""
213     _search_messages = nmlib.notmuch_query_search_messages
214     _search_messages.restype = c_void_p
215
216     def __init__(self, db, querystr):
217         """
218         :param db: An open database which we derive the Query from.
219         :type db: :class:`Database`
220         :param querystr: The query string for the message.
221         :type querystr: str
222         """
223         self._db = None
224         self._query = None
225         self.create(db, querystr)
226
227     def create(self, db, querystr):
228         """Creates a new query derived from a Database.
229
230         This function is utilized by __init__() and usually does not need to 
231         be called directly.
232
233         :param db: Database to create the query from.
234         :type db: :class:`Database`
235         :param querystr: The query string
236         :type querystr: str
237         :returns: Nothing
238         :exception: :exc:`NotmuchError`
239
240                       * STATUS.NOT_INITIALIZED if db is not inited
241                       * STATUS.NULL_POINTER if the query creation failed 
242                         (too little memory)
243         """
244         if db.db_p is None:
245             raise NotmuchError(STATUS.NOT_INITIALIZED)            
246         # create reference to parent db to keep it alive
247         self._db = db
248         
249         # create query, return None if too little mem available
250         query_p = Query._create(db.db_p, querystr)
251         if query_p is None:
252             NotmuchError(STATUS.NULL_POINTER)
253         self._query = query_p
254
255     def set_sort(self, sort):
256         """Set the sort order future results will be delivered in
257
258         Wraps the underlying *notmuch_query_set_sort* function.
259
260         :param sort: Sort order (see :attr:`Query.SORT`)
261         :returns: Nothing
262         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not 
263                     been initialized.
264         """
265         if self._query is None:
266             raise NotmuchError(STATUS.NOT_INITIALIZED)
267
268         nmlib.notmuch_query_set_sort(self._query, sort)
269
270     def search_messages(self):
271         """Filter messages according to query and return
272         :class:`Messages` in the defined sort order.
273
274         Technically, it wraps the underlying
275         *notmuch_query_search_messages* function.
276
277         :returns: :class:`Messages`
278         :exception: :exc:`NotmuchError`
279
280                       * STATUS.NOT_INITIALIZED if query is not inited
281                       * STATUS.NULL_POINTER if search_messages failed 
282         """
283         if self._query is None:
284             raise NotmuchError(STATUS.NOT_INITIALIZED)            
285
286         msgs_p = Query._search_messages(self._query)
287
288         if msgs_p is None:
289             NotmuchError(STATUS.NULL_POINTER)
290
291         return Messages(msgs_p,self)
292
293
294     def __del__(self):
295         """Close and free the Query"""
296         if self._query is not None:
297             logging.debug("Freeing the Query now")
298             nmlib.notmuch_query_destroy (self._query)
299
300 #------------------------------------------------------------------------------
301 class Tags(object):
302     """Represents a list of notmuch tags
303
304     This object provides an iterator over a list of notmuch tags. Do
305     note that the underlying library only provides a one-time iterator
306     (it cannot reset the iterator to the start). Thus iterating over
307     the function will "exhaust" the list of tags, and a subsequent
308     iteration attempt will raise a :exc:`NotmuchError`
309     STATUS.NOT_INITIALIZED. Also note, that any function that uses
310     iteration (nearly all) will also exhaust the tags. So both::
311
312       for tag in tags: print tag 
313
314     as well as::
315
316        number_of_tags = len(tags)
317
318     and even a simple::
319
320        #str() iterates over all tags to construct a space separated list
321        print(str(tags))
322
323     will "exhaust" the Tags. If you need to re-iterate over a list of
324     tags you will need to retrieve a new :class:`Tags` object.
325     """
326
327     #notmuch_tags_get
328     _get = nmlib.notmuch_tags_get
329     _get.restype = c_char_p
330
331     def __init__(self, tags_p, parent=None):
332         """
333         :param tags_p: A pointer to an underlying *notmuch_tags_t*
334              structure. These are not publically exposed, so a user
335              will almost never instantiate a :class:`Tags` object
336              herself. They are usually handed back as a result,
337              e.g. in :meth:`Database.get_all_tags`.  *tags_p* must be
338              valid, we will raise an :exc:`NotmuchError`
339              (STATUS.NULL_POINTER) if it is `None`.
340         :type tags_p: :class:`ctypes.c_void_p`
341         :param parent: The parent object (ie :class:`Database` or 
342              :class:`Message` these tags are derived from, and saves a
343              reference to it, so we can automatically delete the db object
344              once all derived objects are dead.
345         :TODO: Make the iterator optionally work more than once by
346                cache the tags in the Python object(?)
347         """
348         if tags_p is None:
349             NotmuchError(STATUS.NULL_POINTER)
350
351         self._tags = tags_p
352         #save reference to parent object so we keep it alive
353         self._parent = parent
354         logging.debug("Inited Tags derived from %s" %(repr(parent)))
355     
356     def __iter__(self):
357         """ Make Tags an iterator """
358         return self
359
360     def next(self):
361         if self._tags is None:
362             raise NotmuchError(STATUS.NOT_INITIALIZED)
363
364         if not nmlib.notmuch_tags_valid(self._tags):
365             self._tags = None
366             raise StopIteration
367
368         tag = Tags._get (self._tags)
369         nmlib.notmuch_tags_move_to_next(self._tags)
370         return tag
371
372     def __len__(self):
373         """len(:class:`Tags`) returns the number of contained tags
374
375         .. note:: As this iterates over the tags, we will not be able
376                to iterate over them again (as in retrieve them)! If
377                the tags have been exhausted already, this will raise a
378                :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
379                subsequent attempts.
380         """
381         if self._tags is None:
382             raise NotmuchError(STATUS.NOT_INITIALIZED)
383
384         i=0
385         while nmlib.notmuch_tags_valid(self._msgs):
386             nmlib.notmuch_tags_move_to_next(self._msgs)
387             i += 1
388         self._tags = None
389         return i
390
391     def __str__(self):
392         """The str() representation of Tags() is a space separated list of tags
393
394         .. note:: As this iterates over the tags, we will not be able
395                to iterate over them again (as in retrieve them)! If
396                the tags have been exhausted already, this will raise a
397                :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
398                subsequent attempts.
399         """
400         return " ".join(self)
401
402     def __del__(self):
403         """Close and free the notmuch tags"""
404         if self._tags is not None:
405             logging.debug("Freeing the Tags now")
406             nmlib.notmuch_tags_destroy (self._tags)
407
408
409 #------------------------------------------------------------------------------
410 class Messages(object):
411     """Represents a list of notmuch messages
412
413     This object provides an iterator over a list of notmuch messages
414     (Technically, it provides a wrapper for the underlying
415     *notmuch_messages_t* structure). Do note that the underlying
416     library only provides a one-time iterator (it cannot reset the
417     iterator to the start). Thus iterating over the function will
418     "exhaust" the list of messages, and a subsequent iteration attempt
419     will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
420     note, that any function that uses iteration will also
421     exhaust the messages. So both::
422
423       for msg in msgs: print msg 
424
425     as well as::
426
427        number_of_msgs = len(msgs)
428
429     will "exhaust" the Messages. If you need to re-iterate over a list of
430     messages you will need to retrieve a new :class:`Messages` object.
431
432     Things are not as bad as it seems though, you can store and reuse
433     the single Message objects as often as you want as long as you
434     keep the parent Messages object around. (Recall that due to
435     hierarchical memory allocation, all derived Message objects will
436     be invalid when we delete the parent Messages() object, even if it
437     was already "exhausted".) So this works::
438
439       db   = Database()
440       msgs = Query(db,'').search_messages() #get a Messages() object
441       msglist = []
442       for m in msgs:
443          msglist.append(m)
444
445       # msgs is "exhausted" now and even len(msgs) will raise an exception.
446       # However it will be kept around until all retrieved Message() objects are
447       # also deleted. If you did e.g. an explicit del(msgs) here, the 
448       # following lines would fail.
449       
450       # You can reiterate over *msglist* however as often as you want. 
451       # It is simply a list with Message objects.
452
453       print (msglist[0].get_filename())
454       print (msglist[1].get_filename())
455       print (msglist[0].get_message_id())
456     """
457
458     #notmuch_tags_get
459     _get = nmlib.notmuch_messages_get
460     _get.restype = c_void_p
461
462     _collect_tags = nmlib.notmuch_messages_collect_tags
463     _collect_tags.restype = c_void_p
464
465     def __init__(self, msgs_p, parent=None):
466         """
467         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
468              structure. These are not publically exposed, so a user
469              will almost never instantiate a :class:`Messages` object
470              herself. They are usually handed back as a result,
471              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
472              valid, we will raise an :exc:`NotmuchError`
473              (STATUS.NULL_POINTER) if it is `None`.
474         :type msgs_p: :class:`ctypes.c_void_p`
475         :param parent: The parent object
476              (ie :class:`Query`) these tags are derived from. It saves
477              a reference to it, so we can automatically delete the db
478              object once all derived objects are dead.
479         :TODO: Make the iterator work more than once and cache the tags in 
480                the Python object.(?)
481         """
482         if msgs_p is None:
483             NotmuchError(STATUS.NULL_POINTER)
484
485         self._msgs = msgs_p
486         #store parent, so we keep them alive as long as self  is alive
487         self._parent = parent
488         logging.debug("Inited Messages derived from %s" %(str(parent)))
489
490     def collect_tags(self):
491         """Return the unique :class:`Tags` in the contained messages
492
493         :returns: :class:`Tags`
494         :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
495
496         .. note:: :meth:`collect_tags` will iterate over the messages and
497           therefore will not allow further iterations.
498         """
499         if self._msgs is None:
500             raise NotmuchError(STATUS.NOT_INITIALIZED)
501
502         # collect all tags (returns NULL on error)
503         tags_p = Messages._collect_tags (self._msgs)
504         #reset _msgs as we iterated over it and can do so only once
505         self._msgs = None
506
507         if tags_p == None:
508             raise NotmuchError(STATUS.NULL_POINTER)
509         return Tags(tags_p, self)
510
511     def __iter__(self):
512         """ Make Messages an iterator """
513         return self
514
515     def next(self):
516         if self._msgs is None:
517             raise NotmuchError(STATUS.NOT_INITIALIZED)
518
519         if not nmlib.notmuch_messages_valid(self._msgs):
520             self._msgs = None
521             raise StopIteration
522
523         msg = Message(Messages._get (self._msgs), self)
524         nmlib.notmuch_messages_move_to_next(self._msgs)
525         return msg
526
527     def __len__(self):
528         """len(:class:`Messages`) returns the number of contained messages
529
530         .. note:: As this iterates over the messages, we will not be able to 
531                iterate over them again (as in retrieve them)!
532         """
533         if self._msgs is None:
534             raise NotmuchError(STATUS.NOT_INITIALIZED)
535
536         i=0
537         while nmlib.notmuch_messages_valid(self._msgs):
538             nmlib.notmuch_messages_move_to_next(self._msgs)
539             i += 1
540         self._msgs = None
541         return i
542
543
544
545     def __del__(self):
546         """Close and free the notmuch Messages"""
547         if self._msgs is not None:
548             logging.debug("Freeing the Messages now")
549             nmlib.notmuch_messages_destroy (self._msgs)
550
551
552 #------------------------------------------------------------------------------
553 class Message(object):
554     """Wrapper around notmuch_message_t"""
555
556     """notmuch_message_get_filename (notmuch_message_t *message)"""
557     _get_filename = nmlib.notmuch_message_get_filename
558     _get_filename.restype = c_char_p 
559     """notmuch_message_get_message_id (notmuch_message_t *message)"""
560     _get_message_id = nmlib.notmuch_message_get_message_id
561     _get_message_id.restype = c_char_p 
562
563     """notmuch_message_get_tags (notmuch_message_t *message)"""
564     _get_tags = nmlib.notmuch_message_get_tags
565     _get_tags.restype = c_void_p
566
567     _get_date = nmlib.notmuch_message_get_date
568     _get_date.restype = c_uint64
569
570     _get_header = nmlib.notmuch_message_get_header
571     _get_header.restype = c_char_p
572
573     def __init__(self, msg_p, parent=None):
574         """
575         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
576         we will raise an NotmuchError(STATUS.NULL_POINTER).
577
578         Is a 'parent' object is passed which this message is derived from,
579         we save a reference to it, so we can automatically delete the parent
580         object once all derived objects are dead.
581         """
582         if msg_p is None:
583             NotmuchError(STATUS.NULL_POINTER)
584         self._msg = msg_p
585         #keep reference to parent, so we keep it alive
586         self._parent = parent
587         logging.debug("Inited Message derived from %s" %(str(parent)))
588
589
590     def get_message_id(self):
591         """ return the msg id
592         
593         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
594         """
595         if self._msg is None:
596             raise NotmuchError(STATUS.NOT_INITIALIZED)
597         return Message._get_message_id(self._msg)
598
599     def get_date(self):
600         """returns time_t of the message date
601
602         For the original textual representation of the Date header from the
603         message call notmuch_message_get_header() with a header value of
604         "date".
605         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
606         """
607         if self._msg is None:
608             raise NotmuchError(STATUS.NOT_INITIALIZED)
609         return Message._get_date(self._msg)
610
611     def get_header(self, header):
612         """ TODO document me"""
613         if self._msg is None:
614             raise NotmuchError(STATUS.NOT_INITIALIZED)
615
616         #Returns NULL if any error occurs.
617         header = Message._get_header (self._msg, header)
618         if header == None:
619             raise NotmuchError(STATUS.NULL_POINTER)
620         return header
621
622     def get_filename(self):
623         """ return the msg filename
624         
625         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
626         """
627         if self._msg is None:
628             raise NotmuchError(STATUS.NOT_INITIALIZED)
629         return Message._get_filename(self._msg)
630
631     def get_tags(self):
632         """ return the msg tags
633         
634         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
635         Raises NotmuchError(STATUS.NULL_POINTER) on error.
636         """
637         if self._msg is None:
638             raise NotmuchError(STATUS.NOT_INITIALIZED)
639
640         tags_p = Message._get_tags(self._msg)
641         if tags_p == None:
642             raise NotmuchError(STATUS.NULL_POINTER)
643         return Tags(tags_p, self)
644
645     def __str__(self):
646         """A message() is represented by a 1-line summary"""
647         msg = {}
648         msg['from'] = self.get_header('from')
649         msg['tags'] = str(self.get_tags())
650         msg['date'] = date.fromtimestamp(self.get_date())
651         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
652
653     def format_as_text(self):
654         """ Output like notmuch show """
655         return str(self)
656
657     def __del__(self):
658         """Close and free the notmuch Message"""
659         if self._msg is not None:
660             logging.debug("Freeing the Message now")
661             nmlib.notmuch_message_destroy (self._msg)