8762f393467c7b89030c6ef75efd3b1e6d1b3e91
[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
4 import logging
5 from datetime import date
6
7 class Database(object):
8     """ Wrapper around a notmuch_database_t
9
10     Do note that as soon as we tear down this object, all derived queries,
11     threads, and messages will be freed as well.
12     """
13     #constants
14     MODE_READ_ONLY = 0
15     MODE_READ_WRITE = 1
16
17     _std_db_path = None
18     """Class attribute of users default database"""
19
20     """notmuch_database_get_path (notmuch_database_t *database)"""
21     _get_path = nmlib.notmuch_database_get_path
22     _get_path.restype = c_char_p
23
24     """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
25     _open = nmlib.notmuch_database_open 
26     _open.restype = c_void_p
27
28     """ notmuch_database_find_message """
29     _find_message = nmlib.notmuch_database_find_message
30     _find_message.restype = c_void_p
31
32     """notmuch_database_get_all_tags (notmuch_database_t *database)"""
33     _get_all_tags = nmlib.notmuch_database_get_all_tags
34     _get_all_tags.restype = c_void_p
35
36     """ notmuch_database_create(const char *path):"""
37     _create = nmlib.notmuch_database_create
38     _create.restype = c_void_p
39
40     def __init__(self, path=None, create=False, status= MODE_READ_ONLY):
41         """ Open or create a notmuch database
42
43         If path is None, we will try to read a users notmuch configuration and
44         use his default database.
45         Throws a NotmuchError in case of failure.
46         """
47         self._db = None
48         if path is None:
49             # no path specified. use a user's default database
50             if Database._std_db_path is None:
51                 #the following line throws a NotmuchError if it fails
52                 Database._std_db_path = self._get_user_default_db()
53             path = Database._std_db_path
54
55         if create == False:
56             self.open(path, status)
57         else:
58             self.create(path, status)
59
60     def create(self, path, status=MODE_READ_ONLY):
61         """ notmuch_database_create(const char *path)
62
63         :returns: Raises :exc:`notmuch.NotmuchError` in case
64                   of any failure (after printing an error message on stderr).
65         """
66         res = Database._create(path, status)
67
68         if res is None:
69             raise NotmuchError(
70                 message="Could not create the specified database")
71         self._db = res
72
73     def open(self, path, status= MODE_READ_ONLY): 
74         """calls notmuch_database_open
75
76         :returns: Raises :exc:`notmuch.NotmuchError` in case
77                   of any failure (after printing an error message on stderr).
78         """
79         res = Database._open(path, status)
80
81         if res is None:
82             raise NotmuchError(
83                 message="Could not open the specified database")
84         self._db = res
85
86     def get_path(self):
87         """notmuch_database_get_path (notmuch_database_t *database);  """
88         return Database._get_path(self._db)
89
90     def find_message(self, msgid):
91         """notmuch_database_find_message
92         :param msgid: The message id
93         :ptype msgid: string
94
95         :returns: Message() or None if no message is found or if an
96                   out-of-memory situation occurs.
97         """
98         if self._db is None:
99             raise NotmuchError(STATUS.NOT_INITIALIZED)
100         msg_p = Database._find_message(self._db, msgid)
101         if msg_p is None:
102             return None
103         return Message(msg_p, self)
104
105     def get_all_tags(self):
106         """Return a Tags() object (list of all tags found in the database)
107
108         :returns: Tags() object or raises :exc:`NotmuchError` with 
109                   STATUS.NULL_POINTER on error
110         """
111         if self._db is None:
112             raise NotmuchError(STATUS.NOT_INITIALIZED)
113
114         tags_p = Database._get_all_tags (self._db)
115         if tags_p == None:
116             raise NotmuchError(STATUS.NULL_POINTER)
117         return Tags(tags_p, self)
118
119     def __repr__(self):
120         return "'Notmuch DB " + self.get_path() + "'"
121
122     def __del__(self):
123         """Close and free the notmuch database if needed"""
124         if self._db is not None:
125             logging.debug("Freeing the database now")
126             nmlib.notmuch_database_close(self._db)
127
128     def _get_user_default_db(self):
129         """ Reads a user's notmuch config and returns his db location
130
131         Throws a NotmuchError if it cannot find it"""
132         from ConfigParser import SafeConfigParser
133         import os.path
134         config = SafeConfigParser()
135         config.read(os.path.expanduser('~/.notmuch-config'))
136         if not config.has_option('database','path'):
137             raise NotmuchError(message=
138                                "No DB path specified and no user default found")
139         return config.get('database','path')
140
141     @property
142     def db_p(self):
143         """Returns a pointer to the current notmuch_database_t or None"""
144         return self._db
145
146 #------------------------------------------------------------------------------
147 class Query(object):
148     """ Wrapper around a notmuch_query_t
149
150     Do note that as soon as we tear down this object, all derived
151     threads, and messages will be freed as well.
152     """
153     # constants
154     SORT_OLDEST_FIRST = 0
155     SORT_NEWEST_FIRST = 1
156     SORT_MESSAGE_ID = 2
157
158     """notmuch_query_create"""
159     _create = nmlib.notmuch_query_create
160     _create.restype = c_void_p
161
162     """notmuch_query_search_messages"""
163     _search_messages = nmlib.notmuch_query_search_messages
164     _search_messages.restype = c_void_p
165
166     def __init__(self, db, querystr):
167         """TODO: document"""
168         self._db = None
169         self._query = None
170         self.create(db, querystr)
171
172     def create(self, db, querystr):
173         """db is a Database() and querystr a string
174
175         raises NotmuchError STATUS.NOT_INITIALIZED if db is not inited and
176         STATUS.NULL_POINTER if the query creation failed (too little memory)
177         """
178         if db.db_p is None:
179             raise NotmuchError(STATUS.NOT_INITIALIZED)            
180         # create reference to parent db to keep it alive
181         self._db = db
182         
183         # create query, return None if too little mem available
184         query_p = Query._create(db.db_p, querystr)
185         if query_p is None:
186             NotmuchError(STATUS.NULL_POINTER)
187         self._query = query_p
188
189     def set_sort(self, sort):
190         """notmuch_query_set_sort
191
192         :param sort: one of Query.SORT_OLDEST_FIRST|SORT_NEWEST_FIRST|SORT_MESSAGE_ID
193         :returns: Nothing, but raises NotmuchError if query is not inited
194         """
195         if self._query is None:
196             raise NotmuchError(STATUS.NOT_INITIALIZED)
197
198         nmlib.notmuch_query_set_sort(self._query, sort)
199
200     def search_messages(self):
201         """notmuch_query_search_messages
202         Returns Messages() or a raises a NotmuchError()
203         """
204         if self._query is None:
205             raise NotmuchError(STATUS.NOT_INITIALIZED)            
206
207         msgs_p = Query._search_messages(self._query)
208
209         if msgs_p is None:
210             NotmuchError(STATUS.NULL_POINTER)
211
212         return Messages(msgs_p,self)
213
214
215     def __del__(self):
216         """Close and free the Query"""
217         if self._query is not None:
218             logging.debug("Freeing the Query now")
219             nmlib.notmuch_query_destroy (self._query)
220
221 #------------------------------------------------------------------------------
222 class Tags(object):
223     """Wrapper around notmuch_tags_t"""
224
225     #notmuch_tags_get
226     _get = nmlib.notmuch_tags_get
227     _get.restype = c_char_p
228
229     def __init__(self, tags_p, parent=None):
230         """
231         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
232         we will raise an NotmuchError(STATUS.NULL_POINTER).
233
234         Is passed the parent these tags are derived from, and saves a
235         reference to it, so we can automatically delete the db object
236         once all derived objects are dead.
237
238         Tags() provides an iterator over all contained tags. However, you will
239         only be able to iterate over the Tags once, because the underlying C
240         function only allows iterating once.
241         #TODO: make the iterator work more than once and cache the tags in 
242                the Python object.
243         """
244         if tags_p is None:
245             NotmuchError(STATUS.NULL_POINTER)
246
247         self._tags = tags_p
248         #save reference to parent object so we keep it alive
249         self._parent = parent
250         logging.debug("Inited Tags derived from %s" %(repr(parent)))
251     
252     def __iter__(self):
253         """ Make Tags an iterator """
254         return self
255
256     def next(self):
257         if self._tags is None:
258             raise NotmuchError(STATUS.NOT_INITIALIZED)
259
260         if not nmlib.notmuch_tags_valid(self._tags):
261             self._tags = None
262             raise StopIteration
263
264         tag = Tags._get (self._tags)
265         nmlib.notmuch_tags_move_to_next(self._tags)
266         return tag
267
268     def __str__(self):
269         """str() of Tags() is a space separated list of tags
270
271         This iterates over the list of Tags and will therefore 'exhaust' Tags()
272         """
273         return " ".join(self)
274
275     def __del__(self):
276         """Close and free the notmuch tags"""
277         if self._tags is not None:
278             logging.debug("Freeing the Tags now")
279             nmlib.notmuch_tags_destroy (self._tags)
280
281
282 #------------------------------------------------------------------------------
283 class Messages(object):
284     """Wrapper around notmuch_messages_t"""
285
286     #notmuch_tags_get
287     _get = nmlib.notmuch_messages_get
288     _get.restype = c_void_p
289
290     _collect_tags = nmlib.notmuch_messages_collect_tags
291     _collect_tags.restype = c_void_p
292
293     def __init__(self, msgs_p, parent=None):
294         """
295         msg_p is a pointer to an notmuch_messages_t Structure. If it is None,
296         we will raise an NotmuchError(STATUS.NULL_POINTER).
297
298         If passed the parent query this Messages() is derived from, it saves a
299         reference to it, so we can automatically delete the parent query object
300         once all derived objects are dead.
301
302         Messages() provides an iterator over all contained Message()s.
303         However, you will only be able to iterate over it once,
304         because the underlying C function only allows iterating once.
305         #TODO: make the iterator work more than once and cache the tags in 
306                the Python object.
307         """
308         if msgs_p is None:
309             NotmuchError(STATUS.NULL_POINTER)
310
311         self._msgs = msgs_p
312         #store parent, so we keep them alive as long as self  is alive
313         self._parent = parent
314         logging.debug("Inited Messages derived from %s" %(str(parent)))
315
316     def collect_tags(self):
317         """ return the Tags() belonging to the messages
318         
319         Do note that collect_tags will iterate over the messages and
320         therefore will not allow further iterationsl
321         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
322         """
323         if self._msgs is None:
324             raise NotmuchError(STATUS.NOT_INITIALIZED)
325
326         # collect all tags (returns NULL on error)
327         tags_p = Messages._collect_tags (self._msgs)
328         #reset _msgs as we iterated over it and can do so only once
329         self._msgs = None
330
331         if tags_p == None:
332             raise NotmuchError(STATUS.NULL_POINTER)
333         return Tags(tags_p, self)
334
335     def __iter__(self):
336         """ Make Messages an iterator """
337         return self
338
339     def next(self):
340         if self._msgs is None:
341             raise NotmuchError(STATUS.NOT_INITIALIZED)
342
343         if not nmlib.notmuch_messages_valid(self._msgs):
344             self._msgs = None
345             raise StopIteration
346
347         msg = Message(Messages._get (self._msgs), self)
348         nmlib.notmuch_messages_move_to_next(self._msgs)
349         return msg
350
351     def __len__(self):
352         """ Returns the number of contained messages
353
354         :note: As this iterates over the messages, we will not be able to 
355                iterate over them again (as in retrieve them)!
356         """
357         if self._msgs is None:
358             raise NotmuchError(STATUS.NOT_INITIALIZED)
359
360         i=0
361         while nmlib.notmuch_messages_valid(self._msgs):
362             nmlib.notmuch_messages_move_to_next(self._msgs)
363             i += 1
364         self._msgs = None
365         return i
366
367
368
369     def __del__(self):
370         """Close and free the notmuch Messages"""
371         if self._msgs is not None:
372             logging.debug("Freeing the Messages now")
373             nmlib.notmuch_messages_destroy (self._msgs)
374
375
376 #------------------------------------------------------------------------------
377 class Message(object):
378     """Wrapper around notmuch_message_t"""
379
380     """notmuch_message_get_filename (notmuch_message_t *message)"""
381     _get_filename = nmlib.notmuch_message_get_filename
382     _get_filename.restype = c_char_p 
383     """notmuch_message_get_message_id (notmuch_message_t *message)"""
384     _get_message_id = nmlib.notmuch_message_get_message_id
385     _get_message_id.restype = c_char_p 
386
387     """notmuch_message_get_tags (notmuch_message_t *message)"""
388     _get_tags = nmlib.notmuch_message_get_tags
389     _get_tags.restype = c_void_p
390
391     _get_date = nmlib.notmuch_message_get_date
392     _get_date.restype = c_uint64
393
394     _get_header = nmlib.notmuch_message_get_header
395     _get_header.restype = c_char_p
396
397     def __init__(self, msg_p, parent=None):
398         """
399         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
400         we will raise an NotmuchError(STATUS.NULL_POINTER).
401
402         Is a 'parent' object is passed which this message is derived from,
403         we save a reference to it, so we can automatically delete the parent
404         object once all derived objects are dead.
405         """
406         if msg_p is None:
407             NotmuchError(STATUS.NULL_POINTER)
408         self._msg = msg_p
409         #keep reference to parent, so we keep it alive
410         self._parent = parent
411         logging.debug("Inited Message derived from %s" %(str(parent)))
412
413
414     def get_message_id(self):
415         """ return the msg id
416         
417         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
418         """
419         if self._msg is None:
420             raise NotmuchError(STATUS.NOT_INITIALIZED)
421         return Message._get_message_id(self._msg)
422
423     def get_date(self):
424         """returns time_t of the message date
425
426         For the original textual representation of the Date header from the
427         message call notmuch_message_get_header() with a header value of
428         "date".
429         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
430         """
431         if self._msg is None:
432             raise NotmuchError(STATUS.NOT_INITIALIZED)
433         return Message._get_date(self._msg)
434
435     def get_header(self, header):
436         """ TODO document me"""
437         if self._msg is None:
438             raise NotmuchError(STATUS.NOT_INITIALIZED)
439
440         #Returns NULL if any error occurs.
441         header = Message._get_header (self._msg, header)
442         if header == None:
443             raise NotmuchError(STATUS.NULL_POINTER)
444         return header
445
446     def get_filename(self):
447         """ return the msg filename
448         
449         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
450         """
451         if self._msg is None:
452             raise NotmuchError(STATUS.NOT_INITIALIZED)
453         return Message._get_filename(self._msg)
454
455     def get_tags(self):
456         """ return the msg tags
457         
458         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
459         Raises NotmuchError(STATUS.NULL_POINTER) on error.
460         """
461         if self._msg is None:
462             raise NotmuchError(STATUS.NOT_INITIALIZED)
463
464         tags_p = Message._get_tags(self._msg)
465         if tags_p == None:
466             raise NotmuchError(STATUS.NULL_POINTER)
467         return Tags(tags_p, self)
468
469     def __str__(self):
470         """A message() is represented by a 1-line summary"""
471         msg = {}
472         msg['from'] = self.get_header('from')
473         msg['tags'] = str(self.get_tags())
474         msg['date'] = date.fromtimestamp(self.get_date())
475         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
476
477     def format_as_text(self):
478         """ Output like notmuch show """
479         return str(self)
480
481     def __del__(self):
482         """Close and free the notmuch Message"""
483         if self._msg is not None:
484             logging.debug("Freeing the Message now")
485             nmlib.notmuch_message_destroy (self._msg)