带附件的账单

给事务邮件附上一份 PDF,并清楚大小上限在哪里。

附件放在同一个 POST /emails 请求里,base64 编码。每个附件是文件名、编码后的字节和内容类型;邮件会被组装成标准的 MIME multipart,并和其他邮件一样签名。

用 curl

把文件编码成一行——解码器不接受 base64 中间的换行,而 GNU base64 默认每 76 个字符换一行。去掉换行的写法在 Linux 和 macOS 上都一样。

PDF=$(base64 < invoice-1042.pdf | tr -d '\n')

curl -X POST https://api.rovela.dev/emails \
  -H "Authorization: Bearer re_your_sending_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-1042" \
  -d @- <<EOF
{
  "from": "Acme 账务 <billing@mail.acme.com>",
  "to": "accounts@customer.com",
  "subject": "Acme 账单 INV-1042",
  "html": "<p>你好,</p><p>附件是你 9 月的账单。应付金额:<strong>¥3,480.00</strong>,请于 10 月 15 日前支付。</p>",
  "text": "你好,\n\n附件是你 9 月的账单。应付金额:¥3,480.00,请于 10 月 15 日前支付。",
  "attachments": [
    {
      "filename": "INV-1042.pdf",
      "content": "$PDF",
      "content_type": "application/pdf"
    }
  ]
}
EOF

不写 content_type 的话,这个部分会以 application/octet-stream 发送,很多客户端会把它显示成未知文件而不是直接预览。记得设置。

用 Node.js

import { readFile } from "node:fs/promises"

export async function sendInvoice(invoice, pdfPath) {
  const pdf = await readFile(pdfPath)

  const res = await fetch("https://api.rovela.dev/emails", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.ROVELA_API_KEY,
      "Content-Type": "application/json",
      // 账单号就是天然的幂等键:任务重试也不会重复发账单。
      "Idempotency-Key": "invoice-" + invoice.number,
    },
    body: JSON.stringify({
      from: "Acme 账务 <billing@mail.acme.com>",
      to: invoice.billingEmail,
      reply_to: "billing@acme.com",
      subject: "Acme 账单 " + invoice.number,
      html: "<p>附件是你的账单 " + invoice.number + "。</p>",
      text: "附件是你的账单 " + invoice.number + "。",
      attachments: [
        {
          filename: invoice.number + ".pdf",
          content: pdf.toString("base64"),
          content_type: "application/pdf",
        },
      ],
    }),
  })
  if (!res.ok) throw new Error("invoice email failed: " + res.status)
  return (await res.json()).id
}

reply_to 让回信去到一个真正有人看的邮箱,而邮件本身仍然从已验证的发信子域名发出。

大小上限

整个 JSON 请求的上限是 2 MB。base64 会让文件变大约三分之一,所以所有附件加起来大约只能有 1.5 MB。超过上限的请求会直接得到 413,什么都不会发出。

普通的账单或收据绰绰有余。更大的东西——报表、导出文件、带扫描页的对账单——放到你自己的存储里,邮件里发链接。这对收件人也更友好:大附件会被复制进它到达的每一个邮箱,而且有些企业邮件网关会去掉或隔离它们。

好习惯

  • 关键信息在正文里也写一遍——单号、金额、截止日期。预览窗格和手机通知不会打开 PDF。
  • 文件名不要带空格或个人信息;它会出现在每一个客户端里,也会跟着转发出去。
  • 账单每张一个请求,不要走 /emails/batch:批量请求共享同一个 2 MB 上限,而且一条无效就会让整批被拒。见 批量与定时发送。