1
2
3
4
5
6
7 """
8 Useful functions to compose and send emails.
9
10 For short:
11
12 >>> payload, mail_from, rcpt_to, msg_id=compose_mail((u'Me', 'me@foo.com'),
13 ... [(u'Him', 'him@bar.com')], u'the subject', 'iso-8859-1', ('Hello world', 'us-ascii'),
14 ... attachments=[('attached', 'text', 'plain', 'text.txt', 'us-ascii')])
15 ... #doctest: +SKIP
16 >>> error=send_mail(payload, mail_from, rcpt_to, 'localhost', smtp_port=25)
17 ... #doctest: +SKIP
18 """
19
20 import os, sys
21 import time
22 import base64
23 import smtplib, socket
24 import email
25 import email.encoders
26 import email.header
27 import email.utils
28 import email.mime
29 import email.mime.base
30 import email.mime.text
31 import email.mime.image
32 import email.mime.multipart
33
34 import utils
35
111
112
113 -def build_mail(text, html=None, attachments=[], embeddeds=[]):
114 """
115 Generate the core of the email message regarding the parameters.
116 The structure of the MIME email may vary, but the general one is as follow::
117
118 multipart/mixed (only if attachments are included)
119 |
120 +-- multipart/related (only if embedded contents are included)
121 | |
122 | +-- multipart/alternative (only if text AND html are available)
123 | | |
124 | | +-- text/plain (text version of the message)
125 | | +-- text/html (html version of the message)
126 | |
127 | +-- image/gif (where to include embedded contents)
128 |
129 +-- application/msword (where to add attachments)
130
131 @param text: the text version of the message, under the form of a tuple:
132 C{(encoded_content, encoding)} where I{encoded_content} is a byte string
133 encoded using I{encoding}.
134 I{text} can be None if the message has no text version.
135 @type text: tuple or None
136 @keyword html: the HTML version of the message, under the form of a tuple:
137 C{(encoded_content, encoding)} where I{encoded_content} is a byte string
138 encoded using I{encoding}
139 I{html} can be None if the message has no HTML version.
140 @type html: tuple or None
141 @keyword attachments: the list of attachments to include into the mail, in the
142 form [(data, maintype, subtype, filename, charset), ..] where :
143 - I{data} : is the raw data, or a I{charset} encoded string for 'text'
144 content.
145 - I{maintype} : is a MIME main type like : 'text', 'image', 'application' ....
146 - I{subtype} : is a MIME sub type of the above I{maintype} for example :
147 'plain', 'png', 'msword' for respectively 'text/plain', 'image/png',
148 'application/msword'.
149 - I{filename} this is the filename of the attachment, it must be a
150 'us-ascii' string or a tuple of the form
151 C{(encoding, language, encoded_filename)}
152 following the RFC2231 requirement, for example
153 C{('iso-8859-1', 'fr', u'r\\xe9pertoir.png'.encode('iso-8859-1'))}
154 - I{charset} : if I{maintype} is 'text', then I{data} must be encoded
155 using this I{charset}. It can be None for non 'text' content.
156 @type attachments: list
157 @keyword embeddeds: is a list of documents embedded inside the HTML or text
158 version of the message. It is similar to the I{attachments} list,
159 but I{filename} is replaced by I{content_id} that is related to
160 the B{cid} reference into the HTML or text version of the message.
161 @type embeddeds: list
162 @rtype: inherit from email.Message
163 @return: the message in a MIME object
164
165 >>> mail=build_mail(('Hello world', 'us-ascii'), attachments=[('attached', 'text', 'plain', 'text.txt', 'us-ascii')])
166 >>> mail.set_boundary('===limit1==')
167 >>> print mail.as_string(unixfrom=False)
168 Content-Type: multipart/mixed; boundary="===limit1=="
169 MIME-Version: 1.0
170 <BLANKLINE>
171 --===limit1==
172 Content-Type: text/plain; charset="us-ascii"
173 MIME-Version: 1.0
174 Content-Transfer-Encoding: 7bit
175 <BLANKLINE>
176 Hello world
177 --===limit1==
178 Content-Type: text/plain; charset="us-ascii"
179 MIME-Version: 1.0
180 Content-Transfer-Encoding: 7bit
181 Content-Disposition: attachment; filename="text.txt"
182 <BLANKLINE>
183 attached
184 --===limit1==--
185 """
186
187 main=text_part=html_part=None
188 if text:
189 content, charset=text
190 main=text_part=email.mime.text.MIMEText(content, 'plain', charset)
191
192 if html:
193 content, charset=html
194 main=html_part=email.mime.text.MIMEText(content, 'html', charset)
195
196 if not text_part and not html_part:
197 main=text_part=email.mime.text.MIMEText('', 'plain', 'us-ascii')
198 elif text_part and html_part:
199
200 main=email.mime.multipart.MIMEMultipart('alternative', None, [text_part, html_part])
201
202 if embeddeds:
203 related=email.mime.multipart.MIMEMultipart('related')
204 related.attach(main)
205 for part in embeddeds:
206 if not isinstance(part, email.mime.base.MIMEBase):
207 data, maintype, subtype, content_id, charset=part
208 if (maintype=='text'):
209 part=email.mime.text.MIMEText(data, subtype, charset)
210 else:
211 part=email.mime.base.MIMEBase(maintype, subtype)
212 part.set_payload(data)
213 email.encoders.encode_base64(part)
214 part.add_header('Content-ID', '<'+content_id+'>')
215 part.add_header('Content-Disposition', 'inline')
216 related.attach(part)
217 main=related
218
219 if attachments:
220 mixed=email.mime.multipart.MIMEMultipart('mixed')
221 mixed.attach(main)
222 for part in attachments:
223 if not isinstance(part, email.mime.base.MIMEBase):
224 data, maintype, subtype, filename, charset=part
225 if (maintype=='text'):
226 part=email.mime.text.MIMEText(data, subtype, charset)
227 else:
228 part=email.mime.base.MIMEBase(maintype, subtype)
229 part.set_payload(data)
230 email.encoders.encode_base64(part)
231 part.add_header('Content-Disposition', 'attachment', filename=filename)
232 mixed.attach(part)
233 main=mixed
234
235 return main
236
237 -def complete_mail(message, sender, recipients, subject, default_charset, cc=[], bcc=[], message_id_string=None, date=None, headers=[]):
238 """
239 Fill in the From, To, Cc, Subject, Date and Message-Id I{headers} of
240 one existing message regarding the parameters.
241
242 @type message:email.Message
243 @param message: the message to fill in
244 @type sender: tuple
245 @param sender: a tuple of the form (u'Sender Name', 'sender.address@domain.com')
246 @type recipients: list
247 @param recipients: a list of addresses. Address can be tuple or string like
248 expected by L{format_addresses()}, for example: C{[ 'address@dmain.com',
249 (u'Recipient Name', 'recipient.address@domain.com'), ... ]}
250 @type subject: str
251 @param subject: The subject of the message, can be a unicode string or a
252 string encoded using I{default_charset} encoding. Prefert unicode to
253 byte string here.
254 @type default_charset: str
255 @param default_charset: The default charset for this email. Arguments
256 that are non unicode string are supposed to be encoded using this charset.
257 This I{charset} will be used has an hint when encoding mail content.
258 @type cc: list
259 @keyword cc: The I{carbone copy} addresses. Same format as the I{recipients}
260 argument.
261 @type bcc: list
262 @keyword bcc: The I{blind carbone copy} addresses. Same format as the I{recipients}
263 argument.
264 @type message_id_string: str or None
265 @keyword message_id_string: if None, don't append any I{Message-ID} to the
266 mail, let the SMTP do the job, else use the string to generate a unique
267 I{ID} using C{email.utils.make_msgid()}. The generated value is
268 returned as last argument. For example use the name of your application.
269 @type date: int or None
270 @keyword date: utc time in second from the epoch or None. If None then
271 use curent time C{time.time()} instead.
272 @type headers: list of tuple
273 @keyword headers: a list of C{(field, value)} tuples to fill in the mail
274 header fields. Values are encoded using I{default_charset}.
275 @rtype: tuple
276 @return: B{(payload, mail_from, rcpt_to, msg_id)}
277 - I{payload} (str) is the content of the email, generated from the message
278 - I{mail_from} (str) is the address of the sender to pass to the SMTP host
279 - I{rcpt_to} (list) is a list of the recipients addresses to pass to the SMTP host
280 of the form C{[ 'a@b.com', c@d.com', ]}. This combine all recipients,
281 I{carbone copy} addresses and I{blind carbone copy} addresses.
282 - I{msg_id} (None or str) None if message_id_string==None else the generated value for
283 the message-id. If not None, this I{Message-ID} is already written
284 into the payload.
285
286 >>> import email.mime.text
287 >>> msg=email.mime.text.MIMEText('The text.', 'plain', 'us-ascii')
288 >>> # I could use build_mail() instead
289 >>> payload, mail_from, rcpt_to, msg_id=complete_mail(msg, ('Me', 'me@foo.com'),
290 ... [ ('Him', 'him@bar.com'), ], 'Non unicode subject', 'iso-8859-1',
291 ... cc=['her@bar.com',], date=1313558269, headers=[('User-Agent', u'pyzmail'), ])
292 >>> print payload
293 ... # 3.X encode User-Agent: using 'iso-8859-1' even if it contains only us-asccii
294 ... # doctest: +ELLIPSIS
295 Content-Type: text/plain; charset="us-ascii"
296 MIME-Version: 1.0
297 Content-Transfer-Encoding: 7bit
298 From: Me <me@foo.com>
299 To: Him <him@bar.com>
300 Cc: her@bar.com
301 Subject: =?iso-8859-1?q?Non_unicode_subject?=
302 Date: ...
303 User-Agent: ...pyzmail...
304 <BLANKLINE>
305 The text.
306 >>> print 'mail_from=%r rcpt_to=%r' % (mail_from, rcpt_to)
307 mail_from='me@foo.com' rcpt_to=['him@bar.com', 'her@bar.com']
308 """
309 def getaddr(address):
310 if isinstance(address, tuple):
311 return address[1]
312 else:
313 return address
314
315 mail_from=getaddr(sender[1])
316 rcpt_to=map(getaddr, recipients)
317 rcpt_to.extend(map(getaddr, cc))
318 rcpt_to.extend(map(getaddr, bcc))
319
320 message['From'] = format_addresses([ sender, ], header_name='from', charset=default_charset)
321 if recipients:
322 message['To'] = format_addresses(recipients, header_name='to', charset=default_charset)
323 if cc:
324 message['Cc'] = format_addresses(cc, header_name='cc', charset=default_charset)
325 message['Subject'] = email.header.Header(subject, default_charset)
326 if date:
327 utc_from_epoch=date
328 else:
329 utc_from_epoch=time.time()
330 message['Date'] = email.utils.formatdate(utc_from_epoch, localtime=True)
331
332 if message_id_string:
333 msg_id=message['Message-Id']=email.utils.make_msgid(message_id_string)
334 else:
335 msg_id=None
336
337 for field, value in headers:
338 message[field]=email.header.Header(value, default_charset)
339
340 payload=message.as_string()
341
342 return payload, mail_from, rcpt_to, msg_id
343
344 -def compose_mail(sender, recipients, subject, default_charset, text, html=None, attachments=[], embeddeds=[], cc=[], bcc=[], message_id_string=None, date=None, headers=[]):
345 """
346 Compose an email regarding the arguments. Call L{build_mail()} and
347 L{complete_mail()} at once.
348
349 Read the B{parameters} descriptions of both functions L{build_mail()} and L{complete_mail()}.
350
351 Returned value is the same as for L{build_mail()} and L{complete_mail()}.
352 You can pass the returned values to L{send_mail()} or L{send_mail2()}.
353
354 @rtype: tuple
355 @return: B{(payload, mail_from, rcpt_to, msg_id)}
356
357 >>> payload, mail_from, rcpt_to, msg_id=compose_mail((u'Me', 'me@foo.com'), [(u'Him', 'him@bar.com')], u'the subject', 'iso-8859-1', ('Hello world', 'us-ascii'), attachments=[('attached', 'text', 'plain', 'text.txt', 'us-ascii')])
358 """
359 message=build_mail(text, html, attachments, embeddeds)
360 return complete_mail(message, sender, recipients, subject, default_charset, cc, bcc, message_id_string, date, headers)
361
362
363 -def send_mail2(payload, mail_from, rcpt_to, smtp_host, smtp_port=25, smtp_mode='normal', smtp_login=None, smtp_password=None):
364 """
365 Send the message to a SMTP host. Look at the L{send_mail()} documentation.
366 L{send_mail()} call this function and catch all exceptions to convert them
367 into a user friendly error message. The returned value
368 is always a dictionary. It can be empty if all recipients have been
369 accepted.
370
371 @rtype: dict
372 @return: This function return the value returnd by C{smtplib.SMTP.sendmail()}
373 or raise the same exceptions.
374
375 This method will return normally if the mail is accepted for at least one
376 recipient. Otherwise it will raise an exception. That is, if this
377 method does not raise an exception, then someone should get your mail.
378 If this method does not raise an exception, it returns a dictionary,
379 with one entry for each recipient that was refused. Each entry contains a
380 tuple of the SMTP error code and the accompanying error message sent by the server.
381
382 @raise smtplib.SMTPException: Look at the standard C{smtplib.SMTP.sendmail()} documentation.
383
384 """
385 if smtp_mode=='ssl':
386 smtp=smtplib.SMTP_SSL(smtp_host, smtp_port)
387 else:
388 smtp=smtplib.SMTP(smtp_host, smtp_port)
389 if smtp_mode=='tls':
390 smtp.starttls()
391
392 if smtp_login and smtp_password:
393 if sys.version_info<(3, 0):
394
395
396
397 smtp.login(smtp_login.encode('utf-8'), smtp_password.encode('utf-8'))
398 else:
399
400 smtp.login(smtp_login, smtp_password)
401 try:
402 ret=smtp.sendmail(mail_from, rcpt_to, payload)
403 finally:
404 try:
405 smtp.quit()
406 except Exception, e:
407 pass
408
409 return ret
410
411 -def send_mail(payload, mail_from, rcpt_to, smtp_host, smtp_port=25, smtp_mode='normal', smtp_login=None, smtp_password=None):
412 """
413 Send the message to a SMTP host. Handle SSL, TLS and authentication.
414 I{payload}, I{mail_from} and I{rcpt_to} can come from values returned by
415 L{complete_mail()}. This function call L{send_mail2()} but catch all
416 exceptions and return friendly error message instead.
417
418 @type payload: str
419 @param payload: the mail content.
420 @type mail_from: str
421 @param mail_from: the sender address, for example: C{'me@domain.com'}.
422 @type rcpt_to: list
423 @param rcpt_to: The list of the recipient addresses in the form
424 C{[ 'a@b.com', c@d.com', ]}. No names here, only email addresses.
425 @type smtp_host: str
426 @param smtp_host: the IP address or the name of the SMTP host.
427 @type smtp_port: int
428 @keyword smtp_port: the port to connect to on the SMTP host. Default is C{25}.
429 @type smtp_mode: str
430 @keyword smtp_mode: the way to connect to the SMTP host, can be:
431 C{'normal'}, C{'ssl'} or C{'tls'}. default is C{'normal'}
432 @type smtp_login: str or None
433 @keyword smtp_login: If authentication is required, this is the login.
434 Be carefull to I{UTF8} encode your login if it contains
435 non I{us-ascii} characters.
436 @type smtp_password: str or None
437 @keyword smtp_password: If authentication is required, this is the password.
438 Be carefull to I{UTF8} encode your password if it
439 contains non I{us-ascii} characters.
440
441 @rtype: dict or str
442 @return: This function return a dictionary of failed recipients
443 or a string with an error message.
444
445 If all recipients have been accepted the dictionary is empty. If the
446 returned value is a string, none of the recipients will get the message.
447
448 The dictionary is exactly of the same sort as
449 smtplib.SMTP.sendmail() returns with one entry for each recipient that
450 was refused. Each entry contains a tuple of the SMTP error code and
451 the accompanying error message sent by the server.
452
453 Example:
454
455 >>> send_mail('Subject: hello\\n\\nmessage', 'a@foo.com', [ 'b@bar.com', ], 'localhost') #doctest: +SKIP
456 {}
457
458 Here is how to use the returned value::
459 if isinstance(ret, dict):
460 if ret:
461 print 'failed' recipients:
462 for recipient, (code, msg) in ret.iteritems():
463 print 'code=%d recipient=%s\terror=%s' % (code, recipient, msg)
464 else:
465 print 'success'
466 else:
467 print 'Error:', ret
468
469 To use your GMail account to send your mail::
470 smtp_host='smtp.gmail.com'
471 smtp_port=587
472 smtp_mode='tls'
473 smtp_login='your.gmail.addresse@gmail.com'
474 smtp_password='your.gmail.password'
475
476 Use your GMail address for the sender !
477
478 """
479
480 error=dict()
481 try:
482 ret=send_mail2(payload, mail_from, rcpt_to, smtp_host, smtp_port, smtp_mode, smtp_login, smtp_password)
483 except (socket.error, ), e:
484 error='server %s:%s not responding: %s' % (smtp_host, smtp_port, e)
485 except smtplib.SMTPAuthenticationError, e:
486 error='authentication error: %s' % (e, )
487 except smtplib.SMTPRecipientsRefused, e:
488
489 error='all recipients refused: '+', '.join(e.recipients.keys())
490 except smtplib.SMTPSenderRefused, e:
491
492 error='sender refused: %s' % (e.sender, )
493 except smtplib.SMTPDataError, e:
494 error='SMTP protocol mismatch: %s' % (e, )
495 except smtplib.SMTPHeloError, e:
496 error="server didn't reply properly to the HELO greeting: %s" % (e, )
497 except smtplib.SMTPException, e:
498 error='SMTP error: %s' % (e, )
499
500
501 else:
502
503 error=ret
504
505 return error
506