]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/thread.py
Added tag v0.2.1 for changeset 8f496a1f9b34
[notmuch] / cnotmuch / thread.py
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
6
7 #------------------------------------------------------------------------------
8 class Threads(object):
9     """Represents a list of notmuch threads
10
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::
20
21       for thread in threads: print thread
22
23     as well as::
24
25        number_of_msgs = len(threads)
26
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.
29
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::
36
37       db   = Database()
38       threads = Query(db,'').search_threads() #get a Threads() object
39       threadlist = []
40       for thread in threads:
41          threadlist.append(thread)
42
43       # threads is "exhausted" now and even len(threads) will raise an 
44       # exception.
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.
48       
49       # You can reiterate over *threadlist* however as often as you want. 
50       # It is simply a list with Thread objects.
51
52       print (threadlist[0].get_thread_id())
53       print (threadlist[1].get_thread_id())
54       print (threadlist[0].get_total_messages())
55     """
56
57     #notmuch_threads_get
58     _get = nmlib.notmuch_threads_get
59     _get.restype = c_void_p
60
61     def __init__(self, threads_p, parent=None):
62         """
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 
76                the Python object.(?)
77         """
78         if threads_p is None:
79             NotmuchError(STATUS.NULL_POINTER)
80
81         self._threads = threads_p
82         #store parent, so we keep them alive as long as self  is alive
83         self._parent = parent
84
85     def __iter__(self):
86         """ Make Threads an iterator """
87         return self
88
89     def next(self):
90         if self._threads is None:
91             raise NotmuchError(STATUS.NOT_INITIALIZED)
92
93         if not nmlib.notmuch_threads_valid(self._threads):
94             self._threads = None
95             raise StopIteration
96
97         thread = Thread(Threads._get (self._threads), self)
98         nmlib.notmuch_threads_move_to_next(self._threads)
99         return thread
100
101     def __len__(self):
102         """len(:class:`Threads`) returns the number of contained Threads
103
104         .. note:: As this iterates over the threads, we will not be able to 
105                iterate over them again! So this will fail::
106
107                  #THIS FAILS
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
112         """
113         if self._threads is None:
114             raise NotmuchError(STATUS.NOT_INITIALIZED)
115
116         i=0
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)
120             i += 1
121         # reset self._threads to mark as "exhausted"
122         self._threads = None
123         return i
124
125
126
127     def __del__(self):
128         """Close and free the notmuch Threads"""
129         if self._threads is not None:
130             nmlib.notmuch_messages_destroy (self._threads)
131
132 #------------------------------------------------------------------------------
133 class Thread(object):
134     """Represents a single message thread."""
135
136     """notmuch_thread_get_thread_id"""
137     _get_thread_id = nmlib.notmuch_thread_get_thread_id
138     _get_thread_id.restype = c_char_p
139
140     """notmuch_thread_get_authors"""
141     _get_authors = nmlib.notmuch_thread_get_authors
142     _get_authors.restype = c_char_p
143
144     """notmuch_thread_get_subject"""
145     _get_subject = nmlib.notmuch_thread_get_subject
146     _get_subject.restype = c_char_p
147
148     """notmuch_thread_get_toplevel_messages"""
149     _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
150     _get_toplevel_messages.restype = c_void_p
151
152     _get_newest_date = nmlib.notmuch_thread_get_newest_date
153     _get_newest_date.restype = c_long
154
155     _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
156     _get_oldest_date.restype = c_long
157
158     """notmuch_thread_get_tags"""
159     _get_tags = nmlib.notmuch_thread_get_tags
160     _get_tags.restype = c_void_p
161
162     def __init__(self, thread_p, parent=None):
163         """
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`.
171
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
175               objects are dead.
176         """
177         if thread_p is None:
178             NotmuchError(STATUS.NULL_POINTER)
179         self._thread = thread_p
180         #keep reference to parent, so we keep it alive
181         self._parent = parent
182
183     def get_thread_id(self):
184         """Get the thread ID of 'thread'
185
186         The returned string belongs to 'thread' and will only be valid
187         for as long as the thread is valid.
188
189         :returns: String with a message ID
190         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread 
191                     is not initialized.
192         """
193         if self._thread is None:
194             raise NotmuchError(STATUS.NOT_INITIALIZED)
195         return Thread._get_thread_id(self._thread)
196
197     def get_total_messages(self):
198         """Get the total number of messages in 'thread'
199
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 
204                     is not initialized.
205         """
206         if self._thread is None:
207             raise NotmuchError(STATUS.NOT_INITIALIZED)
208         return nmlib.notmuch_thread_get_total_messages(self._thread)
209
210
211     def get_toplevel_messages(self):
212         """Returns a :class:`Messages` iterator for the top-level messages in
213            'thread'
214
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.
218  
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
222            messages, etc.).
223
224         :returns: :class:`Messages`
225         :exception: :exc:`NotmuchError`
226
227                       * STATUS.NOT_INITIALIZED if query is not inited
228                       * STATUS.NULL_POINTER if search_messages failed 
229         """
230         if self._thread is None:
231             raise NotmuchError(STATUS.NOT_INITIALIZED)
232
233         msgs_p = Thread._get_toplevel_messages(self._thread)
234
235         if msgs_p is None:
236             NotmuchError(STATUS.NULL_POINTER)
237
238         return Messages(msgs_p,self)
239
240     def get_matched_messages(self):
241         """Returns the number of messages in 'thread' that matched the query
242
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 
247                     is not initialized.
248         """
249         if self._thread is None:
250             raise NotmuchError(STATUS.NOT_INITIALIZED)
251         return nmlib.notmuch_thread_get_matched_messages(self._thread)
252
253     def get_authors(self):
254         """Returns the authors of 'thread'
255
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
258         thread.
259
260         The returned string belongs to 'thread' and will only be valid for 
261         as long as this Thread() is not deleted.
262         """
263         if self._thread is None:
264             raise NotmuchError(STATUS.NOT_INITIALIZED)
265         return Thread._get_authors(self._thread)
266
267     def get_subject(self):
268         """Returns the Subject of 'thread'
269
270         The returned string belongs to 'thread' and will only be valid for 
271         as long as this Thread() is not deleted.
272         """
273         if self._thread is None:
274             raise NotmuchError(STATUS.NOT_INITIALIZED)
275         return Thread._get_subject(self._thread)
276
277     def get_newest_date(self):
278         """Returns time_t of the newest message date
279
280         :returns: A time_t timestamp.
281         :rtype: c_unit64
282         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
283                     is not initialized.
284         """
285         if self._thread is None:
286             raise NotmuchError(STATUS.NOT_INITIALIZED)
287         return Thread._get_newest_date(self._thread)
288
289     def get_oldest_date(self):
290         """Returns time_t of the oldest message date
291
292         :returns: A time_t timestamp.
293         :rtype: c_unit64
294         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
295                     is not initialized.
296         """
297         if self._thread is None:
298             raise NotmuchError(STATUS.NOT_INITIALIZED)
299         return Thread._get_oldest_date(self._thread)
300
301     def get_tags(self):
302         """ Returns the message tags
303
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
307         this thread.
308
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).
312
313         :returns: A :class:`Tags` iterator.
314         :exception: :exc:`NotmuchError`
315
316                       * STATUS.NOT_INITIALIZED if the thread 
317                         is not initialized.
318                       * STATUS.NULL_POINTER, on error
319         """
320         if self._thread is None:
321             raise NotmuchError(STATUS.NOT_INITIALIZED)
322
323         tags_p = Thread._get_tags(self._thread)
324         if tags_p == None:
325             raise NotmuchError(STATUS.NULL_POINTER)
326         return Tags(tags_p, self)
327  
328     def __str__(self):
329         """A str(Thread()) is represented by a 1-line summary"""
330         thread = {}
331         thread['id'] = self.get_thread_id()
332
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);
337         #else
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()
345
346         return "thread:%(id)s %(date)12s [%(matched)d/%(total)d] %(authors)s; %(subject)s (%(tags)s)" % (thread)
347
348     def __del__(self):
349         """Close and free the notmuch Thread"""
350         if self._thread is not None:
351             nmlib.notmuch_thread_destroy (self._thread)