1 from ctypes import c_char_p, c_void_p, c_long
2 from cnotmuch.globals import nmlib, STATUS, NotmuchError
3 from cnotmuch.message import Messages
4 from cnotmuch.tag import Tags
5 from datetime import date
7 #------------------------------------------------------------------------------
9 """Represents a list of notmuch threads
11 This object provides an iterator over a list of notmuch threads
12 (Technically, it provides a wrapper for the underlying
13 *notmuch_threads_t* structure). Do note that the underlying
14 library only provides a one-time iterator (it cannot reset the
15 iterator to the start). Thus iterating over the function will
16 "exhaust" the list of threads, and a subsequent iteration attempt
17 will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
18 note, that any function that uses iteration will also
19 exhaust the messages. So both::
21 for thread in threads: print thread
25 number_of_msgs = len(threads)
27 will "exhaust" the threads. If you need to re-iterate over a list of
28 messages you will need to retrieve a new :class:`Threads` object.
30 Things are not as bad as it seems though, you can store and reuse
31 the single Thread objects as often as you want as long as you
32 keep the parent Threads object around. (Recall that due to
33 hierarchical memory allocation, all derived Threads objects will
34 be invalid when we delete the parent Threads() object, even if it
35 was already "exhausted".) So this works::
38 threads = Query(db,'').search_threads() #get a Threads() object
40 for thread in threads:
41 threadlist.append(thread)
43 # threads is "exhausted" now and even len(threads) will raise an
45 # However it will be kept around until all retrieved Thread() objects are
46 # also deleted. If you did e.g. an explicit del(threads) here, the
47 # following lines would fail.
49 # You can reiterate over *threadlist* however as often as you want.
50 # It is simply a list with Thread objects.
52 print (threadlist[0].get_thread_id())
53 print (threadlist[1].get_thread_id())
54 print (threadlist[0].get_total_messages())
58 _get = nmlib.notmuch_threads_get
59 _get.restype = c_void_p
61 def __init__(self, threads_p, parent=None):
63 :param threads_p: A pointer to an underlying *notmuch_threads_t*
64 structure. These are not publically exposed, so a user
65 will almost never instantiate a :class:`Threads` object
66 herself. They are usually handed back as a result,
67 e.g. in :meth:`Query.search_threads`. *threads_p* must be
68 valid, we will raise an :exc:`NotmuchError`
69 (STATUS.NULL_POINTER) if it is `None`.
70 :type threads_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)
81 self._threads = threads_p
82 #store parent, so we keep them alive as long as self is alive
86 """ Make Threads an iterator """
90 if self._threads is None:
91 raise NotmuchError(STATUS.NOT_INITIALIZED)
93 if not nmlib.notmuch_threads_valid(self._threads):
97 thread = Thread(Threads._get (self._threads), self)
98 nmlib.notmuch_threads_move_to_next(self._threads)
102 """len(:class:`Threads`) returns the number of contained Threads
104 .. note:: As this iterates over the threads, we will not be able to
105 iterate over them again! So this will fail::
108 threads = Database().create_query('').search_threads()
109 if len(threads) > 0: #this 'exhausts' threads
110 # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
111 for thread in threads: print thread
113 if self._threads is None:
114 raise NotmuchError(STATUS.NOT_INITIALIZED)
117 # returns 'bool'. On out-of-memory it returns None
118 while nmlib.notmuch_threads_valid(self._threads):
119 nmlib.notmuch_threads_move_to_next(self._threads)
121 # reset self._threads to mark as "exhausted"
128 """Close and free the notmuch Threads"""
129 if self._threads is not None:
130 nmlib.notmuch_messages_destroy (self._threads)
132 #------------------------------------------------------------------------------
133 class Thread(object):
134 """Represents a single message thread."""
136 """notmuch_thread_get_thread_id"""
137 _get_thread_id = nmlib.notmuch_thread_get_thread_id
138 _get_thread_id.restype = c_char_p
140 """notmuch_thread_get_authors"""
141 _get_authors = nmlib.notmuch_thread_get_authors
142 _get_authors.restype = c_char_p
144 """notmuch_thread_get_subject"""
145 _get_subject = nmlib.notmuch_thread_get_subject
146 _get_subject.restype = c_char_p
148 """notmuch_thread_get_toplevel_messages"""
149 _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
150 _get_toplevel_messages.restype = c_void_p
152 _get_newest_date = nmlib.notmuch_thread_get_newest_date
153 _get_newest_date.restype = c_long
155 _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
156 _get_oldest_date.restype = c_long
158 """notmuch_thread_get_tags"""
159 _get_tags = nmlib.notmuch_thread_get_tags
160 _get_tags.restype = c_void_p
162 def __init__(self, thread_p, parent=None):
164 :param thread_p: A pointer to an internal notmuch_thread_t
165 Structure. These are not publically exposed, so a user
166 will almost never instantiate a :class:`Thread` object
167 herself. They are usually handed back as a result,
168 e.g. when iterating through :class:`Threads`. *thread_p*
169 must be valid, we will raise an :exc:`NotmuchError`
170 (STATUS.NULL_POINTER) if it is `None`.
172 :param parent: A 'parent' object is passed which this message is
173 derived from. We save a reference to it, so we can
174 automatically delete the parent object once all derived
178 NotmuchError(STATUS.NULL_POINTER)
179 self._thread = thread_p
180 #keep reference to parent, so we keep it alive
181 self._parent = parent
183 def get_thread_id(self):
184 """Get the thread ID of 'thread'
186 The returned string belongs to 'thread' and will only be valid
187 for as long as the thread is valid.
189 :returns: String with a message ID
190 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
193 if self._thread is None:
194 raise NotmuchError(STATUS.NOT_INITIALIZED)
195 return Thread._get_thread_id(self._thread)
197 def get_total_messages(self):
198 """Get the total number of messages in 'thread'
200 :returns: The number of all messages in the database
201 belonging to this thread. Contrast with
202 :meth:`get_matched_messages`.
203 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
206 if self._thread is None:
207 raise NotmuchError(STATUS.NOT_INITIALIZED)
208 return nmlib.notmuch_thread_get_total_messages(self._thread)
211 def get_toplevel_messages(self):
212 """Returns a :class:`Messages` iterator for the top-level messages in
215 This iterator will not necessarily iterate over all of the messages
216 in the thread. It will only iterate over the messages in the thread
217 which are not replies to other messages in the thread.
219 To iterate over all messages in the thread, the caller will need to
220 iterate over the result of :meth:`Message.get_replies` for each
221 top-level message (and do that recursively for the resulting
224 :returns: :class:`Messages`
225 :exception: :exc:`NotmuchError`
227 * STATUS.NOT_INITIALIZED if query is not inited
228 * STATUS.NULL_POINTER if search_messages failed
230 if self._thread is None:
231 raise NotmuchError(STATUS.NOT_INITIALIZED)
233 msgs_p = Thread._get_toplevel_messages(self._thread)
236 NotmuchError(STATUS.NULL_POINTER)
238 return Messages(msgs_p,self)
240 def get_matched_messages(self):
241 """Returns the number of messages in 'thread' that matched the query
243 :returns: The number of all messages belonging to this thread that
244 matched the :class:`Query`from which this thread was created.
245 Contrast with :meth:`get_total_messages`.
246 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
249 if self._thread is None:
250 raise NotmuchError(STATUS.NOT_INITIALIZED)
251 return nmlib.notmuch_thread_get_matched_messages(self._thread)
253 def get_authors(self):
254 """Returns the authors of 'thread'
256 The returned string is a comma-separated list of the names of the
257 authors of mail messages in the query results that belong to this
260 The returned string belongs to 'thread' and will only be valid for
261 as long as this Thread() is not deleted.
263 if self._thread is None:
264 raise NotmuchError(STATUS.NOT_INITIALIZED)
265 return Thread._get_authors(self._thread)
267 def get_subject(self):
268 """Returns the Subject of 'thread'
270 The returned string belongs to 'thread' and will only be valid for
271 as long as this Thread() is not deleted.
273 if self._thread is None:
274 raise NotmuchError(STATUS.NOT_INITIALIZED)
275 return Thread._get_subject(self._thread)
277 def get_newest_date(self):
278 """Returns time_t of the newest message date
280 :returns: A time_t timestamp.
282 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
285 if self._thread is None:
286 raise NotmuchError(STATUS.NOT_INITIALIZED)
287 return Thread._get_newest_date(self._thread)
289 def get_oldest_date(self):
290 """Returns time_t of the oldest message date
292 :returns: A time_t timestamp.
294 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
297 if self._thread is None:
298 raise NotmuchError(STATUS.NOT_INITIALIZED)
299 return Thread._get_oldest_date(self._thread)
302 """ Returns the message tags
304 In the Notmuch database, tags are stored on individual
305 messages, not on threads. So the tags returned here will be all
306 tags of the messages which matched the search and which belong to
309 The :class:`Tags` object is owned by the thread and as such, will only
310 be valid for as long as this :class:`Thread` is valid (e.g. until the
311 query from which it derived is explicitely deleted).
313 :returns: A :class:`Tags` iterator.
314 :exception: :exc:`NotmuchError`
316 * STATUS.NOT_INITIALIZED if the thread
318 * STATUS.NULL_POINTER, on error
320 if self._thread is None:
321 raise NotmuchError(STATUS.NOT_INITIALIZED)
323 tags_p = Thread._get_tags(self._thread)
325 raise NotmuchError(STATUS.NULL_POINTER)
326 return Tags(tags_p, self)
329 """A str(Thread()) is represented by a 1-line summary"""
331 thread['id'] = self.get_thread_id()
333 ###TODO: How do we find out the current sort order of Threads?
334 ###Add a "sort" attribute to the Threads() object?
335 #if (sort == NOTMUCH_SORT_OLDEST_FIRST)
336 # date = notmuch_thread_get_oldest_date (thread);
338 # date = notmuch_thread_get_newest_date (thread);
339 thread['date'] = date.fromtimestamp(self.get_newest_date())
340 thread['matched'] = self.get_matched_messages()
341 thread['total'] = self.get_total_messages()
342 thread['authors'] = self.get_authors()
343 thread['subject'] = self.get_subject()
344 thread['tags'] = self.get_tags()
346 return "thread:%(id)s %(date)12s [%(matched)d/%(total)d] %(authors)s; %(subject)s (%(tags)s)" % (thread)
349 """Close and free the notmuch Thread"""
350 if self._thread is not None:
351 nmlib.notmuch_thread_destroy (self._thread)