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>'
20 from ctypes import c_char_p, c_void_p, c_long
21 from notmuch.globals import nmlib, STATUS, NotmuchError
22 from notmuch.message import Messages
23 from notmuch.tag import Tags
24 from datetime import date
27 class Threads(object):
28 """Represents a list of notmuch threads
30 This object provides an iterator over a list of notmuch threads
31 (Technically, it provides a wrapper for the underlying
32 *notmuch_threads_t* structure). Do note that the underlying
33 library only provides a one-time iterator (it cannot reset the
34 iterator to the start). Thus iterating over the function will
35 "exhaust" the list of threads, and a subsequent iteration attempt
36 will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
37 note, that any function that uses iteration will also
38 exhaust the messages. So both::
40 for thread in threads: print thread
44 number_of_msgs = len(threads)
46 will "exhaust" the threads. If you need to re-iterate over a list of
47 messages you will need to retrieve a new :class:`Threads` object.
49 Things are not as bad as it seems though, you can store and reuse
50 the single Thread objects as often as you want as long as you
51 keep the parent Threads object around. (Recall that due to
52 hierarchical memory allocation, all derived Threads objects will
53 be invalid when we delete the parent Threads() object, even if it
54 was already "exhausted".) So this works::
57 threads = Query(db,'').search_threads() #get a Threads() object
59 for thread in threads:
60 threadlist.append(thread)
62 # threads is "exhausted" now and even len(threads) will raise an
64 # However it will be kept around until all retrieved Thread() objects are
65 # also deleted. If you did e.g. an explicit del(threads) here, the
66 # following lines would fail.
68 # You can reiterate over *threadlist* however as often as you want.
69 # It is simply a list with Thread objects.
71 print (threadlist[0].get_thread_id())
72 print (threadlist[1].get_thread_id())
73 print (threadlist[0].get_total_messages())
77 _get = nmlib.notmuch_threads_get
78 _get.restype = c_void_p
80 def __init__(self, threads_p, parent=None):
82 :param threads_p: A pointer to an underlying *notmuch_threads_t*
83 structure. These are not publically exposed, so a user
84 will almost never instantiate a :class:`Threads` object
85 herself. They are usually handed back as a result,
86 e.g. in :meth:`Query.search_threads`. *threads_p* must be
87 valid, we will raise an :exc:`NotmuchError`
88 (STATUS.NULL_POINTER) if it is `None`.
89 :type threads_p: :class:`ctypes.c_void_p`
90 :param parent: The parent object
91 (ie :class:`Query`) these tags are derived from. It saves
92 a reference to it, so we can automatically delete the db
93 object once all derived objects are dead.
94 :TODO: Make the iterator work more than once and cache the tags in
98 raise NotmuchError(STATUS.NULL_POINTER)
100 self._threads = threads_p
101 #store parent, so we keep them alive as long as self is alive
102 self._parent = parent
105 """ Make Threads an iterator """
109 if self._threads is None:
110 raise NotmuchError(STATUS.NOT_INITIALIZED)
112 if not nmlib.notmuch_threads_valid(self._threads):
116 thread = Thread(Threads._get(self._threads), self)
117 nmlib.notmuch_threads_move_to_next(self._threads)
121 """len(:class:`Threads`) returns the number of contained Threads
123 .. note:: As this iterates over the threads, we will not be able to
124 iterate over them again! So this will fail::
127 threads = Database().create_query('').search_threads()
128 if len(threads) > 0: #this 'exhausts' threads
129 # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
130 for thread in threads: print thread
132 if self._threads is None:
133 raise NotmuchError(STATUS.NOT_INITIALIZED)
136 # returns 'bool'. On out-of-memory it returns None
137 while nmlib.notmuch_threads_valid(self._threads):
138 nmlib.notmuch_threads_move_to_next(self._threads)
140 # reset self._threads to mark as "exhausted"
144 def __nonzero__(self):
145 """Check if :class:`Threads` contains at least one more valid thread
147 The existence of this function makes 'if Threads: foo' work, as
148 that will implicitely call len() exhausting the iterator if
149 __nonzero__ does not exist. This function makes `bool(Threads())`
152 :return: True if there is at least one more thread in the
153 Iterator, False if not. None on a "Out-of-memory" error.
155 return self._threads is not None and \
156 nmlib.notmuch_threads_valid(self._threads) > 0
159 """Close and free the notmuch Threads"""
160 if self._threads is not None:
161 nmlib.notmuch_messages_destroy(self._threads)
164 class Thread(object):
165 """Represents a single message thread."""
167 """notmuch_thread_get_thread_id"""
168 _get_thread_id = nmlib.notmuch_thread_get_thread_id
169 _get_thread_id.restype = c_char_p
171 """notmuch_thread_get_authors"""
172 _get_authors = nmlib.notmuch_thread_get_authors
173 _get_authors.restype = c_char_p
175 """notmuch_thread_get_subject"""
176 _get_subject = nmlib.notmuch_thread_get_subject
177 _get_subject.restype = c_char_p
179 """notmuch_thread_get_toplevel_messages"""
180 _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
181 _get_toplevel_messages.restype = c_void_p
183 _get_newest_date = nmlib.notmuch_thread_get_newest_date
184 _get_newest_date.restype = c_long
186 _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
187 _get_oldest_date.restype = c_long
189 """notmuch_thread_get_tags"""
190 _get_tags = nmlib.notmuch_thread_get_tags
191 _get_tags.restype = c_void_p
193 def __init__(self, thread_p, parent=None):
195 :param thread_p: A pointer to an internal notmuch_thread_t
196 Structure. These are not publically exposed, so a user
197 will almost never instantiate a :class:`Thread` object
198 herself. They are usually handed back as a result,
199 e.g. when iterating through :class:`Threads`. *thread_p*
200 must be valid, we will raise an :exc:`NotmuchError`
201 (STATUS.NULL_POINTER) if it is `None`.
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 raise NotmuchError(STATUS.NULL_POINTER)
210 self._thread = thread_p
211 #keep reference to parent, so we keep it alive
212 self._parent = parent
214 def get_thread_id(self):
215 """Get the thread ID of 'thread'
217 The returned string belongs to 'thread' and will only be valid
218 for as long as the thread is valid.
220 :returns: String with a message ID
221 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
224 if self._thread is None:
225 raise NotmuchError(STATUS.NOT_INITIALIZED)
226 return Thread._get_thread_id(self._thread)
228 def get_total_messages(self):
229 """Get the total number of messages in 'thread'
231 :returns: The number of all messages in the database
232 belonging to this thread. Contrast with
233 :meth:`get_matched_messages`.
234 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
237 if self._thread is None:
238 raise NotmuchError(STATUS.NOT_INITIALIZED)
239 return nmlib.notmuch_thread_get_total_messages(self._thread)
241 def get_toplevel_messages(self):
242 """Returns a :class:`Messages` iterator for the top-level messages in
245 This iterator will not necessarily iterate over all of the messages
246 in the thread. It will only iterate over the messages in the thread
247 which are not replies to other messages in the thread.
249 To iterate over all messages in the thread, the caller will need to
250 iterate over the result of :meth:`Message.get_replies` for each
251 top-level message (and do that recursively for the resulting
254 :returns: :class:`Messages`
255 :exception: :exc:`NotmuchError`
257 * STATUS.NOT_INITIALIZED if query is not inited
258 * STATUS.NULL_POINTER if search_messages failed
260 if self._thread is None:
261 raise NotmuchError(STATUS.NOT_INITIALIZED)
263 msgs_p = Thread._get_toplevel_messages(self._thread)
266 raise NotmuchError(STATUS.NULL_POINTER)
268 return Messages(msgs_p, self)
270 def get_matched_messages(self):
271 """Returns the number of messages in 'thread' that matched the query
273 :returns: The number of all messages belonging to this thread that
274 matched the :class:`Query`from which this thread was created.
275 Contrast with :meth:`get_total_messages`.
276 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
279 if self._thread is None:
280 raise NotmuchError(STATUS.NOT_INITIALIZED)
281 return nmlib.notmuch_thread_get_matched_messages(self._thread)
283 def get_authors(self):
284 """Returns the authors of 'thread'
286 The returned string is a comma-separated list of the names of the
287 authors of mail messages in the query results that belong to this
290 The returned string belongs to 'thread' and will only be valid for
291 as long as this Thread() is not deleted.
293 if self._thread is None:
294 raise NotmuchError(STATUS.NOT_INITIALIZED)
295 authors = Thread._get_authors(self._thread)
298 return authors.decode('UTF-8')
300 def get_subject(self):
301 """Returns the Subject of 'thread'
303 The returned string belongs to 'thread' and will only be valid for
304 as long as this Thread() is not deleted.
306 if self._thread is None:
307 raise NotmuchError(STATUS.NOT_INITIALIZED)
308 subject = Thread._get_subject(self._thread)
311 return subject.decode('UTF-8')
313 def get_newest_date(self):
314 """Returns time_t of the newest message date
316 :returns: A time_t timestamp.
318 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
321 if self._thread is None:
322 raise NotmuchError(STATUS.NOT_INITIALIZED)
323 return Thread._get_newest_date(self._thread)
325 def get_oldest_date(self):
326 """Returns time_t of the oldest message date
328 :returns: A time_t timestamp.
330 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
333 if self._thread is None:
334 raise NotmuchError(STATUS.NOT_INITIALIZED)
335 return Thread._get_oldest_date(self._thread)
338 """ Returns the message tags
340 In the Notmuch database, tags are stored on individual
341 messages, not on threads. So the tags returned here will be all
342 tags of the messages which matched the search and which belong to
345 The :class:`Tags` object is owned by the thread and as such, will only
346 be valid for as long as this :class:`Thread` is valid (e.g. until the
347 query from which it derived is explicitely deleted).
349 :returns: A :class:`Tags` iterator.
350 :exception: :exc:`NotmuchError`
352 * STATUS.NOT_INITIALIZED if the thread
354 * STATUS.NULL_POINTER, on error
356 if self._thread is None:
357 raise NotmuchError(STATUS.NOT_INITIALIZED)
359 tags_p = Thread._get_tags(self._thread)
361 raise NotmuchError(STATUS.NULL_POINTER)
362 return Tags(tags_p, self)
365 """A str(Thread()) is represented by a 1-line summary"""
367 thread['id'] = self.get_thread_id()
369 ###TODO: How do we find out the current sort order of Threads?
370 ###Add a "sort" attribute to the Threads() object?
371 #if (sort == NOTMUCH_SORT_OLDEST_FIRST)
372 # date = notmuch_thread_get_oldest_date (thread);
374 # date = notmuch_thread_get_newest_date (thread);
375 thread['date'] = date.fromtimestamp(self.get_newest_date())
376 thread['matched'] = self.get_matched_messages()
377 thread['total'] = self.get_total_messages()
378 thread['authors'] = self.get_authors()
379 thread['subject'] = self.get_subject()
380 thread['tags'] = self.get_tags()
382 return "thread:%s %12s [%d/%d] %s; %s (%s)" % (thread['id'],
391 """Close and free the notmuch Thread"""
392 if self._thread is not None:
393 nmlib.notmuch_thread_destroy(self._thread)