How do I account for sending multiple (or no) attachments (via request.FILES) in Django using the Gmail API, so I can store the legacy message ID (ie "FBf…MiD") for future retrieval to reply in the same thread/thread_ID? I am switching from the SMTP (to be fully deprecated by Google) by Django's email.send(), which appears to be significantly different in how it handles file types from Gmail's API upload.
This already works with Django's email. How can I convert it to use Gmail's API for multiple-type attachments?
I am specifically struggling to find out how to attach multiple files of different types to the Gmail API. (unsure if I am supposed to serialize the EmailMultiAlternatives, or how to do so)
This is where the magic/headaches happens, and have issues. I am not sure why I feel like I'm reinventing the wheel, and can't find any packages for the server side that make it easier. I can get the 'attachments' to 'attach' to the email and show up in the UI, but the files are not clickable because they are 0 bytes, and sometimes empty MIME types given their actual type.

def create_body_message_with_attachments(
sender,
to,
subject,
msgHtml,
msgPlain,
attachmentFiles,
cc=None,
bcc=None,
):
"""Create a message for an email.
Args:
sender: Email address of the sender.
to: Email address of the receiver.
subject: The subject of the email message.
msgHtml: Html message to be sent
msgPlain: Alternative plain text message for older email clients
attachmentFile: The path to the file to be attached.
Returns:
An object containing a base64url encoded email object.
"""
# allow either one recipient as string, or multiple as list
if isinstance(to, list):
to = ", ".join(to)
if cc:
if isinstance(cc, list):
cc = ", ".join(cc)
if bcc:
if isinstance(bcc, list):
bcc = ", ".join(bcc)
message = MIMEMultipart("mixed")
message["to"] = to
message["from"] = sender
message["subject"] = subject
…
# allow either one attachment as string, or multiple as list
if not isinstance(attachmentFiles, list):
attachmentFiles = [attachmentFiles]
# attachmentFiles
# [
# <InMemoryUploadedFile: Screenshot.jpg (image/jpg)>,
# <InMemoryUploadedFile: Screenshot2.png (image/png)>,
# <_io.BufferedReader name='/path/to/quote.pdf'>
# ]
messageA = MIMEMultipart("alternative")
messageR = MIMEMultipart("related")
messageR.attach(MIMEText(msgHtml, "html"))
messageA.attach(MIMEText(msgPlain, "plain"))
messageA.attach(messageR)
message.attach(messageA)
for attachment in attachmentFiles:
# Trying to separate the filename from the file content for different types
# File Name
if hasattr(attachment, "temporary_file_path"):
filename = os.path.basename(attachment.temporary_file_path())
elif hasattr(attachment, "name"):
filename = os.path.basename(attachment.name)
else:
filename = os.path.basename(attachment)
# File Contents
# Content Data
if isinstance(attachment, str) and os.path.exists(attachment):
content_type, _ = mimetypes.guess_type(attachment) or (content_type, None)
with open(attachment, "rb") as f:
file_data = f.read()
# Handle BufferedReader (BytesIO)
elif isinstance(attachment, io.BytesIO):
file_data = attachment.getvalue() # Ensure correct byte data is read
# Handle Django InMemoryUploadedFile
elif isinstance(attachment, InMemoryUploadedFile):
content_type = attachment.content_type or content_type
file_data = attachment.read()
# Type
# I have tried different ways to get the MIME type,
# but am unsure of the most pythonic way to do so, many opions out there
mim = magic.Magic(mime=True)
try:
c_t = mim.from_file(filename)
except OSError as e:
# Magic needs to differentiate?! between files and streams
c_t = mim.from_buffer(attachment.read(2048))
# Magic often returns 'application/x-empty'
print(f"file: {attachment} with {content_type=} and {c_t=}")
main_type, sub_type = content_type.split("/", 1)
if main_type == "text":
# Unsure if I have to open and close for each type
# fp = open(attachment, "rb")
msg_attachment = MIMEText(file_data, _subtype=sub_type)
# fp.close()
elif main_type == "image":
# Unsure if I have to open and close for each type
# fp = open(attachment, "rb")
msg_attachment = MIMEImage(file_data, _subtype=sub_type)
# fp.close()
elif main_type == "audio":
msg_attachment = MIMEAudio(file_data, _subtype=sub_type)
elif main_type == "application" and sub_type == "pdf":
msg_attachment = MIMEApplication(attachment.read(), _subtype=sub_type)
else:
msg_attachment = MIMEBase(main_type, sub_type)
msg_attachment.set_payload(attachment.read())
encoders.encode_base64(msg_attachment)
msg_attachment.add_header(
'Content-Disposition', 'attachment', filename=f'{filename}'
)
message.attach(msg_attachment)
raw = base64.urlsafe_b64encode(message.as_bytes())
raw = raw.decode()
body = {"raw": raw}
return body
Trying to passing the EmailMultiAlternatives directly to the Gamil API I get RFC822 payload message string or uploading message via /upload/* URL required", 'domain': 'global', 'reason': 'invalidArgument' and trying to encode the message to JSON I get errors similar to 'utf-8' codec can't decode byte 0x93 in position 10: invalid start byte. Not sure how to pass correctly encoded attachments and a message to the API.
Notes(that help differentiate other SO questions)
- Not using SMTP, as that has already begun sundowning/deprecating, and I am are trying to avoid using the google account setting of "less secure apps"
- Using
django.core.mail's EmailMultiAlternatives to compose and send, whichmail.send(fail_silently=True)does not return the message, or give an ID on the gmail server, so finding the exact email is not functional/deterministic given the smilarities - In Django, when the upload files come through the
request.FILES, they are a of typeInMemoryUploadedFilefromdjango.core.files.uploadedfile¿with the potential to be different MIME Types?, but the internal attachment of a generated PDF is of type_io.BufferedReader. This is presumably causing my headaches - using Python 3.11.7 in dev and Python 3.12.3 in prod, and aware that in 3.13 that
mimetypes.guess_type()will be deprecated per this - Sending the EmailMultiAlternatives directly to the Gmail API
send().execute()I get an error ofObject of type EmailMultiAlternatives is not JSON serializable - The MIME information from the received email is:
.
--===============2268240127970561189==
Content-Type: image/png
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Screenshot 2025-03-12 at 1.54.01PM.png"
--===============2268240127970561189==
Content-Type: image/png
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Screenshot 2025-03-12 at 4.44.11PM.png"
--===============2268240127970561189==
Content-Type: application/octet-stream
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Q-DFD-12345-031525-0XEH-1153.pdf"
--===============2268240127970561189==--
- I have searched many SO questions, walkthroughs, API guides, etc…. I believe there not to be any current examples that satisfy all the following: Django InMemoryUploadedFile, Gmail API, multiple attachments and of different types, non SMTP, traceable email/threadIDs, and mostly updated python. Feel free to share if you find one.
Other files that may help understand flow
view.py
class MakeQuoteWithItems(…, CreateView):
def post(self, request, *args, **kwargs):
# init a Django Model
quote = QuoteClass(request)
# with a generated PDF, and send the associated email
quote.make_and_email_quote()
Current working way (SMTP)
from django.core.mail import EmailMultiAlternatives
def emailTheQuote(quote, attachments=None):
from_email = …
subject = …
email_context = …
html_content = render_to_string("email/tempalte.html", email_context, quote.request)
to_emails = … (string, [] or whatever format needed)
…
#
# Current working way sending through SMTP
#
# Django settings somehow gets the email out
# EMAIL_USE_TLS = False
# EMAIL_HOST = os.getenv("EMAIL_PROVIDER_SMTP_NOTIFIER_HOST")
# EMAIL_HOST_USER = os.getenv("EMAIL_PROVIDER_SMTP_NOTIFIER_EMAIL")
# EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PROVIDER_SMTP_NOTIFIER_PASSWORD")
# EMAIL_PORT = 587
#
mail = EmailMultiAlternatives(
subject,
strip_tags(html_content),
to=to_emails,
bcc=bcc_emails,
from_email=from_email,
)
mail.attach_alternative(html_content, "text/html")
mail.attach(
quote.pdf_canvas._filename, open(quote.canvas._filename, "rb").read(), "application/pdf"
)
if quote.attachments:
for attachment in quote.attachments:
mail.attach(attachment._name, attachment.read(), attachment.content_type)
mail.send(fail_silently=False)
# But can't get a hold of the mail going out to get it's ID to reply to later
Proposed way (Gmail API)
def emailTheQuote(quote, attachments=None):
from_email = …
subject = …
# Instead -
gmail_email = create_and_send_gmail_message(
from_email,
to_emails,
subject,
html_content,
text_content,
quote.request,
attachmentFiles=attachments, # user uploaded and the generated PDF
)
# Some Django Model to query to get the email.message_id and email.thread_id
GmailEmailLog.objects.create(
gmail_email=gmail_email,
message_id=gmail_email.message_id,
# Thread ids are the same for the first message in a thread, but remain
# the same for all subsequent messages sent within the thread
thread_id=gmail_email.thread_id,
quote_id=quote.id
…
)
return gmail_email
helper_functions.py
def create_and_send_gmail_message(
sender,
to,
subject,
msgHtml,
msgPlain,
request,
attachmentFiles=None,
cc=None,
bcc=None,
):
if attachmentFiles:
message_body = create_body_message_with_attachments(sender,to,subject,msgHtml,msgPlain,attachmentFiles,cc,bcc,)
else:
# This is not an issue
message_body = create_messag_body_html(sender,to,subject,msgHtml,msgPlain,cc,bcc,)
result = SendMessageInternal(message_body, request)
return result
def SendMessageInternal(incoming_message_body, request):
credentials = get_credentials(request)
service = discovery.build("gmail", "v1", credentials=credentials)
user_id = settings.EMAIL_GMAIL_USERID
try:
msg = (
service.users()
.messages()
.send(
userId=user_id,
body=incoming_message_body,
)
.execute()
)
print(f"Message Id: {msg['id']}")
return msg
except errors.HttpError as error:
print(f"An error occurred: {error}")
return f"{error=}"
return "OK"
EMAIL Raw files
THIS IS THE PREVIOUS WORKING EMAIL, sudo-anonymised
Delivered-To: [email protected]
Received: by 20:…:ca with SMTP id sp132931;
Wed, 15 Feb 2023 19:45:01 -0500 (ET)
X-Received: by 20:…:5c with SMTP id 20….….…06;
Wed, 15 Feb 2023 19:45:01 -0500 (ET)
ARC-Seal: i=3; a=rsa-sha256; t=1676522553; cv=pass;
d=google.com; s=arc-20160816;
b=FBh… …zfk==
ARC-Message-Signature: i=3; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
h=message-id:date:to:from:subject:mime-version:dkim-signature
:delivered-to;
bh=Mn…tY=;
b=zA…fO+wb…9Z+X+GZ…l8+QC…w3+rs…RW+ch…DQ==
ARC-Authentication-Results: i=3; mx.google.com;
dkim=pass [email protected] header.s=emailProvider header.b=er…YA;
arc=pass (i=2 dkim=pass dkdomain=production_domain.com);
spf=neutral (google.com: 209.….41 is neither permitted nor denied by best guess record for domain of my_=gmailAccount=gmail.com@personal_test_domain.org) smtp.mailfrom="my_=gmailAccount=gmail.com@personal_test_domain.org"
Return-Path: <my_=gmailAccount=gmail.com@personal_test_domain.org>
Received: from mail-sor-f41.google.com (mail-sor-f41.google.com. [209.….41])
by mx.google.com with SMTPS id a5…23.ip.ad.dr.e.ss
for <[email protected]>
(Google Transport Security);
Wed, 15 Feb 2023 19:45:01 -0500 (ET)
Received-SPF: neutral (google.com: 209.….41 is neither permitted nor denied by best guess record for domain of my_=gmailAccount=gmail.com@personal_test_domain.org) client-ip=209.….41;
Authentication-Results: mx.google.com;
dkim=pass header.i=@production_domain.com header.s=emailProvider header.b=er…YA;
arc=pass (i=2 dkim=pass dkdomain=production_domain.com);
spf=neutral (google.com: 209.….41 is neither permitted nor denied by best guess record for domain of my_=gmailAccount=gmail.com@personal_test_domain.org) smtp.mailfrom="my_=gmailAccount=gmail.com@personal_test_domain.org"
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20210112;
h=message-id:date:to:from:subject:mime-version:dkim-signature
:delivered-to:x-gm-message-state:from:to:cc:subject:date:message-id
:reply-to;
bh=f3lQdqf4Na3Uj8i45MnNgmpkfOMWo5t8aOYO8suzazE=;
b=6m1jghQ3ciL+qfXsAIFeM5EZ54BIjxX5aCebYBX/neCEaKXoVycDZAC0bAl4FpeiNv
UwkST9cVeWQweICf6HKwQ1J2rQSELlhRLjTqNvM5pBbPMQZXc+g/wrATZd+2botCqZO/
Y6zog9xQWHs/IXeZYV2T+H1AoBZIow9DiYhvl9nD8/zjwAsC5BfvANVQVhpmERKPksYN
9T0L9SX83HokibmO3bZzb5DTK1eJGQDeysgznNHDERZIHTF7W6rq+7lVoqG3wj7auX3F
jsVllk6E7yXxtuBeJ3PQO9ldtaNU/TxaLy3u7Cq2sqlaR5ttqS003cIO/M1IZo/Kr3oT
NtqQ==
X-Gm-Message-State: AO0yUKUng8IxDTR5Pa4seNHrOauqQx1ULgwNakQLuDabR5Df/CR+pbfh 52r6R/0O8UXEuIp4MustAWlEXSMAeWz8hcEWmUwGn5aF1s8PEz6f+UvEcEg=
X-Received: by 2002:a50:bb48:0:b0:4ac:b8e1:7410 with SMTP id y66-20020a50bb48000000b004acb8e17410mr2364158ede.6.1676522552160;
Wed, 15 Feb 2023 19:45:01 -0500 (ET)
X-Forwarded-To: [email protected]
X-Forwarded-For: chris@personal_test_domain.org [email protected]
Delivered-To: jerome@personal_test_domain.org
Received: by 2002:a54:2789:0:b0:1f9:34b:de17 with SMTP id n9csp225579ecp;
Wed, 15 Feb 2023 19:45:01 -0500 (ET)
X-Google-Smtp-Source: AK7set9WzXuuJYVtYFjdtn1LrQnmLKtM2tv4acM4vnzclcNgqHEAS0FQQMXr004S9ccLFIJOWep/
X-Received: by 2002:a17:902:e5c3:b0:19a:a80e:a6d5 with SMTP id u3-20020a170902e5c300b0019aa80ea6d5mr5626264plf.23.1676522545554;
Wed, 15 Feb 2023 19:45:01 -0500 (ET)
ARC-Seal: i=2; a=rsa-sha256; t=1676522545; cv=pass;
d=google.com; s=arc-20160816;
b=fWktl8SgvQeHJZHh5vkX/H+BDnrVsEHD2gJQ7z5kAAcO+0G3MzKJiksm5Q3Tma46/s
vPBk1I9HeFFlmOVDNfZzrpSqNtKzrbRh6KDSFgumiAl/IYWzyul/Y9izd3uWs0IoQhBT
+SutRjEE5ZqgR5bLbNbBBaAkpVIWIbj3PEHxHR3fIrykqReaC0S9x/IlcTBRXdji0I/Y
HbVFL9oiyLU3JoV5HUuU//oQbT648XPTZeawUxP41Hz8PJDYj3riyo32XmlxRNLXRvTZ
+Zb2x6EPOQezwDXb7XR8CgAIQ4KJvxIl7IuArmOXQTRf45gCZywhEMHUxKtW0o/IkZNw
qzjA==
ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
h=message-id:date:to:from:subject:mime-version:dkim-signature;
bh=f3lQdqf4Na3Uj8i45MnNgmpkfOMWo5t8aOYO8suzazE=;
b=ED7g4/sG1svFF6LH/QjWHutvwM/kYqlW3n6IUmuCdvqUHsRR9JFqwwE4Sj/1Xjf8qA
gUUUWgGWSxsVC6Oqoqt48PjmRGuVq8y5LYIIGNHgfe/FOScOYl2w1mJup16MwTrXlq51
QF9jJe6fGH9P/uBLUC0QwpwFhAmHVjbwMXsw1zoobjmkKNHRERJWUTzLjNWiiVYmeVog
CvwzW49kRjiapIlQnGCnIje7c4ywLtsU9z6g6VIxwyHoJHEWMO4HdHbGsiwx6LL3VT5O
rv0bJ5lHZnpZnnhWZES+Q8ewr/BcKB/0bSFclfDMPBtbKWM4AVF1dfNcIjRTh8cRdPV2
/LNQ==
ARC-Authentication-Results: i=2; mx.google.com;
dkim=pass header.i=@production_domain.com header.s=emailProvider header.b=er…YA;
arc=pass (i=1);
spf=neutral (google.com: 23.83.212.48 is neither permitted nor denied by best guess record for domain of development-quote@production_domain.com) smtp.mailfrom=DEVELOPMENT-quote@production_domain.com
Return-Path: <DEVELOPMENT-quote@production_domain.com>
Received: from dog.elm.relay.mailchannels.net (dog.elm.relay.mailchannels.net. [23.83.212.48])
by mx.google.com with ESMTPS id e10-20020a170902784a00b00194a1f665b5si293781pln.570.2023.02.15.20.42.23
(version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128);
Wed, 15 Feb 2023 19:45:01 -0500 (ET)
Received-SPF: neutral (google.com: 23.83.212.48 is neither permitted nor denied by best guess record for domain of development-quote@production_domain.com) client-ip=23.83.212.48;
X-Sender-Id: emailProvider|x-authsender|[email protected]_domain.com
Received: from relay.mailchannels.net (localhost [127.0.0.1]) by relay.mailchannels.net (Postfix) with ESMTP id 3E82F8810AA; Thu, 16 Feb 2023 04:42:23 +0000 (UTC)
Received: from pdx1-sub0-mail-a306.emailProvider.com (unknown [127.0.0.6]) (Authenticated sender: emailProvider) by relay.mailchannels.net (Postfix) with ESMTPA id 8A…14; Thu, 16 Feb 2023 04:42:22 +0000 (UTC)
ARC-Seal: i=1; s=arc-2022; d=mailchannels.net; t=76…54; a=rsa-sha256; cv=none; b=qk…nC/bA…de/ Dc…mf/s8…3u+XQ…Wx+8c/D…g/SR…up+zl…zf…==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=mailchannels.net; s=arc-2022; t=16…25; h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
to:to:cc:mime-version:mime-version:content-type:content-type:
dkim-signature; bh=lQ…sY=; b=eK…+/Qu…+m2CZ9JA7 lp…+tY…+lvjD 7T…+b…JK…+dh…+D/yytOf Yt…+ht/Kf0rAlyIeuNn7tT9Wu1au4/dR…+ch…w1/gz…+7Z==
ARC-Authentication-Results: i=1; rspamd-b9c55767f-ngqs4; auth=pass smtp.auth=emailProvider smtp.mailfrom=DEVELOPMENT-quote@production_domain.com
X-Sender-Id: emailProvider|x-authsender|[email protected]_domain.com
X-MC-Relay: Neutral
X-MailChannels-SenderId: emailProvider|x-authsender|[email protected]_domain.com
X-MailChannels-Auth-Id: emailProvider
X-Relation-Continue: 21…07
X-MC-Loop-Signature: 22…30:69…94
X-MC-Ingress-Time: 65…54
Received: from pdx1-sub0-mail-a306.emailProvider.com (pop.emailProvider.com [ip.ad.dr.es.s1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384) by ip.ad.dr.es.s9 (trex/6.7.1); Thu, 16 Feb 2023 04:42:23 +0000
Received: from 1.0.0…0.0.0.0.0.0.ip6.arpa (cpe-71-68-87-31.carolina.res.rr.com [ip.ad.dr.es.s]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) (Authenticated sender: [email protected]_domain.com) by pdx1-sub0-mail-a306.emailProvider.com (Postfix) with ESMTPSA id Mk…Qz; Wed, 15 Feb 2023 19:45:01 -0500 (ET)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=production_domain.com; s=emailProvider; t=16…25; bh=lQ…az=; h=Content-Type:Subject:From:To:Date; b=er…YA…gV/wq…9G
Ky…z3/iA…QG/eb…we+aY/4p…cD/y8…7d
WtajPHs+5Z…YB+xX…5D
Z6…UP+j2…KJ
I2…ue/6H…l6/Rx…83+RZ…8D/zD…mP
jB…iC/w==
Content-Type: multipart/mixed; boundary="===============867…5309=="
MIME-Version: 1.0
Subject: Quote - QUOTE-FSL-021523-2342
From: DEVELOPMENT-quote@production_domain.com
To: other@personal_test_domain.org, second@personal_test_domain.org, third@personal_test_domain.org, [email protected]
Date: Thu, 16 Feb 2023 04:42:19 -0000
Message-ID: <867…5309@1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa>
--===============45…72==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
High Level Notes:
quote with attachments
Purchase Order: na
Regular thank you text.
Regular body Text.
--===============45…72==
Content-Type: application/pdf
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="/path/to/quote.pdf"
--===============45…72==
Content-Type: image/jpeg
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="_bosch_cp1.jpeg"
--===============45…72==
Content-Type: image/jpeg
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="_cr_valve_bosch.jpeg"
--===============45…72==
Content-Type: application/pdf
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="prod_SERVER_quote.pdf"
--===============45…72==--
THIS IS THE GMAIL API MESSAGE, received with three "empty" attachments, sudo-anonymised
Received: from 21…17 named unknown by gmailapi.google.com with
HTTPREST; Mon, 17 Mar 2025 09:34:56 -0500
Received: from 21…17 named unknown by gmailapi.google.com with HTTPREST; Mon, 17 Mar 2025 09:34:56 -0500
Content-Type: multipart/mixed; boundary="===============50…13=="
MIME-Version: 1.0
From: [email protected]
To: q@dev_domain.com
Cc: cc@dev_domain.com
Subject: Quote - Q-DFD-12345-031725-L0KQ-1034
Date: Mon, 17 Mar 2025 09:34:56 -0500
Message-Id: <oB…[email protected]>
--===============50…13==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
DEFAULT MESSAGE No message body text was provided - Have a nice day
--===============50…13==
Content-Type: image/png
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Screenshot 2025-03-12 at 4.44.11PM.png"
--===============50…13==
Content-Type: image/png
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Screenshot 2025-03-12 at 1.54.01PM.png"
--===============50…13==
Content-Type: application/pdf
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Q-DFD-12345-031725-UNIQ-1034.pdf"
--===============50…13==--
Thanks in advance :)

emailTheQuote(with gmail api) function the variable input isattachmentsbut you passattachmentFiles=email_attachmentswhereemail_attachmentsdefined[<InMemoryUploadedFile: Screenshot.jpg (image/jpg)>, <InMemoryUploadedFile: Screenshot2.png (image/png)>, <_io.BufferedReader name='/path/to/quote.pdf'>]. some are user uploaded, one is the generated pdf. Ill update the argument var name to match.