]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/message.py
database: add n_d_index_file (deprecates n_d_add_message)
[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 <https://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_long, c_uint, c_int
23 from datetime import date
24 from .globals import (
25     nmlib,
26     Enum,
27     _str,
28     Python3StringMixIn,
29     NotmuchTagsP,
30     NotmuchMessageP,
31     NotmuchMessagesP,
32     NotmuchFilenamesP,
33 )
34 from .errors import (
35     STATUS,
36     NotmuchError,
37     NullPointerError,
38     NotInitializedError,
39 )
40 from .tag import Tags
41 from .filenames import Filenames
42
43 import email
44
45
46 class Message(Python3StringMixIn):
47     """Represents a single Email message
48
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.
52
53     As it implements :meth:`__cmp__`, it is possible to compare two
54     :class:`Message`\s using `if msg1 == msg2: ...`.
55     """
56
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
61
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
66
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
71
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
76
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
81
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
86
87     """notmuch_message_get_replies"""
88     _get_replies = nmlib.notmuch_message_get_replies
89     _get_replies.argtypes = [NotmuchMessageP]
90     _get_replies.restype = NotmuchMessagesP
91
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
96
97     _get_date = nmlib.notmuch_message_get_date
98     _get_date.argtypes = [NotmuchMessageP]
99     _get_date.restype = c_long
100
101     _get_header = nmlib.notmuch_message_get_header
102     _get_header.argtypes = [NotmuchMessageP, c_char_p]
103     _get_header.restype = c_char_p
104
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
109
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
114
115     #Constants: Flags that can be set/get with set_flag
116     FLAG = Enum(['MATCH'])
117
118     def __init__(self, msg_p, parent=None):
119         """
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`.
123
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
127               objects are dead.
128         """
129         if not msg_p:
130             raise NullPointerError()
131         self._msg = msg_p
132         #keep reference to parent, so we keep it alive
133         self._parent = parent
134
135     def get_message_id(self):
136         """Returns the message ID
137
138         :returns: String with a message ID
139         :raises: :exc:`NotInitializedError` if the message
140                     is not initialized.
141         """
142         if not self._msg:
143             raise NotInitializedError()
144         return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
145
146     def get_thread_id(self):
147         """Returns the thread ID
148
149         The returned string belongs to 'message' will only be valid for as
150         long as the message is valid.
151
152         This function will not return `None` since Notmuch ensures that every
153         message belongs to a single thread.
154
155         :returns: String with a thread ID
156         :raises: :exc:`NotInitializedError` if the message
157                     is not initialized.
158         """
159         if not self._msg:
160             raise NotInitializedError()
161
162         return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
163
164     def get_replies(self):
165         """Gets all direct replies to this message as :class:`Messages`
166         iterator
167
168         .. note::
169
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.
177
178         :returns: :class:`Messages`.
179         :raises: :exc:`NotInitializedError` if the message
180                     is not initialized.
181         """
182         if not self._msg:
183             raise NotInitializedError()
184
185         msgs_p = Message._get_replies(self._msg)
186
187         from .messages import Messages, EmptyMessagesResult
188
189         if not msgs_p:
190             return EmptyMessagesResult(self)
191
192         return Messages(msgs_p, self)
193
194     def get_date(self):
195         """Returns time_t of the message date
196
197         For the original textual representation of the Date header from the
198         message call notmuch_message_get_header() with a header value of
199         "date".
200
201         :returns: A time_t timestamp.
202         :rtype: c_unit64
203         :raises: :exc:`NotInitializedError` if the message
204                     is not initialized.
205         """
206         if not self._msg:
207             raise NotInitializedError()
208         return Message._get_date(self._msg)
209
210     def get_header(self, header):
211         """Get the value of the specified header.
212
213         The value will be read from the actual message file, not from
214         the notmuch database. The header name is case insensitive.
215
216         Returns an empty string ("") if the message does not contain a
217         header line matching 'header'.
218
219         :param header: The name of the header to be retrieved.
220                        It is not case-sensitive.
221         :type header: str
222         :returns: The header value as string
223         :raises: :exc:`NotInitializedError` if the message is not
224                  initialized
225         :raises: :exc:`NullPointerError` if any error occured
226         """
227         if not self._msg:
228             raise NotInitializedError()
229
230         #Returns NULL if any error occurs.
231         header = Message._get_header(self._msg, _str(header))
232         if header == None:
233             raise NullPointerError()
234         return header.decode('UTF-8', 'ignore')
235
236     def get_filename(self):
237         """Returns the file path of the message file
238
239         :returns: Absolute file path & name of the message file
240         :raises: :exc:`NotInitializedError` if the message
241               is not initialized.
242         """
243         if not self._msg:
244             raise NotInitializedError()
245         return Message._get_filename(self._msg).decode('utf-8', 'ignore')
246
247     def get_filenames(self):
248         """Get all filenames for the email corresponding to 'message'
249
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."""
253         if not self._msg:
254             raise NotInitializedError()
255
256         files_p = Message._get_filenames(self._msg)
257
258         return Filenames(files_p, self)
259
260     def get_flag(self, flag):
261         """Checks whether a specific flag is set for this message
262
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.
266
267         :param flag: One of the :attr:`Message.FLAG` values (currently only
268                      *Message.FLAG.MATCH*
269         :returns: An unsigned int (0/1), indicating whether the flag is set.
270         :raises: :exc:`NotInitializedError` if the message
271               is not initialized.
272         """
273         if not self._msg:
274             raise NotInitializedError()
275         return Message._get_flag(self._msg, flag)
276
277     def set_flag(self, flag, value):
278         """Sets/Unsets a specific flag for this message
279
280         :param flag: One of the :attr:`Message.FLAG` values (currently only
281                      *Message.FLAG.MATCH*
282         :param value: A bool indicating whether to set or unset the flag.
283
284         :raises: :exc:`NotInitializedError` if the message
285               is not initialized.
286         """
287         if not self._msg:
288             raise NotInitializedError()
289         self._set_flag(self._msg, flag, value)
290
291     def get_tags(self):
292         """Returns the message tags
293
294         :returns: A :class:`Tags` iterator.
295         :raises: :exc:`NotInitializedError` if the message is not
296                  initialized
297         :raises: :exc:`NullPointerError` if any error occured
298         """
299         if not self._msg:
300             raise NotInitializedError()
301
302         tags_p = Message._get_tags(self._msg)
303         if not tags_p:
304             raise NullPointerError()
305         return Tags(tags_p, self)
306
307     _add_tag = nmlib.notmuch_message_add_tag
308     _add_tag.argtypes = [NotmuchMessageP, c_char_p]
309     _add_tag.restype = c_uint
310
311     def add_tag(self, tag, sync_maildir_flags=False):
312         """Adds a tag to the given message
313
314         Adds a tag to the current message. The maximal tag length is defined in
315         the notmuch library and is currently 200 bytes.
316
317         :param tag: String with a 'tag' to be added.
318
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.
326
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
335                  initialized
336         """
337         if not self._msg:
338             raise NotInitializedError()
339
340         status = self._add_tag(self._msg, _str(tag))
341
342         # bail out on failure
343         if status != STATUS.SUCCESS:
344             raise NotmuchError(status)
345
346         if sync_maildir_flags:
347             self.tags_to_maildir_flags()
348         return STATUS.SUCCESS
349
350     _remove_tag = nmlib.notmuch_message_remove_tag
351     _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
352     _remove_tag.restype = c_uint
353
354     def remove_tag(self, tag, sync_maildir_flags=False):
355         """Removes a tag from the given message
356
357         If the message has no such tag, this is a non-operation and
358         will report success anyway.
359
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.
368
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
378                  initialized
379         """
380         if not self._msg:
381             raise NotInitializedError()
382
383         status = self._remove_tag(self._msg, _str(tag))
384         # bail out on error
385         if status != STATUS.SUCCESS:
386             raise NotmuchError(status)
387
388         if sync_maildir_flags:
389             self.tags_to_maildir_flags()
390         return STATUS.SUCCESS
391
392     _remove_all_tags = nmlib.notmuch_message_remove_all_tags
393     _remove_all_tags.argtypes = [NotmuchMessageP]
394     _remove_all_tags.restype = c_uint
395
396     def remove_all_tags(self, sync_maildir_flags=False):
397         """Removes all tags from the given message.
398
399         See :meth:`freeze` for an example showing how to safely
400         replace tag values.
401
402
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.
410
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
416                  initialized
417         """
418         if not self._msg:
419             raise NotInitializedError()
420
421         status = self._remove_all_tags(self._msg)
422
423         # bail out on error
424         if status != STATUS.SUCCESS:
425             raise NotmuchError(status)
426
427         if sync_maildir_flags:
428             self.tags_to_maildir_flags()
429         return STATUS.SUCCESS
430
431     _freeze = nmlib.notmuch_message_freeze
432     _freeze.argtypes = [NotmuchMessageP]
433     _freeze.restype = c_uint
434
435     def freeze(self):
436         """Freezes the current state of 'message' within the database
437
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.
441
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.
445
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::
449
450           msg.freeze()
451           msg.remove_all_tags(False)
452           for tag in new_tags:
453               msg.add_tag(tag, False)
454           msg.thaw()
455           msg.tags_to_maildir_flags()
456
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.
460
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`.
465
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
471                  initialized
472         """
473         if not self._msg:
474             raise NotInitializedError()
475
476         status = self._freeze(self._msg)
477
478         if STATUS.SUCCESS == status:
479             # return on success
480             return status
481
482         raise NotmuchError(status)
483
484     _thaw = nmlib.notmuch_message_thaw
485     _thaw.argtypes = [NotmuchMessageP]
486     _thaw.restype = c_uint
487
488     def thaw(self):
489         """Thaws the current 'message'
490
491         Thaw the current 'message', synchronizing any changes that may have
492         occurred while 'message' was frozen into the notmuch database.
493
494         See :meth:`freeze` for an example of how to use this
495         function to safely provide tag changes.
496
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.
500
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
506                  :meth:`thaw`.
507         :raises: :exc:`NotInitializedError` if message has not been
508                  initialized
509         """
510         if not self._msg:
511             raise NotInitializedError()
512
513         status = self._thaw(self._msg)
514
515         if STATUS.SUCCESS == status:
516             # return on success
517             return status
518
519         raise NotmuchError(status)
520
521     def is_match(self):
522         """(Not implemented)"""
523         return self.get_flag(Message.FLAG.MATCH)
524
525     def tags_to_maildir_flags(self):
526         """Synchronize notmuch tags to file Maildir flags
527
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
533
534         Any existing flags unmentioned in the list above will be
535         preserved in the renaming.
536
537         Also, if this filename is in a directory named "new", rename it
538         to be within the neighboring directory named "cur".
539
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
542         to the database.
543
544         :returns: a :class:`STATUS` value. In short, you want to see
545             notmuch.STATUS.SUCCESS here. See there for details."""
546         if not self._msg:
547             raise NotInitializedError()
548         return Message._tags_to_maildir_flags(self._msg)
549
550     def maildir_flags_to_tags(self):
551         """Synchronize file Maildir flags to notmuch tags
552
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
560
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.)
567
568         As a convenience, you can set the sync_maildir_flags parameter in
569         :meth:`Database.index_file` to implicitly call this.
570
571         :returns: a :class:`STATUS`. In short, you want to see
572             notmuch.STATUS.SUCCESS here. See there for details."""
573         if not self._msg:
574             raise NotInitializedError()
575         return Message._maildir_flags_to_tags(self._msg)
576
577     def __repr__(self):
578         """Represent a Message() object by str()"""
579         return self.__str__()
580
581     def __unicode__(self):
582         format = "%s (%s) (%s)"
583         return format % (self.get_header('from'),
584                          self.get_tags(),
585                          date.fromtimestamp(self.get_date()),
586                         )
587
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)
592         fp.close()
593
594         out = []
595         for msg in email_msg.walk():
596             if not msg.is_multipart():
597                 out.append(msg)
598         return out
599
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)):
604             return ""
605         else:
606             out_part = parts[(num - 1)]
607             return out_part.get_payload(decode=True)
608
609     def __hash__(self):
610         """Implement hash(), so we can use Message() sets"""
611         file = self.get_filename()
612         if not file:
613             return None
614         return hash(file)
615
616     def __cmp__(self, other):
617         """Implement cmp(), so we can compare Message()s
618
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
624         any more)."""
625         res = cmp(self.get_message_id(), other.get_message_id())
626         if res:
627             res = cmp(list(self.get_filenames()), list(other.get_filenames()))
628         return res
629
630     _destroy = nmlib.notmuch_message_destroy
631     _destroy.argtypes = [NotmuchMessageP]
632     _destroy.restype = None
633
634     def __del__(self):
635         """Close and free the notmuch Message"""
636         if self._msg:
637             self._destroy(self._msg)