How do we account for sending multiple (or no) attachments (via request.FILES) in Django using the Gmail API, so we 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) of Django's email.send(), which appears to be significantly different in how it handles file types. 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) 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 emailContactsTheOptionsQuote(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 emailContactsTheOptionsQuote(quote, attachments=None): from_email = … subject = … # Instead - gmail_email = create_and_send_gmail_message( from_email, to_emails, subject, html_content, text_content, options_quote.request, attachmentFiles=email_attachments, ) # 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" 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 from 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 one clickable, and sometimes empty given their 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 Notes(that help differentiate other SO questions) Not using SMTP, as that has already begun sundowning/deprecating, and we are trying to use the google account setting of "less secure apps" Using django.core.mail's EmailMultiAlternatives to compose and send, which mail.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 type InMemoryUploadedFile from django.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 of Object 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==-- Thanks in advance Continue reading...