]> git.cworth.org Git - notmuch/blob - bindings/python/notmuch/message.py
de0fb4151e52e50c3f3eb684932b65820d74cf82
[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     """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         return value.value.decode('utf-8') if value is not None else None
486
487     def get_properties(self, prop="", exact=False):
488         """ Get the properties of the message, returning a generator of
489         name, value pairs.
490
491         The generator will yield once per value. There might be more than one
492         value on each name, so the generator might yield the same name several
493         times.
494
495         :param prop: The name of the property to get. Otherwise it will return
496                      the full list of properties of the message.
497         :param exact: if True, require exact match with key. Otherwise
498                       treat as prefix.
499         :yields:  Each property values as a pair of `name, value`
500         :ytype:   pairs of str
501         :raises: :exc:`NotInitializedError` if message has not been
502                  initialized
503         """
504         if not self._msg:
505             raise NotInitializedError()
506
507         properties = Message._get_properties(self._msg, _str(prop), exact)
508         while Message._properties_valid(properties):
509             key = Message._properties_key(properties)
510             value = Message._properties_value(properties)
511             yield key.decode("utf-8"), value.decode("utf-8")
512             Message._properties_move_to_next(properties)
513
514     def freeze(self):
515         """Freezes the current state of 'message' within the database
516
517         This means that changes to the message state, (via :meth:`add_tag`,
518         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
519         committed to the database until the message is :meth:`thaw` ed.
520
521         Multiple calls to freeze/thaw are valid and these calls will
522         "stack". That is there must be as many calls to thaw as to freeze
523         before a message is actually thawed.
524
525         The ability to do freeze/thaw allows for safe transactions to
526         change tag values. For example, explicitly setting a message to
527         have a given set of tags might look like this::
528
529           msg.freeze()
530           msg.remove_all_tags(False)
531           for tag in new_tags:
532               msg.add_tag(tag, False)
533           msg.thaw()
534           msg.tags_to_maildir_flags()
535
536         With freeze/thaw used like this, the message in the database is
537         guaranteed to have either the full set of original tag values, or
538         the full set of new tag values, but nothing in between.
539
540         Imagine the example above without freeze/thaw and the operation
541         somehow getting interrupted. This could result in the message being
542         left with no tags if the interruption happened after
543         :meth:`remove_all_tags` but before :meth:`add_tag`.
544
545         :returns: STATUS.SUCCESS if the message was successfully frozen.
546                   Raises an exception otherwise.
547         :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
548                  in read-only mode so message cannot be modified
549         :raises: :exc:`NotInitializedError` if message has not been
550                  initialized
551         """
552         if not self._msg:
553             raise NotInitializedError()
554
555         status = self._freeze(self._msg)
556
557         if STATUS.SUCCESS == status:
558             # return on success
559             return status
560
561         raise NotmuchError(status)
562
563     _thaw = nmlib.notmuch_message_thaw
564     _thaw.argtypes = [NotmuchMessageP]
565     _thaw.restype = c_uint
566
567     def thaw(self):
568         """Thaws the current 'message'
569
570         Thaw the current 'message', synchronizing any changes that may have
571         occurred while 'message' was frozen into the notmuch database.
572
573         See :meth:`freeze` for an example of how to use this
574         function to safely provide tag changes.
575
576         Multiple calls to freeze/thaw are valid and these calls with
577         "stack". That is there must be as many calls to thaw as to freeze
578         before a message is actually thawed.
579
580         :returns: STATUS.SUCCESS if the message was successfully frozen.
581                   Raises an exception otherwise.
582         :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
583                  to thaw an unfrozen message. That is, there have been
584                  an unbalanced number of calls to :meth:`freeze` and
585                  :meth:`thaw`.
586         :raises: :exc:`NotInitializedError` if message has not been
587                  initialized
588         """
589         if not self._msg:
590             raise NotInitializedError()
591
592         status = self._thaw(self._msg)
593
594         if STATUS.SUCCESS == status:
595             # return on success
596             return status
597
598         raise NotmuchError(status)
599
600     def is_match(self):
601         """(Not implemented)"""
602         return self.get_flag(Message.FLAG.MATCH)
603
604     def tags_to_maildir_flags(self):
605         """Synchronize notmuch tags to file Maildir flags
606
607               'D' if the message has the "draft" tag
608               'F' if the message has the "flagged" tag
609               'P' if the message has the "passed" tag
610               'R' if the message has the "replied" tag
611               'S' if the message does not have the "unread" tag
612
613         Any existing flags unmentioned in the list above will be
614         preserved in the renaming.
615
616         Also, if this filename is in a directory named "new", rename it
617         to be within the neighboring directory named "cur".
618
619         Do note that calling this method while a message is frozen might
620         not work yet, as the modified tags have not been committed yet
621         to the database.
622
623         :returns: a :class:`STATUS` value. In short, you want to see
624             notmuch.STATUS.SUCCESS here. See there for details."""
625         if not self._msg:
626             raise NotInitializedError()
627         return Message._tags_to_maildir_flags(self._msg)
628
629     def maildir_flags_to_tags(self):
630         """Synchronize file Maildir flags to notmuch tags
631
632             Flag    Action if present
633             ----    -----------------
634             'D'     Adds the "draft" tag to the message
635             'F'     Adds the "flagged" tag to the message
636             'P'     Adds the "passed" tag to the message
637             'R'     Adds the "replied" tag to the message
638             'S'     Removes the "unread" tag from the message
639
640         For each flag that is not present, the opposite action
641         (add/remove) is performed for the corresponding tags.  If there
642         are multiple filenames associated with this message, the flag is
643         considered present if it appears in one or more filenames. (That
644         is, the flags from the multiple filenames are combined with the
645         logical OR operator.)
646
647         As a convenience, you can set the sync_maildir_flags parameter in
648         :meth:`Database.index_file` to implicitly call this.
649
650         :returns: a :class:`STATUS`. In short, you want to see
651             notmuch.STATUS.SUCCESS here. See there for details."""
652         if not self._msg:
653             raise NotInitializedError()
654         return Message._maildir_flags_to_tags(self._msg)
655
656     def __repr__(self):
657         """Represent a Message() object by str()"""
658         return self.__str__()
659
660     def __unicode__(self):
661         format = "%s (%s) (%s)"
662         return format % (self.get_header('from'),
663                          self.get_tags(),
664                          date.fromtimestamp(self.get_date()),
665                         )
666
667     def get_message_parts(self):
668         """Output like notmuch show"""
669         fp = open(self.get_filename(), 'rb')
670         if sys.version_info[0] < 3:
671             email_msg = email.message_from_file(fp)
672         else:
673             email_msg = email.message_from_binary_file(fp)
674         fp.close()
675
676         out = []
677         for msg in email_msg.walk():
678             if not msg.is_multipart():
679                 out.append(msg)
680         return out
681
682     def get_part(self, num):
683         """Returns the nth message body part"""
684         parts = self.get_message_parts()
685         if (num <= 0 or num > len(parts)):
686             return ""
687         else:
688             out_part = parts[(num - 1)]
689             return out_part.get_payload(decode=True)
690
691     def __hash__(self):
692         """Implement hash(), so we can use Message() sets"""
693         file = self.get_filename()
694         if not file:
695             return None
696         return hash(file)
697
698     def __cmp__(self, other):
699         """Implement cmp(), so we can compare Message()s
700
701         2 messages are considered equal if they point to the same
702         Message-Id and if they point to the same file names. If 2
703         Messages derive from different queries where some files have
704         been added or removed, the same messages would not be considered
705         equal (as they do not point to the same set of files
706         any more)."""
707         res = cmp(self.get_message_id(), other.get_message_id())
708         if res:
709             res = cmp(list(self.get_filenames()), list(other.get_filenames()))
710         return res
711
712     _destroy = nmlib.notmuch_message_destroy
713     _destroy.argtypes = [NotmuchMessageP]
714     _destroy.restype = None
715
716     def __del__(self):
717         """Close and free the notmuch Message"""
718         if self._msg:
719             self._destroy(self._msg)