How to include dynamically created PDF files in Mailgun emails using Python Django

I’m working on migrating our Django app from the standard email system to Mailgun API. Everything works fine for regular emails, but I’m stuck when trying to send emails with PDF attachments that are created on the fly.

The PDFs are generated dynamically by our application and aren’t saved to disk. Here’s how I currently create the PDF:

document = BytesIO()
create_report(document, parameters)
document.seek(0)
file_attachment = MIMEApplication(document.read())
file_attachment.add_header("Content-Disposition", "attachment", filename=report_name)
document.close()

Then I send it using Django’s built-in email:

from django.core.mail import EmailMultiAlternatives
email_msg = EmailMultiAlternatives(email_subject, plain_text, sender_email, recipient_list)

if html_text:
    email_msg.attach_alternative(html_text, "text/html")

if file_attachment:
    email_msg.attach(file_attachment)

email_msg.send()

This approach works perfectly. Now I need to do the same thing with Mailgun’s API but I can’t figure out the right way to format the attachment.

I tried this but it doesn’t work:

response = requests.post(mailgun_endpoint, auth=("api", api_key), data=email_data, files=file_attachment)

The request works fine when I remove the files parameter. The email_data dict contains all the standard fields like to, from, subject, and tags.

Can someone help me figure out the correct way to structure the PDF attachment for Mailgun’s API?

Had this exact issue last month! You’re passing a MIMEApplication object but Mailgun wants plain file data. After creating your BytesIO document, just do: files = {'attachment': (report_name, document.getvalue(), 'application/pdf')} and pass it to requests normally. Skip the MIMEApplication wrapper completely - Mailgun’s API doesn’t need it.

You’re mixing Django’s email formatting with Mailgun’s API requirements. I hit this exact same issue when switching to Mailgun - MIMEApplication objects don’t work with the requests library’s files parameter.

Here’s what fixed it after hours of debugging:

document = BytesIO()
create_report(document, parameters)
document.seek(0)
pdf_data = document.read()
document.close()

files = [('attachment', (report_name, pdf_data, 'application/pdf'))]
response = requests.post(mailgun_endpoint, auth=("api", api_key), data=email_data, files=files)

The trick is storing the PDF data in a variable first, then passing it as a tuple with filename and content type. Just make sure you read the BytesIO content before closing it or you’ll get empty attachments.

This has worked perfectly in production for our dynamic invoices and reports.

You’re passing a MIMEApplication object to the files parameter, but Mailgun wants a different format. It expects raw file data as a tuple. After generating your PDF, try this instead: python document = BytesIO() create_report(document, parameters) document.seek(0) # Skip MIMEApplication - prepare for Mailgun directly files = [("attachment", (report_name, document.read(), "application/pdf"))] response = requests.post(mailgun_endpoint, auth=("api", api_key), data=email_data, files=files) The files parameter wants a list of tuples: field name, filename, file content, and MIME type. I’ve used this setup for months in production with dynamically generated PDFs - works perfectly. Just remember to call document.read() only once since it consumes the stream.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.