]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/message.py
Implement Message().get|set_flag()
[notmuch] / cnotmuch / message.py
1 from ctypes import c_char_p, c_void_p, c_long, c_bool
2 from datetime import date
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
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_flag"""
170     _get_flag = nmlib.notmuch_message_get_flag
171     _get_flag.restype = c_bool
172
173     """notmuch_message_get_message_id (notmuch_message_t *message)"""
174     _get_message_id = nmlib.notmuch_message_get_message_id
175     _get_message_id.restype = c_char_p 
176
177     """notmuch_message_get_thread_id"""
178     _get_thread_id = nmlib.notmuch_message_get_thread_id
179     _get_thread_id.restype = c_char_p
180
181     """notmuch_message_get_replies"""
182     _get_replies = nmlib.notmuch_message_get_replies
183     _get_replies.restype = c_void_p
184
185     """notmuch_message_get_tags (notmuch_message_t *message)"""
186     _get_tags = nmlib.notmuch_message_get_tags
187     _get_tags.restype = c_void_p
188
189     _get_date = nmlib.notmuch_message_get_date
190     _get_date.restype = c_long
191
192     _get_header = nmlib.notmuch_message_get_header
193     _get_header.restype = c_char_p
194
195     #Constants: Flags that can be set/get with set_flag
196     FLAG = Enum(['MATCH'])
197
198     def __init__(self, msg_p, parent=None):
199         """
200         :param msg_p: A pointer to an internal notmuch_message_t
201             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
202             STATUS.NULL_POINTER.
203         :param parent: A 'parent' object is passed which this message is
204               derived from. We save a reference to it, so we can
205               automatically delete the parent object once all derived
206               objects are dead.
207         """
208         if msg_p is None:
209             NotmuchError(STATUS.NULL_POINTER)
210         self._msg = msg_p
211         #keep reference to parent, so we keep it alive
212         self._parent = parent
213
214
215     def get_message_id(self):
216         """Returns the message ID
217         
218         :returns: String with a message ID
219         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
220                     is not initialized.
221         """
222         if self._msg is None:
223             raise NotmuchError(STATUS.NOT_INITIALIZED)
224         return Message._get_message_id(self._msg)
225
226     def get_thread_id(self):
227         """Returns the thread ID
228
229         The returned string belongs to 'message' will only be valid for as 
230         long as the message is valid.
231
232         This function will not return None since Notmuch ensures that every
233         message belongs to a single thread.
234
235         :returns: String with a thread ID
236         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
237                     is not initialized.
238         """
239         if self._msg is None:
240             raise NotmuchError(STATUS.NOT_INITIALIZED)
241
242         return Message._get_thread_id (self._msg);
243
244     def get_replies(self):
245         """Gets all direct replies to this message as :class:`Messages` iterator
246
247         .. note:: This call only makes sense if 'message' was
248           ultimately obtained from a :class:`Thread` object, (such as
249           by coming directly from the result of calling
250           :meth:`Thread.get_toplevel_messages` or by any number of
251           subsequent calls to :meth:`get_replies`). If this message was
252           obtained through some non-thread means, (such as by a call
253           to :meth:`Query.search_messages`), then this function will
254           return `None`.
255
256         :returns: :class:`Messages` or `None` if there are no replies to 
257             this message.
258         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
259                     is not initialized.
260         """
261         if self._msg is None:
262             raise NotmuchError(STATUS.NOT_INITIALIZED)
263
264         msgs_p = Message._get_replies(self._msg);
265
266         if msgs_p is None:
267             return None
268
269         return Messages(msgs_p,self)
270
271     def get_date(self):
272         """Returns time_t of the message date
273
274         For the original textual representation of the Date header from the
275         message call notmuch_message_get_header() with a header value of
276         "date".
277
278         :returns: A time_t timestamp.
279         :rtype: c_unit64
280         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
281                     is not initialized.
282         """
283         if self._msg is None:
284             raise NotmuchError(STATUS.NOT_INITIALIZED)
285         return Message._get_date(self._msg)
286
287     def get_header(self, header):
288         """Returns a message header
289         
290         This returns any message header that is stored in the notmuch database.
291         This is only a selected subset of headers, which is currently:
292
293           TODO: add stored headers
294
295         :param header: The name of the header to be retrieved.
296                        It is not case-sensitive (TODO: confirm).
297         :type header: str
298         :returns: The header value as string
299         :exception: :exc:`NotmuchError`
300
301                     * STATUS.NOT_INITIALIZED if the message 
302                       is not initialized.
303                     * STATUS.NULL_POINTER, if no header was found
304         """
305         if self._msg is None:
306             raise NotmuchError(STATUS.NOT_INITIALIZED)
307
308         #Returns NULL if any error occurs.
309         header = Message._get_header (self._msg, header)
310         if header == None:
311             raise NotmuchError(STATUS.NULL_POINTER)
312         return header
313
314     def get_filename(self):
315         """Returns the file path of the message file
316
317         :returns: Absolute file path & name of the message file
318         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
319               is not initialized.
320         """
321         if self._msg is None:
322             raise NotmuchError(STATUS.NOT_INITIALIZED)
323         return Message._get_filename(self._msg)
324
325     def get_flag(self, flag):
326         """Checks whether a specific flag is set for this message
327
328         The method :meth:`Query.search_threads` sets
329         *Message.FLAG.MATCH* for those messages that match the
330         query. This method allows us to get the value of this flag.
331
332         :param flag: One of the :attr:`Message.FLAG` values (currently only 
333                      *Message.FLAG.MATCH*
334         :returns: A bool, indicating whether the flag is set.
335         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
336               is not initialized.
337         """
338         if self._msg is None:
339             raise NotmuchError(STATUS.NOT_INITIALIZED)
340         return Message._get_flag(self._msg, flag)
341
342     def set_flag(self, flag, value):
343         """Sets/Unsets a specific flag for this message
344
345         :param flag: One of the :attr:`Message.FLAG` values (currently only 
346                      *Message.FLAG.MATCH*
347         :param value: A bool indicating whether to set or unset the flag.
348
349         :returns: Nothing
350         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
351               is not initialized.
352         """
353         if self._msg is None:
354             raise NotmuchError(STATUS.NOT_INITIALIZED)
355         nmlib.notmuch_message_set_flag(self._msg, flag, value)
356
357     def get_tags(self):
358         """Returns the message tags
359
360         :returns: A :class:`Tags` iterator.
361         :exception: :exc:`NotmuchError`
362
363                       * STATUS.NOT_INITIALIZED if the message 
364                         is not initialized.
365                       * STATUS.NULL_POINTER, on error
366         """
367         if self._msg is None:
368             raise NotmuchError(STATUS.NOT_INITIALIZED)
369
370         tags_p = Message._get_tags(self._msg)
371         if tags_p == None:
372             raise NotmuchError(STATUS.NULL_POINTER)
373         return Tags(tags_p, self)
374
375     def add_tag(self, tag):
376         """Adds a tag to the given message
377
378         Adds a tag to the current message. The maximal tag length is defined in
379         the notmuch library and is currently 200 bytes.
380
381         :param tag: String with a 'tag' to be added.
382         :returns: STATUS.SUCCESS if the tag was successfully added.
383                   Raises an exception otherwise.
384         :exception: :exc:`NotmuchError`. They have the following meaning:
385
386                   STATUS.NULL_POINTER
387                     The 'tag' argument is NULL
388                   STATUS.TAG_TOO_LONG
389                     The length of 'tag' is too long 
390                     (exceeds Message.NOTMUCH_TAG_MAX)
391                   STATUS.READ_ONLY_DATABASE
392                     Database was opened in read-only mode so message cannot be 
393                     modified.
394                   STATUS.NOT_INITIALIZED
395                      The message has not been initialized.
396        """
397         if self._msg is None:
398             raise NotmuchError(STATUS.NOT_INITIALIZED)
399
400         status = nmlib.notmuch_message_add_tag (self._msg, tag)
401
402         if STATUS.SUCCESS == status:
403             # return on success
404             return status
405
406         raise NotmuchError(status)
407
408     def remove_tag(self, tag):
409         """Removes a tag from the given message
410
411         If the message has no such tag, this is a non-operation and
412         will report success anyway.
413
414         :param tag: String with a 'tag' to be removed.
415         :returns: STATUS.SUCCESS if the tag was successfully removed or if 
416                   the message had no such tag.
417                   Raises an exception otherwise.
418         :exception: :exc:`NotmuchError`. They have the following meaning:
419
420                    STATUS.NULL_POINTER
421                      The 'tag' argument is NULL
422                    STATUS.TAG_TOO_LONG
423                      The length of 'tag' is too long
424                      (exceeds NOTMUCH_TAG_MAX)
425                    STATUS.READ_ONLY_DATABASE
426                      Database was opened in read-only mode so message cannot 
427                      be modified.
428                    STATUS.NOT_INITIALIZED
429                      The message has not been initialized.
430         """
431         if self._msg is None:
432             raise NotmuchError(STATUS.NOT_INITIALIZED)
433
434         status = nmlib.notmuch_message_remove_tag(self._msg, tag)
435
436         if STATUS.SUCCESS == status:
437             # return on success
438             return status
439
440         raise NotmuchError(status)
441
442     def remove_all_tags(self):
443         """Removes all tags from the given message.
444
445         See :meth:`freeze` for an example showing how to safely
446         replace tag values.
447
448         :returns: STATUS.SUCCESS if the tags were successfully removed.
449                   Raises an exception otherwise.
450         :exception: :exc:`NotmuchError`. They have the following meaning:
451
452                    STATUS.READ_ONLY_DATABASE
453                      Database was opened in read-only mode so message cannot 
454                      be modified.
455                    STATUS.NOT_INITIALIZED
456                      The message has not been initialized.
457         """
458         if self._msg is None:
459             raise NotmuchError(STATUS.NOT_INITIALIZED)
460  
461         status = nmlib.notmuch_message_remove_all_tags(self._msg)
462
463         if STATUS.SUCCESS == status:
464             # return on success
465             return status
466
467         raise NotmuchError(status)
468
469     def freeze(self):
470         """Freezes the current state of 'message' within the database
471
472         This means that changes to the message state, (via :meth:`add_tag`, 
473         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be 
474         committed to the database until the message is :meth:`thaw`ed.
475
476         Multiple calls to freeze/thaw are valid and these calls will
477         "stack". That is there must be as many calls to thaw as to freeze
478         before a message is actually thawed.
479
480         The ability to do freeze/thaw allows for safe transactions to
481         change tag values. For example, explicitly setting a message to
482         have a given set of tags might look like this::
483
484           msg.freeze()
485           msg.remove_all_tags()
486           for tag in new_tags:
487               msg.add_tag(tag)
488           msg.thaw()
489
490         With freeze/thaw used like this, the message in the database is
491         guaranteed to have either the full set of original tag values, or
492         the full set of new tag values, but nothing in between.
493
494         Imagine the example above without freeze/thaw and the operation
495         somehow getting interrupted. This could result in the message being
496         left with no tags if the interruption happened after
497         :meth:`remove_all_tags` but before :meth:`add_tag`.
498
499         :returns: STATUS.SUCCESS if the message was successfully frozen.
500                   Raises an exception otherwise.
501         :exception: :exc:`NotmuchError`. They have the following meaning:
502
503                    STATUS.READ_ONLY_DATABASE
504                      Database was opened in read-only mode so message cannot 
505                      be modified.
506                    STATUS.NOT_INITIALIZED
507                      The message has not been initialized.
508         """
509         if self._msg is None:
510             raise NotmuchError(STATUS.NOT_INITIALIZED)
511  
512         status = nmlib.notmuch_message_freeze(self._msg)
513
514         if STATUS.SUCCESS == status:
515             # return on success
516             return status
517
518         raise NotmuchError(status)
519
520     def thaw(self):
521         """Thaws the current 'message'
522
523         Thaw the current 'message', synchronizing any changes that may have 
524         occurred while 'message' was frozen into the notmuch database.
525
526         See :meth:`freeze` for an example of how to use this
527         function to safely provide tag changes.
528
529         Multiple calls to freeze/thaw are valid and these calls with
530         "stack". That is there must be as many calls to thaw as to freeze
531         before a message is actually thawed.
532
533         :returns: STATUS.SUCCESS if the message was successfully frozen.
534                   Raises an exception otherwise.
535         :exception: :exc:`NotmuchError`. They have the following meaning:
536
537                    STATUS.UNBALANCED_FREEZE_THAW
538                      An attempt was made to thaw an unfrozen message. 
539                      That is, there have been an unbalanced number of calls 
540                      to :meth:`freeze` and :meth:`thaw`.
541                    STATUS.NOT_INITIALIZED
542                      The message has not been initialized.
543         """
544         if self._msg is None:
545             raise NotmuchError(STATUS.NOT_INITIALIZED)
546  
547         status = nmlib.notmuch_message_thaw(self._msg)
548
549         if STATUS.SUCCESS == status:
550             # return on success
551             return status
552
553         raise NotmuchError(status)
554
555     
556     def __str__(self):
557         """A message() is represented by a 1-line summary"""
558         msg = {}
559         msg['from'] = self.get_header('from')
560         msg['tags'] = str(self.get_tags())
561         msg['date'] = date.fromtimestamp(self.get_date())
562         replies = self.get_replies()
563         msg['replies'] = len(replies) if replies is not None else -1
564         return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg)
565
566     def format_as_text(self):
567         """Output like notmuch show (Not implemented)"""
568         return str(self)
569
570     def __del__(self):
571         """Close and free the notmuch Message"""
572         if self._msg is not None:
573             nmlib.notmuch_message_destroy (self._msg)