creating a MIME email template with images to send with python / django - python

In my web application I send emails occasionally using a reusable mailer application like this:
user - self.user
subject = ("My subject")
from = "me#mydomain.com"
message = render_to_string("welcomeEmail/welcome.eml", {
"user" : user,
})
send_mail(subject, message, from, [email], priority="high" )
I want to send an email with embedded images in it, so I tried making the mail in a mail client, viewing the source, and putting it into my template (welcome.eml), but I've been unable to get it to render correctly in mail clients when its sent.
Does anyone know of an easy way for me to create mime encoded mail templates with inline images that will render correctly when I send them?

Update
Many thanks to Saqib Ali for resurrecting this old question nearly 5 years after my reply.
The instructions I gave at the time no longer work. I suspect there have been some improvements to Django in the intervening years which mean that send_mail() enforces plain text. No matter what you put in the content, it will always be delivered as plain text.
The most recent Django documentation explains that send_mail() is really just a convenience for creating an instance of the django.core.mail.EmailMessage class, and then calling send() on that instance. EmailMessage has this note for the body parameter, which explains the results we're seeing now in 2014:
body: The body text. This should be a plain text message.
... somewhat later in the docs ...
By default, the MIME type of the body parameter in an EmailMessage is "text/plain". It is good practice to leave this alone.
Fair enough (I confess I haven't taken the time to investigate why the 2009 instructions worked - I did test them back in 2009 - or when it changed). Django does provide, and document, a django.core.mail.EmailMultiAlternatives class to make it easier for sending a plain text and HTML representation of the same message.
The case in this question is slightly different. We're not seeking to append an alternative per se, but to append related parts to one of the alternatives. Within the HTML version (and it doesn't matter if you have or omit the plain text version), we want to embed an image data part. Not an alternative view of the content, but related content that is referenced in the HTML body.
Sending an embedded image is still possible, but I don't see a straightforward way to do it using send_mail. It's time to dispense with the convenience function and to instantiate an EmailMessage directly.
Here's an update to the previous example:
from django.core.mail import EmailMessage
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# Load the image you want to send as bytes
img_data = open('logo.jpg', 'rb').read()
# Create a "related" message container that will hold the HTML
# message and the image. These are "related" (not "alternative")
# because they are different, unique parts of the HTML message,
# not alternative (html vs. plain text) views of the same content.
html_part = MIMEMultipart(_subtype='related')
# Create the body with HTML. Note that the image, since it is inline, is
# referenced with the URL cid:myimage... you should take care to make
# "myimage" unique
body = MIMEText('<p>Hello <img src="cid:myimage" /></p>', _subtype='html')
html_part.attach(body)
# Now create the MIME container for the image
img = MIMEImage(img_data, 'jpeg')
img.add_header('Content-Id', '<myimage>') # angle brackets are important
img.add_header("Content-Disposition", "inline", filename="myimage") # David Hess recommended this edit
html_part.attach(img)
# Configure and send an EmailMessage
# Note we are passing None for the body (the 2nd parameter). You could pass plain text
# to create an alternative part for this message
msg = EmailMessage('Subject Line', None, 'foo#bar.com', ['bar#foo.com'])
msg.attach(html_part) # Attach the raw MIMEBase descendant. This is a public method on EmailMessage
msg.send()
Original reply from 2009:
To send an e-mail with embedded images, use python's built-in email module to build up the MIME parts.
The following should do it:
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# Load the image you want to send at bytes
img_data = open('logo.jpg', 'rb').read()
# Create a "related" message container that will hold the HTML
# message and the image
msg = MIMEMultipart(_subtype='related')
# Create the body with HTML. Note that the image, since it is inline, is
# referenced with the URL cid:myimage... you should take care to make
# "myimage" unique
body = MIMEText('<p>Hello <img src="cid:myimage" /></p>', _subtype='html')
msg.attach(body)
# Now create the MIME container for the image
img = MIMEImage(img_data, 'jpeg')
img.add_header('Content-Id', '<myimage>') # angle brackets are important
msg.attach(img)
send_mail(subject, msg.as_string(), from, [to], priority="high")
In reality, you'll probably want to send the HTML along with a plain-text alternative. In that case, use MIMEMultipart to create the "related" mimetype container as the root. Then create another MIMEMultipart with the subtype "alternative", and attach both a MIMEText (subtype html) and a MIMEText (subtype plain) to the alternative part. Then attach the image to the related root.

I was having trouble with Jarret's recipe on Django 1.10 - was getting MIME and encoding errors for the various ways you can attach MIME data.
Here's a simple multipart transactional template for an email with an embedded coupon_image file object that works on django 1.10:
from django.core.mail import EmailMultiAlternatives
from email.mime.image import MIMEImage
def send_mail(coupon_image):
params = {'foo':'bar'} # create a template context
text_body = render_to_string('coupon_email.txt', params)
html_body = render_to_string('coupon_email.html', params)
img_data = coupon_image.read() #should be a file object, or ImageField
img = MIMEImage(img_data)
img.add_header('Content-ID', '<coupon_image>')
img.add_header('Content-Disposition', 'inline', filename="coupon_image")
email = EmailMultiAlternatives(
subject="Here's your coupon!",
body=text_body,
from_email='noreply#example.com',
to=['someone#example.com',]
)
email.attach_alternative(html_body, "text/html")
email.mixed_subtype = 'related'
email.attach(img)
email.send(fail_silently=False)

Version using a text alternative
Jarret Hardie's answer says "You could pass plain text to create an alternative part for this message". However, I have found that doing so would lead gmail (though not outlook) to display both parts of the email, one after the other, as described here
As recommended there and elsewhere, I simply used EmailMultiAlternatives. The resulting code looks somewhat like this:
from django.core.mail import EmailMultiAlternatives
from email.mime.image import MIMEImage
# Load the image you want to send as bytes
img_data = open('logo.jpg', 'rb').read()
# Create the body with HTML. Note that the image, since it is inline, is
# referenced with the URL cid:myimage... you should take care to make
# "myimage" unique
html_content = '<p>Hello <img src="cid:myimage" /></p>'
text_content = 'Hello'
# Configure an EmailMultiAlternatives
msg = EmailMultiAlternatives('Subject Line', text_content, 'foo#bar.com', ['bar#foo.com'])
msg.attach_alternative(html_content)
# Now create the MIME container for the image
img = MIMEImage(img_data, 'jpeg')
img.add_header('Content-Id', '<myimage>') # angle brackets are important
img.add_header("Content-Disposition", "inline", filename="myimage") # David Hess recommended this edit
msg.attach(img)
# Finally, send the whole thing.
msg.send()

Related

Cannot get the image position in the body from Outlook Python API

I need some help, I am currently trying to do some scripting to automatize some tasks. I would like to fetch the received mail and send the body somewhere.
For that I am using win32.com with the outlook API.
But the issue is that if there is image in the body of the mail. I can't fetch it with the initial body. I Thought about using attachment, which is working but in the end, I have images and the body. But in the body I don't have the image position information... So I can only send the images and cannot set them correctly. Which can be difficult to understand if there is a lot of images...
So far the code looks like something like this :
import os
import win32com.client
outlook = win32com.client.Dispatch('outlook.application')
mapi = outlook.GetNamespace("MAPI")
inbox = mapi.GetDefaultFolder(6)
messages = inbox.Items
message = messages[len(messages) - 1]
body = message.body
attachments = message.attachments
attachment = attachments[0]
file_name = attachment.filename
path = "D:\\Documents\\tmp"
attachment.SaveAsFile(path + os.sep + attachment.FileName)
Do you have any help on this ?
Thanks for your help :)
PS : Do you know where I can find the Python documentation for outlook API, I just find the Rest API one and there is some difference from both. Or if we can get the source code to check directly.
In the message body you may check for <img/> tags. If any of them contains this tag with a file name prefixed with cid: string, for example:
<img src=cid:Filename/>
Then you deal with an embedded image which can be found in attached files.
Also you may check the PR_ATTACH_CONTENT_ID property on the attached files in the following way:
Const PR_ATTACH_CONTENT_ID = "http://schemas.microsoft.com/mapi/proptag/0x3712001F"
Function IsEmbedded(Att As Attachment) As Boolean
Dim PropAccessor As PropertyAccessor
Set PropAccessor = Att.PropertyAccessor
IsEmbedded = (PropAccessor.GetProperty(PR_ATTACH_CONTENT_ID) <> "")
End Function
The PropertyAccessor can help you to deal with low-level MAPI properties in Outlook.

How do I store a base64-encoded image in AppEngine?

I'm trying to create a blobstore entry from an image data-uri object, but am getting stuck.
Basically, I'm posting via ajax the data-uri as text, an example of the payload:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPA...
I'm trying to receive this payload with the following handler. I'm assuming I need to convert the data-uri back into an image before storing? So am using the PIL library.
My python handler is as follows:
import os
import urllib
import webapp2
from google.appengine.ext.webapp import template
from google.appengine.ext import blobstore
from google.appengine.ext.webapp import blobstore_handlers
from google.appengine.api import images
class ImageItem(db.Model):
section = db.StringProperty(required=False)
description = db.StringProperty(required=False)
img_url = db.StringProperty()
blob_info = blobstore.BlobReferenceProperty()
when = db.DateTimeProperty(auto_now_add=True)
#Paste upload handler
class PasteUpload(webapp2.RequestHandler):
def post(self):
from PIL import Image
import io
import base64
data = self.request.body
#file_name = data['file_name']
img_data = data.split('data:image/png;base64,')[1]
#Convert base64 to jpeg bytes
f = Image.open(io.BytesIO(base64.b64decode(img_data)))
img = ImageItem(description=self.request.get('description'), section=self.request.get('section') )
img.blob_info = f.key()
img.img_url = images.get_serving_url( f.key() )
img.put()
This is likely all kinds of wrong. I get the following error when posting:
img.blob_info = f.key()
AttributeError: 'PngImageFile' object has no attribute 'key'
What am I doing wrong here? Is there an easier way to do this? I'm guessing I don't need to convert the data-uri into an image to store as a blob?
I also want this Handler to return the URL of the image created in the blobstore.
There are a couple of ways to view your question and the sample code you posted, and it's a little confusing what you need because you are mixing strategies and technologies.
POST base64 to _ah/upload/...
Your service uses create_upload_url() to make a one-time upload URL/session for your client. Your client makes a POST to that URL and the data never touches your service (no HTTP-request-size restrictions, no CPU-time spent handling the POST). An App Engine internal "blob service" receives that POST and saves the body as a Blob in the Blobstore. App Engine then hands control back to your service in the BlobstoreUploadHandler class you write and then you can determine how you want to respond to the successful POST. In the case of the example/tutorial, PhotoUploadHandler redirects the client to the photo that was just uploaded.
That POST from your client must be encoded as multipart/mixed and use the fields shown in the example HTML <form>.
The multipart form can take the optional parameter, Content-Transfer-Encoding, and the App Engine internal handler will properly decode base64 data. From blob_upload.py:
base64_encoding = (form_item.headers.get('Content-Transfer-Encoding') ==
'base64')
...
if base64_encoding:
blob_file = cStringIO.StringIO(base64.urlsafe_b64decode(blob_file.read()))
...
Here's a complete multipart form I tested with cURL, based on the fields used in the example. I found out how to do this over at Is there a way to pass the content of a file to curl?:
myconfig.txt:
header = "Content-length: 435"
header = "Content-type: multipart/mixed; boundary=XX
data-binary = "#myrequestbody.txt"
myrequestbody.txt:
--XX
Content-Disposition: form-data; name="file"; filename="test.gif"
Content-Type: image/gif
Content-Transfer-Encoding: base64
R0lGODdhDwAPAIEAAAAAzMzM/////wAAACwAAAAADwAPAAAIcQABCBxIsODAAAACAAgAIACAAAAiSgwAIACAAAACAAgAoGPHACBDigwAoKTJkyhTqlwpQACAlwIEAJhJc6YAAQByChAAoKfPn0CDCh1KtKhRAAEAKF0KIACApwACBAAQIACAqwECAAgQAIDXr2DDAggIADs=
--XX
Content-Disposition: form-data; name="submit"
Submit
--XX--
and then run like:
curl --config myconfig.txt "http://127.0.0.1:8080/_ah/upload/..."
You'll need to create/mock-up the multipart form in your client.
Also, as an alternative to Blobstore, you can use Cloud Storage if you want to save a little on storage costs or have some need to share the data without your API. Follow the documentation for Setting Up Google Cloud Storage, and then modify your service to create the upload URL for your bucket of choice:
create_upload_url(gs_bucket_name=...)
It's a little more complicated than just that, but reading the section Using the Blobstore API with Google Cloud Storage in the Blobstore document will get you pointed in the right direction.
POST base64 directly to your service/handler
Kind of like you coded in the original post, your service receives the POST from your client and you then decide if you need to manipulate the image and where you want to store it (Datastore, Blobstore, Cloud Storage).
If you need to manipulate the image, then using PIL is good:
from io import BytesIO
from PIL import Image
from StringIO import StringIO
data = self.request.body
#file_name = data['file_name']
img_data = data.split('data:image/png;base64,')[1]
# Decode base64 and open as Image
img = Image.open(BytesIO(base64.b64decode(img_data)))
# Create thumbnail
img.thumbnail((128, 128))
# Save img output as blob-able string
output = StringIO()
img.save(output, format=img.format)
img_blob = output.getvalue()
# now you choose how to save img_blob
If you don't need to manipulate the image, just stop at b64decode():
img_blob = base64.b64decode(img_data)
An image object (https://cloud.google.com/appengine/docs/standard/python/refdocs/google.appengine.api.images) isn't a Datastore entity, so it has no key. You need to actually save the image to blobstore[2] or Google Cloud Storage[1] then get a serving url for your image.
[1] https://cloud.google.com/appengine/docs/standard/python/googlecloudstorageclient/setting-up-cloud-storage
[2] https://cloud.google.com/appengine/docs/standard/python/blobstore/

django - persisting an email to database to be able to send it later?

In my django project I want to store an email in the database to be able to retrieve it later and send it. I'm implementing a throttling mechanism for sending emails to many users.
I thought it would be as easy as storing 'to, from, subject, body' but then I realized there are attachments, multipart emails, etc, there is at least two classes EmailMessage and EmailMultiAlternatives... too many variables and options!
I thought of storing the raw email that one gets with the message() method, but then not sure how to construct the email back. Also tried pickle but got can't pickle lock objects.
Any ideas?
I'm going to make a guess based on your can't pickle lock objects message that you may be trying to pickle your objects with the default SMTP connection included.
Try it without it - grepping through the source it is the smtp module that has a self._lock. Pass connection=None to the constructor of the messages.
On Django 1.9, this works for me (Python2):
from django.core.mail import EmailMessage
from django.core.mail import EmailMultiAlternatives
import pickle
email = EmailMessage(
'Hello',
'Body goes here',
'from#example.com',
['to1#example.com', 'to2#example.com'],
['bcc#example.com'],
reply_to=['another#example.com'],
headers={'Message-ID': 'foo'},
connection=None,
)
email.attach("foo", [l for l in open(__file__)], 'text')
print len(pickle.dumps(email))
subject, from_email, to = 'hello', 'from#example.com', 'to#example.com'
text_content = 'This is an important message.'
html_content = '<p>This is an <strong>important</strong> message.</p>'
msg = EmailMultiAlternatives(subject, text_content, from_email, [to], connection=None)
msg.attach_alternative(html_content, "text/html")
print len(pickle.dumps(msg))
According to the Django code in messages.py, later when you call send() it will attempt to use get_connection() from django.core.mail to get your default connection if EmailMessage.connection is None.
....Alternatively, if you want to use json, this also worked with connection=None:
import json
print json.dumps(msg.__dict__)
print json.dumps(email.__dict__)
This means you could fairly easily write a JSONEncoder and JSONDecoder to serialize/deserialize your objects as well by basically using the __dict__ of the object.
More on JSON:
As I showed above, encoding the __dict__ makes the encoding easy. You could do msg.__dict__ = json.load(...), but what makes it difficult is the EmailMessage object must be created before you change its values. So you could initialize msg with an EmailMessage that has dummy values, and then assign the __dict__, or decode the JSON and construct the object explicitly by passing the arguments (...and functions) stored in the JSON to the constructor. This requires you to know the internals, though.
I'd go with pickle, although there are security implications.
This SO question covers some other alternatives as well.

Adding metadata to MIME multipart message

I'm sending emails using python. Currently my technique is this:
msg = email.mime.Multipart.MIMEMultipart()
msg["From"] = username
msg["To"] = recipient
msg["Subject"] = subject
mimeText = email.mime.Text.MIMEText(body, "html")
msg.attach(mimeText)
stringMsg = msg.as_string()
I would like to also add some metadata to the message - specifically, a unique identifier of the task the email is achieving, so that when it is checked later (possibly by a different service), I can avoid sending a duplicate email.
This metadata doesn't need to be completely secret or secure, just something that standard email clients don't render. Obviously there are the options of including a bogus BCC email address containing the id, or adding a hidden html node to the body.
<div style="display:none;">123456789</div>
But both of those seem quite "hacky". Is there anything like this that will get persisted and sent as part of the email, that I can check using imaplib later?
msg["secretMetadata"] = "123456789"
User defined fields are permitted and explained in RFC822. Basically you can prefix your custom field with X- and this will not conflict with any existing fields nor extension fields.
So, something like msg["X-secretMetadata"] = "123456789" should suffice.
It sounds like you might want to use X-headers. Information in the X-headers of an email message is usually used for application-specific purposes, and this information is usually not displayed by mail clients.

GAE - how to use blobstore stub in testbed?

My code goes like this:
self.testbed.init_blobstore_stub()
upload_url = blobstore.create_upload_url('/image')
upload_url = re.sub('^http://testbed\.example\.com', '', upload_url)
response = self.testapp.post(upload_url, params={
'shopid': id,
'description': 'JLo',
}, upload_files=[('file', imgPath)])
self.assertEqual(response.status_int, 200)
how come it shows 404 error? For some reasons the upload path does not seem to exist at all.
You can't do this. I think the problem is that webtest (which I assume is where self.testapp came from) doesn't work well with testbed blobstore functionality. You can find some info at this question.
My solution was to override unittest.TestCase and add the following methods:
def create_blob(self, contents, mime_type):
"Since uploading blobs doesn't work in testing, create them this way."
fn = files.blobstore.create(mime_type = mime_type,
_blobinfo_uploaded_filename = "foo.blt")
with files.open(fn, 'a') as f:
f.write(contents)
files.finalize(fn)
return files.blobstore.get_blob_key(fn)
def get_blob(self, key):
return self.blobstore_stub.storage.OpenBlob(key).read()
You will also need the solution here.
For my tests where I would normally do a get or post to a blobstore handler, I instead call one of the two methods above. It is a bit hacky but it works.
Another solution I am considering is to use Selenium's HtmlUnit driver. This would require the dev server to be running but should allow full testing of blobstore and also javascript (as a side benefit).
I think Kekito is right, you cannot POST to the upload_url directly.
But if you want to test the BlobstoreUploadHandler, you can fake the POST request it would normally received from the blobstore in the following way. Assuming your handler is at /handler :
import email
...
def test_upload(self):
blob_key = 'abcd'
# The blobstore upload handler receives a multipart form request
# containing uploaded files. But instead of containing the actual
# content, the files contain an 'email' message that has some meta
# information about the file. They also contain a blob-key that is
# the key to get the blob from the blobstore
# see blobstore._get_upload_content
m = email.message.Message()
m.add_header('Content-Type', 'image/png')
m.add_header('Content-Length', '100')
m.add_header('X-AppEngine-Upload-Creation', '2014-03-02 23:04:05.123456')
# This needs to be valie base64 encoded
m.add_header('content-md5', 'd74682ee47c3fffd5dcd749f840fcdd4')
payload = m.as_string()
# The blob-key in the Content-type is important
params = [('file', webtest.forms.Upload('test.png', payload,
'image/png; blob-key='+blob_key))]
self.testapp.post('/handler', params, content_type='blob-key')
I figured that out by digging into the blobstore code. The important bit is that the POST request that the blobstore sends to the UploadHandler doesn't contain the file content. Instead, it contains an "email message" (well, informations encoded like in an email) with metadata about the file (content-type, content-length, upload time and md5). It also contains a blob-key that can be used to retrieve the file from the blobstore.

Categories

Resources