]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/thread.py
972f426b53b080e3d20c794c7054aec1fd23c090
[notmuch] / cnotmuch / thread.py
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
5
6 #------------------------------------------------------------------------------
7 class Threads(object):
8     """Represents a list of notmuch threads
9
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::
19
20       for thread in threads: print thread
21
22     as well as::
23
24        number_of_msgs = len(threads)
25
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.
28
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::
35
36       db   = Database()
37       threads = Query(db,'').search_threads() #get a Threads() object
38       threadlist = []
39       for thread in threads:
40          threadlist.append(thread)
41
42       # threads is "exhausted" now and even len(threads) will raise an 
43       # exception.
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.
47       
48       # You can reiterate over *threadlist* however as often as you want. 
49       # It is simply a list with Thread objects.
50
51       print (threadlist[0].get_thread_id())
52       print (threadlist[1].get_thread_id())
53       print (threadlist[0].get_total_messages())
54     """
55
56     #notmuch_threads_get
57     _get = nmlib.notmuch_threads_get
58     _get.restype = c_void_p
59
60     def __init__(self, threads_p, parent=None):
61         """
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 
75                the Python object.(?)
76         """
77         if threads_p is None:
78             NotmuchError(STATUS.NULL_POINTER)
79
80         self._threads = threads_p
81         #store parent, so we keep them alive as long as self  is alive
82         self._parent = parent
83
84     def __iter__(self):
85         """ Make Threads an iterator """
86         return self
87
88     def next(self):
89         if self._threads is None:
90             raise NotmuchError(STATUS.NOT_INITIALIZED)
91
92         if not nmlib.notmuch_threads_valid(self._threads):
93             self._threads = None
94             raise StopIteration
95
96         thread = Thread(Threads._get (self._threads), self)
97         nmlib.notmuch_threads_move_to_next(self._threads)
98         return thread
99
100     def __len__(self):
101         """len(:class:`Threads`) returns the number of contained Threads
102
103         .. note:: As this iterates over the threads, we will not be able to 
104                iterate over them again! So this will fail::
105
106                  #THIS FAILS
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
111         """
112         if self._threads is None:
113             raise NotmuchError(STATUS.NOT_INITIALIZED)
114
115         i=0
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)
119             i += 1
120         # reset self._threads to mark as "exhausted"
121         self._threads = None
122         return i
123
124
125
126     def __del__(self):
127         """Close and free the notmuch Threads"""
128         if self._threads is not None:
129             nmlib.notmuch_messages_destroy (self._threads)
130
131 #------------------------------------------------------------------------------
132 class Thread(object):
133     """Represents a single message thread."""
134
135     """notmuch_thread_get_thread_id"""
136     _get_thread_id = nmlib.notmuch_thread_get_thread_id
137     _get_thread_id.restype = c_char_p
138
139     """notmuch_thread_get_authors"""
140     _get_authors = nmlib.notmuch_thread_get_authors
141     _get_authors.restype = c_char_p
142
143     """notmuch_thread_get_subject"""
144     _get_subject = nmlib.notmuch_thread_get_subject
145     _get_subject.restype = c_char_p
146
147     _get_newest_date = nmlib.notmuch_thread_get_newest_date
148     _get_newest_date.restype = c_uint64
149
150     _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
151     _get_oldest_date.restype = c_uint64
152
153     """notmuch_thread_get_tags"""
154     _get_tags = nmlib.notmuch_thread_get_tags
155     _get_tags.restype = c_void_p
156
157     def __init__(self, thread_p, parent=None):
158         """
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`.
166
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
170               objects are dead.
171         """
172         if thread_p is None:
173             NotmuchError(STATUS.NULL_POINTER)
174         self._thread = thread_p
175         #keep reference to parent, so we keep it alive
176         self._parent = parent
177
178     def get_thread_id(self):
179         """Get the thread ID of 'thread'
180
181         The returned string belongs to 'thread' and will only be valid
182         for as long as the thread is valid.
183
184         :returns: String with a message ID
185         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread 
186                     is not initialized.
187         """
188         if self._thread is None:
189             raise NotmuchError(STATUS.NOT_INITIALIZED)
190         return Thread._get_thread_id(self._thread)
191
192     def get_total_messages(self):
193         """Get the total number of messages in 'thread'
194
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 
199                     is not initialized.
200         """
201         if self._thread is None:
202             raise NotmuchError(STATUS.NOT_INITIALIZED)
203         return nmlib.notmuch_thread_get_total_messages(self._thread)
204
205
206     ###TODO: notmuch_messages_t * notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
207
208     def get_matched_messages(self):
209         """Returns the number of messages in 'thread' that matched the query
210
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 
215                     is not initialized.
216         """
217         if self._thread is None:
218             raise NotmuchError(STATUS.NOT_INITIALIZED)
219         return nmlib.notmuch_thread_get_matched_messages(self._thread)
220
221     def get_authors(self):
222         """Returns the authors of 'thread'
223
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
226         thread.
227
228         The returned string belongs to 'thread' and will only be valid for 
229         as long as this Thread() is not deleted.
230         """
231         if self._thread is None:
232             raise NotmuchError(STATUS.NOT_INITIALIZED)
233         return Thread._get_authors(self._thread)
234
235     def get_subject(self):
236         """Returns the Subject of 'thread'
237
238         The returned string belongs to 'thread' and will only be valid for 
239         as long as this Thread() is not deleted.
240         """
241         if self._thread is None:
242             raise NotmuchError(STATUS.NOT_INITIALIZED)
243         return Thread._get_subject(self._thread)
244
245     def get_newest_date(self):
246         """Returns time_t of the newest message date
247
248         :returns: A time_t timestamp.
249         :rtype: c_unit64
250         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
251                     is not initialized.
252         """
253         if self._thread is None:
254             raise NotmuchError(STATUS.NOT_INITIALIZED)
255         return Thread._get_newest_date(self._thread)
256
257     def get_oldest_date(self):
258         """Returns time_t of the oldest message date
259
260         :returns: A time_t timestamp.
261         :rtype: c_unit64
262         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
263                     is not initialized.
264         """
265         if self._thread is None:
266             raise NotmuchError(STATUS.NOT_INITIALIZED)
267         return Thread._get_oldest_date(self._thread)
268
269     def get_tags(self):
270         """ Returns the message tags
271
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
275         this thread.
276
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).
280
281         :returns: A :class:`Tags` iterator.
282         :exception: :exc:`NotmuchError`
283
284                       * STATUS.NOT_INITIALIZED if the thread 
285                         is not initialized.
286                       * STATUS.NULL_POINTER, on error
287         """
288         if self._thread is None:
289             raise NotmuchError(STATUS.NOT_INITIALIZED)
290
291         tags_p = Thread._get_tags(self._thread)
292         if tags_p == None:
293             raise NotmuchError(STATUS.NULL_POINTER)
294         return Tags(tags_p, self)
295  
296     def __str__(self):
297         """A str(Thread()) is represented by a 1-line summary"""
298         thread = {}
299         thread['id'] = self.get_thread_id()
300
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);
305         #else
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()
313
314         return "thread:%(id)s %(date)12s [%(matched)d/%(total)d] %(authors)s; %(subject)s (%(tags)s)" % (thread)
315
316     def __del__(self):
317         """Close and free the notmuch Thread"""
318         if self._thread is not None:
319             nmlib.notmuch_thread_destroy (self._thread)