]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/message.py
Simplify (& fix) Message().__str__()
[notmuch] / bindings / python / notmuch / message.py
1 """
2 This file is part of notmuch.
3
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.
8
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
12 for more details.
13
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/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
18                Jesse Rosenthal <jrosenthal@jhu.edu>
19 """
20
21         
22 from ctypes import c_char_p, c_void_p, c_long, c_uint, c_int
23 from datetime import date
24 from notmuch.globals import nmlib, STATUS, NotmuchError, Enum
25 from notmuch.tag import Tags
26 from notmuch.filename import Filenames
27 import sys
28 import email
29 import types
30 try:
31     import simplejson as json
32 except ImportError:
33     import json
34 #------------------------------------------------------------------------------
35 class Messages(object):
36     """Represents a list of notmuch messages
37
38     This object provides an iterator over a list of notmuch messages
39     (Technically, it provides a wrapper for the underlying
40     *notmuch_messages_t* structure). Do note that the underlying library
41     only provides a one-time iterator (it cannot reset the iterator to
42     the start). Thus iterating over the function will "exhaust" the list
43     of messages, and a subsequent iteration attempt will raise a
44     :exc:`NotmuchError` STATUS.NOT_INITIALIZED. If you need to
45     re-iterate over a list of messages you will need to retrieve a new
46     :class:`Messages` object or cache your :class:`Message`\s in a list
47     via::
48
49        msglist = list(msgs)
50
51     You can store and reuse the single :class:`Message` objects as often
52     as you want as long as you keep the parent :class:`Messages` object
53     around. (Due to hierarchical memory allocation, all derived
54     :class:`Message` objects will be invalid when we delete the parent
55     :class:`Messages` object, even if it was already exhausted.) So
56     this works::
57
58       db   = Database()
59       msgs = Query(db,'').search_messages() #get a Messages() object
60       msglist = list(msgs)
61
62       # msgs is "exhausted" now and msgs.next() will raise an exception.
63       # However it will be kept alive until all retrieved Message()
64       # objects are also deleted. If you do e.g. an explicit del(msgs)
65       # here, the following lines would fail.
66       
67       # You can reiterate over *msglist* however as often as you want. 
68       # It is simply a list with :class:`Message`s.
69
70       print (msglist[0].get_filename())
71       print (msglist[1].get_filename())
72       print (msglist[0].get_message_id())
73
74
75     As :class:`Message` implements both __hash__() and __cmp__(), it is
76     possible to make sets out of :class:`Messages` and use set
77     arithmetic (this happens in python and will of course be *much*
78     slower than redoing a proper query with the appropriate filters::
79
80         s1, s2 = set(msgs1), set(msgs2)
81         s.union(s2)
82         s1 -= s2
83         ...
84
85     Be careful when using set arithmetic between message sets derived
86     from different Databases (ie the same database reopened after
87     messages have changed). If messages have added or removed associated
88     files in the meantime, it is possible that the same message would be
89     considered as a different object (as it points to a different file).
90     """
91
92     #notmuch_messages_get
93     _get = nmlib.notmuch_messages_get
94     _get.restype = c_void_p
95
96     _collect_tags = nmlib.notmuch_messages_collect_tags
97     _collect_tags.restype = c_void_p
98
99     def __init__(self, msgs_p, parent=None):
100         """
101         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
102              structure. These are not publically exposed, so a user
103              will almost never instantiate a :class:`Messages` object
104              herself. They are usually handed back as a result,
105              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
106              valid, we will raise an :exc:`NotmuchError`
107              (STATUS.NULL_POINTER) if it is `None`.
108         :type msgs_p: :class:`ctypes.c_void_p`
109         :param parent: The parent object
110              (ie :class:`Query`) these tags are derived from. It saves
111              a reference to it, so we can automatically delete the db
112              object once all derived objects are dead.
113         :TODO: Make the iterator work more than once and cache the tags in 
114                the Python object.(?)
115         """
116         if msgs_p is None:
117             NotmuchError(STATUS.NULL_POINTER)
118
119         self._msgs = msgs_p
120         #store parent, so we keep them alive as long as self  is alive
121         self._parent = parent
122
123     def collect_tags(self):
124         """Return the unique :class:`Tags` in the contained messages
125
126         :returns: :class:`Tags`
127         :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
128
129         .. note:: :meth:`collect_tags` will iterate over the messages and
130           therefore will not allow further iterations.
131         """
132         if self._msgs is None:
133             raise NotmuchError(STATUS.NOT_INITIALIZED)
134
135         # collect all tags (returns NULL on error)
136         tags_p = Messages._collect_tags (self._msgs)
137         #reset _msgs as we iterated over it and can do so only once
138         self._msgs = None
139
140         if tags_p == None:
141             raise NotmuchError(STATUS.NULL_POINTER)
142         return Tags(tags_p, self)
143
144     def __iter__(self):
145         """ Make Messages an iterator """
146         return self
147
148     def next(self):
149         if self._msgs is None:
150             raise NotmuchError(STATUS.NOT_INITIALIZED)
151
152         if not nmlib.notmuch_messages_valid(self._msgs):
153             self._msgs = None
154             raise StopIteration
155
156         msg = Message(Messages._get (self._msgs), self)
157         nmlib.notmuch_messages_move_to_next(self._msgs)
158         return msg
159
160     def __nonzero__(self):
161         """
162         :return: True if there is at least one more thread in the
163             Iterator, False if not."""
164         return self._msgs is not None and \
165             nmlib.notmuch_messages_valid(self._msgs) > 0
166
167     def __del__(self):
168         """Close and free the notmuch Messages"""
169         if self._msgs is not None:
170             nmlib.notmuch_messages_destroy (self._msgs)
171
172     def print_messages(self, format, indent=0, entire_thread=False):
173         """Outputs messages as needed for 'notmuch show' to sys.stdout
174
175         :param format: A string of either 'text' or 'json'.
176         :param indent: A number indicating the reply depth of these messages.
177         :param entire_thread: A bool, indicating whether we want to output 
178                        whole threads or only the matching messages.
179         """
180         if format.lower() == "text":
181             set_start = ""
182             set_end = ""
183             set_sep = ""
184         elif format.lower() == "json":
185             set_start = "["
186             set_end = "]"
187             set_sep = ", "
188         else:
189             raise Exception
190
191         first_set = True
192
193         sys.stdout.write(set_start)
194
195         # iterate through all toplevel messages in this thread
196         for msg in self:
197             # if not msg:
198             #     break 
199             if not first_set:
200                 sys.stdout.write(set_sep)
201             first_set = False
202
203             sys.stdout.write(set_start)
204             match = msg.is_match()
205             next_indent = indent
206
207             if (match or entire_thread):
208                 if format.lower() == "text":
209                     sys.stdout.write(msg.format_message_as_text(indent))
210                 elif format.lower() == "json":
211                     sys.stdout.write(msg.format_message_as_json(indent))
212                 else:
213                     raise NotmuchError
214                 next_indent = indent + 1
215
216             # get replies and print them also out (if there are any)
217             replies = msg.get_replies()
218             if not replies is None:
219                 sys.stdout.write(set_sep)
220                 replies.print_messages(format, next_indent, entire_thread)
221
222             sys.stdout.write(set_end)
223         sys.stdout.write(set_end)
224
225 #------------------------------------------------------------------------------
226 class Message(object):
227     """Represents a single Email message
228
229     Technically, this wraps the underlying *notmuch_message_t*
230     structure. A user will usually not create these objects themselves
231     but get them as search results.
232
233     As it implements :meth:`__cmp__`, it is possible to compare two
234     :class:`Message`\s using `if msg1 == msg2: ...`.
235     """
236
237     """notmuch_message_get_filename (notmuch_message_t *message)"""
238     _get_filename = nmlib.notmuch_message_get_filename
239     _get_filename.restype = c_char_p 
240
241     """return all filenames for a message"""
242     _get_filenames = nmlib.notmuch_message_get_filenames
243     _get_filenames.restype = c_void_p
244
245     """notmuch_message_get_flag"""
246     _get_flag = nmlib.notmuch_message_get_flag
247     _get_flag.restype = c_uint
248
249     """notmuch_message_get_message_id (notmuch_message_t *message)"""
250     _get_message_id = nmlib.notmuch_message_get_message_id
251     _get_message_id.restype = c_char_p 
252
253     """notmuch_message_get_thread_id"""
254     _get_thread_id = nmlib.notmuch_message_get_thread_id
255     _get_thread_id.restype = c_char_p
256
257     """notmuch_message_get_replies"""
258     _get_replies = nmlib.notmuch_message_get_replies
259     _get_replies.restype = c_void_p
260
261     """notmuch_message_get_tags (notmuch_message_t *message)"""
262     _get_tags = nmlib.notmuch_message_get_tags
263     _get_tags.restype = c_void_p
264
265     _get_date = nmlib.notmuch_message_get_date
266     _get_date.restype = c_long
267
268     _get_header = nmlib.notmuch_message_get_header
269     _get_header.restype = c_char_p
270
271     """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
272     _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
273     _tags_to_maildir_flags.restype = c_int
274
275     """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
276     _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
277     _maildir_flags_to_tags.restype = c_int
278
279     #Constants: Flags that can be set/get with set_flag
280     FLAG = Enum(['MATCH'])
281
282     def __init__(self, msg_p, parent=None):
283         """
284         :param msg_p: A pointer to an internal notmuch_message_t
285             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
286             STATUS.NULL_POINTER.
287
288         :param parent: A 'parent' object is passed which this message is
289               derived from. We save a reference to it, so we can
290               automatically delete the parent object once all derived
291               objects are dead.
292         """
293         if msg_p is None:
294             NotmuchError(STATUS.NULL_POINTER)
295         self._msg = msg_p
296         #keep reference to parent, so we keep it alive
297         self._parent = parent
298
299
300     def get_message_id(self):
301         """Returns the message ID
302         
303         :returns: String with a message ID
304         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
305                     is not initialized.
306         """
307         if self._msg is None:
308             raise NotmuchError(STATUS.NOT_INITIALIZED)
309         return Message._get_message_id(self._msg)
310
311     def get_thread_id(self):
312         """Returns the thread ID
313
314         The returned string belongs to 'message' will only be valid for as 
315         long as the message is valid.
316
317         This function will not return `None` since Notmuch ensures that every
318         message belongs to a single thread.
319
320         :returns: String with a thread ID
321         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
322                     is not initialized.
323         """
324         if self._msg is None:
325             raise NotmuchError(STATUS.NOT_INITIALIZED)
326
327         return Message._get_thread_id (self._msg);
328
329     def get_replies(self):
330         """Gets all direct replies to this message as :class:`Messages` iterator
331
332         .. note:: This call only makes sense if 'message' was
333           ultimately obtained from a :class:`Thread` object, (such as
334           by coming directly from the result of calling
335           :meth:`Thread.get_toplevel_messages` or by any number of
336           subsequent calls to :meth:`get_replies`). If this message was
337           obtained through some non-thread means, (such as by a call
338           to :meth:`Query.search_messages`), then this function will
339           return `None`.
340
341         :returns: :class:`Messages` or `None` if there are no replies to 
342             this message.
343         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
344                     is not initialized.
345         """
346         if self._msg is None:
347             raise NotmuchError(STATUS.NOT_INITIALIZED)
348
349         msgs_p = Message._get_replies(self._msg);
350
351         if msgs_p is None:
352             return None
353
354         return Messages(msgs_p,self)
355
356     def get_date(self):
357         """Returns time_t of the message date
358
359         For the original textual representation of the Date header from the
360         message call notmuch_message_get_header() with a header value of
361         "date".
362
363         :returns: A time_t timestamp.
364         :rtype: c_unit64
365         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
366                     is not initialized.
367         """
368         if self._msg is None:
369             raise NotmuchError(STATUS.NOT_INITIALIZED)
370         return Message._get_date(self._msg)
371
372     def get_header(self, header):
373         """Returns a message header
374         
375         This returns any message header that is stored in the notmuch database.
376         This is only a selected subset of headers, which is currently:
377
378           TODO: add stored headers
379
380         :param header: The name of the header to be retrieved.
381                        It is not case-sensitive (TODO: confirm).
382         :type header: str
383         :returns: The header value as string
384         :exception: :exc:`NotmuchError`
385
386                     * STATUS.NOT_INITIALIZED if the message 
387                       is not initialized.
388                     * STATUS.NULL_POINTER, if no header was found
389         """
390         if self._msg is None:
391             raise NotmuchError(STATUS.NOT_INITIALIZED)
392
393         #Returns NULL if any error occurs.
394         header = Message._get_header (self._msg, header)
395         if header == None:
396             raise NotmuchError(STATUS.NULL_POINTER)
397         return header
398
399     def get_filename(self):
400         """Returns the file path of the message file
401
402         :returns: Absolute file path & name of the message file
403         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
404               is not initialized.
405         """
406         if self._msg is None:
407             raise NotmuchError(STATUS.NOT_INITIALIZED)
408         return Message._get_filename(self._msg)
409
410     def get_filenames(self):
411         """Get all filenames for the email corresponding to 'message'
412
413         Returns a Filenames() generator with all absolute filepaths for
414         messages recorded to have the same Message-ID. These files must
415         not necessarily have identical content."""
416         if self._msg is None:
417             raise NotmuchError(STATUS.NOT_INITIALIZED)
418         
419         files_p = Message._get_filenames(self._msg)
420
421         return Filenames(files_p, self).as_generator()
422
423     def get_flag(self, flag):
424         """Checks whether a specific flag is set for this message
425
426         The method :meth:`Query.search_threads` sets
427         *Message.FLAG.MATCH* for those messages that match the
428         query. This method allows us to get the value of this flag.
429
430         :param flag: One of the :attr:`Message.FLAG` values (currently only 
431                      *Message.FLAG.MATCH*
432         :returns: An unsigned int (0/1), indicating whether the flag is set.
433         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
434               is not initialized.
435         """
436         if self._msg is None:
437             raise NotmuchError(STATUS.NOT_INITIALIZED)
438         return Message._get_flag(self._msg, flag)
439
440     def set_flag(self, flag, value):
441         """Sets/Unsets a specific flag for this message
442
443         :param flag: One of the :attr:`Message.FLAG` values (currently only 
444                      *Message.FLAG.MATCH*
445         :param value: A bool indicating whether to set or unset the flag.
446
447         :returns: Nothing
448         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
449               is not initialized.
450         """
451         if self._msg is None:
452             raise NotmuchError(STATUS.NOT_INITIALIZED)
453         nmlib.notmuch_message_set_flag(self._msg, flag, value)
454
455     def get_tags(self):
456         """Returns the message tags
457
458         :returns: A :class:`Tags` iterator.
459         :exception: :exc:`NotmuchError`
460
461                       * STATUS.NOT_INITIALIZED if the message 
462                         is not initialized.
463                       * STATUS.NULL_POINTER, on error
464         """
465         if self._msg is None:
466             raise NotmuchError(STATUS.NOT_INITIALIZED)
467
468         tags_p = Message._get_tags(self._msg)
469         if tags_p == None:
470             raise NotmuchError(STATUS.NULL_POINTER)
471         return Tags(tags_p, self)
472
473     def add_tag(self, tag, sync_maildir_flags=True):
474         """Adds a tag to the given message
475
476         Adds a tag to the current message. The maximal tag length is defined in
477         the notmuch library and is currently 200 bytes.
478
479         :param tag: String with a 'tag' to be added.
480
481         :param sync_maildir_flags: If notmuch configuration is set to do
482             this, add maildir flags corresponding to notmuch tags. See
483             :meth:`tags_to_maildir_flags`. Use False if you want to
484             add/remove many tags on a message without having to
485             physically rename the file every time. Do note, that this
486             will do nothing when a message is frozen, as tag changes
487             will not be committed to the database yet.
488
489         :returns: STATUS.SUCCESS if the tag was successfully added.
490                   Raises an exception otherwise.
491         :exception: :exc:`NotmuchError`. They have the following meaning:
492
493                   STATUS.NULL_POINTER
494                     The 'tag' argument is NULL
495                   STATUS.TAG_TOO_LONG
496                     The length of 'tag' is too long 
497                     (exceeds Message.NOTMUCH_TAG_MAX)
498                   STATUS.READ_ONLY_DATABASE
499                     Database was opened in read-only mode so message cannot be 
500                     modified.
501                   STATUS.NOT_INITIALIZED
502                      The message has not been initialized.
503        """
504         if self._msg is None:
505             raise NotmuchError(STATUS.NOT_INITIALIZED)
506
507         status = nmlib.notmuch_message_add_tag (self._msg, tag)
508
509         # bail out on failure
510         if status != STATUS.SUCCESS:
511             raise NotmuchError(status)
512
513         if sync_maildir_flags:
514             self.tags_to_maildir_flags()
515         return STATUS.SUCCESS
516
517     def remove_tag(self, tag, sync_maildir_flags=True):
518         """Removes a tag from the given message
519
520         If the message has no such tag, this is a non-operation and
521         will report success anyway.
522
523         :param tag: String with a 'tag' to be removed.
524         :param sync_maildir_flags: If notmuch configuration is set to do
525             this, add maildir flags corresponding to notmuch tags. See
526             :meth:`tags_to_maildir_flags`. Use False if you want to
527             add/remove many tags on a message without having to
528             physically rename the file every time. Do note, that this
529             will do nothing when a message is frozen, as tag changes
530             will not be committed to the database yet.
531
532         :returns: STATUS.SUCCESS if the tag was successfully removed or if 
533                   the message had no such tag.
534                   Raises an exception otherwise.
535         :exception: :exc:`NotmuchError`. They have the following meaning:
536
537                    STATUS.NULL_POINTER
538                      The 'tag' argument is NULL
539                    STATUS.TAG_TOO_LONG
540                      The length of 'tag' is too long
541                      (exceeds NOTMUCH_TAG_MAX)
542                    STATUS.READ_ONLY_DATABASE
543                      Database was opened in read-only mode so message cannot 
544                      be modified.
545                    STATUS.NOT_INITIALIZED
546                      The message has not been initialized.
547         """
548         if self._msg is None:
549             raise NotmuchError(STATUS.NOT_INITIALIZED)
550
551         status = nmlib.notmuch_message_remove_tag(self._msg, tag)
552         # bail out on error
553         if status != STATUS.SUCCESS:
554             raise NotmuchError(status)
555
556         if sync_maildir_flags:
557             self.tags_to_maildir_flags()
558         return STATUS.SUCCESS
559
560
561
562     def remove_all_tags(self, sync_maildir_flags=True):
563         """Removes all tags from the given message.
564
565         See :meth:`freeze` for an example showing how to safely
566         replace tag values.
567
568         :param sync_maildir_flags: If notmuch configuration is set to do
569             this, add maildir flags corresponding to notmuch tags. See
570             :meth:`tags_to_maildir_flags`. Use False if you want to
571             add/remove many tags on a message without having to
572             physically rename the file every time. Do note, that this
573             will do nothing when a message is frozen, as tag changes
574             will not be committed to the database yet.
575
576         :returns: STATUS.SUCCESS if the tags were successfully removed.
577                   Raises an exception otherwise.
578         :exception: :exc:`NotmuchError`. They have the following meaning:
579
580                    STATUS.READ_ONLY_DATABASE
581                      Database was opened in read-only mode so message cannot 
582                      be modified.
583                    STATUS.NOT_INITIALIZED
584                      The message has not been initialized.
585         """
586         if self._msg is None:
587             raise NotmuchError(STATUS.NOT_INITIALIZED)
588  
589         status = nmlib.notmuch_message_remove_all_tags(self._msg)
590
591         # bail out on error
592         if status != STATUS.SUCCESS:
593             raise NotmuchError(status)
594
595         if sync_maildir_flags:
596             self.tags_to_maildir_flags()
597         return STATUS.SUCCESS
598
599     def freeze(self):
600         """Freezes the current state of 'message' within the database
601
602         This means that changes to the message state, (via :meth:`add_tag`, 
603         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be 
604         committed to the database until the message is :meth:`thaw`ed.
605
606         Multiple calls to freeze/thaw are valid and these calls will
607         "stack". That is there must be as many calls to thaw as to freeze
608         before a message is actually thawed.
609
610         The ability to do freeze/thaw allows for safe transactions to
611         change tag values. For example, explicitly setting a message to
612         have a given set of tags might look like this::
613
614           msg.freeze()
615           msg.remove_all_tags(False)
616           for tag in new_tags:
617               msg.add_tag(tag, False)
618           msg.thaw()
619           msg.tags_to_maildir_flags()
620
621         With freeze/thaw used like this, the message in the database is
622         guaranteed to have either the full set of original tag values, or
623         the full set of new tag values, but nothing in between.
624
625         Imagine the example above without freeze/thaw and the operation
626         somehow getting interrupted. This could result in the message being
627         left with no tags if the interruption happened after
628         :meth:`remove_all_tags` but before :meth:`add_tag`.
629
630         :returns: STATUS.SUCCESS if the message was successfully frozen.
631                   Raises an exception otherwise.
632         :exception: :exc:`NotmuchError`. They have the following meaning:
633
634                    STATUS.READ_ONLY_DATABASE
635                      Database was opened in read-only mode so message cannot 
636                      be modified.
637                    STATUS.NOT_INITIALIZED
638                      The message has not been initialized.
639         """
640         if self._msg is None:
641             raise NotmuchError(STATUS.NOT_INITIALIZED)
642  
643         status = nmlib.notmuch_message_freeze(self._msg)
644
645         if STATUS.SUCCESS == status:
646             # return on success
647             return status
648
649         raise NotmuchError(status)
650
651     def thaw(self):
652         """Thaws the current 'message'
653
654         Thaw the current 'message', synchronizing any changes that may have 
655         occurred while 'message' was frozen into the notmuch database.
656
657         See :meth:`freeze` for an example of how to use this
658         function to safely provide tag changes.
659
660         Multiple calls to freeze/thaw are valid and these calls with
661         "stack". That is there must be as many calls to thaw as to freeze
662         before a message is actually thawed.
663
664         :returns: STATUS.SUCCESS if the message was successfully frozen.
665                   Raises an exception otherwise.
666         :exception: :exc:`NotmuchError`. They have the following meaning:
667
668                    STATUS.UNBALANCED_FREEZE_THAW
669                      An attempt was made to thaw an unfrozen message. 
670                      That is, there have been an unbalanced number of calls 
671                      to :meth:`freeze` and :meth:`thaw`.
672                    STATUS.NOT_INITIALIZED
673                      The message has not been initialized.
674         """
675         if self._msg is None:
676             raise NotmuchError(STATUS.NOT_INITIALIZED)
677  
678         status = nmlib.notmuch_message_thaw(self._msg)
679
680         if STATUS.SUCCESS == status:
681             # return on success
682             return status
683
684         raise NotmuchError(status)
685
686
687     def is_match(self):
688         """(Not implemented)"""
689         return self.get_flag(Message.FLAG.MATCH)
690
691     def tags_to_maildir_flags(self):
692         """Synchronize notmuch tags to file Maildir flags
693
694               'D' if the message has the "draft" tag
695               'F' if the message has the "flagged" tag
696               'P' if the message has the "passed" tag
697               'R' if the message has the "replied" tag
698               'S' if the message does not have the "unread" tag
699             
700         Any existing flags unmentioned in the list above will be
701         preserved in the renaming.
702             
703         Also, if this filename is in a directory named "new", rename it
704         to be within the neighboring directory named "cur".
705
706         Usually, you do not need to call this manually as
707         tag changing methods should be implicitly calling it.
708
709         :returns: a :class:`STATUS`. In short, you want to see
710             notmuch.STATUS.SUCCESS here. See there for details."""
711         if self._msg is None:
712             raise NotmuchError(STATUS.NOT_INITIALIZED)
713         status = Message._tags_to_maildir_flags(self._msg)
714
715     def maildir_flags_to_tags(self):
716         """Synchronize file Maildir flags to notmuch tags
717
718             Flag    Action if present
719             ----    -----------------
720             'D'     Adds the "draft" tag to the message
721             'F'     Adds the "flagged" tag to the message
722             'P'     Adds the "passed" tag to the message
723             'R'     Adds the "replied" tag to the message
724             'S'     Removes the "unread" tag from the message
725
726         For each flag that is not present, the opposite action
727         (add/remove) is performed for the corresponding tags.  If there
728         are multiple filenames associated with this message, the flag is
729         considered present if it appears in one or more filenames. (That
730         is, the flags from the multiple filenames are combined with the
731         logical OR operator.)
732
733         Usually, you do not need to call this manually as
734         :meth:`Database.add_message` implicitly calls it.
735
736         :returns: a :class:`STATUS`. In short, you want to see
737             notmuch.STATUS.SUCCESS here. See there for details."""
738         if self._msg is None:
739             raise NotmuchError(STATUS.NOT_INITIALIZED)
740         status = Message._tags_to_maildir_flags(self._msg)
741
742     def __repr__(self):
743         """Represent a Message() object by str()"""
744         return self.__str__()
745
746     def __str__(self):
747         """A message() is represented by a 1-line summary"""
748         msg = {}
749         msg['from'] = self.get_header('from')
750         msg['tags'] = str(self.get_tags())
751         msg['date'] = date.fromtimestamp(self.get_date())
752         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
753
754
755     def get_message_parts(self):
756         """Output like notmuch show"""
757         fp = open(self.get_filename())
758         email_msg = email.message_from_file(fp)
759         fp.close()
760
761         out = []
762         for msg in email_msg.walk():
763             if not msg.is_multipart():
764                 out.append(msg)
765         return out
766
767     def get_part(self, num):
768         """Returns the nth message body part"""
769         parts = self.get_message_parts()
770         if (num <= 0 or num > len(parts)):
771             return ""
772         else:
773             out_part = parts[(num - 1)]
774             return out_part.get_payload(decode=True)
775
776     def format_message_internal(self):
777         """Create an internal representation of the message parts,
778         which can easily be output to json, text, or another output
779         format. The argument match tells whether this matched a
780         query."""
781         output = {}
782         output["id"] = self.get_message_id()
783         output["match"] = self.is_match()
784         output["filename"] = self.get_filename()
785         output["tags"] = list(self.get_tags())
786
787         headers = {}
788         for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
789             headers[h] = self.get_header(h)
790         output["headers"] = headers
791
792         body = []
793         parts = self.get_message_parts()
794         for i in xrange(len(parts)):
795             msg = parts[i]
796             part_dict = {}
797             part_dict["id"] = i + 1
798             # We'll be using this is a lot, so let's just get it once.
799             cont_type = msg.get_content_type()
800             part_dict["content-type"] = cont_type
801             # NOTE:
802             # Now we emulate the current behaviour, where it ignores
803             # the html if there's a text representation. 
804             #
805             # This is being worked on, but it will be easier to fix
806             # here in the future than to end up with another
807             # incompatible solution.
808             disposition = msg["Content-Disposition"]
809             if disposition and disposition.lower().startswith("attachment"):
810                 part_dict["filename"] = msg.get_filename()
811             else:
812                 if cont_type.lower() == "text/plain":
813                     part_dict["content"] = msg.get_payload()
814                 elif (cont_type.lower() == "text/html" and 
815                       i == 0):
816                     part_dict["content"] = msg.get_payload()
817             body.append(part_dict)
818
819         output["body"] = body
820
821         return output
822
823     def format_message_as_json(self, indent=0):
824         """Outputs the message as json. This is essentially the same
825         as python's dict format, but we run it through, just so we
826         don't have to worry about the details."""
827         return json.dumps(self.format_message_internal())
828
829     def format_message_as_text(self, indent=0):
830         """Outputs it in the old-fashioned notmuch text form. Will be
831         easy to change to a new format when the format changes."""
832
833         format = self.format_message_internal()
834         output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
835                  % (format['id'], indent, format['match'], format['filename'])
836         output += "\n\fheader{"
837
838         #Todo: this date is supposed to be prettified, as in the index.
839         output += "\n%s (%s) (" % (format["headers"]["From"],
840                                    format["headers"]["Date"])
841         output += ", ".join(format["tags"])
842         output += ")"
843
844         output += "\nSubject: %s" % format["headers"]["Subject"]
845         output += "\nFrom: %s" % format["headers"]["From"]
846         output += "\nTo: %s" % format["headers"]["To"]
847         if format["headers"]["Cc"]:
848             output += "\nCc: %s" % format["headers"]["Cc"]
849         if format["headers"]["Bcc"]:
850             output += "\nBcc: %s" % format["headers"]["Bcc"]
851         output += "\nDate: %s" % format["headers"]["Date"]
852         output += "\n\fheader}"
853
854         output += "\n\fbody{"
855
856         parts = format["body"]
857         parts.sort(key=lambda x: x['id'])
858         for p in parts:
859             if not p.has_key("filename"):
860                 output += "\n\fpart{ "
861                 output += "ID: %d, Content-type: %s\n" % (p["id"], 
862                                                          p["content-type"])
863                 if p.has_key("content"):
864                     output += "\n%s\n" % p["content"]
865                 else:
866                     output += "Non-text part: %s\n" % p["content-type"]
867                     output += "\n\fpart}"                    
868             else:
869                 output += "\n\fattachment{ "
870                 output += "ID: %d, Content-type:%s\n" % (p["id"], 
871                                                          p["content-type"])
872                 output += "Attachment: %s\n" % p["filename"]
873                 output += "\n\fattachment}\n"
874
875         output += "\n\fbody}\n"
876         output += "\n\fmessage}"
877
878         return output
879
880     def __hash__(self):
881         """Implement hash(), so we can use Message() sets"""
882         file = self.get_filename()
883         if file is None:
884             return None
885         return hash(file)
886
887     def __cmp__(self, other):
888         """Implement cmp(), so we can compare Message()s
889
890         2 messages are considered equal if they point to the same
891         Message-Id and if they point to the same file names. If 2
892         Messages derive from different queries where some files have
893         been added or removed, the same messages would not be considered
894         equal (as they do not point to the same set of files
895         any more)."""
896         res =  cmp(self.get_message_id(), other.get_message_id())
897         if res:
898             res = cmp(list(self.get_filenames()), list(other.get_filenames()))
899         return res
900
901     def __del__(self):
902         """Close and free the notmuch Message"""
903         if self._msg is not None:
904             nmlib.notmuch_message_destroy (self._msg)