1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
| | import os
import pathlib
import notdb._base as base
import notdb._capi as capi
import notdb._errors as errors
import notdb._tags as tags
class Message(base.NotmuchObject):
"""An email message stored in the notmuch database.
This should not be directly created, instead it will be returned
by calling methods on :class:`Database`. A message keeps a
reference to the database object since the database object can not
be released while the message is in use.
Note that this represents a message in the notmuch database. For
full email functionality you may want to use the :mod:`email`
package from Python's standard library. You could e.g. create
this as such::
notmuch_msg = db.get_message(msgid) # or from a query
parser = email.parser.BytesParser(policy=email.policy.default)
with notmuch_msg.path.open('rb) as fp:
email_msg = parser.parse(fp)
Most commonly the functionality provided by notmuch is sufficient
to read email however.
:param db: The database instance this message is associated with.
:type db: Database
:param msg_p: The C pointer to the ``notmuch_message_t``.
:type msg_p: <cdata>
:param dup: Whether the message was a duplicate on insertion.
:type dup: None or bool
"""
_msg_p = base.MemoryPointer()
def __init__(self, db, msg_p):
self._db = db
self._msg_p = msg_p
@property
def alive(self):
if not self._db.alive:
return False
try:
self._msg_p
except errors.ObjectDestroyedError:
return False
else:
return True
def __del__(self):
"""Destroy the message, freeing the memory.
Note that when an owning object, like the containing query, is
destroyed the messages also get destoryed.
"""
self.destroy()
def destroy(self):
"""Destroy the object and all children.
This will destroy the object, freeing all memory for it and
it's children. You should not normally need to call this, it
will be called automatically by Python garbage collection.
The main reason for it's existence is for parent objects being
able to destroy their children, this is required when Python's
garbage collection does not guarantee ordered deletion,
e.g. at intepreter shutdown.
:param parent: Used by the parent to indicate it has already
destroyed itself, and thus all it's children. In this case
this object only marks itself as already destroyed to avoid
double freeing memory.
"""
if self.alive:
capi.lib.notmuch_message_destroy(self._msg_p)
self._msg_p = None
@property
def messageid(self):
"""The message ID as a string.
The message ID is decoded with the ignore error handler. This
is fine as long as the message ID is well formed. If it is
not valid ASCII then this will be lossy. So if you need to be
able to write the exact same message ID back you should use
:attr:`messageidb`.
Note that notmuch will decode the message ID value and thus
strip off the surrounding ``<`` and ``>`` characters. This is
different from Python's :mod:`email` package behaviour which
leaves these characters in place.
:returns: The message ID.
:rtype: :class:`BinString`, this is a normal str but calling
bytes() on it will return the original bytes used to create
it.
:raises ObjectDestroyedError: if used after destoryed.
"""
ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
return base.BinString(capi.ffi.string(ret))
@property
def threadid(self):
"""The thread ID.
The thread ID is decoded with the surrogateescape error
handler so that it is possible to reconstruct the original
thread ID if it is not valid UTF-8.
"""
raise NotImplementedError
@property
def path(self):
"""A pathname of the message as a pathlib.Path instance.
If multiple files in the database contain the same message ID
this will be just one of the files, chosen at random.
:raises ObjectDestroyedError: if used after destoryed.
"""
ret = capi.lib.notmuch_message_get_filename(self._msg_p)
return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
@property
def pathb(self):
"""A pathname of the message as a bytes object.
See :attr:`path` for details, this is the same but does return
the path as a bytes object which is faster but less convenient.
:raises ObjectDestroyedError: if used after destoryed.
"""
ret = capi.lib.notmuch_message_get_filename(self._msg_p)
return capi.ffi.string(ret)
def pathnames(self):
"""Return an iterator of all files for this message.
If multiple files contained the same message ID they will all
be returned here. The files are returned as intances of
:class:`pathlib.Path`.
:raises ObjectDestroyedError: if used after destoryed.
"""
raise NotImplementedError
def pathnamesb(self):
"""Return an iterator of all files for this message.
This is like :meth:`pathnames` but the files are returned as
byte objects instead.
:raises ObjectDestroyedError: if used after destoryed.
"""
raise NotImplementedError
@property
def ghost(self):
"""Indicates whether this message is a ghost message.
A ghost message if a message which we know exists, but it has
no files or content associated with it. This can happen if
it was referenced by some other message. Only the
:attr:`messageid` and :attr:`threadid` attributes are valid
for it.
"""
raise NotImplementedError
@property
def date(self):
"""The message date.
XXX Figure out which format to provide this in.
"""
raise NotImplementedError
def header(self, name):
"""Return the value of the named header.
Returns the header from notmuch, some common headers are
stored in the database, others are read from the file.
Headers are returned with their newlines stripped and
collapsed concatenated together if they occur multiple times.
You may be better off using the standard library email
package's ``email.message_from_file(msg.path.open())`` if that
is not sufficient for you.
:param header: Case-insensitive header name to retrieve.
:type header: str
:returns: The header value, an empty string if the header is
not present.
:rtype: str
:raises NoSuchHeaderError: if the header is not present.
"""
raise NotImplementedError
@property
def tags(self):
"""The tags associated with the message.
This behaves as a set. But removing and adding items to the
set removes and adds them to the message in the database.
"""
# By caching the tagset this creates a circular reference.
# This is fine on CPython 3.4+. We could improve this by using
# weakref.finalizer instead of __del__.
try:
return self._cached_tagset
except AttributeError:
self._cached_tagset = tags.MutableTagSet(
self, '_msg_p', capi.lib.notmuch_message_get_tags)
return self._cached_tagset
def tags_to_flags(self):
"""Sync the notmuch tags to maildir flags.
This will rename the pathname of the message so that the
maildir flags match the current set of notmuch tags. The
mappings are:
flag tag
----- --------------
``D`` ``draft``
``F`` ``flagged``
``P`` ``passed``
``R`` ``replied``
``S`` not ``unread``
Any other flags are preserved in the renaming. If the
existing flag format is invalid, e.g. flags repeated, not in
ASCII order file not ending in ``:2,``, the file is not
renamed.
"""
raise NotImplementedError
def flags_to_tags(self):
"""Sync the maildir flags to notmuch tags.
This synchronizes the opposite way as described in
:meth:`rags_to_flags`.
"""
raise NotImplementedError
def frozen(self):
"""Context manager to freeze the message state.
This allows you to perform atomic tag updates::
with msg.frozen():
msg.tags.clear()
msg.tags.add('foo')
Using This would ensure the message never ends up with no tags
applied at all.
It is safe to nest calls to this context manager.
"""
raise NotImplementedError
@property
def properties(self):
"""A map of arbitrary key-value pairs associated with the message.
Be aware that properties may be used by other extensions to
store state in. So delete or modify with care.
"""
raise NotImplementedError
class MessageProperties:
# XXX This will need to decide what to do with encoding. Easiest
# is to store bytes and leave it to the user to call .encode()
# .decode().
pass
|