2 This file is part of notmuch.
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.
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
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/>.
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
18 Jesse Rosenthal <jrosenthal@jhu.edu>
22 from ctypes import c_char_p, c_long, c_uint, c_int, POINTER, byref
23 from datetime import date
24 from .globals import (
32 NotmuchMessagePropertiesP,
42 from .filenames import Filenames
48 class Message(Python3StringMixIn):
49 r"""Represents a single Email message
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.
55 As it implements :meth:`__cmp__`, it is possible to compare two
56 :class:`Message`\s using `if msg1 == msg2: ...`.
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
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
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
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
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
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
89 """notmuch_message_get_replies"""
90 _get_replies = nmlib.notmuch_message_get_replies
91 _get_replies.argtypes = [NotmuchMessageP]
92 _get_replies.restype = NotmuchMessagesP
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
99 _get_date = nmlib.notmuch_message_get_date
100 _get_date.argtypes = [NotmuchMessageP]
101 _get_date.restype = c_long
103 _get_header = nmlib.notmuch_message_get_header
104 _get_header.argtypes = [NotmuchMessageP, c_char_p]
105 _get_header.restype = c_char_p
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
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
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
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
127 """notmuch_message_properties_valid"""
128 _properties_valid = nmlib.notmuch_message_properties_valid
129 _properties_valid.argtypes = [NotmuchMessagePropertiesP]
130 _properties_valid.restype = bool
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
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
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
147 #Constants: Flags that can be set/get with set_flag
148 FLAG = Enum(['MATCH'])
150 def __init__(self, msg_p, parent=None):
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`.
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
162 raise NullPointerError()
164 #keep reference to parent, so we keep it alive
165 self._parent = parent
167 def get_message_id(self):
168 """Returns the message ID
170 :returns: String with a message ID
171 :raises: :exc:`NotInitializedError` if the message
175 raise NotInitializedError()
176 return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
178 def get_thread_id(self):
179 """Returns the thread ID
181 The returned string belongs to 'message' will only be valid for as
182 long as the message is valid.
184 This function will not return `None` since Notmuch ensures that every
185 message belongs to a single thread.
187 :returns: String with a thread ID
188 :raises: :exc:`NotInitializedError` if the message
192 raise NotInitializedError()
194 return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
196 def get_replies(self):
197 """Gets all direct replies to this message as :class:`Messages`
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.
210 :returns: :class:`Messages`.
211 :raises: :exc:`NotInitializedError` if the message
215 raise NotInitializedError()
217 msgs_p = Message._get_replies(self._msg)
219 from .messages import Messages, EmptyMessagesResult
222 return EmptyMessagesResult(self)
224 return Messages(msgs_p, self)
227 """Returns time_t of the message date
229 For the original textual representation of the Date header from the
230 message call notmuch_message_get_header() with a header value of
233 :returns: A time_t timestamp.
235 :raises: :exc:`NotInitializedError` if the message
239 raise NotInitializedError()
240 return Message._get_date(self._msg)
242 def get_header(self, header):
243 """Get the value of the specified header.
245 The value will be read from the actual message file, not from
246 the notmuch database. The header name is case insensitive.
248 Returns an empty string ("") if the message does not contain a
249 header line matching 'header'.
251 :param header: The name of the header to be retrieved.
252 It is not case-sensitive.
254 :returns: The header value as string
255 :raises: :exc:`NotInitializedError` if the message is not
257 :raises: :exc:`NullPointerError` if any error occurred
260 raise NotInitializedError()
262 #Returns NULL if any error occurs.
263 header = Message._get_header(self._msg, _str(header))
265 raise NullPointerError()
266 return header.decode('UTF-8', 'ignore')
268 def get_filename(self):
269 """Returns the file path of the message file
271 :returns: Absolute file path & name of the message file
272 :raises: :exc:`NotInitializedError` if the message
276 raise NotInitializedError()
277 return Message._get_filename(self._msg).decode('utf-8', 'ignore')
279 def get_filenames(self):
280 """Get all filenames for the email corresponding to 'message'
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."""
286 raise NotInitializedError()
288 files_p = Message._get_filenames(self._msg)
290 return Filenames(files_p, self)
292 def get_flag(self, flag):
293 """Checks whether a specific flag is set for this message
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.
299 :param flag: One of the :attr:`Message.FLAG` values (currently only
301 :returns: An unsigned int (0/1), indicating whether the flag is set.
302 :raises: :exc:`NotInitializedError` if the message
306 raise NotInitializedError()
307 return Message._get_flag(self._msg, flag)
309 def set_flag(self, flag, value):
310 """Sets/Unsets a specific flag for this message
312 :param flag: One of the :attr:`Message.FLAG` values (currently only
314 :param value: A bool indicating whether to set or unset the flag.
316 :raises: :exc:`NotInitializedError` if the message
320 raise NotInitializedError()
321 self._set_flag(self._msg, flag, value)
324 """Returns the message tags
326 :returns: A :class:`Tags` iterator.
327 :raises: :exc:`NotInitializedError` if the message is not
329 :raises: :exc:`NullPointerError` if any error occurred
332 raise NotInitializedError()
334 tags_p = Message._get_tags(self._msg)
336 raise NullPointerError()
337 return Tags(tags_p, self)
339 _add_tag = nmlib.notmuch_message_add_tag
340 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
341 _add_tag.restype = c_uint
343 def add_tag(self, tag, sync_maildir_flags=False):
344 """Adds a tag to the given message
346 Adds a tag to the current message. The maximal tag length is defined in
347 the notmuch library and is currently 200 bytes.
349 :param tag: String with a 'tag' to be added.
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.
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
370 raise NotInitializedError()
372 status = self._add_tag(self._msg, _str(tag))
374 # bail out on failure
375 if status != STATUS.SUCCESS:
376 raise NotmuchError(status)
378 if sync_maildir_flags:
379 self.tags_to_maildir_flags()
380 return STATUS.SUCCESS
382 _remove_tag = nmlib.notmuch_message_remove_tag
383 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
384 _remove_tag.restype = c_uint
386 def remove_tag(self, tag, sync_maildir_flags=False):
387 """Removes a tag from the given message
389 If the message has no such tag, this is a non-operation and
390 will report success anyway.
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.
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
413 raise NotInitializedError()
415 status = self._remove_tag(self._msg, _str(tag))
417 if status != STATUS.SUCCESS:
418 raise NotmuchError(status)
420 if sync_maildir_flags:
421 self.tags_to_maildir_flags()
422 return STATUS.SUCCESS
424 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
425 _remove_all_tags.argtypes = [NotmuchMessageP]
426 _remove_all_tags.restype = c_uint
428 def remove_all_tags(self, sync_maildir_flags=False):
429 """Removes all tags from the given message.
431 See :meth:`freeze` for an example showing how to safely
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.
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
451 raise NotInitializedError()
453 status = self._remove_all_tags(self._msg)
456 if status != STATUS.SUCCESS:
457 raise NotmuchError(status)
459 if sync_maildir_flags:
460 self.tags_to_maildir_flags()
461 return STATUS.SUCCESS
463 _freeze = nmlib.notmuch_message_freeze
464 _freeze.argtypes = [NotmuchMessageP]
465 _freeze.restype = c_uint
467 def get_property(self, prop):
468 """ Retrieve the value for a single property key
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
478 raise NotInitializedError()
481 status = Message._get_property(self._msg, _str(prop), byref(value))
483 raise NotmuchError(status)
485 if value is None or value.value is None:
487 return value.value.decode('utf-8')
489 def get_properties(self, prop="", exact=False):
490 """ Get the properties of the message, returning a generator of
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
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
501 :yields: Each property values as a pair of `name, value`
503 :raises: :exc:`NotInitializedError` if message has not been
507 raise NotInitializedError()
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)
517 """Freezes the current state of 'message' within the database
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.
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.
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::
532 msg.remove_all_tags(False)
534 msg.add_tag(tag, False)
536 msg.tags_to_maildir_flags()
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.
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`.
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
555 raise NotInitializedError()
557 status = self._freeze(self._msg)
559 if STATUS.SUCCESS == status:
563 raise NotmuchError(status)
565 _thaw = nmlib.notmuch_message_thaw
566 _thaw.argtypes = [NotmuchMessageP]
567 _thaw.restype = c_uint
570 """Thaws the current 'message'
572 Thaw the current 'message', synchronizing any changes that may have
573 occurred while 'message' was frozen into the notmuch database.
575 See :meth:`freeze` for an example of how to use this
576 function to safely provide tag changes.
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.
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
588 :raises: :exc:`NotInitializedError` if message has not been
592 raise NotInitializedError()
594 status = self._thaw(self._msg)
596 if STATUS.SUCCESS == status:
600 raise NotmuchError(status)
603 """(Not implemented)"""
604 return self.get_flag(Message.FLAG.MATCH)
606 def tags_to_maildir_flags(self):
607 """Synchronize notmuch tags to file Maildir flags
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
615 Any existing flags unmentioned in the list above will be
616 preserved in the renaming.
618 Also, if this filename is in a directory named "new", rename it
619 to be within the neighboring directory named "cur".
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
625 :returns: a :class:`STATUS` value. In short, you want to see
626 notmuch.STATUS.SUCCESS here. See there for details."""
628 raise NotInitializedError()
629 return Message._tags_to_maildir_flags(self._msg)
631 def maildir_flags_to_tags(self):
632 """Synchronize file Maildir flags to notmuch tags
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
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.)
649 As a convenience, you can set the sync_maildir_flags parameter in
650 :meth:`Database.index_file` to implicitly call this.
652 :returns: a :class:`STATUS`. In short, you want to see
653 notmuch.STATUS.SUCCESS here. See there for details."""
655 raise NotInitializedError()
656 return Message._maildir_flags_to_tags(self._msg)
659 """Represent a Message() object by str()"""
660 return self.__str__()
662 def __unicode__(self):
663 format = "%s (%s) (%s)"
664 return format % (self.get_header('from'),
666 date.fromtimestamp(self.get_date()),
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)
675 email_msg = email.message_from_binary_file(fp)
679 for msg in email_msg.walk():
680 if not msg.is_multipart():
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)):
690 out_part = parts[(num - 1)]
691 return out_part.get_payload(decode=True)
694 """Implement hash(), so we can use Message() sets"""
695 file = self.get_filename()
700 def __cmp__(self, other):
701 """Implement cmp(), so we can compare Message()s
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
709 res = cmp(self.get_message_id(), other.get_message_id())
711 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
714 _destroy = nmlib.notmuch_message_destroy
715 _destroy.argtypes = [NotmuchMessageP]
716 _destroy.restype = None
719 """Close and free the notmuch Message"""
721 self._destroy(self._msg)