2 This file is part of notmuch.
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 You should have received a copy of the GNU General Public License
15 along with notmuch. If not, see <http://www.gnu.org/licenses/>.
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
18 Jesse Rosenthal <jrosenthal@jhu.edu>
21 from .globals import (
32 from .message import Message
36 class Messages(object):
37 """Represents a list of notmuch messages
39 This object provides an iterator over a list of notmuch messages
40 (Technically, it provides a wrapper for the underlying
41 *notmuch_messages_t* structure). Do note that the underlying library
42 only provides a one-time iterator (it cannot reset the iterator to
43 the start). Thus iterating over the function will "exhaust" the list
44 of messages, and a subsequent iteration attempt will raise a
45 :exc:`NotInitializedError`. If you need to
46 re-iterate over a list of messages you will need to retrieve a new
47 :class:`Messages` object or cache your :class:`Message`\s in a list
52 You can store and reuse the single :class:`Message` objects as often
53 as you want as long as you keep the parent :class:`Messages` object
54 around. (Due to hierarchical memory allocation, all derived
55 :class:`Message` objects will be invalid when we delete the parent
56 :class:`Messages` object, even if it was already exhausted.) So
60 msgs = Query(db,'').search_messages() #get a Messages() object
63 # msgs is "exhausted" now and msgs.next() will raise an exception.
64 # However it will be kept alive until all retrieved Message()
65 # objects are also deleted. If you do e.g. an explicit del(msgs)
66 # here, the following lines would fail.
68 # You can reiterate over *msglist* however as often as you want.
69 # It is simply a list with :class:`Message`s.
71 print (msglist[0].get_filename())
72 print (msglist[1].get_filename())
73 print (msglist[0].get_message_id())
76 As :class:`Message` implements both __hash__() and __cmp__(), it is
77 possible to make sets out of :class:`Messages` and use set
78 arithmetic (this happens in python and will of course be *much*
79 slower than redoing a proper query with the appropriate filters::
81 s1, s2 = set(msgs1), set(msgs2)
86 Be careful when using set arithmetic between message sets derived
87 from different Databases (ie the same database reopened after
88 messages have changed). If messages have added or removed associated
89 files in the meantime, it is possible that the same message would be
90 considered as a different object (as it points to a different file).
94 _get = nmlib.notmuch_messages_get
95 _get.argtypes = [NotmuchMessagesP]
96 _get.restype = NotmuchMessageP
98 _collect_tags = nmlib.notmuch_messages_collect_tags
99 _collect_tags.argtypes = [NotmuchMessagesP]
100 _collect_tags.restype = NotmuchTagsP
102 def __init__(self, msgs_p, parent=None):
104 :param msgs_p: A pointer to an underlying *notmuch_messages_t*
105 structure. These are not publically exposed, so a user
106 will almost never instantiate a :class:`Messages` object
107 herself. They are usually handed back as a result,
108 e.g. in :meth:`Query.search_messages`. *msgs_p* must be
109 valid, we will raise an :exc:`NullPointerError` if it is
111 :type msgs_p: :class:`ctypes.c_void_p`
112 :param parent: The parent object
113 (ie :class:`Query`) these tags are derived from. It saves
114 a reference to it, so we can automatically delete the db
115 object once all derived objects are dead.
116 :TODO: Make the iterator work more than once and cache the tags in
117 the Python object.(?)
120 raise NullPointerError()
123 #store parent, so we keep them alive as long as self is alive
124 self._parent = parent
126 def collect_tags(self):
127 """Return the unique :class:`Tags` in the contained messages
129 :returns: :class:`Tags`
130 :exceptions: :exc:`NotInitializedError` if not init'ed
134 :meth:`collect_tags` will iterate over the messages and therefore
135 will not allow further iterations.
138 raise NotInitializedError()
140 # collect all tags (returns NULL on error)
141 tags_p = Messages._collect_tags(self._msgs)
142 #reset _msgs as we iterated over it and can do so only once
146 raise NullPointerError()
147 return Tags(tags_p, self)
150 """ Make Messages an iterator """
153 _valid = nmlib.notmuch_messages_valid
154 _valid.argtypes = [NotmuchMessagesP]
155 _valid.restype = bool
157 _move_to_next = nmlib.notmuch_messages_move_to_next
158 _move_to_next.argtypes = [NotmuchMessagesP]
159 _move_to_next.restype = None
163 raise NotInitializedError()
165 if not self._valid(self._msgs):
169 msg = Message(Messages._get(self._msgs), self)
170 self._move_to_next(self._msgs)
172 next = __next__ # python2.x iterator protocol compatibility
174 def __nonzero__(self):
176 Implement truth value testing. If __nonzero__ is not
177 implemented, the python runtime would fall back to `len(..) >
178 0` thus exhausting the iterator.
180 :returns: True if the wrapped iterator has at least one more object
183 return self._msgs and self._valid(self._msgs)
185 _destroy = nmlib.notmuch_messages_destroy
186 _destroy.argtypes = [NotmuchMessagesP]
187 _destroy.restype = None
190 """Close and free the notmuch Messages"""
192 self._destroy(self._msgs)
194 def format_messages(self, format, indent=0, entire_thread=False):
195 """Formats messages as needed for 'notmuch show'.
197 :param format: A string of either 'text' or 'json'.
198 :param indent: A number indicating the reply depth of these messages.
199 :param entire_thread: A bool, indicating whether we want to output
200 whole threads or only the matching messages.
201 :return: a list of lines
205 if format.lower() == "text":
209 elif format.lower() == "json":
214 raise TypeError("format must be either 'text' or 'json'")
218 result.append(set_start)
220 # iterate through all toplevel messages in this thread
225 result.append(set_sep)
228 result.append(set_start)
229 match = msg.is_match()
232 if (match or entire_thread):
233 if format.lower() == "text":
234 result.append(msg.format_message_as_text(indent))
236 result.append(msg.format_message_as_json(indent))
237 next_indent = indent + 1
239 # get replies and print them also out (if there are any)
240 replies = msg.get_replies().format_messages(format, next_indent, entire_thread)
242 result.append(set_sep)
243 result.extend(replies)
245 result.append(set_end)
246 result.append(set_end)
250 def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout):
251 """Outputs messages as needed for 'notmuch show' to a file like object.
253 :param format: A string of either 'text' or 'json'.
254 :param handle: A file like object to print to (default is sys.stdout).
255 :param indent: A number indicating the reply depth of these messages.
256 :param entire_thread: A bool, indicating whether we want to output
257 whole threads or only the matching messages.
259 handle.write(''.join(self.format_messages(format, indent, entire_thread)))
261 class EmptyMessagesResult(Messages):
262 def __init__(self, parent):
264 self._parent = parent
267 raise StopIteration()