2b51e5ea34a9fd0a0291f3eac57b3b6260c704fd
[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: 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. 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__() 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__() 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:`notmuch.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 *notmuch_database_find_message*
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` object or raises :exc:`NotmuchError` with 
150                   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     """ Wrapper around a notmuch_query_t
192
193     Do note that as soon as we tear down this object, all derived
194     threads, and messages will be freed as well.
195     """
196     # constants
197     SORT_OLDEST_FIRST = 0
198     SORT_NEWEST_FIRST = 1
199     SORT_MESSAGE_ID = 2
200
201     """notmuch_query_create"""
202     _create = nmlib.notmuch_query_create
203     _create.restype = c_void_p
204
205     """notmuch_query_search_messages"""
206     _search_messages = nmlib.notmuch_query_search_messages
207     _search_messages.restype = c_void_p
208
209     def __init__(self, db, querystr):
210         """TODO: document"""
211         self._db = None
212         self._query = None
213         self.create(db, querystr)
214
215     def create(self, db, querystr):
216         """db is a Database() and querystr a string
217
218         raises NotmuchError STATUS.NOT_INITIALIZED if db is not inited and
219         STATUS.NULL_POINTER if the query creation failed (too little memory)
220         """
221         if db.db_p is None:
222             raise NotmuchError(STATUS.NOT_INITIALIZED)            
223         # create reference to parent db to keep it alive
224         self._db = db
225         
226         # create query, return None if too little mem available
227         query_p = Query._create(db.db_p, querystr)
228         if query_p is None:
229             NotmuchError(STATUS.NULL_POINTER)
230         self._query = query_p
231
232     def set_sort(self, sort):
233         """notmuch_query_set_sort
234
235         :param sort: one of Query.SORT_OLDEST_FIRST|SORT_NEWEST_FIRST|SORT_MESSAGE_ID
236         :returns: Nothing, but raises NotmuchError if query is not inited
237         """
238         if self._query is None:
239             raise NotmuchError(STATUS.NOT_INITIALIZED)
240
241         nmlib.notmuch_query_set_sort(self._query, sort)
242
243     def search_messages(self):
244         """notmuch_query_search_messages
245         Returns Messages() or a raises a NotmuchError()
246         """
247         if self._query is None:
248             raise NotmuchError(STATUS.NOT_INITIALIZED)            
249
250         msgs_p = Query._search_messages(self._query)
251
252         if msgs_p is None:
253             NotmuchError(STATUS.NULL_POINTER)
254
255         return Messages(msgs_p,self)
256
257
258     def __del__(self):
259         """Close and free the Query"""
260         if self._query is not None:
261             logging.debug("Freeing the Query now")
262             nmlib.notmuch_query_destroy (self._query)
263
264 #------------------------------------------------------------------------------
265 class Tags(object):
266     """Wrapper around notmuch_tags_t"""
267
268     #notmuch_tags_get
269     _get = nmlib.notmuch_tags_get
270     _get.restype = c_char_p
271
272     def __init__(self, tags_p, parent=None):
273         """
274         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
275         we will raise an NotmuchError(STATUS.NULL_POINTER).
276
277         Is passed the parent these tags are derived from, and saves a
278         reference to it, so we can automatically delete the db object
279         once all derived objects are dead.
280
281         Tags() provides an iterator over all contained tags. However, you will
282         only be able to iterate over the Tags once, because the underlying C
283         function only allows iterating once.
284         #TODO: make the iterator work more than once and cache the tags in 
285                the Python object.
286         """
287         if tags_p is None:
288             NotmuchError(STATUS.NULL_POINTER)
289
290         self._tags = tags_p
291         #save reference to parent object so we keep it alive
292         self._parent = parent
293         logging.debug("Inited Tags derived from %s" %(repr(parent)))
294     
295     def __iter__(self):
296         """ Make Tags an iterator """
297         return self
298
299     def next(self):
300         if self._tags is None:
301             raise NotmuchError(STATUS.NOT_INITIALIZED)
302
303         if not nmlib.notmuch_tags_valid(self._tags):
304             self._tags = None
305             raise StopIteration
306
307         tag = Tags._get (self._tags)
308         nmlib.notmuch_tags_move_to_next(self._tags)
309         return tag
310
311     def __str__(self):
312         """str() of Tags() is a space separated list of tags
313
314         This iterates over the list of Tags and will therefore 'exhaust' Tags()
315         """
316         return " ".join(self)
317
318     def __del__(self):
319         """Close and free the notmuch tags"""
320         if self._tags is not None:
321             logging.debug("Freeing the Tags now")
322             nmlib.notmuch_tags_destroy (self._tags)
323
324
325 #------------------------------------------------------------------------------
326 class Messages(object):
327     """Wrapper around notmuch_messages_t"""
328
329     #notmuch_tags_get
330     _get = nmlib.notmuch_messages_get
331     _get.restype = c_void_p
332
333     _collect_tags = nmlib.notmuch_messages_collect_tags
334     _collect_tags.restype = c_void_p
335
336     def __init__(self, msgs_p, parent=None):
337         """
338         msg_p is a pointer to an notmuch_messages_t Structure. If it is None,
339         we will raise an NotmuchError(STATUS.NULL_POINTER).
340
341         If passed the parent query this Messages() is derived from, it saves a
342         reference to it, so we can automatically delete the parent query object
343         once all derived objects are dead.
344
345         Messages() provides an iterator over all contained Message()s.
346         However, you will only be able to iterate over it once,
347         because the underlying C function only allows iterating once.
348         #TODO: make the iterator work more than once and cache the tags in 
349                the Python object.
350         """
351         if msgs_p is None:
352             NotmuchError(STATUS.NULL_POINTER)
353
354         self._msgs = msgs_p
355         #store parent, so we keep them alive as long as self  is alive
356         self._parent = parent
357         logging.debug("Inited Messages derived from %s" %(str(parent)))
358
359     def collect_tags(self):
360         """ return the Tags() belonging to the messages
361         
362         Do note that collect_tags will iterate over the messages and
363         therefore will not allow further iterationsl
364         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
365         """
366         if self._msgs is None:
367             raise NotmuchError(STATUS.NOT_INITIALIZED)
368
369         # collect all tags (returns NULL on error)
370         tags_p = Messages._collect_tags (self._msgs)
371         #reset _msgs as we iterated over it and can do so only once
372         self._msgs = None
373
374         if tags_p == None:
375             raise NotmuchError(STATUS.NULL_POINTER)
376         return Tags(tags_p, self)
377
378     def __iter__(self):
379         """ Make Messages an iterator """
380         return self
381
382     def next(self):
383         if self._msgs is None:
384             raise NotmuchError(STATUS.NOT_INITIALIZED)
385
386         if not nmlib.notmuch_messages_valid(self._msgs):
387             self._msgs = None
388             raise StopIteration
389
390         msg = Message(Messages._get (self._msgs), self)
391         nmlib.notmuch_messages_move_to_next(self._msgs)
392         return msg
393
394     def __len__(self):
395         """ Returns the number of contained messages
396
397         :note: As this iterates over the messages, we will not be able to 
398                iterate over them again (as in retrieve them)!
399         """
400         if self._msgs is None:
401             raise NotmuchError(STATUS.NOT_INITIALIZED)
402
403         i=0
404         while nmlib.notmuch_messages_valid(self._msgs):
405             nmlib.notmuch_messages_move_to_next(self._msgs)
406             i += 1
407         self._msgs = None
408         return i
409
410
411
412     def __del__(self):
413         """Close and free the notmuch Messages"""
414         if self._msgs is not None:
415             logging.debug("Freeing the Messages now")
416             nmlib.notmuch_messages_destroy (self._msgs)
417
418
419 #------------------------------------------------------------------------------
420 class Message(object):
421     """Wrapper around notmuch_message_t"""
422
423     """notmuch_message_get_filename (notmuch_message_t *message)"""
424     _get_filename = nmlib.notmuch_message_get_filename
425     _get_filename.restype = c_char_p 
426     """notmuch_message_get_message_id (notmuch_message_t *message)"""
427     _get_message_id = nmlib.notmuch_message_get_message_id
428     _get_message_id.restype = c_char_p 
429
430     """notmuch_message_get_tags (notmuch_message_t *message)"""
431     _get_tags = nmlib.notmuch_message_get_tags
432     _get_tags.restype = c_void_p
433
434     _get_date = nmlib.notmuch_message_get_date
435     _get_date.restype = c_uint64
436
437     _get_header = nmlib.notmuch_message_get_header
438     _get_header.restype = c_char_p
439
440     def __init__(self, msg_p, parent=None):
441         """
442         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
443         we will raise an NotmuchError(STATUS.NULL_POINTER).
444
445         Is a 'parent' object is passed which this message is derived from,
446         we save a reference to it, so we can automatically delete the parent
447         object once all derived objects are dead.
448         """
449         if msg_p is None:
450             NotmuchError(STATUS.NULL_POINTER)
451         self._msg = msg_p
452         #keep reference to parent, so we keep it alive
453         self._parent = parent
454         logging.debug("Inited Message derived from %s" %(str(parent)))
455
456
457     def get_message_id(self):
458         """ return the msg id
459         
460         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
461         """
462         if self._msg is None:
463             raise NotmuchError(STATUS.NOT_INITIALIZED)
464         return Message._get_message_id(self._msg)
465
466     def get_date(self):
467         """returns time_t of the message date
468
469         For the original textual representation of the Date header from the
470         message call notmuch_message_get_header() with a header value of
471         "date".
472         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
473         """
474         if self._msg is None:
475             raise NotmuchError(STATUS.NOT_INITIALIZED)
476         return Message._get_date(self._msg)
477
478     def get_header(self, header):
479         """ TODO document me"""
480         if self._msg is None:
481             raise NotmuchError(STATUS.NOT_INITIALIZED)
482
483         #Returns NULL if any error occurs.
484         header = Message._get_header (self._msg, header)
485         if header == None:
486             raise NotmuchError(STATUS.NULL_POINTER)
487         return header
488
489     def get_filename(self):
490         """ return the msg filename
491         
492         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
493         """
494         if self._msg is None:
495             raise NotmuchError(STATUS.NOT_INITIALIZED)
496         return Message._get_filename(self._msg)
497
498     def get_tags(self):
499         """ return the msg tags
500         
501         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
502         Raises NotmuchError(STATUS.NULL_POINTER) on error.
503         """
504         if self._msg is None:
505             raise NotmuchError(STATUS.NOT_INITIALIZED)
506
507         tags_p = Message._get_tags(self._msg)
508         if tags_p == None:
509             raise NotmuchError(STATUS.NULL_POINTER)
510         return Tags(tags_p, self)
511
512     def __str__(self):
513         """A message() is represented by a 1-line summary"""
514         msg = {}
515         msg['from'] = self.get_header('from')
516         msg['tags'] = str(self.get_tags())
517         msg['date'] = date.fromtimestamp(self.get_date())
518         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
519
520     def format_as_text(self):
521         """ Output like notmuch show """
522         return str(self)
523
524     def __del__(self):
525         """Close and free the notmuch Message"""
526         if self._msg is not None:
527             logging.debug("Freeing the Message now")
528             nmlib.notmuch_message_destroy (self._msg)