]> git.cworth.org Git - notmuch/blob - bindings/python/notmuch/message.py
lib: add NOTMUCH_STATUS_NO_DATABASE
[notmuch] / bindings / python / notmuch / message.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 <https://www.gnu.org/licenses/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
18                Jesse Rosenthal <jrosenthal@jhu.edu>
19 """
20
21
22 from ctypes import c_char_p, c_long, c_uint, c_int, POINTER, byref
23 from datetime import date
24 from .globals import (
25     nmlib,
26     Enum,
27     _str,
28     Python3StringMixIn,
29     NotmuchTagsP,
30     NotmuchMessageP,
31     NotmuchMessagesP,
32     NotmuchMessagePropertiesP,
33     NotmuchFilenamesP,
34 )
35 from .errors import (
36     STATUS,
37     NotmuchError,
38     NullPointerError,
39     NotInitializedError,
40 )
41 from .tag import Tags
42 from .filenames import Filenames
43
44 import email
45 import sys
46
47
48 class Message(Python3StringMixIn):
49     r"""Represents a single Email message
50
51     Technically, this wraps the underlying *notmuch_message_t*
52     structure. A user will usually not create these objects themselves
53     but get them as search results.
54
55     As it implements :meth:`__cmp__`, it is possible to compare two
56     :class:`Message`\s using `if msg1 == msg2: ...`.
57     """
58
59     """notmuch_message_get_filename (notmuch_message_t *message)"""
60     _get_filename = nmlib.notmuch_message_get_filename
61     _get_filename.argtypes = [NotmuchMessageP]
62     _get_filename.restype = c_char_p
63
64     """return all filenames for a message"""
65     _get_filenames = nmlib.notmuch_message_get_filenames
66     _get_filenames.argtypes = [NotmuchMessageP]
67     _get_filenames.restype = NotmuchFilenamesP
68
69     """notmuch_message_get_flag"""
70     _get_flag = nmlib.notmuch_message_get_flag
71     _get_flag.argtypes = [NotmuchMessageP, c_uint]
72     _get_flag.restype = bool
73
74     """notmuch_message_set_flag"""
75     _set_flag = nmlib.notmuch_message_set_flag
76     _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
77     _set_flag.restype = None
78
79     """notmuch_message_get_message_id (notmuch_message_t *message)"""
80     _get_message_id = nmlib.notmuch_message_get_message_id
81     _get_message_id.argtypes = [NotmuchMessageP]
82     _get_message_id.restype = c_char_p
83
84     """notmuch_message_get_thread_id"""
85     _get_thread_id = nmlib.notmuch_message_get_thread_id
86     _get_thread_id.argtypes = [NotmuchMessageP]
87     _get_thread_id.restype = c_char_p
88
89     """notmuch_message_get_replies"""
90     _get_replies = nmlib.notmuch_message_get_replies
91     _get_replies.argtypes = [NotmuchMessageP]
92     _get_replies.restype = NotmuchMessagesP
93
94     """notmuch_message_get_tags (notmuch_message_t *message)"""
95     _get_tags = nmlib.notmuch_message_get_tags
96     _get_tags.argtypes = [NotmuchMessageP]
97     _get_tags.restype = NotmuchTagsP
98
99     _get_date = nmlib.notmuch_message_get_date
100     _get_date.argtypes = [NotmuchMessageP]
101     _get_date.restype = c_long
102
103     _get_header = nmlib.notmuch_message_get_header
104     _get_header.argtypes = [NotmuchMessageP, c_char_p]
105     _get_header.restype = c_char_p
106
107     """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
108     _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
109     _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
110     _tags_to_maildir_flags.restype = c_int
111
112     """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
113     _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
114     _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
115     _maildir_flags_to_tags.restype = c_int
116
117     """notmuch_message_get_property"""
118     _get_property = nmlib.notmuch_message_get_property
119     _get_property.argtypes = [NotmuchMessageP, c_char_p, POINTER(c_char_p)]
120     _get_property.restype = c_int
121
122     """notmuch_message_get_properties"""
123     _get_properties = nmlib.notmuch_message_get_properties
124     _get_properties.argtypes = [NotmuchMessageP, c_char_p, c_int]
125     _get_properties.restype = NotmuchMessagePropertiesP
126
127     """notmuch_message_properties_valid"""
128     _properties_valid = nmlib.notmuch_message_properties_valid
129     _properties_valid.argtypes = [NotmuchMessagePropertiesP]
130     _properties_valid.restype = bool
131
132     """notmuch_message_properties_value"""
133     _properties_value = nmlib.notmuch_message_properties_value
134     _properties_value.argtypes = [NotmuchMessagePropertiesP]
135     _properties_value.restype = c_char_p
136
137     """notmuch_message_properties_key"""
138     _properties_key = nmlib.notmuch_message_properties_key
139     _properties_key.argtypes = [NotmuchMessagePropertiesP]
140     _properties_key.restype = c_char_p
141
142     """notmuch_message_properties_move_to_next"""
143     _properties_move_to_next = nmlib.notmuch_message_properties_move_to_next
144     _properties_move_to_next.argtypes = [NotmuchMessagePropertiesP]
145     _properties_move_to_next.restype = None
146
147     #Constants: Flags that can be set/get with set_flag
148     FLAG = Enum(['MATCH'])
149
150     def __init__(self, msg_p, parent=None):
151         """
152         :param msg_p: A pointer to an internal notmuch_message_t
153             Structure.  If it is `None`, we will raise an
154             :exc:`NullPointerError`.
155
156         :param parent: A 'parent' object is passed which this message is
157               derived from. We save a reference to it, so we can
158               automatically delete the parent object once all derived
159               objects are dead.
160         """
161         if not msg_p:
162             raise NullPointerError()
163         self._msg = msg_p
164         #keep reference to parent, so we keep it alive
165         self._parent = parent
166
167     def get_message_id(self):
168         """Returns the message ID
169
170         :returns: String with a message ID
171         :raises: :exc:`NotInitializedError` if the message
172                     is not initialized.
173         """
174         if not self._msg:
175             raise NotInitializedError()
176         return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
177
178     def get_thread_id(self):
179         """Returns the thread ID
180
181         The returned string belongs to 'message' will only be valid for as
182         long as the message is valid.
183
184         This function will not return `None` since Notmuch ensures that every
185         message belongs to a single thread.
186
187         :returns: String with a thread ID
188         :raises: :exc:`NotInitializedError` if the message
189                     is not initialized.
190         """
191         if not self._msg:
192             raise NotInitializedError()
193
194         return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
195
196     def get_replies(self):
197         """Gets all direct replies to this message as :class:`Messages`
198         iterator
199
200         .. note::
201
202             This call only makes sense if 'message' was ultimately obtained from
203             a :class:`Thread` object, (such as by coming directly from the
204             result of calling :meth:`Thread.get_toplevel_messages` or by any
205             number of subsequent calls to :meth:`get_replies`). If this message
206             was obtained through some non-thread means, (such as by a call to
207             :meth:`Query.search_messages`), then this function will return
208             an empty Messages iterator.
209
210         :returns: :class:`Messages`.
211         :raises: :exc:`NotInitializedError` if the message
212                     is not initialized.
213         """
214         if not self._msg:
215             raise NotInitializedError()
216
217         msgs_p = Message._get_replies(self._msg)
218
219         from .messages import Messages, EmptyMessagesResult
220
221         if not msgs_p:
222             return EmptyMessagesResult(self)
223
224         return Messages(msgs_p, self)
225
226     def get_date(self):
227         """Returns time_t of the message date
228
229         For the original textual representation of the Date header from the
230         message call notmuch_message_get_header() with a header value of
231         "date".
232
233         :returns: A time_t timestamp.
234         :rtype: c_unit64
235         :raises: :exc:`NotInitializedError` if the message
236                     is not initialized.
237         """
238         if not self._msg:
239             raise NotInitializedError()
240         return Message._get_date(self._msg)
241
242     def get_header(self, header):
243         """Get the value of the specified header.
244
245         The value will be read from the actual message file, not from
246         the notmuch database. The header name is case insensitive.
247
248         Returns an empty string ("") if the message does not contain a
249         header line matching 'header'.
250
251         :param header: The name of the header to be retrieved.
252                        It is not case-sensitive.
253         :type header: str
254         :returns: The header value as string
255         :raises: :exc:`NotInitializedError` if the message is not
256                  initialized
257         :raises: :exc:`NullPointerError` if any error occurred
258         """
259         if not self._msg:
260             raise NotInitializedError()
261
262         #Returns NULL if any error occurs.
263         header = Message._get_header(self._msg, _str(header))
264         if header == None:
265             raise NullPointerError()
266         return header.decode('UTF-8', 'ignore')
267
268     def get_filename(self):
269         """Returns the file path of the message file
270
271         :returns: Absolute file path & name of the message file
272         :raises: :exc:`NotInitializedError` if the message
273               is not initialized.
274         """
275         if not self._msg:
276             raise NotInitializedError()
277         return Message._get_filename(self._msg).decode('utf-8', 'ignore')
278
279     def get_filenames(self):
280         """Get all filenames for the email corresponding to 'message'
281
282         Returns a Filenames() generator with all absolute filepaths for
283         messages recorded to have the same Message-ID. These files must
284         not necessarily have identical content."""
285         if not self._msg:
286             raise NotInitializedError()
287
288         files_p = Message._get_filenames(self._msg)
289
290         return Filenames(files_p, self)
291
292     def get_flag(self, flag):
293         """Checks whether a specific flag is set for this message
294
295         The method :meth:`Query.search_threads` sets
296         *Message.FLAG.MATCH* for those messages that match the
297         query. This method allows us to get the value of this flag.
298
299         :param flag: One of the :attr:`Message.FLAG` values (currently only
300                      *Message.FLAG.MATCH*
301         :returns: An unsigned int (0/1), indicating whether the flag is set.
302         :raises: :exc:`NotInitializedError` if the message
303               is not initialized.
304         """
305         if not self._msg:
306             raise NotInitializedError()
307         return Message._get_flag(self._msg, flag)
308
309     def set_flag(self, flag, value):
310         """Sets/Unsets a specific flag for this message
311
312         :param flag: One of the :attr:`Message.FLAG` values (currently only
313                      *Message.FLAG.MATCH*
314         :param value: A bool indicating whether to set or unset the flag.
315
316         :raises: :exc:`NotInitializedError` if the message
317               is not initialized.
318         """
319         if not self._msg:
320             raise NotInitializedError()
321         self._set_flag(self._msg, flag, value)
322
323     def get_tags(self):
324         """Returns the message tags
325
326         :returns: A :class:`Tags` iterator.
327         :raises: :exc:`NotInitializedError` if the message is not
328                  initialized
329         :raises: :exc:`NullPointerError` if any error occurred
330         """
331         if not self._msg:
332             raise NotInitializedError()
333
334         tags_p = Message._get_tags(self._msg)
335         if not tags_p:
336             raise NullPointerError()
337         return Tags(tags_p, self)
338
339     _add_tag = nmlib.notmuch_message_add_tag
340     _add_tag.argtypes = [NotmuchMessageP, c_char_p]
341     _add_tag.restype = c_uint
342
343     def add_tag(self, tag, sync_maildir_flags=False):
344         """Adds a tag to the given message
345
346         Adds a tag to the current message. The maximal tag length is defined in
347         the notmuch library and is currently 200 bytes.
348
349         :param tag: String with a 'tag' to be added.
350
351         :param sync_maildir_flags: If notmuch configuration is set to do
352             this, add maildir flags corresponding to notmuch tags. See
353             underlying method :meth:`tags_to_maildir_flags`. Use False
354             if you want to add/remove many tags on a message without
355             having to physically rename the file every time. Do note,
356             that this will do nothing when a message is frozen, as tag
357             changes will not be committed to the database yet.
358
359         :returns: STATUS.SUCCESS if the tag was successfully added.
360                   Raises an exception otherwise.
361         :raises: :exc:`NullPointerError` if the `tag` argument is NULL
362         :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
363                  Message.NOTMUCH_TAG_MAX)
364         :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
365                  in read-only mode so message cannot be modified
366         :raises: :exc:`NotInitializedError` if message has not been
367                  initialized
368         """
369         if not self._msg:
370             raise NotInitializedError()
371
372         status = self._add_tag(self._msg, _str(tag))
373
374         # bail out on failure
375         if status != STATUS.SUCCESS:
376             raise NotmuchError(status)
377
378         if sync_maildir_flags:
379             self.tags_to_maildir_flags()
380         return STATUS.SUCCESS
381
382     _remove_tag = nmlib.notmuch_message_remove_tag
383     _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
384     _remove_tag.restype = c_uint
385
386     def remove_tag(self, tag, sync_maildir_flags=False):
387         """Removes a tag from the given message
388
389         If the message has no such tag, this is a non-operation and
390         will report success anyway.
391
392         :param tag: String with a 'tag' to be removed.
393         :param sync_maildir_flags: If notmuch configuration is set to do
394             this, add maildir flags corresponding to notmuch tags. See
395             underlying method :meth:`tags_to_maildir_flags`. Use False
396             if you want to add/remove many tags on a message without
397             having to physically rename the file every time. Do note,
398             that this will do nothing when a message is frozen, as tag
399             changes will not be committed to the database yet.
400
401         :returns: STATUS.SUCCESS if the tag was successfully removed or if
402                   the message had no such tag.
403                   Raises an exception otherwise.
404         :raises: :exc:`NullPointerError` if the `tag` argument is NULL
405         :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
406                  Message.NOTMUCH_TAG_MAX)
407         :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
408                  in read-only mode so message cannot be modified
409         :raises: :exc:`NotInitializedError` if message has not been
410                  initialized
411         """
412         if not self._msg:
413             raise NotInitializedError()
414
415         status = self._remove_tag(self._msg, _str(tag))
416         # bail out on error
417         if status != STATUS.SUCCESS:
418             raise NotmuchError(status)
419
420         if sync_maildir_flags:
421             self.tags_to_maildir_flags()
422         return STATUS.SUCCESS
423
424     _remove_all_tags = nmlib.notmuch_message_remove_all_tags
425     _remove_all_tags.argtypes = [NotmuchMessageP]
426     _remove_all_tags.restype = c_uint
427
428     def remove_all_tags(self, sync_maildir_flags=False):
429         """Removes all tags from the given message.
430
431         See :meth:`freeze` for an example showing how to safely
432         replace tag values.
433
434
435         :param sync_maildir_flags: If notmuch configuration is set to do
436             this, add maildir flags corresponding to notmuch tags. See
437             :meth:`tags_to_maildir_flags`. Use False if you want to
438             add/remove many tags on a message without having to
439             physically rename the file every time. Do note, that this
440             will do nothing when a message is frozen, as tag changes
441             will not be committed to the database yet.
442
443         :returns: STATUS.SUCCESS if the tags were successfully removed.
444                   Raises an exception otherwise.
445         :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
446                  in read-only mode so message cannot be modified
447         :raises: :exc:`NotInitializedError` if message has not been
448                  initialized
449         """
450         if not self._msg:
451             raise NotInitializedError()
452
453         status = self._remove_all_tags(self._msg)
454
455         # bail out on error
456         if status != STATUS.SUCCESS:
457             raise NotmuchError(status)
458
459         if sync_maildir_flags:
460             self.tags_to_maildir_flags()
461         return STATUS.SUCCESS
462
463     _freeze = nmlib.notmuch_message_freeze
464     _freeze.argtypes = [NotmuchMessageP]
465     _freeze.restype = c_uint
466
467     def get_property(self, prop):
468         """ Retrieve the value for a single property key
469
470         :param prop: The name of the property to get.
471         :returns: String with the property value or None if there is no such
472                   key. In the case of multiple values for the given key, the
473                   first one is retrieved.
474         :raises: :exc:`NotInitializedError` if message has not been
475                  initialized
476         """
477         if not self._msg:
478             raise NotInitializedError()
479
480         value = c_char_p()
481         status = Message._get_property(self._msg, _str(prop), byref(value))
482         if status != 0:
483             raise NotmuchError(status)
484
485         if value is None or value.value is None:
486             return None
487         return value.value.decode('utf-8')
488
489     def get_properties(self, prop="", exact=False):
490         """ Get the properties of the message, returning a generator of
491         name, value pairs.
492
493         The generator will yield once per value. There might be more than one
494         value on each name, so the generator might yield the same name several
495         times.
496
497         :param prop: The name of the property to get. Otherwise it will return
498                      the full list of properties of the message.
499         :param exact: if True, require exact match with key. Otherwise
500                       treat as prefix.
501         :yields:  Each property values as a pair of `name, value`
502         :ytype:   pairs of str
503         :raises: :exc:`NotInitializedError` if message has not been
504                  initialized
505         """
506         if not self._msg:
507             raise NotInitializedError()
508
509         properties = Message._get_properties(self._msg, _str(prop), exact)
510         while Message._properties_valid(properties):
511             key = Message._properties_key(properties)
512             value = Message._properties_value(properties)
513             yield key.decode("utf-8"), value.decode("utf-8")
514             Message._properties_move_to_next(properties)
515
516     def freeze(self):
517         """Freezes the current state of 'message' within the database
518
519         This means that changes to the message state, (via :meth:`add_tag`,
520         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
521         committed to the database until the message is :meth:`thaw` ed.
522
523         Multiple calls to freeze/thaw are valid and these calls will
524         "stack". That is there must be as many calls to thaw as to freeze
525         before a message is actually thawed.
526
527         The ability to do freeze/thaw allows for safe transactions to
528         change tag values. For example, explicitly setting a message to
529         have a given set of tags might look like this::
530
531           msg.freeze()
532           msg.remove_all_tags(False)
533           for tag in new_tags:
534               msg.add_tag(tag, False)
535           msg.thaw()
536           msg.tags_to_maildir_flags()
537
538         With freeze/thaw used like this, the message in the database is
539         guaranteed to have either the full set of original tag values, or
540         the full set of new tag values, but nothing in between.
541
542         Imagine the example above without freeze/thaw and the operation
543         somehow getting interrupted. This could result in the message being
544         left with no tags if the interruption happened after
545         :meth:`remove_all_tags` but before :meth:`add_tag`.
546
547         :returns: STATUS.SUCCESS if the message was successfully frozen.
548                   Raises an exception otherwise.
549         :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
550                  in read-only mode so message cannot be modified
551         :raises: :exc:`NotInitializedError` if message has not been
552                  initialized
553         """
554         if not self._msg:
555             raise NotInitializedError()
556
557         status = self._freeze(self._msg)
558
559         if STATUS.SUCCESS == status:
560             # return on success
561             return status
562
563         raise NotmuchError(status)
564
565     _thaw = nmlib.notmuch_message_thaw
566     _thaw.argtypes = [NotmuchMessageP]
567     _thaw.restype = c_uint
568
569     def thaw(self):
570         """Thaws the current 'message'
571
572         Thaw the current 'message', synchronizing any changes that may have
573         occurred while 'message' was frozen into the notmuch database.
574
575         See :meth:`freeze` for an example of how to use this
576         function to safely provide tag changes.
577
578         Multiple calls to freeze/thaw are valid and these calls with
579         "stack". That is there must be as many calls to thaw as to freeze
580         before a message is actually thawed.
581
582         :returns: STATUS.SUCCESS if the message was successfully frozen.
583                   Raises an exception otherwise.
584         :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
585                  to thaw an unfrozen message. That is, there have been
586                  an unbalanced number of calls to :meth:`freeze` and
587                  :meth:`thaw`.
588         :raises: :exc:`NotInitializedError` if message has not been
589                  initialized
590         """
591         if not self._msg:
592             raise NotInitializedError()
593
594         status = self._thaw(self._msg)
595
596         if STATUS.SUCCESS == status:
597             # return on success
598             return status
599
600         raise NotmuchError(status)
601
602     def is_match(self):
603         """(Not implemented)"""
604         return self.get_flag(Message.FLAG.MATCH)
605
606     def tags_to_maildir_flags(self):
607         """Synchronize notmuch tags to file Maildir flags
608
609               'D' if the message has the "draft" tag
610               'F' if the message has the "flagged" tag
611               'P' if the message has the "passed" tag
612               'R' if the message has the "replied" tag
613               'S' if the message does not have the "unread" tag
614
615         Any existing flags unmentioned in the list above will be
616         preserved in the renaming.
617
618         Also, if this filename is in a directory named "new", rename it
619         to be within the neighboring directory named "cur".
620
621         Do note that calling this method while a message is frozen might
622         not work yet, as the modified tags have not been committed yet
623         to the database.
624
625         :returns: a :class:`STATUS` value. In short, you want to see
626             notmuch.STATUS.SUCCESS here. See there for details."""
627         if not self._msg:
628             raise NotInitializedError()
629         return Message._tags_to_maildir_flags(self._msg)
630
631     def maildir_flags_to_tags(self):
632         """Synchronize file Maildir flags to notmuch tags
633
634             Flag    Action if present
635             ----    -----------------
636             'D'     Adds the "draft" tag to the message
637             'F'     Adds the "flagged" tag to the message
638             'P'     Adds the "passed" tag to the message
639             'R'     Adds the "replied" tag to the message
640             'S'     Removes the "unread" tag from the message
641
642         For each flag that is not present, the opposite action
643         (add/remove) is performed for the corresponding tags.  If there
644         are multiple filenames associated with this message, the flag is
645         considered present if it appears in one or more filenames. (That
646         is, the flags from the multiple filenames are combined with the
647         logical OR operator.)
648
649         As a convenience, you can set the sync_maildir_flags parameter in
650         :meth:`Database.index_file` to implicitly call this.
651
652         :returns: a :class:`STATUS`. In short, you want to see
653             notmuch.STATUS.SUCCESS here. See there for details."""
654         if not self._msg:
655             raise NotInitializedError()
656         return Message._maildir_flags_to_tags(self._msg)
657
658     def __repr__(self):
659         """Represent a Message() object by str()"""
660         return self.__str__()
661
662     def __unicode__(self):
663         format = "%s (%s) (%s)"
664         return format % (self.get_header('from'),
665                          self.get_tags(),
666                          date.fromtimestamp(self.get_date()),
667                         )
668
669     def get_message_parts(self):
670         """Output like notmuch show"""
671         fp = open(self.get_filename(), 'rb')
672         if sys.version_info[0] < 3:
673             email_msg = email.message_from_file(fp)
674         else:
675             email_msg = email.message_from_binary_file(fp)
676         fp.close()
677
678         out = []
679         for msg in email_msg.walk():
680             if not msg.is_multipart():
681                 out.append(msg)
682         return out
683
684     def get_part(self, num):
685         """Returns the nth message body part"""
686         parts = self.get_message_parts()
687         if (num <= 0 or num > len(parts)):
688             return ""
689         else:
690             out_part = parts[(num - 1)]
691             return out_part.get_payload(decode=True)
692
693     def __hash__(self):
694         """Implement hash(), so we can use Message() sets"""
695         file = self.get_filename()
696         if not file:
697             return None
698         return hash(file)
699
700     def __cmp__(self, other):
701         """Implement cmp(), so we can compare Message()s
702
703         2 messages are considered equal if they point to the same
704         Message-Id and if they point to the same file names. If 2
705         Messages derive from different queries where some files have
706         been added or removed, the same messages would not be considered
707         equal (as they do not point to the same set of files
708         any more)."""
709         res = cmp(self.get_message_id(), other.get_message_id())
710         if res:
711             res = cmp(list(self.get_filenames()), list(other.get_filenames()))
712         return res
713
714     _destroy = nmlib.notmuch_message_destroy
715     _destroy.argtypes = [NotmuchMessageP]
716     _destroy.restype = None
717
718     def __del__(self):
719         """Close and free the notmuch Message"""
720         if self._msg:
721             self._destroy(self._msg)