1 from ctypes import c_char_p, c_void_p, c_uint64
2 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
3 from cnotmuch.tags import Tags
4 from datetime import date
6 #------------------------------------------------------------------------------
8 """Represents a list of notmuch threads
10 This object provides an iterator over a list of notmuch threads
11 (Technically, it provides a wrapper for the underlying
12 *notmuch_threads_t* structure). Do note that the underlying
13 library only provides a one-time iterator (it cannot reset the
14 iterator to the start). Thus iterating over the function will
15 "exhaust" the list of threads, and a subsequent iteration attempt
16 will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
17 note, that any function that uses iteration will also
18 exhaust the messages. So both::
20 for thread in threads: print thread
24 number_of_msgs = len(threads)
26 will "exhaust" the threads. If you need to re-iterate over a list of
27 messages you will need to retrieve a new :class:`Threads` object.
29 Things are not as bad as it seems though, you can store and reuse
30 the single Thread objects as often as you want as long as you
31 keep the parent Threads object around. (Recall that due to
32 hierarchical memory allocation, all derived Threads objects will
33 be invalid when we delete the parent Threads() object, even if it
34 was already "exhausted".) So this works::
37 threads = Query(db,'').search_threads() #get a Threads() object
39 for thread in threads:
40 threadlist.append(thread)
42 # threads is "exhausted" now and even len(threads) will raise an
44 # However it will be kept around until all retrieved Thread() objects are
45 # also deleted. If you did e.g. an explicit del(threads) here, the
46 # following lines would fail.
48 # You can reiterate over *threadlist* however as often as you want.
49 # It is simply a list with Thread objects.
51 print (threadlist[0].get_thread_id())
52 print (threadlist[1].get_thread_id())
53 print (threadlist[0].get_total_messages())
57 _get = nmlib.notmuch_threads_get
58 _get.restype = c_void_p
60 def __init__(self, threads_p, parent=None):
62 :param threads_p: A pointer to an underlying *notmuch_threads_t*
63 structure. These are not publically exposed, so a user
64 will almost never instantiate a :class:`Threads` object
65 herself. They are usually handed back as a result,
66 e.g. in :meth:`Query.search_threads`. *threads_p* must be
67 valid, we will raise an :exc:`NotmuchError`
68 (STATUS.NULL_POINTER) if it is `None`.
69 :type threads_p: :class:`ctypes.c_void_p`
70 :param parent: The parent object
71 (ie :class:`Query`) these tags are derived from. It saves
72 a reference to it, so we can automatically delete the db
73 object once all derived objects are dead.
74 :TODO: Make the iterator work more than once and cache the tags in
78 NotmuchError(STATUS.NULL_POINTER)
80 self._threads = threads_p
81 #store parent, so we keep them alive as long as self is alive
85 """ Make Threads an iterator """
89 if self._threads is None:
90 raise NotmuchError(STATUS.NOT_INITIALIZED)
92 if not nmlib.notmuch_threads_valid(self._threads):
96 thread = Thread(Threads._get (self._threads), self)
97 nmlib.notmuch_threads_move_to_next(self._threads)
101 """len(:class:`Threads`) returns the number of contained Threads
103 .. note:: As this iterates over the threads, we will not be able to
104 iterate over them again! So this will fail::
107 threads = Database().create_query('').search_threads()
108 if len(threads) > 0: #this 'exhausts' threads
109 # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
110 for thread in threads: print thread
112 if self._threads is None:
113 raise NotmuchError(STATUS.NOT_INITIALIZED)
116 # returns 'bool'. On out-of-memory it returns None
117 while nmlib.notmuch_threads_valid(self._threads):
118 nmlib.notmuch_threads_move_to_next(self._threads)
120 # reset self._threads to mark as "exhausted"
127 """Close and free the notmuch Threads"""
128 if self._threads is not None:
129 nmlib.notmuch_messages_destroy (self._threads)
131 #------------------------------------------------------------------------------
132 class Thread(object):
133 """Represents a single message thread."""
135 """notmuch_thread_get_thread_id"""
136 _get_thread_id = nmlib.notmuch_thread_get_thread_id
137 _get_thread_id.restype = c_char_p
139 """notmuch_thread_get_authors"""
140 _get_authors = nmlib.notmuch_thread_get_authors
141 _get_authors.restype = c_char_p
143 """notmuch_thread_get_subject"""
144 _get_subject = nmlib.notmuch_thread_get_subject
145 _get_subject.restype = c_char_p
147 _get_newest_date = nmlib.notmuch_thread_get_newest_date
148 _get_newest_date.restype = c_uint64
150 _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
151 _get_oldest_date.restype = c_uint64
153 """notmuch_thread_get_tags"""
154 _get_tags = nmlib.notmuch_thread_get_tags
155 _get_tags.restype = c_void_p
157 def __init__(self, thread_p, parent=None):
159 :param thread_p: A pointer to an internal notmuch_thread_t
160 Structure. These are not publically exposed, so a user
161 will almost never instantiate a :class:`Thread` object
162 herself. They are usually handed back as a result,
163 e.g. when iterating through :class:`Threads`. *thread_p*
164 must be valid, we will raise an :exc:`NotmuchError`
165 (STATUS.NULL_POINTER) if it is `None`.
167 :param parent: A 'parent' object is passed which this message is
168 derived from. We save a reference to it, so we can
169 automatically delete the parent object once all derived
173 NotmuchError(STATUS.NULL_POINTER)
174 self._thread = thread_p
175 #keep reference to parent, so we keep it alive
176 self._parent = parent
178 def get_thread_id(self):
179 """Get the thread ID of 'thread'
181 The returned string belongs to 'thread' and will only be valid
182 for as long as the thread is valid.
184 :returns: String with a message ID
185 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
188 if self._thread is None:
189 raise NotmuchError(STATUS.NOT_INITIALIZED)
190 return Thread._get_thread_id(self._thread)
192 def get_total_messages(self):
193 """Get the total number of messages in 'thread'
195 :returns: The number of all messages in the database
196 belonging to this thread. Contrast with
197 :meth:`get_matched_messages`.
198 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
201 if self._thread is None:
202 raise NotmuchError(STATUS.NOT_INITIALIZED)
203 return nmlib.notmuch_thread_get_total_messages(self._thread)
206 ###TODO: notmuch_messages_t * notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
208 def get_matched_messages(self):
209 """Returns the number of messages in 'thread' that matched the query
211 :returns: The number of all messages belonging to this thread that
212 matched the :class:`Query`from which this thread was created.
213 Contrast with :meth:`get_total_messages`.
214 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
217 if self._thread is None:
218 raise NotmuchError(STATUS.NOT_INITIALIZED)
219 return nmlib.notmuch_thread_get_matched_messages(self._thread)
221 def get_authors(self):
222 """Returns the authors of 'thread'
224 The returned string is a comma-separated list of the names of the
225 authors of mail messages in the query results that belong to this
228 The returned string belongs to 'thread' and will only be valid for
229 as long as this Thread() is not deleted.
231 if self._thread is None:
232 raise NotmuchError(STATUS.NOT_INITIALIZED)
233 return Thread._get_authors(self._thread)
235 def get_subject(self):
236 """Returns the Subject of 'thread'
238 The returned string belongs to 'thread' and will only be valid for
239 as long as this Thread() is not deleted.
241 if self._thread is None:
242 raise NotmuchError(STATUS.NOT_INITIALIZED)
243 return Thread._get_subject(self._thread)
245 def get_newest_date(self):
246 """Returns time_t of the newest message date
248 :returns: A time_t timestamp.
250 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
253 if self._thread is None:
254 raise NotmuchError(STATUS.NOT_INITIALIZED)
255 return Thread._get_newest_date(self._thread)
257 def get_oldest_date(self):
258 """Returns time_t of the oldest message date
260 :returns: A time_t timestamp.
262 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
265 if self._thread is None:
266 raise NotmuchError(STATUS.NOT_INITIALIZED)
267 return Thread._get_oldest_date(self._thread)
270 """ Returns the message tags
272 In the Notmuch database, tags are stored on individual
273 messages, not on threads. So the tags returned here will be all
274 tags of the messages which matched the search and which belong to
277 The :class:`Tags` object is owned by the thread and as such, will only
278 be valid for as long as this :class:`Thread` is valid (e.g. until the
279 query from which it derived is explicitely deleted).
281 :returns: A :class:`Tags` iterator.
282 :exception: :exc:`NotmuchError`
284 * STATUS.NOT_INITIALIZED if the thread
286 * STATUS.NULL_POINTER, on error
288 if self._thread is None:
289 raise NotmuchError(STATUS.NOT_INITIALIZED)
291 tags_p = Thread._get_tags(self._thread)
293 raise NotmuchError(STATUS.NULL_POINTER)
294 return Tags(tags_p, self)
297 """A str(Thread()) is represented by a 1-line summary"""
299 thread['id'] = self.get_thread_id()
301 ###TODO: How do we find out the current sort order of Threads?
302 ###Add a "sort" attribute to the Threads() object?
303 #if (sort == NOTMUCH_SORT_OLDEST_FIRST)
304 # date = notmuch_thread_get_oldest_date (thread);
306 # date = notmuch_thread_get_newest_date (thread);
307 thread['date'] = date.fromtimestamp(self.get_newest_date())
308 thread['matched'] = self.get_matched_messages()
309 thread['total'] = self.get_total_messages()
310 thread['authors'] = self.get_authors()
311 thread['subject'] = self.get_subject()
312 thread['tags'] = self.get_tags()
314 return "thread:%(id)s %(date)12s [%(matched)d/%(total)d] %(authors)s; %(subject)s (%(tags)s)" % (thread)
317 """Close and free the notmuch Thread"""
318 if self._thread is not None:
319 nmlib.notmuch_thread_destroy (self._thread)