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, c_int
21 from notmuch.globals import (nmlib, STATUS,
22 NotmuchError, NotmuchThreadP, NotmuchThreadsP, NotmuchMessagesP,
24 from notmuch.message import Messages
25 from notmuch.tag import Tags
26 from datetime import date
29 class Threads(object):
30 """Represents a list of notmuch threads
32 This object provides an iterator over a list of notmuch threads
33 (Technically, it provides a wrapper for the underlying
34 *notmuch_threads_t* structure). Do note that the underlying
35 library only provides a one-time iterator (it cannot reset the
36 iterator to the start). Thus iterating over the function will
37 "exhaust" the list of threads, and a subsequent iteration attempt
38 will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
39 note, that any function that uses iteration will also
40 exhaust the messages. So both::
42 for thread in threads: print thread
46 number_of_msgs = len(threads)
48 will "exhaust" the threads. If you need to re-iterate over a list of
49 messages you will need to retrieve a new :class:`Threads` object.
51 Things are not as bad as it seems though, you can store and reuse
52 the single Thread objects as often as you want as long as you
53 keep the parent Threads object around. (Recall that due to
54 hierarchical memory allocation, all derived Threads objects will
55 be invalid when we delete the parent Threads() object, even if it
56 was already "exhausted".) So this works::
59 threads = Query(db,'').search_threads() #get a Threads() object
61 for thread in threads:
62 threadlist.append(thread)
64 # threads is "exhausted" now and even len(threads) will raise an
66 # However it will be kept around until all retrieved Thread() objects are
67 # also deleted. If you did e.g. an explicit del(threads) here, the
68 # following lines would fail.
70 # You can reiterate over *threadlist* however as often as you want.
71 # It is simply a list with Thread objects.
73 print (threadlist[0].get_thread_id())
74 print (threadlist[1].get_thread_id())
75 print (threadlist[0].get_total_messages())
79 _get = nmlib.notmuch_threads_get
80 _get.argtypes = [NotmuchThreadsP]
81 _get.restype = NotmuchThreadP
83 def __init__(self, threads_p, parent=None):
85 :param threads_p: A pointer to an underlying *notmuch_threads_t*
86 structure. These are not publically exposed, so a user
87 will almost never instantiate a :class:`Threads` object
88 herself. They are usually handed back as a result,
89 e.g. in :meth:`Query.search_threads`. *threads_p* must be
90 valid, we will raise an :exc:`NotmuchError`
91 (STATUS.NULL_POINTER) if it is `None`.
92 :type threads_p: :class:`ctypes.c_void_p`
93 :param parent: The parent object
94 (ie :class:`Query`) these tags are derived from. It saves
95 a reference to it, so we can automatically delete the db
96 object once all derived objects are dead.
97 :TODO: Make the iterator work more than once and cache the tags in
100 if threads_p is None:
101 raise NotmuchError(STATUS.NULL_POINTER)
103 self._threads = threads_p
104 #store parent, so we keep them alive as long as self is alive
105 self._parent = parent
108 """ Make Threads an iterator """
111 _valid = nmlib.notmuch_threads_valid
112 _valid.argtypes = [NotmuchThreadsP]
113 _valid.restype = bool
115 _move_to_next = nmlib.notmuch_threads_move_to_next
116 _move_to_next.argtypes = [NotmuchThreadsP]
117 _move_to_next.restype = None
120 if self._threads is None:
121 raise NotmuchError(STATUS.NOT_INITIALIZED)
123 if not self._valid(self._threads):
127 thread = Thread(Threads._get(self._threads), self)
128 self._move_to_next(self._threads)
132 """len(:class:`Threads`) returns the number of contained Threads
134 .. note:: As this iterates over the threads, we will not be able to
135 iterate over them again! So this will fail::
138 threads = Database().create_query('').search_threads()
139 if len(threads) > 0: #this 'exhausts' threads
140 # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
141 for thread in threads: print thread
143 if self._threads is None:
144 raise NotmuchError(STATUS.NOT_INITIALIZED)
147 # returns 'bool'. On out-of-memory it returns None
148 while self._valid(self._threads):
149 self._move_to_next(self._threads)
151 # reset self._threads to mark as "exhausted"
155 def __nonzero__(self):
156 """Check if :class:`Threads` contains at least one more valid thread
158 The existence of this function makes 'if Threads: foo' work, as
159 that will implicitely call len() exhausting the iterator if
160 __nonzero__ does not exist. This function makes `bool(Threads())`
163 :return: True if there is at least one more thread in the
164 Iterator, False if not. None on a "Out-of-memory" error.
166 return self._threads is not None and \
167 self._valid(self._threads) > 0
169 _destroy = nmlib.notmuch_threads_destroy
170 _destroy.argtypes = [NotmuchThreadsP]
171 _destroy.argtypes = None
174 """Close and free the notmuch Threads"""
175 if self._threads is not None:
176 self._destroy(self._threads)
179 class Thread(object):
180 """Represents a single message thread."""
182 """notmuch_thread_get_thread_id"""
183 _get_thread_id = nmlib.notmuch_thread_get_thread_id
184 _get_thread_id.argtypes = [NotmuchThreadP]
185 _get_thread_id.restype = c_char_p
187 """notmuch_thread_get_authors"""
188 _get_authors = nmlib.notmuch_thread_get_authors
189 _get_authors.argtypes = [NotmuchThreadP]
190 _get_authors.restype = c_char_p
192 """notmuch_thread_get_subject"""
193 _get_subject = nmlib.notmuch_thread_get_subject
194 _get_subject.argtypes = [NotmuchThreadP]
195 _get_subject.restype = c_char_p
197 """notmuch_thread_get_toplevel_messages"""
198 _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
199 _get_toplevel_messages.argtypes = [NotmuchThreadP]
200 _get_toplevel_messages.restype = NotmuchMessagesP
202 _get_newest_date = nmlib.notmuch_thread_get_newest_date
203 _get_newest_date.argtypes = [NotmuchThreadP]
204 _get_newest_date.restype = c_long
206 _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
207 _get_oldest_date.argtypes = [NotmuchThreadP]
208 _get_oldest_date.restype = c_long
210 """notmuch_thread_get_tags"""
211 _get_tags = nmlib.notmuch_thread_get_tags
212 _get_tags.argtypes = [NotmuchThreadP]
213 _get_tags.restype = NotmuchTagsP
215 def __init__(self, thread_p, parent=None):
217 :param thread_p: A pointer to an internal notmuch_thread_t
218 Structure. These are not publically exposed, so a user
219 will almost never instantiate a :class:`Thread` object
220 herself. They are usually handed back as a result,
221 e.g. when iterating through :class:`Threads`. *thread_p*
222 must be valid, we will raise an :exc:`NotmuchError`
223 (STATUS.NULL_POINTER) if it is `None`.
225 :param parent: A 'parent' object is passed which this message is
226 derived from. We save a reference to it, so we can
227 automatically delete the parent object once all derived
231 raise NotmuchError(STATUS.NULL_POINTER)
232 self._thread = thread_p
233 #keep reference to parent, so we keep it alive
234 self._parent = parent
236 def get_thread_id(self):
237 """Get the thread ID of 'thread'
239 The returned string belongs to 'thread' and will only be valid
240 for as long as the thread is valid.
242 :returns: String with a message ID
243 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
246 if self._thread is None:
247 raise NotmuchError(STATUS.NOT_INITIALIZED)
248 return Thread._get_thread_id(self._thread)
251 _get_total_messages = nmlib.notmuch_thread_get_total_messages
252 _get_total_messages.argtypes = [NotmuchThreadP]
253 _get_total_messages.restype = c_int
255 def get_total_messages(self):
256 """Get the total number of messages in 'thread'
258 :returns: The number of all messages in the database
259 belonging to this thread. Contrast with
260 :meth:`get_matched_messages`.
261 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
264 if self._thread is None:
265 raise NotmuchError(STATUS.NOT_INITIALIZED)
266 return self._get_total_messages(self._thread)
268 def get_toplevel_messages(self):
269 """Returns a :class:`Messages` iterator for the top-level messages in
272 This iterator will not necessarily iterate over all of the messages
273 in the thread. It will only iterate over the messages in the thread
274 which are not replies to other messages in the thread.
276 To iterate over all messages in the thread, the caller will need to
277 iterate over the result of :meth:`Message.get_replies` for each
278 top-level message (and do that recursively for the resulting
281 :returns: :class:`Messages`
282 :exception: :exc:`NotmuchError`
284 * STATUS.NOT_INITIALIZED if query is not inited
285 * STATUS.NULL_POINTER if search_messages failed
287 if self._thread is None:
288 raise NotmuchError(STATUS.NOT_INITIALIZED)
290 msgs_p = Thread._get_toplevel_messages(self._thread)
293 raise NotmuchError(STATUS.NULL_POINTER)
295 return Messages(msgs_p, self)
297 _get_matched_messages = nmlib.notmuch_thread_get_matched_messages
298 _get_matched_messages.argtypes = [NotmuchThreadP]
299 _get_matched_messages.restype = c_int
301 def get_matched_messages(self):
302 """Returns the number of messages in 'thread' that matched the query
304 :returns: The number of all messages belonging to this thread that
305 matched the :class:`Query`from which this thread was created.
306 Contrast with :meth:`get_total_messages`.
307 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
310 if self._thread is None:
311 raise NotmuchError(STATUS.NOT_INITIALIZED)
312 return self._get_matched_messages(self._thread)
314 def get_authors(self):
315 """Returns the authors of 'thread'
317 The returned string is a comma-separated list of the names of the
318 authors of mail messages in the query results that belong to this
321 The returned string belongs to 'thread' and will only be valid for
322 as long as this Thread() is not deleted.
324 if self._thread is None:
325 raise NotmuchError(STATUS.NOT_INITIALIZED)
326 authors = Thread._get_authors(self._thread)
329 return authors.decode('UTF-8')
331 def get_subject(self):
332 """Returns the Subject of 'thread'
334 The returned string belongs to 'thread' and will only be valid for
335 as long as this Thread() is not deleted.
337 if self._thread is None:
338 raise NotmuchError(STATUS.NOT_INITIALIZED)
339 subject = Thread._get_subject(self._thread)
342 return subject.decode('UTF-8')
344 def get_newest_date(self):
345 """Returns time_t of the newest message date
347 :returns: A time_t timestamp.
349 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
352 if self._thread is None:
353 raise NotmuchError(STATUS.NOT_INITIALIZED)
354 return Thread._get_newest_date(self._thread)
356 def get_oldest_date(self):
357 """Returns time_t of the oldest message date
359 :returns: A time_t timestamp.
361 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
364 if self._thread is None:
365 raise NotmuchError(STATUS.NOT_INITIALIZED)
366 return Thread._get_oldest_date(self._thread)
369 """ Returns the message tags
371 In the Notmuch database, tags are stored on individual
372 messages, not on threads. So the tags returned here will be all
373 tags of the messages which matched the search and which belong to
376 The :class:`Tags` object is owned by the thread and as such, will only
377 be valid for as long as this :class:`Thread` is valid (e.g. until the
378 query from which it derived is explicitely deleted).
380 :returns: A :class:`Tags` iterator.
381 :exception: :exc:`NotmuchError`
383 * STATUS.NOT_INITIALIZED if the thread
385 * STATUS.NULL_POINTER, on error
387 if self._thread is None:
388 raise NotmuchError(STATUS.NOT_INITIALIZED)
390 tags_p = Thread._get_tags(self._thread)
392 raise NotmuchError(STATUS.NULL_POINTER)
393 return Tags(tags_p, self)
396 """A str(Thread()) is represented by a 1-line summary"""
398 thread['id'] = self.get_thread_id()
400 ###TODO: How do we find out the current sort order of Threads?
401 ###Add a "sort" attribute to the Threads() object?
402 #if (sort == NOTMUCH_SORT_OLDEST_FIRST)
403 # date = notmuch_thread_get_oldest_date (thread);
405 # date = notmuch_thread_get_newest_date (thread);
406 thread['date'] = date.fromtimestamp(self.get_newest_date())
407 thread['matched'] = self.get_matched_messages()
408 thread['total'] = self.get_total_messages()
409 thread['authors'] = self.get_authors()
410 thread['subject'] = self.get_subject()
411 thread['tags'] = self.get_tags()
413 return "thread:%s %12s [%d/%d] %s; %s (%s)" % (thread['id'],
421 _destroy = nmlib.notmuch_thread_destroy
422 _destroy.argtypes = [NotmuchThreadP]
423 _destroy.restype = None
426 """Close and free the notmuch Thread"""
427 if self._thread is not None:
428 self._destroy(self._thread)