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 <http://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
45 import simplejson as json
50 class Message(Python3StringMixIn):
51 """Represents a single Email message
53 Technically, this wraps the underlying *notmuch_message_t*
54 structure. A user will usually not create these objects themselves
55 but get them as search results.
57 As it implements :meth:`__cmp__`, it is possible to compare two
58 :class:`Message`\s using `if msg1 == msg2: ...`.
61 """notmuch_message_get_filename (notmuch_message_t *message)"""
62 _get_filename = nmlib.notmuch_message_get_filename
63 _get_filename.argtypes = [NotmuchMessageP]
64 _get_filename.restype = c_char_p
66 """return all filenames for a message"""
67 _get_filenames = nmlib.notmuch_message_get_filenames
68 _get_filenames.argtypes = [NotmuchMessageP]
69 _get_filenames.restype = NotmuchFilenamesP
71 """notmuch_message_get_flag"""
72 _get_flag = nmlib.notmuch_message_get_flag
73 _get_flag.argtypes = [NotmuchMessageP, c_uint]
74 _get_flag.restype = bool
76 """notmuch_message_set_flag"""
77 _set_flag = nmlib.notmuch_message_set_flag
78 _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
79 _set_flag.restype = None
81 """notmuch_message_get_message_id (notmuch_message_t *message)"""
82 _get_message_id = nmlib.notmuch_message_get_message_id
83 _get_message_id.argtypes = [NotmuchMessageP]
84 _get_message_id.restype = c_char_p
86 """notmuch_message_get_thread_id"""
87 _get_thread_id = nmlib.notmuch_message_get_thread_id
88 _get_thread_id.argtypes = [NotmuchMessageP]
89 _get_thread_id.restype = c_char_p
91 """notmuch_message_get_replies"""
92 _get_replies = nmlib.notmuch_message_get_replies
93 _get_replies.argtypes = [NotmuchMessageP]
94 _get_replies.restype = NotmuchMessagesP
96 """notmuch_message_get_tags (notmuch_message_t *message)"""
97 _get_tags = nmlib.notmuch_message_get_tags
98 _get_tags.argtypes = [NotmuchMessageP]
99 _get_tags.restype = NotmuchTagsP
101 _get_date = nmlib.notmuch_message_get_date
102 _get_date.argtypes = [NotmuchMessageP]
103 _get_date.restype = c_long
105 _get_header = nmlib.notmuch_message_get_header
106 _get_header.argtypes = [NotmuchMessageP, c_char_p]
107 _get_header.restype = c_char_p
109 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
110 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
111 _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
112 _tags_to_maildir_flags.restype = c_int
114 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
115 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
116 _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
117 _maildir_flags_to_tags.restype = c_int
119 #Constants: Flags that can be set/get with set_flag
120 FLAG = Enum(['MATCH'])
122 def __init__(self, msg_p, parent=None):
124 :param msg_p: A pointer to an internal notmuch_message_t
125 Structure. If it is `None`, we will raise an
126 :exc:`NullPointerError`.
128 :param parent: A 'parent' object is passed which this message is
129 derived from. We save a reference to it, so we can
130 automatically delete the parent object once all derived
134 raise NullPointerError()
136 #keep reference to parent, so we keep it alive
137 self._parent = parent
139 def get_message_id(self):
140 """Returns the message ID
142 :returns: String with a message ID
143 :raises: :exc:`NotInitializedError` if the message
147 raise NotInitializedError()
148 return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
150 def get_thread_id(self):
151 """Returns the thread ID
153 The returned string belongs to 'message' will only be valid for as
154 long as the message is valid.
156 This function will not return `None` since Notmuch ensures that every
157 message belongs to a single thread.
159 :returns: String with a thread ID
160 :raises: :exc:`NotInitializedError` if the message
164 raise NotInitializedError()
166 return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
168 def get_replies(self):
169 """Gets all direct replies to this message as :class:`Messages`
174 This call only makes sense if 'message' was ultimately obtained from
175 a :class:`Thread` object, (such as by coming directly from the
176 result of calling :meth:`Thread.get_toplevel_messages` or by any
177 number of subsequent calls to :meth:`get_replies`). If this message
178 was obtained through some non-thread means, (such as by a call to
179 :meth:`Query.search_messages`), then this function will return
180 an empty Messages iterator.
182 :returns: :class:`Messages`.
183 :raises: :exc:`NotInitializedError` if the message
187 raise NotInitializedError()
189 msgs_p = Message._get_replies(self._msg)
191 from .messages import Messages, EmptyMessagesResult
194 return EmptyMessagesResult(self)
196 return Messages(msgs_p, self)
199 """Returns time_t of the message date
201 For the original textual representation of the Date header from the
202 message call notmuch_message_get_header() with a header value of
205 :returns: A time_t timestamp.
207 :raises: :exc:`NotInitializedError` if the message
211 raise NotInitializedError()
212 return Message._get_date(self._msg)
214 def get_header(self, header):
215 """Get the value of the specified header.
217 The value will be read from the actual message file, not from
218 the notmuch database. The header name is case insensitive.
220 Returns an empty string ("") if the message does not contain a
221 header line matching 'header'.
223 :param header: The name of the header to be retrieved.
224 It is not case-sensitive.
226 :returns: The header value as string
227 :raises: :exc:`NotInitializedError` if the message is not
229 :raises: :exc:`NullPointerError` if any error occured
232 raise NotInitializedError()
234 #Returns NULL if any error occurs.
235 header = Message._get_header(self._msg, _str(header))
237 raise NullPointerError()
238 return header.decode('UTF-8', 'ignore')
240 def get_filename(self):
241 """Returns the file path of the message file
243 :returns: Absolute file path & name of the message file
244 :raises: :exc:`NotInitializedError` if the message
248 raise NotInitializedError()
249 return Message._get_filename(self._msg).decode('utf-8', 'ignore')
251 def get_filenames(self):
252 """Get all filenames for the email corresponding to 'message'
254 Returns a Filenames() generator with all absolute filepaths for
255 messages recorded to have the same Message-ID. These files must
256 not necessarily have identical content."""
258 raise NotInitializedError()
260 files_p = Message._get_filenames(self._msg)
262 return Filenames(files_p, self)
264 def get_flag(self, flag):
265 """Checks whether a specific flag is set for this message
267 The method :meth:`Query.search_threads` sets
268 *Message.FLAG.MATCH* for those messages that match the
269 query. This method allows us to get the value of this flag.
271 :param flag: One of the :attr:`Message.FLAG` values (currently only
273 :returns: An unsigned int (0/1), indicating whether the flag is set.
274 :raises: :exc:`NotInitializedError` if the message
278 raise NotInitializedError()
279 return Message._get_flag(self._msg, flag)
281 def set_flag(self, flag, value):
282 """Sets/Unsets a specific flag for this message
284 :param flag: One of the :attr:`Message.FLAG` values (currently only
286 :param value: A bool indicating whether to set or unset the flag.
288 :raises: :exc:`NotInitializedError` if the message
292 raise NotInitializedError()
293 self._set_flag(self._msg, flag, value)
296 """Returns the message tags
298 :returns: A :class:`Tags` iterator.
299 :raises: :exc:`NotInitializedError` if the message is not
301 :raises: :exc:`NullPointerError` if any error occured
304 raise NotInitializedError()
306 tags_p = Message._get_tags(self._msg)
308 raise NullPointerError()
309 return Tags(tags_p, self)
311 _add_tag = nmlib.notmuch_message_add_tag
312 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
313 _add_tag.restype = c_uint
315 def add_tag(self, tag, sync_maildir_flags=False):
316 """Adds a tag to the given message
318 Adds a tag to the current message. The maximal tag length is defined in
319 the notmuch library and is currently 200 bytes.
321 :param tag: String with a 'tag' to be added.
323 :param sync_maildir_flags: If notmuch configuration is set to do
324 this, add maildir flags corresponding to notmuch tags. See
325 underlying method :meth:`tags_to_maildir_flags`. Use False
326 if you want to add/remove many tags on a message without
327 having to physically rename the file every time. Do note,
328 that this will do nothing when a message is frozen, as tag
329 changes will not be committed to the database yet.
331 :returns: STATUS.SUCCESS if the tag was successfully added.
332 Raises an exception otherwise.
333 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
334 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
335 Message.NOTMUCH_TAG_MAX)
336 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
337 in read-only mode so message cannot be modified
338 :raises: :exc:`NotInitializedError` if message has not been
342 raise NotInitializedError()
344 status = self._add_tag(self._msg, _str(tag))
346 # bail out on failure
347 if status != STATUS.SUCCESS:
348 raise NotmuchError(status)
350 if sync_maildir_flags:
351 self.tags_to_maildir_flags()
352 return STATUS.SUCCESS
354 _remove_tag = nmlib.notmuch_message_remove_tag
355 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
356 _remove_tag.restype = c_uint
358 def remove_tag(self, tag, sync_maildir_flags=False):
359 """Removes a tag from the given message
361 If the message has no such tag, this is a non-operation and
362 will report success anyway.
364 :param tag: String with a 'tag' to be removed.
365 :param sync_maildir_flags: If notmuch configuration is set to do
366 this, add maildir flags corresponding to notmuch tags. See
367 underlying method :meth:`tags_to_maildir_flags`. Use False
368 if you want to add/remove many tags on a message without
369 having to physically rename the file every time. Do note,
370 that this will do nothing when a message is frozen, as tag
371 changes will not be committed to the database yet.
373 :returns: STATUS.SUCCESS if the tag was successfully removed or if
374 the message had no such tag.
375 Raises an exception otherwise.
376 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
377 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
378 Message.NOTMUCH_TAG_MAX)
379 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
380 in read-only mode so message cannot be modified
381 :raises: :exc:`NotInitializedError` if message has not been
385 raise NotInitializedError()
387 status = self._remove_tag(self._msg, _str(tag))
389 if status != STATUS.SUCCESS:
390 raise NotmuchError(status)
392 if sync_maildir_flags:
393 self.tags_to_maildir_flags()
394 return STATUS.SUCCESS
396 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
397 _remove_all_tags.argtypes = [NotmuchMessageP]
398 _remove_all_tags.restype = c_uint
400 def remove_all_tags(self, sync_maildir_flags=False):
401 """Removes all tags from the given message.
403 See :meth:`freeze` for an example showing how to safely
407 :param sync_maildir_flags: If notmuch configuration is set to do
408 this, add maildir flags corresponding to notmuch tags. See
409 :meth:`tags_to_maildir_flags`. Use False if you want to
410 add/remove many tags on a message without having to
411 physically rename the file every time. Do note, that this
412 will do nothing when a message is frozen, as tag changes
413 will not be committed to the database yet.
415 :returns: STATUS.SUCCESS if the tags were successfully removed.
416 Raises an exception otherwise.
417 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
418 in read-only mode so message cannot be modified
419 :raises: :exc:`NotInitializedError` if message has not been
423 raise NotInitializedError()
425 status = self._remove_all_tags(self._msg)
428 if status != STATUS.SUCCESS:
429 raise NotmuchError(status)
431 if sync_maildir_flags:
432 self.tags_to_maildir_flags()
433 return STATUS.SUCCESS
435 _freeze = nmlib.notmuch_message_freeze
436 _freeze.argtypes = [NotmuchMessageP]
437 _freeze.restype = c_uint
440 """Freezes the current state of 'message' within the database
442 This means that changes to the message state, (via :meth:`add_tag`,
443 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
444 committed to the database until the message is :meth:`thaw` ed.
446 Multiple calls to freeze/thaw are valid and these calls will
447 "stack". That is there must be as many calls to thaw as to freeze
448 before a message is actually thawed.
450 The ability to do freeze/thaw allows for safe transactions to
451 change tag values. For example, explicitly setting a message to
452 have a given set of tags might look like this::
455 msg.remove_all_tags(False)
457 msg.add_tag(tag, False)
459 msg.tags_to_maildir_flags()
461 With freeze/thaw used like this, the message in the database is
462 guaranteed to have either the full set of original tag values, or
463 the full set of new tag values, but nothing in between.
465 Imagine the example above without freeze/thaw and the operation
466 somehow getting interrupted. This could result in the message being
467 left with no tags if the interruption happened after
468 :meth:`remove_all_tags` but before :meth:`add_tag`.
470 :returns: STATUS.SUCCESS if the message was successfully frozen.
471 Raises an exception otherwise.
472 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
473 in read-only mode so message cannot be modified
474 :raises: :exc:`NotInitializedError` if message has not been
478 raise NotInitializedError()
480 status = self._freeze(self._msg)
482 if STATUS.SUCCESS == status:
486 raise NotmuchError(status)
488 _thaw = nmlib.notmuch_message_thaw
489 _thaw.argtypes = [NotmuchMessageP]
490 _thaw.restype = c_uint
493 """Thaws the current 'message'
495 Thaw the current 'message', synchronizing any changes that may have
496 occurred while 'message' was frozen into the notmuch database.
498 See :meth:`freeze` for an example of how to use this
499 function to safely provide tag changes.
501 Multiple calls to freeze/thaw are valid and these calls with
502 "stack". That is there must be as many calls to thaw as to freeze
503 before a message is actually thawed.
505 :returns: STATUS.SUCCESS if the message was successfully frozen.
506 Raises an exception otherwise.
507 :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
508 to thaw an unfrozen message. That is, there have been
509 an unbalanced number of calls to :meth:`freeze` and
511 :raises: :exc:`NotInitializedError` if message has not been
515 raise NotInitializedError()
517 status = self._thaw(self._msg)
519 if STATUS.SUCCESS == status:
523 raise NotmuchError(status)
526 """(Not implemented)"""
527 return self.get_flag(Message.FLAG.MATCH)
529 def tags_to_maildir_flags(self):
530 """Synchronize notmuch tags to file Maildir flags
532 'D' if the message has the "draft" tag
533 'F' if the message has the "flagged" tag
534 'P' if the message has the "passed" tag
535 'R' if the message has the "replied" tag
536 'S' if the message does not have the "unread" tag
538 Any existing flags unmentioned in the list above will be
539 preserved in the renaming.
541 Also, if this filename is in a directory named "new", rename it
542 to be within the neighboring directory named "cur".
544 Do note that calling this method while a message is frozen might
545 not work yet, as the modified tags have not been committed yet
548 :returns: a :class:`STATUS` value. In short, you want to see
549 notmuch.STATUS.SUCCESS here. See there for details."""
551 raise NotInitializedError()
552 return Message._tags_to_maildir_flags(self._msg)
554 def maildir_flags_to_tags(self):
555 """Synchronize file Maildir flags to notmuch tags
557 Flag Action if present
558 ---- -----------------
559 'D' Adds the "draft" tag to the message
560 'F' Adds the "flagged" tag to the message
561 'P' Adds the "passed" tag to the message
562 'R' Adds the "replied" tag to the message
563 'S' Removes the "unread" tag from the message
565 For each flag that is not present, the opposite action
566 (add/remove) is performed for the corresponding tags. If there
567 are multiple filenames associated with this message, the flag is
568 considered present if it appears in one or more filenames. (That
569 is, the flags from the multiple filenames are combined with the
570 logical OR operator.)
572 As a convenience, you can set the sync_maildir_flags parameter in
573 :meth:`Database.add_message` to implicitly call this.
575 :returns: a :class:`STATUS`. In short, you want to see
576 notmuch.STATUS.SUCCESS here. See there for details."""
578 raise NotInitializedError()
579 return Message._tags_to_maildir_flags(self._msg)
582 """Represent a Message() object by str()"""
583 return self.__str__()
585 def __unicode__(self):
586 format = "%s (%s) (%s)"
587 return format % (self.get_header('from'),
589 date.fromtimestamp(self.get_date()),
592 def get_message_parts(self):
593 """Output like notmuch show"""
594 fp = open(self.get_filename())
595 email_msg = email.message_from_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)