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
23 from datetime import date
24 from .globals import (
41 from .filenames import Filenames
47 class Message(Python3StringMixIn):
48 """Represents a single Email message
50 Technically, this wraps the underlying *notmuch_message_t*
51 structure. A user will usually not create these objects themselves
52 but get them as search results.
54 As it implements :meth:`__cmp__`, it is possible to compare two
55 :class:`Message`\s using `if msg1 == msg2: ...`.
58 """notmuch_message_get_filename (notmuch_message_t *message)"""
59 _get_filename = nmlib.notmuch_message_get_filename
60 _get_filename.argtypes = [NotmuchMessageP]
61 _get_filename.restype = c_char_p
63 """return all filenames for a message"""
64 _get_filenames = nmlib.notmuch_message_get_filenames
65 _get_filenames.argtypes = [NotmuchMessageP]
66 _get_filenames.restype = NotmuchFilenamesP
68 """notmuch_message_get_flag"""
69 _get_flag = nmlib.notmuch_message_get_flag
70 _get_flag.argtypes = [NotmuchMessageP, c_uint]
71 _get_flag.restype = bool
73 """notmuch_message_set_flag"""
74 _set_flag = nmlib.notmuch_message_set_flag
75 _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
76 _set_flag.restype = None
78 """notmuch_message_get_message_id (notmuch_message_t *message)"""
79 _get_message_id = nmlib.notmuch_message_get_message_id
80 _get_message_id.argtypes = [NotmuchMessageP]
81 _get_message_id.restype = c_char_p
83 """notmuch_message_get_thread_id"""
84 _get_thread_id = nmlib.notmuch_message_get_thread_id
85 _get_thread_id.argtypes = [NotmuchMessageP]
86 _get_thread_id.restype = c_char_p
88 """notmuch_message_get_replies"""
89 _get_replies = nmlib.notmuch_message_get_replies
90 _get_replies.argtypes = [NotmuchMessageP]
91 _get_replies.restype = NotmuchMessagesP
93 """notmuch_message_get_tags (notmuch_message_t *message)"""
94 _get_tags = nmlib.notmuch_message_get_tags
95 _get_tags.argtypes = [NotmuchMessageP]
96 _get_tags.restype = NotmuchTagsP
98 _get_date = nmlib.notmuch_message_get_date
99 _get_date.argtypes = [NotmuchMessageP]
100 _get_date.restype = c_long
102 _get_header = nmlib.notmuch_message_get_header
103 _get_header.argtypes = [NotmuchMessageP, c_char_p]
104 _get_header.restype = c_char_p
106 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
107 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
108 _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
109 _tags_to_maildir_flags.restype = c_int
111 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
112 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
113 _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
114 _maildir_flags_to_tags.restype = c_int
116 #Constants: Flags that can be set/get with set_flag
117 FLAG = Enum(['MATCH'])
119 def __init__(self, msg_p, parent=None):
121 :param msg_p: A pointer to an internal notmuch_message_t
122 Structure. If it is `None`, we will raise an
123 :exc:`NullPointerError`.
125 :param parent: A 'parent' object is passed which this message is
126 derived from. We save a reference to it, so we can
127 automatically delete the parent object once all derived
131 raise NullPointerError()
133 #keep reference to parent, so we keep it alive
134 self._parent = parent
136 def get_message_id(self):
137 """Returns the message ID
139 :returns: String with a message ID
140 :raises: :exc:`NotInitializedError` if the message
144 raise NotInitializedError()
145 return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
147 def get_thread_id(self):
148 """Returns the thread ID
150 The returned string belongs to 'message' will only be valid for as
151 long as the message is valid.
153 This function will not return `None` since Notmuch ensures that every
154 message belongs to a single thread.
156 :returns: String with a thread ID
157 :raises: :exc:`NotInitializedError` if the message
161 raise NotInitializedError()
163 return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
165 def get_replies(self):
166 """Gets all direct replies to this message as :class:`Messages`
171 This call only makes sense if 'message' was ultimately obtained from
172 a :class:`Thread` object, (such as by coming directly from the
173 result of calling :meth:`Thread.get_toplevel_messages` or by any
174 number of subsequent calls to :meth:`get_replies`). If this message
175 was obtained through some non-thread means, (such as by a call to
176 :meth:`Query.search_messages`), then this function will return
177 an empty Messages iterator.
179 :returns: :class:`Messages`.
180 :raises: :exc:`NotInitializedError` if the message
184 raise NotInitializedError()
186 msgs_p = Message._get_replies(self._msg)
188 from .messages import Messages, EmptyMessagesResult
191 return EmptyMessagesResult(self)
193 return Messages(msgs_p, self)
196 """Returns time_t of the message date
198 For the original textual representation of the Date header from the
199 message call notmuch_message_get_header() with a header value of
202 :returns: A time_t timestamp.
204 :raises: :exc:`NotInitializedError` if the message
208 raise NotInitializedError()
209 return Message._get_date(self._msg)
211 def get_header(self, header):
212 """Get the value of the specified header.
214 The value will be read from the actual message file, not from
215 the notmuch database. The header name is case insensitive.
217 Returns an empty string ("") if the message does not contain a
218 header line matching 'header'.
220 :param header: The name of the header to be retrieved.
221 It is not case-sensitive.
223 :returns: The header value as string
224 :raises: :exc:`NotInitializedError` if the message is not
226 :raises: :exc:`NullPointerError` if any error occured
229 raise NotInitializedError()
231 #Returns NULL if any error occurs.
232 header = Message._get_header(self._msg, _str(header))
234 raise NullPointerError()
235 return header.decode('UTF-8', 'ignore')
237 def get_filename(self):
238 """Returns the file path of the message file
240 :returns: Absolute file path & name of the message file
241 :raises: :exc:`NotInitializedError` if the message
245 raise NotInitializedError()
246 return Message._get_filename(self._msg).decode('utf-8', 'ignore')
248 def get_filenames(self):
249 """Get all filenames for the email corresponding to 'message'
251 Returns a Filenames() generator with all absolute filepaths for
252 messages recorded to have the same Message-ID. These files must
253 not necessarily have identical content."""
255 raise NotInitializedError()
257 files_p = Message._get_filenames(self._msg)
259 return Filenames(files_p, self)
261 def get_flag(self, flag):
262 """Checks whether a specific flag is set for this message
264 The method :meth:`Query.search_threads` sets
265 *Message.FLAG.MATCH* for those messages that match the
266 query. This method allows us to get the value of this flag.
268 :param flag: One of the :attr:`Message.FLAG` values (currently only
270 :returns: An unsigned int (0/1), indicating whether the flag is set.
271 :raises: :exc:`NotInitializedError` if the message
275 raise NotInitializedError()
276 return Message._get_flag(self._msg, flag)
278 def set_flag(self, flag, value):
279 """Sets/Unsets a specific flag for this message
281 :param flag: One of the :attr:`Message.FLAG` values (currently only
283 :param value: A bool indicating whether to set or unset the flag.
285 :raises: :exc:`NotInitializedError` if the message
289 raise NotInitializedError()
290 self._set_flag(self._msg, flag, value)
293 """Returns the message tags
295 :returns: A :class:`Tags` iterator.
296 :raises: :exc:`NotInitializedError` if the message is not
298 :raises: :exc:`NullPointerError` if any error occured
301 raise NotInitializedError()
303 tags_p = Message._get_tags(self._msg)
305 raise NullPointerError()
306 return Tags(tags_p, self)
308 _add_tag = nmlib.notmuch_message_add_tag
309 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
310 _add_tag.restype = c_uint
312 def add_tag(self, tag, sync_maildir_flags=False):
313 """Adds a tag to the given message
315 Adds a tag to the current message. The maximal tag length is defined in
316 the notmuch library and is currently 200 bytes.
318 :param tag: String with a 'tag' to be added.
320 :param sync_maildir_flags: If notmuch configuration is set to do
321 this, add maildir flags corresponding to notmuch tags. See
322 underlying method :meth:`tags_to_maildir_flags`. Use False
323 if you want to add/remove many tags on a message without
324 having to physically rename the file every time. Do note,
325 that this will do nothing when a message is frozen, as tag
326 changes will not be committed to the database yet.
328 :returns: STATUS.SUCCESS if the tag was successfully added.
329 Raises an exception otherwise.
330 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
331 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
332 Message.NOTMUCH_TAG_MAX)
333 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
334 in read-only mode so message cannot be modified
335 :raises: :exc:`NotInitializedError` if message has not been
339 raise NotInitializedError()
341 status = self._add_tag(self._msg, _str(tag))
343 # bail out on failure
344 if status != STATUS.SUCCESS:
345 raise NotmuchError(status)
347 if sync_maildir_flags:
348 self.tags_to_maildir_flags()
349 return STATUS.SUCCESS
351 _remove_tag = nmlib.notmuch_message_remove_tag
352 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
353 _remove_tag.restype = c_uint
355 def remove_tag(self, tag, sync_maildir_flags=False):
356 """Removes a tag from the given message
358 If the message has no such tag, this is a non-operation and
359 will report success anyway.
361 :param tag: String with a 'tag' to be removed.
362 :param sync_maildir_flags: If notmuch configuration is set to do
363 this, add maildir flags corresponding to notmuch tags. See
364 underlying method :meth:`tags_to_maildir_flags`. Use False
365 if you want to add/remove many tags on a message without
366 having to physically rename the file every time. Do note,
367 that this will do nothing when a message is frozen, as tag
368 changes will not be committed to the database yet.
370 :returns: STATUS.SUCCESS if the tag was successfully removed or if
371 the message had no such tag.
372 Raises an exception otherwise.
373 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
374 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
375 Message.NOTMUCH_TAG_MAX)
376 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
377 in read-only mode so message cannot be modified
378 :raises: :exc:`NotInitializedError` if message has not been
382 raise NotInitializedError()
384 status = self._remove_tag(self._msg, _str(tag))
386 if status != STATUS.SUCCESS:
387 raise NotmuchError(status)
389 if sync_maildir_flags:
390 self.tags_to_maildir_flags()
391 return STATUS.SUCCESS
393 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
394 _remove_all_tags.argtypes = [NotmuchMessageP]
395 _remove_all_tags.restype = c_uint
397 def remove_all_tags(self, sync_maildir_flags=False):
398 """Removes all tags from the given message.
400 See :meth:`freeze` for an example showing how to safely
404 :param sync_maildir_flags: If notmuch configuration is set to do
405 this, add maildir flags corresponding to notmuch tags. See
406 :meth:`tags_to_maildir_flags`. Use False if you want to
407 add/remove many tags on a message without having to
408 physically rename the file every time. Do note, that this
409 will do nothing when a message is frozen, as tag changes
410 will not be committed to the database yet.
412 :returns: STATUS.SUCCESS if the tags were successfully removed.
413 Raises an exception otherwise.
414 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
415 in read-only mode so message cannot be modified
416 :raises: :exc:`NotInitializedError` if message has not been
420 raise NotInitializedError()
422 status = self._remove_all_tags(self._msg)
425 if status != STATUS.SUCCESS:
426 raise NotmuchError(status)
428 if sync_maildir_flags:
429 self.tags_to_maildir_flags()
430 return STATUS.SUCCESS
432 _freeze = nmlib.notmuch_message_freeze
433 _freeze.argtypes = [NotmuchMessageP]
434 _freeze.restype = c_uint
437 """Freezes the current state of 'message' within the database
439 This means that changes to the message state, (via :meth:`add_tag`,
440 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
441 committed to the database until the message is :meth:`thaw` ed.
443 Multiple calls to freeze/thaw are valid and these calls will
444 "stack". That is there must be as many calls to thaw as to freeze
445 before a message is actually thawed.
447 The ability to do freeze/thaw allows for safe transactions to
448 change tag values. For example, explicitly setting a message to
449 have a given set of tags might look like this::
452 msg.remove_all_tags(False)
454 msg.add_tag(tag, False)
456 msg.tags_to_maildir_flags()
458 With freeze/thaw used like this, the message in the database is
459 guaranteed to have either the full set of original tag values, or
460 the full set of new tag values, but nothing in between.
462 Imagine the example above without freeze/thaw and the operation
463 somehow getting interrupted. This could result in the message being
464 left with no tags if the interruption happened after
465 :meth:`remove_all_tags` but before :meth:`add_tag`.
467 :returns: STATUS.SUCCESS if the message was successfully frozen.
468 Raises an exception otherwise.
469 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
470 in read-only mode so message cannot be modified
471 :raises: :exc:`NotInitializedError` if message has not been
475 raise NotInitializedError()
477 status = self._freeze(self._msg)
479 if STATUS.SUCCESS == status:
483 raise NotmuchError(status)
485 _thaw = nmlib.notmuch_message_thaw
486 _thaw.argtypes = [NotmuchMessageP]
487 _thaw.restype = c_uint
490 """Thaws the current 'message'
492 Thaw the current 'message', synchronizing any changes that may have
493 occurred while 'message' was frozen into the notmuch database.
495 See :meth:`freeze` for an example of how to use this
496 function to safely provide tag changes.
498 Multiple calls to freeze/thaw are valid and these calls with
499 "stack". That is there must be as many calls to thaw as to freeze
500 before a message is actually thawed.
502 :returns: STATUS.SUCCESS if the message was successfully frozen.
503 Raises an exception otherwise.
504 :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
505 to thaw an unfrozen message. That is, there have been
506 an unbalanced number of calls to :meth:`freeze` and
508 :raises: :exc:`NotInitializedError` if message has not been
512 raise NotInitializedError()
514 status = self._thaw(self._msg)
516 if STATUS.SUCCESS == status:
520 raise NotmuchError(status)
523 """(Not implemented)"""
524 return self.get_flag(Message.FLAG.MATCH)
526 def tags_to_maildir_flags(self):
527 """Synchronize notmuch tags to file Maildir flags
529 'D' if the message has the "draft" tag
530 'F' if the message has the "flagged" tag
531 'P' if the message has the "passed" tag
532 'R' if the message has the "replied" tag
533 'S' if the message does not have the "unread" tag
535 Any existing flags unmentioned in the list above will be
536 preserved in the renaming.
538 Also, if this filename is in a directory named "new", rename it
539 to be within the neighboring directory named "cur".
541 Do note that calling this method while a message is frozen might
542 not work yet, as the modified tags have not been committed yet
545 :returns: a :class:`STATUS` value. In short, you want to see
546 notmuch.STATUS.SUCCESS here. See there for details."""
548 raise NotInitializedError()
549 return Message._tags_to_maildir_flags(self._msg)
551 def maildir_flags_to_tags(self):
552 """Synchronize file Maildir flags to notmuch tags
554 Flag Action if present
555 ---- -----------------
556 'D' Adds the "draft" tag to the message
557 'F' Adds the "flagged" tag to the message
558 'P' Adds the "passed" tag to the message
559 'R' Adds the "replied" tag to the message
560 'S' Removes the "unread" tag from the message
562 For each flag that is not present, the opposite action
563 (add/remove) is performed for the corresponding tags. If there
564 are multiple filenames associated with this message, the flag is
565 considered present if it appears in one or more filenames. (That
566 is, the flags from the multiple filenames are combined with the
567 logical OR operator.)
569 As a convenience, you can set the sync_maildir_flags parameter in
570 :meth:`Database.index_file` to implicitly call this.
572 :returns: a :class:`STATUS`. In short, you want to see
573 notmuch.STATUS.SUCCESS here. See there for details."""
575 raise NotInitializedError()
576 return Message._maildir_flags_to_tags(self._msg)
579 """Represent a Message() object by str()"""
580 return self.__str__()
582 def __unicode__(self):
583 format = "%s (%s) (%s)"
584 return format % (self.get_header('from'),
586 date.fromtimestamp(self.get_date()),
589 def get_message_parts(self):
590 """Output like notmuch show"""
591 fp = open(self.get_filename(), 'rb')
592 if sys.version_info[0] < 3:
593 email_msg = email.message_from_file(fp)
595 email_msg = email.message_from_binary_file(fp)
599 for msg in email_msg.walk():
600 if not msg.is_multipart():
604 def get_part(self, num):
605 """Returns the nth message body part"""
606 parts = self.get_message_parts()
607 if (num <= 0 or num > len(parts)):
610 out_part = parts[(num - 1)]
611 return out_part.get_payload(decode=True)
614 """Implement hash(), so we can use Message() sets"""
615 file = self.get_filename()
620 def __cmp__(self, other):
621 """Implement cmp(), so we can compare Message()s
623 2 messages are considered equal if they point to the same
624 Message-Id and if they point to the same file names. If 2
625 Messages derive from different queries where some files have
626 been added or removed, the same messages would not be considered
627 equal (as they do not point to the same set of files
629 res = cmp(self.get_message_id(), other.get_message_id())
631 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
634 _destroy = nmlib.notmuch_message_destroy
635 _destroy.argtypes = [NotmuchMessageP]
636 _destroy.restype = None
639 """Close and free the notmuch Message"""
641 self._destroy(self._msg)