]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/message.py
try c_long rather c_int64 for time_t
[notmuch] / cnotmuch / message.py
1 from ctypes import c_char_p, c_void_p, c_long
2 from datetime import date
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError
4 from cnotmuch.tag import Tags
5 #------------------------------------------------------------------------------
6 class Messages(object):
7     """Represents a list of notmuch messages
8
9     This object provides an iterator over a list of notmuch messages
10     (Technically, it provides a wrapper for the underlying
11     *notmuch_messages_t* structure). Do note that the underlying
12     library only provides a one-time iterator (it cannot reset the
13     iterator to the start). Thus iterating over the function will
14     "exhaust" the list of messages, and a subsequent iteration attempt
15     will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
16     note, that any function that uses iteration will also
17     exhaust the messages. So both::
18
19       for msg in msgs: print msg 
20
21     as well as::
22
23        number_of_msgs = len(msgs)
24
25     will "exhaust" the Messages. If you need to re-iterate over a list of
26     messages you will need to retrieve a new :class:`Messages` object.
27
28     Things are not as bad as it seems though, you can store and reuse
29     the single Message objects as often as you want as long as you
30     keep the parent Messages object around. (Recall that due to
31     hierarchical memory allocation, all derived Message objects will
32     be invalid when we delete the parent Messages() object, even if it
33     was already "exhausted".) So this works::
34
35       db   = Database()
36       msgs = Query(db,'').search_messages() #get a Messages() object
37       msglist = []
38       for m in msgs:
39          msglist.append(m)
40
41       # msgs is "exhausted" now and even len(msgs) will raise an exception.
42       # However it will be kept around until all retrieved Message() objects are
43       # also deleted. If you did e.g. an explicit del(msgs) here, the 
44       # following lines would fail.
45       
46       # You can reiterate over *msglist* however as often as you want. 
47       # It is simply a list with Message objects.
48
49       print (msglist[0].get_filename())
50       print (msglist[1].get_filename())
51       print (msglist[0].get_message_id())
52     """
53
54     #notmuch_tags_get
55     _get = nmlib.notmuch_messages_get
56     _get.restype = c_void_p
57
58     _collect_tags = nmlib.notmuch_messages_collect_tags
59     _collect_tags.restype = c_void_p
60
61     def __init__(self, msgs_p, parent=None):
62         """
63         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
64              structure. These are not publically exposed, so a user
65              will almost never instantiate a :class:`Messages` object
66              herself. They are usually handed back as a result,
67              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
68              valid, we will raise an :exc:`NotmuchError`
69              (STATUS.NULL_POINTER) if it is `None`.
70         :type msgs_p: :class:`ctypes.c_void_p`
71         :param parent: The parent object
72              (ie :class:`Query`) these tags are derived from. It saves
73              a reference to it, so we can automatically delete the db
74              object once all derived objects are dead.
75         :TODO: Make the iterator work more than once and cache the tags in 
76                the Python object.(?)
77         """
78         if msgs_p is None:
79             NotmuchError(STATUS.NULL_POINTER)
80
81         self._msgs = msgs_p
82         #store parent, so we keep them alive as long as self  is alive
83         self._parent = parent
84
85     def collect_tags(self):
86         """Return the unique :class:`Tags` in the contained messages
87
88         :returns: :class:`Tags`
89         :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
90
91         .. note:: :meth:`collect_tags` will iterate over the messages and
92           therefore will not allow further iterations.
93         """
94         if self._msgs is None:
95             raise NotmuchError(STATUS.NOT_INITIALIZED)
96
97         # collect all tags (returns NULL on error)
98         tags_p = Messages._collect_tags (self._msgs)
99         #reset _msgs as we iterated over it and can do so only once
100         self._msgs = None
101
102         if tags_p == None:
103             raise NotmuchError(STATUS.NULL_POINTER)
104         return Tags(tags_p, self)
105
106     def __iter__(self):
107         """ Make Messages an iterator """
108         return self
109
110     def next(self):
111         if self._msgs is None:
112             raise NotmuchError(STATUS.NOT_INITIALIZED)
113
114         if not nmlib.notmuch_messages_valid(self._msgs):
115             self._msgs = None
116             raise StopIteration
117
118         msg = Message(Messages._get (self._msgs), self)
119         nmlib.notmuch_messages_move_to_next(self._msgs)
120         return msg
121
122     def __len__(self):
123         """len(:class:`Messages`) returns the number of contained messages
124
125         .. note:: As this iterates over the messages, we will not be able to 
126                iterate over them again! So this will fail::
127
128                  #THIS FAILS
129                  msgs = Database().create_query('').search_message()
130                  if len(msgs) > 0:              #this 'exhausts' msgs
131                      # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
132                      for msg in msgs: print msg
133
134                Most of the time, using the
135                :meth:`Query.count_messages` is therefore more
136                appropriate (and much faster). While not guaranteeing
137                that it will return the exact same number than len(),
138                in my tests it effectively always did so.
139         """
140         if self._msgs is None:
141             raise NotmuchError(STATUS.NOT_INITIALIZED)
142
143         i=0
144         while nmlib.notmuch_messages_valid(self._msgs):
145             nmlib.notmuch_messages_move_to_next(self._msgs)
146             i += 1
147         self._msgs = None
148         return i
149
150
151
152     def __del__(self):
153         """Close and free the notmuch Messages"""
154         if self._msgs is not None:
155             nmlib.notmuch_messages_destroy (self._msgs)
156
157
158 #------------------------------------------------------------------------------
159 class Message(object):
160     """Represents a single Email message
161
162     Technically, this wraps the underlying *notmuch_message_t* structure.
163     """
164
165     """notmuch_message_get_filename (notmuch_message_t *message)"""
166     _get_filename = nmlib.notmuch_message_get_filename
167     _get_filename.restype = c_char_p 
168
169     """notmuch_message_get_message_id (notmuch_message_t *message)"""
170     _get_message_id = nmlib.notmuch_message_get_message_id
171     _get_message_id.restype = c_char_p 
172
173     """notmuch_message_get_thread_id"""
174     _get_thread_id = nmlib.notmuch_message_get_thread_id
175     _get_thread_id.restype = c_char_p
176
177     """notmuch_message_get_replies"""
178     _get_replies = nmlib.notmuch_message_get_replies
179     _get_replies.restype = c_void_p
180
181     """notmuch_message_get_tags (notmuch_message_t *message)"""
182     _get_tags = nmlib.notmuch_message_get_tags
183     _get_tags.restype = c_void_p
184
185     _get_date = nmlib.notmuch_message_get_date
186     _get_date.restype = c_long
187
188     _get_header = nmlib.notmuch_message_get_header
189     _get_header.restype = c_char_p
190
191     def __init__(self, msg_p, parent=None):
192         """
193         :param msg_p: A pointer to an internal notmuch_message_t
194             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
195             STATUS.NULL_POINTER.
196         :param parent: A 'parent' object is passed which this message is
197               derived from. We save a reference to it, so we can
198               automatically delete the parent object once all derived
199               objects are dead.
200         """
201         if msg_p is None:
202             NotmuchError(STATUS.NULL_POINTER)
203         self._msg = msg_p
204         #keep reference to parent, so we keep it alive
205         self._parent = parent
206
207
208     def get_message_id(self):
209         """Returns the message ID
210         
211         :returns: String with a message ID
212         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
213                     is not initialized.
214         """
215         if self._msg is None:
216             raise NotmuchError(STATUS.NOT_INITIALIZED)
217         return Message._get_message_id(self._msg)
218
219     def get_thread_id(self):
220         """Returns the thread ID
221
222         The returned string belongs to 'message' will only be valid for as 
223         long as the message is valid.
224
225         This function will not return None since Notmuch ensures that every
226         message belongs to a single thread.
227
228         :returns: String with a thread ID
229         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
230                     is not initialized.
231         """
232         if self._msg is None:
233             raise NotmuchError(STATUS.NOT_INITIALIZED)
234
235         return Message._get_thread_id (self._msg);
236
237     def get_replies(self):
238         """Gets all direct replies to this message as :class:`Messages` iterator
239
240         .. note:: This call only makes sense if 'message' was
241           ultimately obtained from a :class:`Thread` object, (such as
242           by coming directly from the result of calling
243           :meth:`Thread.get_toplevel_messages` or by any number of
244           subsequent calls to :meth:`get_replies`). If this message was
245           obtained through some non-thread means, (such as by a call
246           to :meth:`Query.search_messages`), then this function will
247           return `None`.
248
249         :returns: :class:`Messages` or `None` if there are no replies to 
250             this message.
251         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
252                     is not initialized.
253         """
254         if self._msg is None:
255             raise NotmuchError(STATUS.NOT_INITIALIZED)
256
257         msgs_p = Message._get_replies(self._msg);
258
259         if msgs_p is None:
260             return None
261
262         return Messages(msgs_p,self)
263
264     def get_date(self):
265         """Returns time_t of the message date
266
267         For the original textual representation of the Date header from the
268         message call notmuch_message_get_header() with a header value of
269         "date".
270
271         :returns: A time_t timestamp.
272         :rtype: c_unit64
273         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
274                     is not initialized.
275         """
276         if self._msg is None:
277             raise NotmuchError(STATUS.NOT_INITIALIZED)
278         return Message._get_date(self._msg)
279
280     def get_header(self, header):
281         """Returns a message header
282         
283         This returns any message header that is stored in the notmuch database.
284         This is only a selected subset of headers, which is currently:
285
286           TODO: add stored headers
287
288         :param header: The name of the header to be retrieved.
289                        It is not case-sensitive (TODO: confirm).
290         :type header: str
291         :returns: The header value as string
292         :exception: :exc:`NotmuchError`
293
294                     * STATUS.NOT_INITIALIZED if the message 
295                       is not initialized.
296                     * STATUS.NULL_POINTER, if no header was found
297         """
298         if self._msg is None:
299             raise NotmuchError(STATUS.NOT_INITIALIZED)
300
301         #Returns NULL if any error occurs.
302         header = Message._get_header (self._msg, header)
303         if header == None:
304             raise NotmuchError(STATUS.NULL_POINTER)
305         return header
306
307     def get_filename(self):
308         """Returns the file path of the message file
309
310         :returns: Absolute file path & name of the message file
311         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
312               is not initialized.
313         """
314         if self._msg is None:
315             raise NotmuchError(STATUS.NOT_INITIALIZED)
316         return Message._get_filename(self._msg)
317
318     def get_tags(self):
319         """Returns the message tags
320
321         :returns: A :class:`Tags` iterator.
322         :exception: :exc:`NotmuchError`
323
324                       * STATUS.NOT_INITIALIZED if the message 
325                         is not initialized.
326                       * STATUS.NULL_POINTER, on error
327         """
328         if self._msg is None:
329             raise NotmuchError(STATUS.NOT_INITIALIZED)
330
331         tags_p = Message._get_tags(self._msg)
332         if tags_p == None:
333             raise NotmuchError(STATUS.NULL_POINTER)
334         return Tags(tags_p, self)
335
336     def add_tag(self, tag):
337         """Adds a tag to the given message
338
339         Adds a tag to the current message. The maximal tag length is defined in
340         the notmuch library and is currently 200 bytes.
341
342         :param tag: String with a 'tag' to be added.
343         :returns: STATUS.SUCCESS if the tag was successfully added.
344                   Raises an exception otherwise.
345         :exception: :exc:`NotmuchError`. They have the following meaning:
346
347                   STATUS.NULL_POINTER
348                     The 'tag' argument is NULL
349                   STATUS.TAG_TOO_LONG
350                     The length of 'tag' is too long 
351                     (exceeds Message.NOTMUCH_TAG_MAX)
352                   STATUS.READ_ONLY_DATABASE
353                     Database was opened in read-only mode so message cannot be 
354                     modified.
355                   STATUS.NOT_INITIALIZED
356                      The message has not been initialized.
357        """
358         if self._msg is None:
359             raise NotmuchError(STATUS.NOT_INITIALIZED)
360
361         status = nmlib.notmuch_message_add_tag (self._msg, tag)
362
363         if STATUS.SUCCESS == status:
364             # return on success
365             return status
366
367         raise NotmuchError(status)
368
369     def remove_tag(self, tag):
370         """Removes a tag from the given message
371
372         If the message has no such tag, this is a non-operation and
373         will report success anyway.
374
375         :param tag: String with a 'tag' to be removed.
376         :returns: STATUS.SUCCESS if the tag was successfully removed or if 
377                   the message had no such tag.
378                   Raises an exception otherwise.
379         :exception: :exc:`NotmuchError`. They have the following meaning:
380
381                    STATUS.NULL_POINTER
382                      The 'tag' argument is NULL
383                    STATUS.TAG_TOO_LONG
384                      The length of 'tag' is too long
385                      (exceeds NOTMUCH_TAG_MAX)
386                    STATUS.READ_ONLY_DATABASE
387                      Database was opened in read-only mode so message cannot 
388                      be modified.
389                    STATUS.NOT_INITIALIZED
390                      The message has not been initialized.
391         """
392         if self._msg is None:
393             raise NotmuchError(STATUS.NOT_INITIALIZED)
394
395         status = nmlib.notmuch_message_remove_tag(self._msg, tag)
396
397         if STATUS.SUCCESS == status:
398             # return on success
399             return status
400
401         raise NotmuchError(status)
402
403     def remove_all_tags(self):
404         """Removes all tags from the given message.
405
406         See :meth:`freeze` for an example showing how to safely
407         replace tag values.
408
409         :returns: STATUS.SUCCESS if the tags were successfully removed.
410                   Raises an exception otherwise.
411         :exception: :exc:`NotmuchError`. They have the following meaning:
412
413                    STATUS.READ_ONLY_DATABASE
414                      Database was opened in read-only mode so message cannot 
415                      be modified.
416                    STATUS.NOT_INITIALIZED
417                      The message has not been initialized.
418         """
419         if self._msg is None:
420             raise NotmuchError(STATUS.NOT_INITIALIZED)
421  
422         status = nmlib.notmuch_message_remove_all_tags(self._msg)
423
424         if STATUS.SUCCESS == status:
425             # return on success
426             return status
427
428         raise NotmuchError(status)
429
430     def freeze(self):
431         """Freezes the current state of 'message' within the database
432
433         This means that changes to the message state, (via :meth:`add_tag`, 
434         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be 
435         committed to the database until the message is :meth:`thaw`ed.
436
437         Multiple calls to freeze/thaw are valid and these calls will
438         "stack". That is there must be as many calls to thaw as to freeze
439         before a message is actually thawed.
440
441         The ability to do freeze/thaw allows for safe transactions to
442         change tag values. For example, explicitly setting a message to
443         have a given set of tags might look like this::
444
445           msg.freeze()
446           msg.remove_all_tags()
447           for tag in new_tags:
448               msg.add_tag(tag)
449           msg.thaw()
450
451         With freeze/thaw used like this, the message in the database is
452         guaranteed to have either the full set of original tag values, or
453         the full set of new tag values, but nothing in between.
454
455         Imagine the example above without freeze/thaw and the operation
456         somehow getting interrupted. This could result in the message being
457         left with no tags if the interruption happened after
458         :meth:`remove_all_tags` but before :meth:`add_tag`.
459
460         :returns: STATUS.SUCCESS if the message was successfully frozen.
461                   Raises an exception otherwise.
462         :exception: :exc:`NotmuchError`. They have the following meaning:
463
464                    STATUS.READ_ONLY_DATABASE
465                      Database was opened in read-only mode so message cannot 
466                      be modified.
467                    STATUS.NOT_INITIALIZED
468                      The message has not been initialized.
469         """
470         if self._msg is None:
471             raise NotmuchError(STATUS.NOT_INITIALIZED)
472  
473         status = nmlib.notmuch_message_freeze(self._msg)
474
475         if STATUS.SUCCESS == status:
476             # return on success
477             return status
478
479         raise NotmuchError(status)
480
481     def thaw(self):
482         """Thaws the current 'message'
483
484         Thaw the current 'message', synchronizing any changes that may have 
485         occurred while 'message' was frozen into the notmuch database.
486
487         See :meth:`freeze` for an example of how to use this
488         function to safely provide tag changes.
489
490         Multiple calls to freeze/thaw are valid and these calls with
491         "stack". That is there must be as many calls to thaw as to freeze
492         before a message is actually thawed.
493
494         :returns: STATUS.SUCCESS if the message was successfully frozen.
495                   Raises an exception otherwise.
496         :exception: :exc:`NotmuchError`. They have the following meaning:
497
498                    STATUS.UNBALANCED_FREEZE_THAW
499                      An attempt was made to thaw an unfrozen message. 
500                      That is, there have been an unbalanced number of calls 
501                      to :meth:`freeze` and :meth:`thaw`.
502                    STATUS.NOT_INITIALIZED
503                      The message has not been initialized.
504         """
505         if self._msg is None:
506             raise NotmuchError(STATUS.NOT_INITIALIZED)
507  
508         status = nmlib.notmuch_message_thaw(self._msg)
509
510         if STATUS.SUCCESS == status:
511             # return on success
512             return status
513
514         raise NotmuchError(status)
515
516     
517     def __str__(self):
518         """A message() is represented by a 1-line summary"""
519         msg = {}
520         msg['from'] = self.get_header('from')
521         msg['tags'] = str(self.get_tags())
522         msg['date'] = date.fromtimestamp(self.get_date())
523         replies = self.get_replies()
524         msg['replies'] = len(replies) if replies is not None else -1
525         return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg)
526
527     def format_as_text(self):
528         """Output like notmuch show (Not implemented)"""
529         return str(self)
530
531     def __del__(self):
532         """Close and free the notmuch Message"""
533         if self._msg is not None:
534             nmlib.notmuch_message_destroy (self._msg)