PAYATHON 2026

Refund 邮件添加图片附件时出现 splitlines 错误

支付小周

结论

EmailMessage 的第二个参数 body 必须是字符串,不能直接传入 Refund 模型实例。

当前代码是:

msg = EmailMessage('Refund', r, ...)

应先根据 r 的字段生成纯文本或 HTML 字符串,再把这个字符串作为邮件正文传入。图片附件代码和当前错误无关。

错误原因

创建邮件时,EmailMessage 会把正文交给 Python 的邮件处理模块。该模块需要调用正文的 splitlines() 方法。

但 r 是一个 Refund 对象:

r = Refund(...)

它不是字符串,也没有 splitlines() 方法,所以会报错:

AttributeError: 'Refund' object has no attribute 'splitlines'

即使模型定义了:

def __str__(self):
    return self.email

Django 也不保证此处会自动执行 str(r)。况且按照当前的 __str__() 实现,str(r) 只会返回邮箱地址,无法生成包含全部退款信息的邮件正文。

推荐修改方式

先构造邮件正文,再创建 EmailMessage:

from django.core.mail import EmailMessage
from email.mime.image import MIMEImage
from django.utils.html import escape
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Refund


class RefundView(APIView):
    permission_classes = [permissions.IsAuthenticated]

    def post(self, request, format=None):
        r = Refund(
            name=request.data["name"],
            email=request.data["email"],
            address=request.data["address"],
            date=request.data["date"],
            message=request.data["message"],
            amount=request.data["amount"],
            image=request.FILES["image"],
        )

        body = f"""
        <h2>Refund request</h2>
        <p><strong>Name:</strong> {escape(r.name)}</p>
        <p><strong>Email:</strong> {escape(r.email)}</p>
        <p><strong>Address:</strong> {escape(r.address)}</p>
        <p><strong>Date:</strong> {escape(str(r.date))}</p>
        <p><strong>Amount:</strong> {escape(r.amount)}</p>
        <p><strong>Message:</strong><br>{escape(r.message)}</p>
        """

        msg = EmailMessage(
            subject="Refund",
            body=body,
            from_email="user@gmail.com",
            to=["admin@gmail.com"],
            headers={"Message-ID": "foo"},
        )
        msg.content_subtype = "html"

        image = request.FILES["image"]
        mime_image = MIMEImage(
            image.read(),
            _subtype=image.content_type.split("/", 1)[-1],
        )
        mime_image.add_header(
            "Content-Disposition",
            "attachment",
            filename=image.name,
        )
        msg.attach(mime_image)

        msg.send(fail_silently=False)

        return Response({"success": "Sent"})

这里用 request.data 读取普通字段。对于 DRF 视图,这通常比直接使用 request.POST 更合适,也能兼容 DRF 已解析的请求数据。

是否需要保存 Refund

原代码只创建了 Refund 对象,并没有将它保存到数据库。如果需要持久化退款申请,应在数据验证通过后调用:

r.save()

更合适的做法是通过 DRF Serializer 验证并保存字段:

serializer = RefundSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
r = serializer.save()

保存后,再用 r.name、r.email 等字段构建邮件正文。

如果只发送邮件而不保存记录,就不必调用 save()。这种情况下也可以直接使用 serializer.validated_data,无需创建一个未保存的模型对象。

纯文本邮件的简单写法

如果不需要 HTML,可以直接使用普通字符串,也不必设置 content_subtype:

body = (
    f"Name: {r.name}\n"
    f"Email: {r.email}\n"
    f"Address: {r.address}\n"
    f"Date: {r.date}\n"
    f"Amount: {r.amount}\n"
    f"Message: {r.message}\n"
)

msg = EmailMessage(
    "Refund",
    body,
    "user@gmail.com",
    ["admin@gmail.com"],
)

改成下面这样也能避免 splitlines 错误:

body = str(r)

不过,根据当前的 __str__() 实现,正文只会包含 r.email,不适合作为完整的退款申请内容。

图片类型不要通过文件名最后一个字符判断

原代码使用了:

_subtype=str(r.image.name)[-1]

这段代码取得的是文件名的最后一个字符。例如,photo.jpg 得到的是 g,并不是有效的图片子类型。

处理上传文件时,可以优先从 content_type 中获取类型:

image_subtype = image.content_type.split("/", 1)[-1]

如果不能信任客户端提供的 MIME 类型,就需要在服务端验证图片格式。也可以让 MIMEImage 尝试识别类型:

mime_image = MIMEImage(image.read())

如果无法识别,它可能会抛出异常。因此在生产环境中,仍应通过 Serializer 或上传校验逻辑限制允许的图片格式和文件大小。

Content-ID 通常用于在 HTML 正文中内嵌图片:

<img src="cid:image">

如果图片只是供收件人下载的普通附件,应使用 Content-Disposition: attachment,无需设置 Content-ID。

备注:内容仅供参考。