0dac522c5aa30a8477afd65a1fe9a9a006a5306e
[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 (
22     nmlib,
23     NullPointerError,
24     NotInitializedError,
25     NotmuchThreadP,
26     NotmuchMessagesP,
27     NotmuchTagsP,
28 )
29 from .messages import Messages
30 from notmuch.tag import Tags
31 from datetime import date
32
33 class Thread(object):
34     """Represents a single message thread."""
35
36     """notmuch_thread_get_thread_id"""
37     _get_thread_id = nmlib.notmuch_thread_get_thread_id
38     _get_thread_id.argtypes = [NotmuchThreadP]
39     _get_thread_id.restype = c_char_p
40
41     """notmuch_thread_get_authors"""
42     _get_authors = nmlib.notmuch_thread_get_authors
43     _get_authors.argtypes = [NotmuchThreadP]
44     _get_authors.restype = c_char_p
45
46     """notmuch_thread_get_subject"""
47     _get_subject = nmlib.notmuch_thread_get_subject
48     _get_subject.argtypes = [NotmuchThreadP]
49     _get_subject.restype = c_char_p
50
51     """notmuch_thread_get_toplevel_messages"""
52     _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
53     _get_toplevel_messages.argtypes = [NotmuchThreadP]
54     _get_toplevel_messages.restype = NotmuchMessagesP
55
56     _get_newest_date = nmlib.notmuch_thread_get_newest_date
57     _get_newest_date.argtypes = [NotmuchThreadP]
58     _get_newest_date.restype = c_long
59
60     _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
61     _get_oldest_date.argtypes = [NotmuchThreadP]
62     _get_oldest_date.restype = c_long
63
64     """notmuch_thread_get_tags"""
65     _get_tags = nmlib.notmuch_thread_get_tags
66     _get_tags.argtypes = [NotmuchThreadP]
67     _get_tags.restype = NotmuchTagsP
68
69     def __init__(self, thread_p, parent=None):
70         """
71         :param thread_p: A pointer to an internal notmuch_thread_t
72             Structure.  These are not publically exposed, so a user
73             will almost never instantiate a :class:`Thread` object
74             herself. They are usually handed back as a result,
75             e.g. when iterating through :class:`Threads`. *thread_p*
76             must be valid, we will raise an :exc:`NullPointerError`
77             if it is `None`.
78
79         :param parent: A 'parent' object is passed which this message is
80               derived from. We save a reference to it, so we can
81               automatically delete the parent object once all derived
82               objects are dead.
83         """
84         if not thread_p:
85             raise NullPointerError()
86         self._thread = thread_p
87         #keep reference to parent, so we keep it alive
88         self._parent = parent
89
90     def get_thread_id(self):
91         """Get the thread ID of 'thread'
92
93         The returned string belongs to 'thread' and will only be valid
94         for as long as the thread is valid.
95
96         :returns: String with a message ID
97         :raises: :exc:`NotInitializedError` if the thread
98                     is not initialized.
99         """
100         if not self._thread:
101             raise NotInitializedError()
102         return Thread._get_thread_id(self._thread).decode('utf-8', 'ignore')
103
104     _get_total_messages = nmlib.notmuch_thread_get_total_messages
105     _get_total_messages.argtypes = [NotmuchThreadP]
106     _get_total_messages.restype = c_int
107
108     def get_total_messages(self):
109         """Get the total number of messages in 'thread'
110
111         :returns: The number of all messages in the database
112                   belonging to this thread. Contrast with
113                   :meth:`get_matched_messages`.
114         :raises: :exc:`NotInitializedError` if the thread
115                     is not initialized.
116         """
117         if not self._thread:
118             raise NotInitializedError()
119         return self._get_total_messages(self._thread)
120
121     def get_toplevel_messages(self):
122         """Returns a :class:`Messages` iterator for the top-level messages in
123            'thread'
124
125            This iterator will not necessarily iterate over all of the messages
126            in the thread. It will only iterate over the messages in the thread
127            which are not replies to other messages in the thread.
128
129            To iterate over all messages in the thread, the caller will need to
130            iterate over the result of :meth:`Message.get_replies` for each
131            top-level message (and do that recursively for the resulting
132            messages, etc.).
133
134         :returns: :class:`Messages`
135         :raises: :exc:`NotInitializedError` if query is not initialized
136         :raises: :exc:`NullPointerError` if search_messages failed
137         """
138         if not self._thread:
139             raise NotInitializedError()
140
141         msgs_p = Thread._get_toplevel_messages(self._thread)
142
143         if not msgs_p:
144             raise NullPointerError()
145
146         return Messages(msgs_p, self)
147
148     _get_matched_messages = nmlib.notmuch_thread_get_matched_messages
149     _get_matched_messages.argtypes = [NotmuchThreadP]
150     _get_matched_messages.restype = c_int
151
152     def get_matched_messages(self):
153         """Returns the number of messages in 'thread' that matched the query
154
155         :returns: The number of all messages belonging to this thread that
156                   matched the :class:`Query`from which this thread was created.
157                   Contrast with :meth:`get_total_messages`.
158         :raises: :exc:`NotInitializedError` if the thread
159                     is not initialized.
160         """
161         if not self._thread:
162             raise NotInitializedError()
163         return self._get_matched_messages(self._thread)
164
165     def get_authors(self):
166         """Returns the authors of 'thread'
167
168         The returned string is a comma-separated list of the names of the
169         authors of mail messages in the query results that belong to this
170         thread.
171
172         The returned string belongs to 'thread' and will only be valid for
173         as long as this Thread() is not deleted.
174         """
175         if not self._thread:
176             raise NotInitializedError()
177         authors = Thread._get_authors(self._thread)
178         if not authors:
179             return None
180         return authors.decode('UTF-8', 'ignore')
181
182     def get_subject(self):
183         """Returns the Subject of 'thread'
184
185         The returned string belongs to 'thread' and will only be valid for
186         as long as this Thread() is not deleted.
187         """
188         if not self._thread:
189             raise NotInitializedError()
190         subject = Thread._get_subject(self._thread)
191         if not subject:
192             return None
193         return subject.decode('UTF-8', 'ignore')
194
195     def get_newest_date(self):
196         """Returns time_t of the newest message date
197
198         :returns: A time_t timestamp.
199         :rtype: c_unit64
200         :raises: :exc:`NotInitializedError` if the message
201                     is not initialized.
202         """
203         if not self._thread:
204             raise NotInitializedError()
205         return Thread._get_newest_date(self._thread)
206
207     def get_oldest_date(self):
208         """Returns time_t of the oldest message date
209
210         :returns: A time_t timestamp.
211         :rtype: c_unit64
212         :raises: :exc:`NotInitializedError` if the message
213                     is not initialized.
214         """
215         if not self._thread:
216             raise NotInitializedError()
217         return Thread._get_oldest_date(self._thread)
218
219     def get_tags(self):
220         """ Returns the message tags
221
222         In the Notmuch database, tags are stored on individual
223         messages, not on threads. So the tags returned here will be all
224         tags of the messages which matched the search and which belong to
225         this thread.
226
227         The :class:`Tags` object is owned by the thread and as such, will only
228         be valid for as long as this :class:`Thread` is valid (e.g. until the
229         query from which it derived is explicitely deleted).
230
231         :returns: A :class:`Tags` iterator.
232         :raises: :exc:`NotInitializedError` if query is not initialized
233         :raises: :exc:`NullPointerError` if search_messages failed
234         """
235         if not self._thread:
236             raise NotInitializedError()
237
238         tags_p = Thread._get_tags(self._thread)
239         if tags_p == None:
240             raise NullPointerError()
241         return Tags(tags_p, self)
242
243     def __unicode__(self):
244         frm = "thread:%s %12s [%d/%d] %s; %s (%s)"
245
246         return frm % (self.get_thread_id(),
247                       date.fromtimestamp(self.get_newest_date()),
248                       self.get_matched_messages(),
249                       self.get_total_messages(),
250                       self.get_authors(),
251                       self.get_subject(),
252                       self.get_tags(),
253                      )
254
255     _destroy = nmlib.notmuch_thread_destroy
256     _destroy.argtypes = [NotmuchThreadP]
257     _destroy.restype = None
258
259     def __del__(self):
260         """Close and free the notmuch Thread"""
261         if self._thread is not None:
262             self._destroy(self._thread)