You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django中使用Gmail API发送多类型附件时附件无效(0字节)及获取Gmail消息ID的技术问询

Django中使用Gmail API发送多类型附件时附件无效(0字节)及获取Gmail消息ID的技术问询

我现在需要从Django自带的SMTP发送(email.send())切换到Gmail API(因为Google已经开始逐步废弃SMTP,且不想启用“不安全应用”设置),遇到两个核心问题:

  1. 如何正确处理多个或无附件(来自request.FILES的InMemoryUploadedFile,以及生成PDF时的_io.BufferedReader类型),避免出现附件显示0字节、MIME类型错误的情况?
  2. 如何获取Gmail服务器上的遗留消息ID(比如"FBf…MiD"),以便后续回复时能关联到同一线程?

之前用Django的EmailMultiAlternatives发送完全正常,但切换到Gmail API后,在处理多类型附件时碰到了瓶颈——我试过序列化EmailMultiAlternatives但没找到正确方法,现在能让附件在邮件UI中显示,但文件都是0字节,有时MIME类型还不对,就像这张图显示的那样:
邮件中0字节附件的UI截图

核心问题代码

我当前用于构建邮件内容的函数如下,也是问题集中出现的地方:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 19:09:34