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。
备注:内容仅供参考。
