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 """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 return value.value.decode('utf-8') if value is not None else None
487 def get_properties(self, prop="", exact=False):
488 """ Get the properties of the message, returning a generator of
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
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
499 :yields: Each property values as a pair of `name, value`
501 :raises: :exc:`NotInitializedError` if message has not been
505 raise NotInitializedError()
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)
515 """Freezes the current state of 'message' within the database
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.
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.
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::
530 msg.remove_all_tags(False)
532 msg.add_tag(tag, False)
534 msg.tags_to_maildir_flags()
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.
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`.
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
553 raise NotInitializedError()
555 status = self._freeze(self._msg)
557 if STATUS.SUCCESS == status:
561 raise NotmuchError(status)
563 _thaw = nmlib.notmuch_message_thaw
564 _thaw.argtypes = [NotmuchMessageP]
565 _thaw.restype = c_uint
568 """Thaws the current 'message'
570 Thaw the current 'message', synchronizing any changes that may have
571 occurred while 'message' was frozen into the notmuch database.
573 See :meth:`freeze` for an example of how to use this
574 function to safely provide tag changes.
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.
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
586 :raises: :exc:`NotInitializedError` if message has not been
590 raise NotInitializedError()
592 status = self._thaw(self._msg)
594 if STATUS.SUCCESS == status:
598 raise NotmuchError(status)
601 """(Not implemented)"""
602 return self.get_flag(Message.FLAG.MATCH)
604 def tags_to_maildir_flags(self):
605 """Synchronize notmuch tags to file Maildir flags
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
613 Any existing flags unmentioned in the list above will be
614 preserved in the renaming.
616 Also, if this filename is in a directory named "new", rename it
617 to be within the neighboring directory named "cur".
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
623 :returns: a :class:`STATUS` value. In short, you want to see
624 notmuch.STATUS.SUCCESS here. See there for details."""
626 raise NotInitializedError()
627 return Message._tags_to_maildir_flags(self._msg)
629 def maildir_flags_to_tags(self):
630 """Synchronize file Maildir flags to notmuch tags
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
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.)
647 As a convenience, you can set the sync_maildir_flags parameter in
648 :meth:`Database.index_file` to implicitly call this.
650 :returns: a :class:`STATUS`. In short, you want to see
651 notmuch.STATUS.SUCCESS here. See there for details."""
653 raise NotInitializedError()
654 return Message._maildir_flags_to_tags(self._msg)
657 """Represent a Message() object by str()"""
658 return self.__str__()
660 def __unicode__(self):
661 format = "%s (%s) (%s)"
662 return format % (self.get_header('from'),
664 date.fromtimestamp(self.get_date()),
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)
673 email_msg = email.message_from_binary_file(fp)
677 for msg in email_msg.walk():
678 if not msg.is_multipart():
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)):
688 out_part = parts[(num - 1)]
689 return out_part.get_payload(decode=True)
692 """Implement hash(), so we can use Message() sets"""
693 file = self.get_filename()
698 def __cmp__(self, other):
699 """Implement cmp(), so we can compare Message()s
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
707 res = cmp(self.get_message_id(), other.get_message_id())
709 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
712 _destroy = nmlib.notmuch_message_destroy
713 _destroy.argtypes = [NotmuchMessageP]
714 _destroy.restype = None
717 """Close and free the notmuch Message"""
719 self._destroy(self._msg)