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