Django中使用Gmail API发送多类型附件时附件无效(0字节)及获取Gmail消息ID的技术问询
Django中使用Gmail API发送多类型附件时附件无效(0字节)及获取Gmail消息ID的技术问询
我现在需要从Django自带的SMTP发送(email.send())切换到Gmail API(因为Google已经开始逐步废弃SMTP,且不想启用“不安全应用”设置),遇到两个核心问题:
- 如何正确处理多个或无附件(来自
request.FILES的InMemoryUploadedFile,以及生成PDF时的_io.BufferedReader类型),避免出现附件显示0字节、MIME类型错误的情况? - 如何获取Gmail服务器上的遗留消息ID(比如"FBf…MiD"),以便后续回复时能关联到同一线程?
之前用Django的EmailMultiAlternatives发送完全正常,但切换到Gmail API后,在处理多类型附件时碰到了瓶颈——我试过序列化EmailMultiAlternatives但没找到正确方法,现在能让附件在邮件UI中显示,但文件都是0字节,有时MIME类型还不对,就像这张图显示的那样:
核心问题代码
我当前用于构建邮件内容的函数如下,也是问题集中出现的地方:
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 # 获取文件名 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) # 获取文件内容 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() # 处理BytesIO类型 elif isinstance(attachment, io.BytesIO): file_data = attachment.getvalue() # 确保读取正确的字节数据 # 处理Django InMemoryUploadedFile elif isinstance(attachment, InMemoryUploadedFile): content_type = attachment.content_type or content_type file_data = attachment.read() # 获取MIME类型 # 尝试了多种方法,但不确定最Pythonic的方式,可选方案太多 mim = magic.Magic(mime=True) try: c_t = mim.from_file(filename) except OSError as e: # Magic需要区分文件和流? c_t = mim.from_buffer(attachment.read(2048)) # Magic经常返回'applications/x-empty' print(f"file: {attachment} with {content_type=} and {c_t=}") main_type, sub_type = content_type.split("/", 1) if main_type == "text": msg_attachment = MIMEText(file_data, _subtype=sub_type) elif main_type == "image": msg_attachment = MIMEImage(file_data, _subtype=sub_type) 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
其他尝试的错误情况
我还尝试过直接把EmailMultiAlternatives传给Gmail API的send().execute(),结果收到错误:
"RFC822 payload message string or uploading message via /upload/* URL required", 'domain': 'global', 'reason': 'invalidArgument'
如果尝试把消息编码成JSON,又会碰到编码错误:
'utf-8' codec can't decode byte 0x93 in position 10: invalid start byte
完全搞不懂怎么把正确编码的附件和消息传给API。
关键补充说明
- 不使用SMTP:因为Google已经开始废弃SMTP,且不想启用“不安全应用”设置
- Django的
EmailMultiAlternatives.send()不返回Gmail消息ID:无法确定发送后的邮件,没法关联后续回复线程 - 附件类型复杂:
request.FILES上传的是InMemoryUploadedFile,生成的PDF是_io.BufferedReader,这应该是问题根源 - Python版本:开发环境用3.11.7,生产环境用3.12.3,知道3.13中
mimetypes.guess_type()会被废弃 - 直接传
EmailMultiAlternatives到API会报错:Object of type EmailMultiAlternatives is not JSON serializable
收到的邮件MIME信息
我查看了收到的邮件的MIME结构,发现存在重复的Content-Transfer-Encoding字段,且部分附件的MIME类型不正确:
--===============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==--
我搜过很多技术问答、教程和API指南,但没找到同时满足Django InMemoryUploadedFile、Gmail API、多类型附件、非SMTP、可追踪消息/线程ID、适配较新Python版本的示例,如果你有相关资源欢迎分享。
相关代码片段
views.py
class MakeQuoteWithItems(…, CreateView): def post(self, request, *args, **kwargs): # 初始化Django模型 quote = QuoteClass(request) # 生成PDF并发送关联邮件 quote.make_and_email_quote()
当前可用的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 = … (支持字符串、列表等格式) … # 当前可用的SMTP发送方式 # Django配置自动处理邮件发送 # EMAIL_USE_TLS = False # EMAIL_HOST = os.getenv("EMAIL_PROVIDER_SMTP_NOTIFIER_HOST") # EMAIL_HOST_USER = os.getenv(...) # ...
备注:内容来源于stack exchange,提问作者chris Frisina
相关产品推荐
相关产品推荐

