remove unused imports
[notmuch] / bindings / python / notmuch / thread.py
1 """
2 This file is part of notmuch.
3
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.
8
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
12 for more details.
13
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/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
18 """
19
20 from ctypes import c_char_p, c_long, c_int
21 from notmuch.globals import (nmlib, STATUS,
22     NotmuchError, NotmuchThreadP, NotmuchThreadsP, NotmuchMessagesP,
23     NotmuchTagsP,)
24 from notmuch.message import Messages
25 from notmuch.tag import Tags
26 from datetime import date
27
28
29 class Threads(object):
30     """Represents a list of notmuch threads
31
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::
41
42       for thread in threads: print thread
43
44     as well as::
45
46        number_of_msgs = len(threads)
47
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.
50
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::
57
58       db   = Database()
59       threads = Query(db,'').search_threads() #get a Threads() object
60       threadlist = []
61       for thread in threads:
62          threadlist.append(thread)
63
64       # threads is "exhausted" now and even len(threads) will raise an
65       # exception.
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.
69
70       # You can reiterate over *threadlist* however as often as you want.
71       # It is simply a list with Thread objects.
72
73       print (threadlist[0].get_thread_id())
74       print (threadlist[1].get_thread_id())
75       print (threadlist[0].get_total_messages())
76     """
77
78     #notmuch_threads_get
79     _get = nmlib.notmuch_threads_get
80     _get.argtypes = [NotmuchThreadsP]
81     _get.restype = NotmuchThreadP
82
83     def __init__(self, threads_p, parent=None):
84         """
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
98                the Python object.(?)
99         """
100         if threads_p is None:
101             raise NotmuchError(STATUS.NULL_POINTER)
102
103         self._threads = threads_p
104         #store parent, so we keep them alive as long as self  is alive
105         self._parent = parent
106
107     def __iter__(self):
108         """ Make Threads an iterator """
109         return self
110
111     _valid = nmlib.notmuch_threads_valid
112     _valid.argtypes = [NotmuchThreadsP]
113     _valid.restype = bool
114
115     _move_to_next = nmlib.notmuch_threads_move_to_next
116     _move_to_next.argtypes = [NotmuchThreadsP]
117     _move_to_next.restype = None
118
119     def next(self):
120         if self._threads is None:
121             raise NotmuchError(STATUS.NOT_INITIALIZED)
122
123         if not self._valid(self._threads):
124             self._threads = None
125             raise StopIteration
126
127         thread = Thread(Threads._get(self._threads), self)
128         self._move_to_next(self._threads)
129         return thread
130
131     def __len__(self):
132         """len(:class:`Threads`) returns the number of contained Threads
133
134         .. note:: As this iterates over the threads, we will not be able to
135                iterate over them again! So this will fail::
136
137                  #THIS FAILS
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
142         """
143         if self._threads is None:
144             raise NotmuchError(STATUS.NOT_INITIALIZED)
145
146         i = 0
147         # returns 'bool'. On out-of-memory it returns None
148         while self._valid(self._threads):
149             self._move_to_next(self._threads)
150             i += 1
151         # reset self._threads to mark as "exhausted"
152         self._threads = None
153         return i
154
155     def __nonzero__(self):
156         """Check if :class:`Threads` contains at least one more valid thread
157
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())`
161         work repeatedly.
162
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.
165         """
166         return self._threads is not None and \
167             self._valid(self._threads) > 0
168
169     _destroy = nmlib.notmuch_threads_destroy
170     _destroy.argtypes = [NotmuchThreadsP]
171     _destroy.argtypes = None
172
173     def __del__(self):
174         """Close and free the notmuch Threads"""
175         if self._threads is not None:
176             self._destroy(self._threads)
177
178
179 class Thread(object):
180     """Represents a single message thread."""
181
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
186
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
191
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
196
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
201
202     _get_newest_date = nmlib.notmuch_thread_get_newest_date
203     _get_newest_date.argtypes = [NotmuchThreadP]
204     _get_newest_date.restype = c_long
205
206     _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
207     _get_oldest_date.argtypes = [NotmuchThreadP]
208     _get_oldest_date.restype = c_long
209
210     """notmuch_thread_get_tags"""
211     _get_tags = nmlib.notmuch_thread_get_tags
212     _get_tags.argtypes = [NotmuchThreadP]
213     _get_tags.restype = NotmuchTagsP
214
215     def __init__(self, thread_p, parent=None):
216         """
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`.
224
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
228               objects are dead.
229         """
230         if thread_p is None:
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
235
236     def get_thread_id(self):
237         """Get the thread ID of 'thread'
238
239         The returned string belongs to 'thread' and will only be valid
240         for as long as the thread is valid.
241
242         :returns: String with a message ID
243         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
244                     is not initialized.
245         """
246         if self._thread is None:
247             raise NotmuchError(STATUS.NOT_INITIALIZED)
248         return Thread._get_thread_id(self._thread)
249
250     _get_total_messages = nmlib.notmuch_thread_get_total_messages
251     _get_total_messages.argtypes = [NotmuchThreadP]
252     _get_total_messages.restype = c_int
253
254     def get_total_messages(self):
255         """Get the total number of messages in 'thread'
256
257         :returns: The number of all messages in the database
258                   belonging to this thread. Contrast with
259                   :meth:`get_matched_messages`.
260         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
261                     is not initialized.
262         """
263         if self._thread is None:
264             raise NotmuchError(STATUS.NOT_INITIALIZED)
265         return self._get_total_messages(self._thread)
266
267     def get_toplevel_messages(self):
268         """Returns a :class:`Messages` iterator for the top-level messages in
269            'thread'
270
271            This iterator will not necessarily iterate over all of the messages
272            in the thread. It will only iterate over the messages in the thread
273            which are not replies to other messages in the thread.
274
275            To iterate over all messages in the thread, the caller will need to
276            iterate over the result of :meth:`Message.get_replies` for each
277            top-level message (and do that recursively for the resulting
278            messages, etc.).
279
280         :returns: :class:`Messages`
281         :exception: :exc:`NotmuchError`
282
283                       * STATUS.NOT_INITIALIZED if query is not inited
284                       * STATUS.NULL_POINTER if search_messages failed
285         """
286         if self._thread is None:
287             raise NotmuchError(STATUS.NOT_INITIALIZED)
288
289         msgs_p = Thread._get_toplevel_messages(self._thread)
290
291         if msgs_p is None:
292             raise NotmuchError(STATUS.NULL_POINTER)
293
294         return Messages(msgs_p, self)
295
296     _get_matched_messages = nmlib.notmuch_thread_get_matched_messages
297     _get_matched_messages.argtypes = [NotmuchThreadP]
298     _get_matched_messages.restype = c_int
299
300     def get_matched_messages(self):
301         """Returns the number of messages in 'thread' that matched the query
302
303         :returns: The number of all messages belonging to this thread that
304                   matched the :class:`Query`from which this thread was created.
305                   Contrast with :meth:`get_total_messages`.
306         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
307                     is not initialized.
308         """
309         if self._thread is None:
310             raise NotmuchError(STATUS.NOT_INITIALIZED)
311         return self._get_matched_messages(self._thread)
312
313     def get_authors(self):
314         """Returns the authors of 'thread'
315
316         The returned string is a comma-separated list of the names of the
317         authors of mail messages in the query results that belong to this
318         thread.
319
320         The returned string belongs to 'thread' and will only be valid for
321         as long as this Thread() is not deleted.
322         """
323         if self._thread is None:
324             raise NotmuchError(STATUS.NOT_INITIALIZED)
325         authors = Thread._get_authors(self._thread)
326         if authors is None:
327             return None
328         return authors.decode('UTF-8')
329
330     def get_subject(self):
331         """Returns the Subject of 'thread'
332
333         The returned string belongs to 'thread' and will only be valid for
334         as long as this Thread() is not deleted.
335         """
336         if self._thread is None:
337             raise NotmuchError(STATUS.NOT_INITIALIZED)
338         subject = Thread._get_subject(self._thread)
339         if subject is None:
340             return None
341         return subject.decode('UTF-8')
342
343     def get_newest_date(self):
344         """Returns time_t of the newest message date
345
346         :returns: A time_t timestamp.
347         :rtype: c_unit64
348         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
349                     is not initialized.
350         """
351         if self._thread is None:
352             raise NotmuchError(STATUS.NOT_INITIALIZED)
353         return Thread._get_newest_date(self._thread)
354
355     def get_oldest_date(self):
356         """Returns time_t of the oldest message date
357
358         :returns: A time_t timestamp.
359         :rtype: c_unit64
360         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
361                     is not initialized.
362         """
363         if self._thread is None:
364             raise NotmuchError(STATUS.NOT_INITIALIZED)
365         return Thread._get_oldest_date(self._thread)
366
367     def get_tags(self):
368         """ Returns the message tags
369
370         In the Notmuch database, tags are stored on individual
371         messages, not on threads. So the tags returned here will be all
372         tags of the messages which matched the search and which belong to
373         this thread.
374
375         The :class:`Tags` object is owned by the thread and as such, will only
376         be valid for as long as this :class:`Thread` is valid (e.g. until the
377         query from which it derived is explicitely deleted).
378
379         :returns: A :class:`Tags` iterator.
380         :exception: :exc:`NotmuchError`
381
382                       * STATUS.NOT_INITIALIZED if the thread
383                         is not initialized.
384                       * STATUS.NULL_POINTER, on error
385         """
386         if self._thread is None:
387             raise NotmuchError(STATUS.NOT_INITIALIZED)
388
389         tags_p = Thread._get_tags(self._thread)
390         if tags_p == None:
391             raise NotmuchError(STATUS.NULL_POINTER)
392         return Tags(tags_p, self)
393
394     def __str__(self):
395         """A str(Thread()) is represented by a 1-line summary"""
396         thread = {}
397         thread['id'] = self.get_thread_id()
398
399         ###TODO: How do we find out the current sort order of Threads?
400         ###Add a "sort" attribute to the Threads() object?
401         #if (sort == NOTMUCH_SORT_OLDEST_FIRST)
402         #         date = notmuch_thread_get_oldest_date (thread);
403         #else
404         #         date = notmuch_thread_get_newest_date (thread);
405         thread['date'] = date.fromtimestamp(self.get_newest_date())
406         thread['matched'] = self.get_matched_messages()
407         thread['total'] = self.get_total_messages()
408         thread['authors'] = self.get_authors()
409         thread['subject'] = self.get_subject()
410         thread['tags'] = self.get_tags()
411
412         return "thread:%s %12s [%d/%d] %s; %s (%s)" % (thread['id'],
413                                                        thread['date'],
414                                                        thread['matched'],
415                                                        thread['total'],
416                                                        thread['authors'],
417                                                        thread['subject'],
418                                                        thread['tags'])
419
420     _destroy = nmlib.notmuch_thread_destroy
421     _destroy.argtypes = [NotmuchThreadP]
422     _destroy.restype = None
423
424     def __del__(self):
425         """Close and free the notmuch Thread"""
426         if self._thread is not None:
427             self._destroy(self._thread)