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
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.
12 from email
.utils
import parseaddr
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
):
25 This fixes the given address so that it is *always* a set() of
26 just email addresses suitable for routing.
30 elif isinstance(addr
, (list, tuple)):
33 for returned_addr
in _decode_header_randomness(a
):
34 addr_set
.add(returned_addr
)
37 elif isinstance(addr
, str):
38 return set([parseaddr(addr
.lower())[1]])
39 elif isinstance(addr
, bytes
):
41 return set([parseaddr(addr
.lower())[1]])
43 raise encoding
.EncodingError("Address must be a string or a list not: %r", type(addr
))
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
54 def __init__(self
, Peer
, From
, To
, Data
):
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
67 self
.From
= _decode_header_randomness(From
).pop()
71 self
.To
= _decode_header_randomness(To
).pop()
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
]
89 return "From: {}".format([self
.Peer
, self
.From
, self
.To
])
92 """Returns all multipart mime parts. This could be an empty list."""
93 return self
.base
.parts
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.
103 return self
.base
.parts
[0].body
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
):
121 Converts this to a string usable for storage into a queue or
124 return encoding
.to_string(self
.base
)
127 return self
.base
.items()
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
)
140 """Recursively walks all attached parts and their children."""
141 for x
in self
.base
.walk():
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
151 self
.bounce
= bounce
.detect(self
)
153 return self
.bounce
.score
> threshold
157 warnings
.warn("MailRequest.original is deprecated, use MailRequest.Data instead",
158 category
=DeprecationWarning, stacklevel
=2)
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):
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
):
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.
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
)
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
,
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,
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
)
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
]
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.
280 self
.base
.parts
.append(part
)
283 data
= open(filename
).read()
285 self
.base
.attach_file(filename
, data
, content_type
, disposition
or 'attachment')
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', {})
311 self
.base
.body
= None
313 self
.base
.attach_text(self
.Body
, 'text/plain')
316 self
.base
.attach_text(self
.Html
, 'text/html')
318 for args
in self
.attachments
:
319 self
._encode
_attachment
(**args
)
321 self
.base
.body
= self
.Body
322 self
.base
.content_encoding
['Content-Type'] = ('text/plain', {})
324 self
.base
.body
= self
.Html
325 self
.base
.content_encoding
['Content-Type'] = ('text/html', {})
327 return encoding
.to_message(self
.base
)
331 Returns all the encoded parts. Only useful for debugging
332 or inspecting after calling to_message().
334 return self
.base
.parts
337 return self
.base
.items()
340 return self
.base
.keys()