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
46 class Message(Python3StringMixIn):
47 """Represents a single Email message
49 Technically, this wraps the underlying *notmuch_message_t*
50 structure. A user will usually not create these objects themselves
51 but get them as search results.
53 As it implements :meth:`__cmp__`, it is possible to compare two
54 :class:`Message`\s using `if msg1 == msg2: ...`.
57 """notmuch_message_get_filename (notmuch_message_t *message)"""
58 _get_filename = nmlib.notmuch_message_get_filename
59 _get_filename.argtypes = [NotmuchMessageP]
60 _get_filename.restype = c_char_p
62 """return all filenames for a message"""
63 _get_filenames = nmlib.notmuch_message_get_filenames
64 _get_filenames.argtypes = [NotmuchMessageP]
65 _get_filenames.restype = NotmuchFilenamesP
67 """notmuch_message_get_flag"""
68 _get_flag = nmlib.notmuch_message_get_flag
69 _get_flag.argtypes = [NotmuchMessageP, c_uint]
70 _get_flag.restype = bool
72 """notmuch_message_set_flag"""
73 _set_flag = nmlib.notmuch_message_set_flag
74 _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
75 _set_flag.restype = None
77 """notmuch_message_get_message_id (notmuch_message_t *message)"""
78 _get_message_id = nmlib.notmuch_message_get_message_id
79 _get_message_id.argtypes = [NotmuchMessageP]
80 _get_message_id.restype = c_char_p
82 """notmuch_message_get_thread_id"""
83 _get_thread_id = nmlib.notmuch_message_get_thread_id
84 _get_thread_id.argtypes = [NotmuchMessageP]
85 _get_thread_id.restype = c_char_p
87 """notmuch_message_get_replies"""
88 _get_replies = nmlib.notmuch_message_get_replies
89 _get_replies.argtypes = [NotmuchMessageP]
90 _get_replies.restype = NotmuchMessagesP
92 """notmuch_message_get_tags (notmuch_message_t *message)"""
93 _get_tags = nmlib.notmuch_message_get_tags
94 _get_tags.argtypes = [NotmuchMessageP]
95 _get_tags.restype = NotmuchTagsP
97 _get_date = nmlib.notmuch_message_get_date
98 _get_date.argtypes = [NotmuchMessageP]
99 _get_date.restype = c_long
101 _get_header = nmlib.notmuch_message_get_header
102 _get_header.argtypes = [NotmuchMessageP, c_char_p]
103 _get_header.restype = c_char_p
105 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
106 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
107 _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
108 _tags_to_maildir_flags.restype = c_int
110 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
111 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
112 _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
113 _maildir_flags_to_tags.restype = c_int
115 #Constants: Flags that can be set/get with set_flag
116 FLAG = Enum(['MATCH'])
118 def __init__(self, msg_p, parent=None):
120 :param msg_p: A pointer to an internal notmuch_message_t
121 Structure. If it is `None`, we will raise an
122 :exc:`NullPointerError`.
124 :param parent: A 'parent' object is passed which this message is
125 derived from. We save a reference to it, so we can
126 automatically delete the parent object once all derived
130 raise NullPointerError()
132 #keep reference to parent, so we keep it alive
133 self._parent = parent
135 def get_message_id(self):
136 """Returns the message ID
138 :returns: String with a message ID
139 :raises: :exc:`NotInitializedError` if the message
143 raise NotInitializedError()
144 return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
146 def get_thread_id(self):
147 """Returns the thread ID
149 The returned string belongs to 'message' will only be valid for as
150 long as the message is valid.
152 This function will not return `None` since Notmuch ensures that every
153 message belongs to a single thread.
155 :returns: String with a thread ID
156 :raises: :exc:`NotInitializedError` if the message
160 raise NotInitializedError()
162 return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
164 def get_replies(self):
165 """Gets all direct replies to this message as :class:`Messages`
170 This call only makes sense if 'message' was ultimately obtained from
171 a :class:`Thread` object, (such as by coming directly from the
172 result of calling :meth:`Thread.get_toplevel_messages` or by any
173 number of subsequent calls to :meth:`get_replies`). If this message
174 was obtained through some non-thread means, (such as by a call to
175 :meth:`Query.search_messages`), then this function will return
176 an empty Messages iterator.
178 :returns: :class:`Messages`.
179 :raises: :exc:`NotInitializedError` if the message
183 raise NotInitializedError()
185 msgs_p = Message._get_replies(self._msg)
187 from .messages import Messages, EmptyMessagesResult
190 return EmptyMessagesResult(self)
192 return Messages(msgs_p, self)
195 """Returns time_t of the message date
197 For the original textual representation of the Date header from the
198 message call notmuch_message_get_header() with a header value of
201 :returns: A time_t timestamp.
203 :raises: :exc:`NotInitializedError` if the message
207 raise NotInitializedError()
208 return Message._get_date(self._msg)
210 def get_header(self, header):
211 """Get the value of the specified header.
213 The value will be read from the actual message file, not from
214 the notmuch database. The header name is case insensitive.
216 Returns an empty string ("") if the message does not contain a
217 header line matching 'header'.
219 :param header: The name of the header to be retrieved.
220 It is not case-sensitive.
222 :returns: The header value as string
223 :raises: :exc:`NotInitializedError` if the message is not
225 :raises: :exc:`NullPointerError` if any error occured
228 raise NotInitializedError()
230 #Returns NULL if any error occurs.
231 header = Message._get_header(self._msg, _str(header))
233 raise NullPointerError()
234 return header.decode('UTF-8', 'ignore')
236 def get_filename(self):
237 """Returns the file path of the message file
239 :returns: Absolute file path & name of the message file
240 :raises: :exc:`NotInitializedError` if the message
244 raise NotInitializedError()
245 return Message._get_filename(self._msg).decode('utf-8', 'ignore')
247 def get_filenames(self):
248 """Get all filenames for the email corresponding to 'message'
250 Returns a Filenames() generator with all absolute filepaths for
251 messages recorded to have the same Message-ID. These files must
252 not necessarily have identical content."""
254 raise NotInitializedError()
256 files_p = Message._get_filenames(self._msg)
258 return Filenames(files_p, self)
260 def get_flag(self, flag):
261 """Checks whether a specific flag is set for this message
263 The method :meth:`Query.search_threads` sets
264 *Message.FLAG.MATCH* for those messages that match the
265 query. This method allows us to get the value of this flag.
267 :param flag: One of the :attr:`Message.FLAG` values (currently only
269 :returns: An unsigned int (0/1), indicating whether the flag is set.
270 :raises: :exc:`NotInitializedError` if the message
274 raise NotInitializedError()
275 return Message._get_flag(self._msg, flag)
277 def set_flag(self, flag, value):
278 """Sets/Unsets a specific flag for this message
280 :param flag: One of the :attr:`Message.FLAG` values (currently only
282 :param value: A bool indicating whether to set or unset the flag.
284 :raises: :exc:`NotInitializedError` if the message
288 raise NotInitializedError()
289 self._set_flag(self._msg, flag, value)
292 """Returns the message tags
294 :returns: A :class:`Tags` iterator.
295 :raises: :exc:`NotInitializedError` if the message is not
297 :raises: :exc:`NullPointerError` if any error occured
300 raise NotInitializedError()
302 tags_p = Message._get_tags(self._msg)
304 raise NullPointerError()
305 return Tags(tags_p, self)
307 _add_tag = nmlib.notmuch_message_add_tag
308 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
309 _add_tag.restype = c_uint
311 def add_tag(self, tag, sync_maildir_flags=False):
312 """Adds a tag to the given message
314 Adds a tag to the current message. The maximal tag length is defined in
315 the notmuch library and is currently 200 bytes.
317 :param tag: String with a 'tag' to be added.
319 :param sync_maildir_flags: If notmuch configuration is set to do
320 this, add maildir flags corresponding to notmuch tags. See
321 underlying method :meth:`tags_to_maildir_flags`. Use False
322 if you want to add/remove many tags on a message without
323 having to physically rename the file every time. Do note,
324 that this will do nothing when a message is frozen, as tag
325 changes will not be committed to the database yet.
327 :returns: STATUS.SUCCESS if the tag was successfully added.
328 Raises an exception otherwise.
329 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
330 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
331 Message.NOTMUCH_TAG_MAX)
332 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
333 in read-only mode so message cannot be modified
334 :raises: :exc:`NotInitializedError` if message has not been
338 raise NotInitializedError()
340 status = self._add_tag(self._msg, _str(tag))
342 # bail out on failure
343 if status != STATUS.SUCCESS:
344 raise NotmuchError(status)
346 if sync_maildir_flags:
347 self.tags_to_maildir_flags()
348 return STATUS.SUCCESS
350 _remove_tag = nmlib.notmuch_message_remove_tag
351 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
352 _remove_tag.restype = c_uint
354 def remove_tag(self, tag, sync_maildir_flags=False):
355 """Removes a tag from the given message
357 If the message has no such tag, this is a non-operation and
358 will report success anyway.
360 :param tag: String with a 'tag' to be removed.
361 :param sync_maildir_flags: If notmuch configuration is set to do
362 this, add maildir flags corresponding to notmuch tags. See
363 underlying method :meth:`tags_to_maildir_flags`. Use False
364 if you want to add/remove many tags on a message without
365 having to physically rename the file every time. Do note,
366 that this will do nothing when a message is frozen, as tag
367 changes will not be committed to the database yet.
369 :returns: STATUS.SUCCESS if the tag was successfully removed or if
370 the message had no such tag.
371 Raises an exception otherwise.
372 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
373 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
374 Message.NOTMUCH_TAG_MAX)
375 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
376 in read-only mode so message cannot be modified
377 :raises: :exc:`NotInitializedError` if message has not been
381 raise NotInitializedError()
383 status = self._remove_tag(self._msg, _str(tag))
385 if status != STATUS.SUCCESS:
386 raise NotmuchError(status)
388 if sync_maildir_flags:
389 self.tags_to_maildir_flags()
390 return STATUS.SUCCESS
392 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
393 _remove_all_tags.argtypes = [NotmuchMessageP]
394 _remove_all_tags.restype = c_uint
396 def remove_all_tags(self, sync_maildir_flags=False):
397 """Removes all tags from the given message.
399 See :meth:`freeze` for an example showing how to safely
403 :param sync_maildir_flags: If notmuch configuration is set to do
404 this, add maildir flags corresponding to notmuch tags. See
405 :meth:`tags_to_maildir_flags`. Use False if you want to
406 add/remove many tags on a message without having to
407 physically rename the file every time. Do note, that this
408 will do nothing when a message is frozen, as tag changes
409 will not be committed to the database yet.
411 :returns: STATUS.SUCCESS if the tags were successfully removed.
412 Raises an exception otherwise.
413 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
414 in read-only mode so message cannot be modified
415 :raises: :exc:`NotInitializedError` if message has not been
419 raise NotInitializedError()
421 status = self._remove_all_tags(self._msg)
424 if status != STATUS.SUCCESS:
425 raise NotmuchError(status)
427 if sync_maildir_flags:
428 self.tags_to_maildir_flags()
429 return STATUS.SUCCESS
431 _freeze = nmlib.notmuch_message_freeze
432 _freeze.argtypes = [NotmuchMessageP]
433 _freeze.restype = c_uint
436 """Freezes the current state of 'message' within the database
438 This means that changes to the message state, (via :meth:`add_tag`,
439 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
440 committed to the database until the message is :meth:`thaw` ed.
442 Multiple calls to freeze/thaw are valid and these calls will
443 "stack". That is there must be as many calls to thaw as to freeze
444 before a message is actually thawed.
446 The ability to do freeze/thaw allows for safe transactions to
447 change tag values. For example, explicitly setting a message to
448 have a given set of tags might look like this::
451 msg.remove_all_tags(False)
453 msg.add_tag(tag, False)
455 msg.tags_to_maildir_flags()
457 With freeze/thaw used like this, the message in the database is
458 guaranteed to have either the full set of original tag values, or
459 the full set of new tag values, but nothing in between.
461 Imagine the example above without freeze/thaw and the operation
462 somehow getting interrupted. This could result in the message being
463 left with no tags if the interruption happened after
464 :meth:`remove_all_tags` but before :meth:`add_tag`.
466 :returns: STATUS.SUCCESS if the message was successfully frozen.
467 Raises an exception otherwise.
468 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
469 in read-only mode so message cannot be modified
470 :raises: :exc:`NotInitializedError` if message has not been
474 raise NotInitializedError()
476 status = self._freeze(self._msg)
478 if STATUS.SUCCESS == status:
482 raise NotmuchError(status)
484 _thaw = nmlib.notmuch_message_thaw
485 _thaw.argtypes = [NotmuchMessageP]
486 _thaw.restype = c_uint
489 """Thaws the current 'message'
491 Thaw the current 'message', synchronizing any changes that may have
492 occurred while 'message' was frozen into the notmuch database.
494 See :meth:`freeze` for an example of how to use this
495 function to safely provide tag changes.
497 Multiple calls to freeze/thaw are valid and these calls with
498 "stack". That is there must be as many calls to thaw as to freeze
499 before a message is actually thawed.
501 :returns: STATUS.SUCCESS if the message was successfully frozen.
502 Raises an exception otherwise.
503 :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
504 to thaw an unfrozen message. That is, there have been
505 an unbalanced number of calls to :meth:`freeze` and
507 :raises: :exc:`NotInitializedError` if message has not been
511 raise NotInitializedError()
513 status = self._thaw(self._msg)
515 if STATUS.SUCCESS == status:
519 raise NotmuchError(status)
522 """(Not implemented)"""
523 return self.get_flag(Message.FLAG.MATCH)
525 def tags_to_maildir_flags(self):
526 """Synchronize notmuch tags to file Maildir flags
528 'D' if the message has the "draft" tag
529 'F' if the message has the "flagged" tag
530 'P' if the message has the "passed" tag
531 'R' if the message has the "replied" tag
532 'S' if the message does not have the "unread" tag
534 Any existing flags unmentioned in the list above will be
535 preserved in the renaming.
537 Also, if this filename is in a directory named "new", rename it
538 to be within the neighboring directory named "cur".
540 Do note that calling this method while a message is frozen might
541 not work yet, as the modified tags have not been committed yet
544 :returns: a :class:`STATUS` value. In short, you want to see
545 notmuch.STATUS.SUCCESS here. See there for details."""
547 raise NotInitializedError()
548 return Message._tags_to_maildir_flags(self._msg)
550 def maildir_flags_to_tags(self):
551 """Synchronize file Maildir flags to notmuch tags
553 Flag Action if present
554 ---- -----------------
555 'D' Adds the "draft" tag to the message
556 'F' Adds the "flagged" tag to the message
557 'P' Adds the "passed" tag to the message
558 'R' Adds the "replied" tag to the message
559 'S' Removes the "unread" tag from the message
561 For each flag that is not present, the opposite action
562 (add/remove) is performed for the corresponding tags. If there
563 are multiple filenames associated with this message, the flag is
564 considered present if it appears in one or more filenames. (That
565 is, the flags from the multiple filenames are combined with the
566 logical OR operator.)
568 As a convenience, you can set the sync_maildir_flags parameter in
569 :meth:`Database.add_message` to implicitly call this.
571 :returns: a :class:`STATUS`. In short, you want to see
572 notmuch.STATUS.SUCCESS here. See there for details."""
574 raise NotInitializedError()
575 return Message._tags_to_maildir_flags(self._msg)
578 """Represent a Message() object by str()"""
579 return self.__str__()
581 def __unicode__(self):
582 format = "%s (%s) (%s)"
583 return format % (self.get_header('from'),
585 date.fromtimestamp(self.get_date()),
588 def get_message_parts(self):
589 """Output like notmuch show"""
590 fp = open(self.get_filename())
591 email_msg = email.message_from_file(fp)
595 for msg in email_msg.walk():
596 if not msg.is_multipart():
600 def get_part(self, num):
601 """Returns the nth message body part"""
602 parts = self.get_message_parts()
603 if (num <= 0 or num > len(parts)):
606 out_part = parts[(num - 1)]
607 return out_part.get_payload(decode=True)
610 """Implement hash(), so we can use Message() sets"""
611 file = self.get_filename()
616 def __cmp__(self, other):
617 """Implement cmp(), so we can compare Message()s
619 2 messages are considered equal if they point to the same
620 Message-Id and if they point to the same file names. If 2
621 Messages derive from different queries where some files have
622 been added or removed, the same messages would not be considered
623 equal (as they do not point to the same set of files
625 res = cmp(self.get_message_id(), other.get_message_id())
627 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
630 _destroy = nmlib.notmuch_message_destroy
631 _destroy.argtypes = [NotmuchMessageP]
632 _destroy.restype = None
635 """Close and free the notmuch Message"""
637 self._destroy(self._msg)