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
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::
19 for msg in msgs: print msg
23 number_of_msgs = len(msgs)
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.
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::
36 msgs = Query(db,'').search_messages() #get a Messages() object
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.
46 # You can reiterate over *msglist* however as often as you want.
47 # It is simply a list with Message objects.
49 print (msglist[0].get_filename())
50 print (msglist[1].get_filename())
51 print (msglist[0].get_message_id())
55 _get = nmlib.notmuch_messages_get
56 _get.restype = c_void_p
58 _collect_tags = nmlib.notmuch_messages_collect_tags
59 _collect_tags.restype = c_void_p
61 def __init__(self, msgs_p, parent=None):
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
79 NotmuchError(STATUS.NULL_POINTER)
82 #store parent, so we keep them alive as long as self is alive
85 def collect_tags(self):
86 """Return the unique :class:`Tags` in the contained messages
88 :returns: :class:`Tags`
89 :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
91 .. note:: :meth:`collect_tags` will iterate over the messages and
92 therefore will not allow further iterations.
94 if self._msgs is None:
95 raise NotmuchError(STATUS.NOT_INITIALIZED)
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
103 raise NotmuchError(STATUS.NULL_POINTER)
104 return Tags(tags_p, self)
107 """ Make Messages an iterator """
111 if self._msgs is None:
112 raise NotmuchError(STATUS.NOT_INITIALIZED)
114 if not nmlib.notmuch_messages_valid(self._msgs):
118 msg = Message(Messages._get (self._msgs), self)
119 nmlib.notmuch_messages_move_to_next(self._msgs)
123 """len(:class:`Messages`) returns the number of contained messages
125 .. note:: As this iterates over the messages, we will not be able to
126 iterate over them again! So this will fail::
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
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.
140 if self._msgs is None:
141 raise NotmuchError(STATUS.NOT_INITIALIZED)
144 while nmlib.notmuch_messages_valid(self._msgs):
145 nmlib.notmuch_messages_move_to_next(self._msgs)
153 """Close and free the notmuch Messages"""
154 if self._msgs is not None:
155 nmlib.notmuch_messages_destroy (self._msgs)
158 #------------------------------------------------------------------------------
159 class Message(object):
160 """Represents a single Email message
162 Technically, this wraps the underlying *notmuch_message_t* structure.
165 """notmuch_message_get_filename (notmuch_message_t *message)"""
166 _get_filename = nmlib.notmuch_message_get_filename
167 _get_filename.restype = c_char_p
169 """notmuch_message_get_flag"""
170 _get_flag = nmlib.notmuch_message_get_flag
171 _get_flag.restype = c_bool
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
177 """notmuch_message_get_thread_id"""
178 _get_thread_id = nmlib.notmuch_message_get_thread_id
179 _get_thread_id.restype = c_char_p
181 """notmuch_message_get_replies"""
182 _get_replies = nmlib.notmuch_message_get_replies
183 _get_replies.restype = c_void_p
185 """notmuch_message_get_tags (notmuch_message_t *message)"""
186 _get_tags = nmlib.notmuch_message_get_tags
187 _get_tags.restype = c_void_p
189 _get_date = nmlib.notmuch_message_get_date
190 _get_date.restype = c_long
192 _get_header = nmlib.notmuch_message_get_header
193 _get_header.restype = c_char_p
195 #Constants: Flags that can be set/get with set_flag
196 FLAG = Enum(['MATCH'])
198 def __init__(self, msg_p, parent=None):
200 :param msg_p: A pointer to an internal notmuch_message_t
201 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
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
209 NotmuchError(STATUS.NULL_POINTER)
211 #keep reference to parent, so we keep it alive
212 self._parent = parent
215 def get_message_id(self):
216 """Returns the message ID
218 :returns: String with a message ID
219 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
222 if self._msg is None:
223 raise NotmuchError(STATUS.NOT_INITIALIZED)
224 return Message._get_message_id(self._msg)
226 def get_thread_id(self):
227 """Returns the thread ID
229 The returned string belongs to 'message' will only be valid for as
230 long as the message is valid.
232 This function will not return None since Notmuch ensures that every
233 message belongs to a single thread.
235 :returns: String with a thread ID
236 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
239 if self._msg is None:
240 raise NotmuchError(STATUS.NOT_INITIALIZED)
242 return Message._get_thread_id (self._msg);
244 def get_replies(self):
245 """Gets all direct replies to this message as :class:`Messages` iterator
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
256 :returns: :class:`Messages` or `None` if there are no replies to
258 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
261 if self._msg is None:
262 raise NotmuchError(STATUS.NOT_INITIALIZED)
264 msgs_p = Message._get_replies(self._msg);
269 return Messages(msgs_p,self)
272 """Returns time_t of the message date
274 For the original textual representation of the Date header from the
275 message call notmuch_message_get_header() with a header value of
278 :returns: A time_t timestamp.
280 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
283 if self._msg is None:
284 raise NotmuchError(STATUS.NOT_INITIALIZED)
285 return Message._get_date(self._msg)
287 def get_header(self, header):
288 """Returns a message header
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:
293 TODO: add stored headers
295 :param header: The name of the header to be retrieved.
296 It is not case-sensitive (TODO: confirm).
298 :returns: The header value as string
299 :exception: :exc:`NotmuchError`
301 * STATUS.NOT_INITIALIZED if the message
303 * STATUS.NULL_POINTER, if no header was found
305 if self._msg is None:
306 raise NotmuchError(STATUS.NOT_INITIALIZED)
308 #Returns NULL if any error occurs.
309 header = Message._get_header (self._msg, header)
311 raise NotmuchError(STATUS.NULL_POINTER)
314 def get_filename(self):
315 """Returns the file path of the message file
317 :returns: Absolute file path & name of the message file
318 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
321 if self._msg is None:
322 raise NotmuchError(STATUS.NOT_INITIALIZED)
323 return Message._get_filename(self._msg)
325 def get_flag(self, flag):
326 """Checks whether a specific flag is set for this message
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.
332 :param flag: One of the :attr:`Message.FLAG` values (currently only
334 :returns: A bool, indicating whether the flag is set.
335 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
338 if self._msg is None:
339 raise NotmuchError(STATUS.NOT_INITIALIZED)
340 return Message._get_flag(self._msg, flag)
342 def set_flag(self, flag, value):
343 """Sets/Unsets a specific flag for this message
345 :param flag: One of the :attr:`Message.FLAG` values (currently only
347 :param value: A bool indicating whether to set or unset the flag.
350 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
353 if self._msg is None:
354 raise NotmuchError(STATUS.NOT_INITIALIZED)
355 nmlib.notmuch_message_set_flag(self._msg, flag, value)
358 """Returns the message tags
360 :returns: A :class:`Tags` iterator.
361 :exception: :exc:`NotmuchError`
363 * STATUS.NOT_INITIALIZED if the message
365 * STATUS.NULL_POINTER, on error
367 if self._msg is None:
368 raise NotmuchError(STATUS.NOT_INITIALIZED)
370 tags_p = Message._get_tags(self._msg)
372 raise NotmuchError(STATUS.NULL_POINTER)
373 return Tags(tags_p, self)
375 def add_tag(self, tag):
376 """Adds a tag to the given message
378 Adds a tag to the current message. The maximal tag length is defined in
379 the notmuch library and is currently 200 bytes.
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:
387 The 'tag' argument is NULL
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
394 STATUS.NOT_INITIALIZED
395 The message has not been initialized.
397 if self._msg is None:
398 raise NotmuchError(STATUS.NOT_INITIALIZED)
400 status = nmlib.notmuch_message_add_tag (self._msg, tag)
402 if STATUS.SUCCESS == status:
406 raise NotmuchError(status)
408 def remove_tag(self, tag):
409 """Removes a tag from the given message
411 If the message has no such tag, this is a non-operation and
412 will report success anyway.
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:
421 The 'tag' argument is NULL
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
428 STATUS.NOT_INITIALIZED
429 The message has not been initialized.
431 if self._msg is None:
432 raise NotmuchError(STATUS.NOT_INITIALIZED)
434 status = nmlib.notmuch_message_remove_tag(self._msg, tag)
436 if STATUS.SUCCESS == status:
440 raise NotmuchError(status)
442 def remove_all_tags(self):
443 """Removes all tags from the given message.
445 See :meth:`freeze` for an example showing how to safely
448 :returns: STATUS.SUCCESS if the tags were successfully removed.
449 Raises an exception otherwise.
450 :exception: :exc:`NotmuchError`. They have the following meaning:
452 STATUS.READ_ONLY_DATABASE
453 Database was opened in read-only mode so message cannot
455 STATUS.NOT_INITIALIZED
456 The message has not been initialized.
458 if self._msg is None:
459 raise NotmuchError(STATUS.NOT_INITIALIZED)
461 status = nmlib.notmuch_message_remove_all_tags(self._msg)
463 if STATUS.SUCCESS == status:
467 raise NotmuchError(status)
470 """Freezes the current state of 'message' within the database
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.
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.
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::
485 msg.remove_all_tags()
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.
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`.
499 :returns: STATUS.SUCCESS if the message was successfully frozen.
500 Raises an exception otherwise.
501 :exception: :exc:`NotmuchError`. They have the following meaning:
503 STATUS.READ_ONLY_DATABASE
504 Database was opened in read-only mode so message cannot
506 STATUS.NOT_INITIALIZED
507 The message has not been initialized.
509 if self._msg is None:
510 raise NotmuchError(STATUS.NOT_INITIALIZED)
512 status = nmlib.notmuch_message_freeze(self._msg)
514 if STATUS.SUCCESS == status:
518 raise NotmuchError(status)
521 """Thaws the current 'message'
523 Thaw the current 'message', synchronizing any changes that may have
524 occurred while 'message' was frozen into the notmuch database.
526 See :meth:`freeze` for an example of how to use this
527 function to safely provide tag changes.
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.
533 :returns: STATUS.SUCCESS if the message was successfully frozen.
534 Raises an exception otherwise.
535 :exception: :exc:`NotmuchError`. They have the following meaning:
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.
544 if self._msg is None:
545 raise NotmuchError(STATUS.NOT_INITIALIZED)
547 status = nmlib.notmuch_message_thaw(self._msg)
549 if STATUS.SUCCESS == status:
553 raise NotmuchError(status)
557 """A message() is represented by a 1-line summary"""
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)
566 def format_as_text(self):
567 """Output like notmuch show (Not implemented)"""
571 """Close and free the notmuch Message"""
572 if self._msg is not None:
573 nmlib.notmuch_message_destroy (self._msg)