Update docs with information about how to use thread-unsafe code in
[salmon.git] / salmon / mail.py
blob606a68e2dd1ae2ad7997161c32cf07e3ee772280
1 """
2 The salmon.mail module contains nothing more than wrappers around the big work
3 done in salmon.encoding. These are the actual APIs that you'll interact with
4 when doing email, and they mostly replicate the salmon.encoding.MailBase
5 functionality.
7 The main design criteria is that MailRequest is mostly for reading email
8 that you've received, so it doesn't have functions for attaching files and such.
9 MailResponse is used when you are going to write an email, so it has the
10 APIs for doing attachments and such.
11 """
12 from email.utils import parseaddr
13 import mimetypes
14 import os
15 import warnings
17 from salmon import bounce, encoding
19 # You can change this to 'Delivered-To' on servers that support it like Postfix
20 ROUTABLE_TO_HEADER = 'to'
23 def _decode_header_randomness(addr):
24 """
25 This fixes the given address so that it is *always* a set() of
26 just email addresses suitable for routing.
27 """
28 if not addr:
29 return set()
30 elif isinstance(addr, (list, tuple)):
31 addr_set = set()
32 for a in addr:
33 for returned_addr in _decode_header_randomness(a):
34 addr_set.add(returned_addr)
36 return addr_set
37 elif isinstance(addr, str):
38 return set([parseaddr(addr.lower())[1]])
39 elif isinstance(addr, bytes):
40 addr = addr.decode()
41 return set([parseaddr(addr.lower())[1]])
42 else:
43 raise encoding.EncodingError("Address must be a string or a list not: %r", type(addr))
46 class MailRequest:
47 """
48 This is what is given to your message handlers. The information you get out
49 of this is *ALWAYS* in Python str and should be usable by any API.
50 Modifying this object will cause other handlers that deal with it to get
51 your modifications, but in general you don't want to do more than maybe tag
52 a few headers.
53 """
54 def __init__(self, Peer, From, To, Data):
55 """
56 Peer is the remote peer making the connection (sometimes the queue
57 name). From and To are what you think they are. Data is the raw
58 full email as received by the server.
60 NOTE: It does not handle multiple From headers, if that's even
61 possible. It will parse the From into a list and take the first
62 one.
63 """
64 self.Peer = Peer
65 self.Data = Data
66 try:
67 self.From = _decode_header_randomness(From).pop()
68 except KeyError:
69 self.From = None
70 try:
71 self.To = _decode_header_randomness(To).pop()
72 except KeyError:
73 self.To = None
75 self.base = encoding.from_string(self.Data)
77 if 'from' not in self.base:
78 self.base['from'] = self.From
79 if 'to' not in self.base:
80 # do NOT use ROUTABLE_TO here
81 self.base['to'] = self.To
83 self.From = self.From or self.base['from']
84 self.To = self.To or self.base[ROUTABLE_TO_HEADER]
86 self.bounce = None
88 def __repr__(self):
89 return "From: {}".format([self.Peer, self.From, self.To])
91 def all_parts(self):
92 """Returns all multipart mime parts. This could be an empty list."""
93 return self.base.parts
95 def body(self):
96 """
97 Always returns a body if there is one. If the message
98 is multipart then it returns the first part's body, if
99 it's not then it just returns the body. If returns
100 None then this message has nothing for a body.
102 if self.base.parts:
103 return self.base.parts[0].body
104 else:
105 return self.base.body
107 def __contains__(self, key):
108 return self.base.__contains__(key)
110 def __getitem__(self, name):
111 return self.base.__getitem__(name)
113 def __setitem__(self, name, val):
114 self.base.__setitem__(name, val)
116 def __delitem__(self, name):
117 del self.base[name]
119 def __str__(self):
121 Converts this to a string usable for storage into a queue or
122 transmission.
124 return encoding.to_string(self.base)
126 def items(self):
127 return self.base.items()
129 def keys(self):
130 return self.base.keys()
132 def to_message(self):
134 Converts this to a Python email message you can use to
135 interact with the python mail APIs.
137 return encoding.to_message(self.base)
139 def walk(self):
140 """Recursively walks all attached parts and their children."""
141 for x in self.base.walk():
142 yield x
144 def is_bounce(self, threshold=0.3):
146 Determines whether the message is a bounce message based on
147 salmon.bounce.BounceAnalzyer given threshold. 0.3 is a good
148 conservative base.
150 if not self.bounce:
151 self.bounce = bounce.detect(self)
153 return self.bounce.score > threshold
155 @property
156 def original(self):
157 warnings.warn("MailRequest.original is deprecated, use MailRequest.Data instead",
158 category=DeprecationWarning, stacklevel=2)
159 return self.Data
162 class MailResponse:
164 You are given MailResponse objects from the salmon.view methods, and
165 whenever you want to generate an email to send to someone. It has
166 the same basic functionality as MailRequest, but it is designed to
167 be written to, rather than read from (although you can do both).
169 You can easily set a Body or Html during creation or after by
170 passing it as __init__ parameters, or by setting those attributes.
172 You can initially set the From, To, and Subject, but they are headers so
173 use the dict notation to change them: ``msg['From'] = 'joe@test.com'``.
175 The message is not fully crafted until right when you convert it with
176 MailResponse.to_message. This lets you change it and work with it, then
177 send it out when it's ready.
179 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None):
180 self.Body = Body
181 self.Html = Html
182 self.base = encoding.MailBase([('To', To), ('From', From), ('Subject', Subject)])
183 self.multipart = self.Body and self.Html
184 self.attachments = []
186 def __contains__(self, key):
187 return self.base.__contains__(key)
189 def __getitem__(self, key):
190 return self.base.__getitem__(key)
192 def __setitem__(self, key, val):
193 return self.base.__setitem__(key, val)
195 def __delitem__(self, name):
196 del self.base[name]
198 def attach(self, filename=None, content_type=None, data=None, disposition=None):
200 Simplifies attaching files from disk or data as files. To attach simple
201 text simple give data and a content_type. To attach a file, give the
202 data/content_type/filename/disposition combination.
204 For convenience, if you don't give data and only a filename, then it
205 will read that file's contents when you call to_message() later. If you
206 give data and filename then it will assume you've filled data with what
207 the file's contents are and filename is just the name to use.
209 if data is None:
210 if filename is None:
211 raise TypeError("You must give a filename or some data to attach.")
212 elif not os.path.exists(filename):
213 raise TypeError("File doesn't exist, and no data given.")
215 self.multipart = True
217 if filename and not content_type:
218 content_type, encoding = mimetypes.guess_type(filename)
220 if not content_type:
221 raise ValueError("No content type given, and couldn't guess from the filename: %r" % filename)
223 self.attachments.append({
224 'filename': filename,
225 'content_type': content_type,
226 'data': data,
227 'disposition': disposition,
230 def attach_part(self, part):
232 Attaches a raw MailBase part from a MailRequest (or anywhere)
233 so that you can copy it over.
235 self.multipart = True
237 self.attachments.append({'filename': None,
238 'content_type': None,
239 'data': None,
240 'disposition': None,
241 'part': part,
244 def attach_all_parts(self, mail_request):
246 Used for copying the attachment parts of a mail.MailRequest
247 object for mailing lists that need to maintain attachments.
249 for part in mail_request.all_parts():
250 self.attach_part(part)
252 def clear(self):
254 Clears out the attachments so you can redo them. Use this to keep the
255 headers for a series of different messages with different attachments.
257 del self.attachments[:]
258 del self.base.parts[:]
259 self.multipart = False
261 def update(self, message):
263 Used to easily set a bunch of headers from another dict like object.
265 for k in message.keys():
266 self.base[k] = message[k]
268 def __str__(self):
270 Converts to a string.
272 return self.to_message().as_string()
274 def _encode_attachment(self, filename=None, content_type=None, data=None, disposition=None, part=None):
276 Used internally to take the attachments mentioned in self.attachments
277 and do the actual encoding in a lazy way when you call to_message.
279 if part:
280 self.base.parts.append(part)
281 elif filename:
282 if not data:
283 data = open(filename).read()
285 self.base.attach_file(filename, data, content_type, disposition or 'attachment')
286 else:
287 self.base.attach_text(data, content_type)
289 ctype = self.base.content_encoding['Content-Type'][0]
291 if ctype and not ctype.startswith('multipart'):
292 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
294 def to_message(self):
296 Figures out all the required steps to finally craft the
297 message you need and return it. The resulting message
298 is also available as a self.base attribute.
300 What is returned is a Python email API message you can
301 use with those APIs. The self.base attribute is the raw
302 salmon.encoding.MailBase.
304 del self.base.parts[:]
306 if self.Body and self.Html:
307 self.multipart = True
308 self.base.content_encoding['Content-Type'] = ('multipart/alternative', {})
310 if self.multipart:
311 self.base.body = None
312 if self.Body:
313 self.base.attach_text(self.Body, 'text/plain')
315 if self.Html:
316 self.base.attach_text(self.Html, 'text/html')
318 for args in self.attachments:
319 self._encode_attachment(**args)
320 elif self.Body:
321 self.base.body = self.Body
322 self.base.content_encoding['Content-Type'] = ('text/plain', {})
323 elif self.Html:
324 self.base.body = self.Html
325 self.base.content_encoding['Content-Type'] = ('text/html', {})
327 return encoding.to_message(self.base)
329 def all_parts(self):
331 Returns all the encoded parts. Only useful for debugging
332 or inspecting after calling to_message().
334 return self.base.parts
336 def items(self):
337 return self.base.items()
339 def keys(self):
340 return self.base.keys()