20d3632955bb3808c4e9c32edca14c28d73c0e4e
[notmuch] / bindings / python / notmuch / messages.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                Jesse Rosenthal <jrosenthal@jhu.edu>
19 """
20
21 from .globals import (
22     nmlib,
23     NullPointerError,
24     NotInitializedError,
25     NotmuchTagsP,
26     NotmuchMessageP,
27     NotmuchMessagesP,
28 )
29 from .tag import Tags
30 from .message import Message
31
32 import sys
33
34 class Messages(object):
35     """Represents a list of notmuch messages
36
37     This object provides an iterator over a list of notmuch messages
38     (Technically, it provides a wrapper for the underlying
39     *notmuch_messages_t* structure). Do note that the underlying library
40     only provides a one-time iterator (it cannot reset the iterator to
41     the start). Thus iterating over the function will "exhaust" the list
42     of messages, and a subsequent iteration attempt will raise a
43     :exc:`NotInitializedError`. If you need to
44     re-iterate over a list of messages you will need to retrieve a new
45     :class:`Messages` object or cache your :class:`Message`\s in a list
46     via::
47
48        msglist = list(msgs)
49
50     You can store and reuse the single :class:`Message` objects as often
51     as you want as long as you keep the parent :class:`Messages` object
52     around. (Due to hierarchical memory allocation, all derived
53     :class:`Message` objects will be invalid when we delete the parent
54     :class:`Messages` object, even if it was already exhausted.) So
55     this works::
56
57       db   = Database()
58       msgs = Query(db,'').search_messages() #get a Messages() object
59       msglist = list(msgs)
60
61       # msgs is "exhausted" now and msgs.next() will raise an exception.
62       # However it will be kept alive until all retrieved Message()
63       # objects are also deleted. If you do e.g. an explicit del(msgs)
64       # here, the following lines would fail.
65
66       # You can reiterate over *msglist* however as often as you want.
67       # It is simply a list with :class:`Message`s.
68
69       print (msglist[0].get_filename())
70       print (msglist[1].get_filename())
71       print (msglist[0].get_message_id())
72
73
74     As :class:`Message` implements both __hash__() and __cmp__(), it is
75     possible to make sets out of :class:`Messages` and use set
76     arithmetic (this happens in python and will of course be *much*
77     slower than redoing a proper query with the appropriate filters::
78
79         s1, s2 = set(msgs1), set(msgs2)
80         s.union(s2)
81         s1 -= s2
82         ...
83
84     Be careful when using set arithmetic between message sets derived
85     from different Databases (ie the same database reopened after
86     messages have changed). If messages have added or removed associated
87     files in the meantime, it is possible that the same message would be
88     considered as a different object (as it points to a different file).
89     """
90
91     #notmuch_messages_get
92     _get = nmlib.notmuch_messages_get
93     _get.argtypes = [NotmuchMessagesP]
94     _get.restype = NotmuchMessageP
95
96     _collect_tags = nmlib.notmuch_messages_collect_tags
97     _collect_tags.argtypes = [NotmuchMessagesP]
98     _collect_tags.restype = NotmuchTagsP
99
100     def __init__(self, msgs_p, parent=None):
101         """
102         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
103              structure. These are not publically exposed, so a user
104              will almost never instantiate a :class:`Messages` object
105              herself. They are usually handed back as a result,
106              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
107              valid, we will raise an :exc:`NullPointerError` if it is
108              `None`.
109         :type msgs_p: :class:`ctypes.c_void_p`
110         :param parent: The parent object
111              (ie :class:`Query`) these tags are derived from. It saves
112              a reference to it, so we can automatically delete the db
113              object once all derived objects are dead.
114         :TODO: Make the iterator work more than once and cache the tags in
115                the Python object.(?)
116         """
117         if not msgs_p:
118             raise NullPointerError()
119
120         self._msgs = msgs_p
121         #store parent, so we keep them alive as long as self  is alive
122         self._parent = parent
123
124     def collect_tags(self):
125         """Return the unique :class:`Tags` in the contained messages
126
127         :returns: :class:`Tags`
128         :exceptions: :exc:`NotInitializedError` if not init'ed
129
130         .. note::
131
132             :meth:`collect_tags` will iterate over the messages and therefore
133             will not allow further iterations.
134         """
135         if not self._msgs:
136             raise NotInitializedError()
137
138         # collect all tags (returns NULL on error)
139         tags_p = Messages._collect_tags(self._msgs)
140         #reset _msgs as we iterated over it and can do so only once
141         self._msgs = None
142
143         if tags_p == None:
144             raise NullPointerError()
145         return Tags(tags_p, self)
146
147     def __iter__(self):
148         """ Make Messages an iterator """
149         return self
150
151     _valid = nmlib.notmuch_messages_valid
152     _valid.argtypes = [NotmuchMessagesP]
153     _valid.restype = bool
154
155     _move_to_next = nmlib.notmuch_messages_move_to_next
156     _move_to_next.argtypes = [NotmuchMessagesP]
157     _move_to_next.restype = None
158
159     def __next__(self):
160         if not self._msgs:
161             raise NotInitializedError()
162
163         if not self._valid(self._msgs):
164             self._msgs = None
165             raise StopIteration
166
167         msg = Message(Messages._get(self._msgs), self)
168         self._move_to_next(self._msgs)
169         return msg
170     next = __next__ # python2.x iterator protocol compatibility
171
172     def __nonzero__(self):
173         """
174         :return: True if there is at least one more thread in the
175             Iterator, False if not."""
176         return self._msgs is not None and \
177             self._valid(self._msgs) > 0
178
179     _destroy = nmlib.notmuch_messages_destroy
180     _destroy.argtypes = [NotmuchMessagesP]
181     _destroy.restype = None
182
183     def __del__(self):
184         """Close and free the notmuch Messages"""
185         if self._msgs is not None:
186             self._destroy(self._msgs)
187
188     def format_messages(self, format, indent=0, entire_thread=False):
189         """Formats messages as needed for 'notmuch show'.
190
191         :param format: A string of either 'text' or 'json'.
192         :param indent: A number indicating the reply depth of these messages.
193         :param entire_thread: A bool, indicating whether we want to output
194                        whole threads or only the matching messages.
195         :return: a list of lines
196         """
197         result = list()
198
199         if format.lower() == "text":
200             set_start = ""
201             set_end = ""
202             set_sep = ""
203         elif format.lower() == "json":
204             set_start = "["
205             set_end = "]"
206             set_sep = ", "
207         else:
208             raise TypeError("format must be either 'text' or 'json'")
209
210         first_set = True
211
212         result.append(set_start)
213
214         # iterate through all toplevel messages in this thread
215         for msg in self:
216             # if not msg:
217             #     break
218             if not first_set:
219                 result.append(set_sep)
220             first_set = False
221
222             result.append(set_start)
223             match = msg.is_match()
224             next_indent = indent
225
226             if (match or entire_thread):
227                 if format.lower() == "text":
228                     result.append(msg.format_message_as_text(indent))
229                 else:
230                     result.append(msg.format_message_as_json(indent))
231                 next_indent = indent + 1
232
233             # get replies and print them also out (if there are any)
234             replies = msg.get_replies().format_messages(format, next_indent, entire_thread)
235             if replies:
236                 result.append(set_sep)
237                 result.extend(replies)
238
239             result.append(set_end)
240         result.append(set_end)
241
242         return result
243
244     def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout):
245         """Outputs messages as needed for 'notmuch show' to a file like object.
246
247         :param format: A string of either 'text' or 'json'.
248         :param handle: A file like object to print to (default is sys.stdout).
249         :param indent: A number indicating the reply depth of these messages.
250         :param entire_thread: A bool, indicating whether we want to output
251                        whole threads or only the matching messages.
252         """
253         handle.write(''.join(self.format_messages(format, indent, entire_thread)))
254
255 class EmptyMessagesResult(Messages):
256     def __init__(self, parent):
257         self._msgs = None
258         self._parent = parent
259
260     def __next__(self):
261         raise StopIteration()
262     next = __next__