当前位置:首页 > 文章列表 > Golang > Go教程 > Go go-message 怎样同时写入纯文本正文和附件

Go go-message 怎样同时写入纯文本正文和附件

来源:17golang原创 2026-10-30 23:45:29 0浏览 收藏

用 go-message 同时写纯文本正文与附件,先用 mail.CreateWriter 创建 multipart/mixed 邮件,再用 CreateSingleInline 写一个 text/plain 正文部件,用 CreateAttachment 写附件。每个部件写完都检查 Close,最后关闭外层邮件 Writer,不能把附件字节直接追加到正文末尾。

官方项目地址:https://github.com/emersion/go-message

假设报表服务要生成一封“周报与明细”:收件人先看到一句说明,再下载 weekly-detail.csv。关键在于让收件端区分正文、附件和各自的编码。下面的原创示例只生成邮件字节,不连接 SMTP、不发送邮件。

先保护正文、附件和完整邮件三份资产

正文应保持可阅读,附件应保持原始内容,最终交付的邮件不能只是写到一半的缓冲区。常见风险包括部件归属错误、重复编码、忽略关闭错误,以及把不可信文件路径或头部直接带入交付流程。

示例固定发件地址、收件地址、主题和附件名,不让外部输入控制邮件头与本地文件路径。正文限64KiB、附件限8MiB,这是示例自行选择的上限,不是库或邮箱服务的通用限制。实际业务仍需限制并发及总量。

问题影响处理方式
附件直接追加正文不能识别独立附件创建Attachment部件
先Base64再写默认编码器附件被编码两次写原始字节
忽略Close编码末尾或邮件边界不完整逐层处理关闭错误
失败后仍输出缓冲区半成品进入交付错误时返回nil数据

选对容器:mixed 下放正文和附件

根容器 multipart/mixed 包含 text/plain 和 text/csv。前者的 Content-Disposition 是 inline,后者是 attachment,附件显示名称为 weekly-detail.csv。它们是兄弟部件,不是正文字符串里的两个段落。

下图分为“邮件容器”和“内容部件”:根关联两个部件,纯文本关联inline,CSV关联attachment,attachment关联文件名。关系表示静态组成,不是发送步骤。

纯文本与CSV附件的MIME静态组成
结构图:multipart/mixed 容纳独立正文和附件,各部件有自己的类型与处置方式。

仅一个纯文本版本时,使用外层 Writer 的 CreateSingleInline 即可。若以后需要纯文本与 HTML 两种等价正文,再考虑 CreateInline 提供的 alternative 容器。独立附件与同一正文的不同表现形式,属于不同层次。

构建示例:部件关闭后再写下一个部件

在独立 Go 模块中加入 github.com/emersion/go-message 依赖。以下代码是 maildemo 包中的构建函数,使用 errors.Join,需要 Go 1.20 或更高版本;它不是单独运行的 main 程序。

package maildemo

import (
    "bytes"
    "errors"
    "fmt"
    "io"
    "time"
    "unicode/utf8"

    "github.com/emersion/go-message/mail"
)

// 同时保留写入与编码器收尾错误;需Go 1.20或更高版本。
func writePart(w io.WriteCloser, data []byte) error {
    _, err := io.Copy(w, bytes.NewReader(data))
    return errors.Join(err, w.Close())
}

// BuildMail 仅构建EML,不连接SMTP;地址与附件名为固定演示值。
func BuildMail(text string, csv []byte) ([]byte, error) {
    if len(text) > 64*1024 || len(csv) > 8*1024*1024 {
        return nil, fmt.Errorf("示例限制:正文64KiB,附件8MiB")
    }
    if !utf8.ValidString(text) {
        return nil, fmt.Errorf("正文必须为有效UTF-8")
    }
    var h mail.Header
    h.SetAddressList("From", []*mail.Address{
        {Name: "报表服务", Address: "reports@example.com"},
    })
    h.SetAddressList("To", []*mail.Address{
        {Name: "收件示例", Address: "reader@example.net"},
    })
    h.SetSubject("周报与明细")
    h.SetDate(time.Now())
    if err := h.GenerateMessageID(); err != nil {
        return nil, fmt.Errorf("消息编号: %w", err)
    }

    // 先在私有缓冲区构建,全部收尾成功后才对外交付。
    var buf bytes.Buffer
    mw, err := mail.CreateWriter(&buf, h)
    if err != nil { return nil, fmt.Errorf("邮件头: %w", err) }

    var bh mail.InlineHeader
    bh.SetContentType("text/plain", map[string]string{"charset": "utf-8"})
    body, err := mw.CreateSingleInline(bh)
    if err != nil { return nil, fmt.Errorf("正文部件: %w", err) }
    if err := writePart(body, []byte(text)); err != nil {
        return nil, fmt.Errorf("正文写入与关闭: %w", err)
    }

    var ah mail.AttachmentHeader
    ah.SetContentType("text/csv", map[string]string{"charset": "utf-8"})
    ah.SetFilename("weekly-detail.csv")
    attachment, err := mw.CreateAttachment(ah)
    if err != nil { return nil, fmt.Errorf("附件部件: %w", err) }
    // 原始CSV字节直接交给部件Writer,不预先Base64。
    if err := writePart(attachment, csv); err != nil {
        return nil, fmt.Errorf("附件写入与关闭: %w", err)
    }
    if err := mw.Close(); err != nil {
        return nil, fmt.Errorf("邮件收尾: %w", err)
    }
    return buf.Bytes(), nil
}

在同包调用函数中使用:

// 在同包调用函数中构建邮件,错误时不得保存或发送结果。
raw, err := BuildMail("本周统计已完成,明细见附件。\r\n",
    []byte("item,count\r\nreviewed,12\r\n"))
if err != nil { return err }
// raw可交给独立的存储或SMTP模块;此处不执行投递。
_ = raw

writePart 的 io.Copy 负责写入,Close 负责结束当前编码器,errors.Join 保留两处错误。正文结束后才创建附件,附件结束后才关闭 mw。不能把全部部件的 defer Close 都放在同一个长函数中,导致跨过下一个部件才收尾。

外层 mw.Close 完成 multipart 最终边界。bytes.Buffer 没有文件描述符要关闭,需要结束的是上层 MIME Writer。示例失败时丢弃私有内存缓冲;如果改为文件或网络输出,调用方还必须管理底层资源及失败清理,不能认为 MIME Writer 会代为关闭文件或连接。

编码与输入边界:附件写原始字节即可

mail 包默认为文本 inline 部件选择 quoted-printable,为附件选择 base64,除非调用方已经显式指定传输编码。底层 Writer 根据声明编码,因此示例不手写boundary或Base64。charset=utf-8 是字符集说明,并不是把任意旧编码自动转成UTF-8的开关。

示例验证text的UTF-8有效性,csv则假定由可信导出模块生成。给CSV声明UTF-8前,要保证其字节确实如此。PDF或其他二进制附件需要相应MIME类型和文件名,仍应写原始字节。SetFilename设置邮件里的显示名称,不负责读取本地文件。

内存构建适合小附件,优点是完整构建前不对外暴露结果;代价是原始数据与编码邮件可能同时驻留,base64也会增加体积。8MiB原始附件不等于8MiB最终邮件。大型邮件可考虑受控流式读取或临时文件暂存,但需保留失败清理和最终交付条件。

第二张图分为“构建数据”和“交付边界”:正文输入、CSV输入、私有缓冲区属于前者;部件Writer、外层Writer、完整EML属于后者。两份输入关联部件Writer,部件Writer关联外层Writer,外层Writer使用私有缓冲区,完整EML由成功结束的缓冲区提供。这是对象归属说明,不是执行证据。

邮件输入Writer与缓冲区的静态关联
结构图:部件与外层收尾均成功后才交付完整EML;邮件生成与SMTP投递是不同职责。

记录失败阶段,明确交付条件

日志记录任务ID、阶段、字节数和错误类别即可,避免打印正文、附件或收件人隐私信息。示例错误前缀区分邮件头、正文部件、附件部件和收尾,便于定位放弃产物的位置。Message-ID提供邮件标识,不自动完成业务任务去重。

按代码逻辑,成功返回的内容应是一封mixed邮件,解码后包含一句中文正文和weekly-detail.csv,附件原始内容中有reviewed,12。boundary与Message-ID会生成,不应要求每次整封EML逐字相同。此处为结构推导,本文制作过程中未实际运行示例。

  • 正文的text/plain与UTF-8字节约定明确。
  • 附件是独立attachment部件,名称与内容一致。
  • 部件写入、编码关闭和外层收尾错误均被处理。
  • 只在成功结束后交付结果,后续另记保存与投递状态。

拿到raw就说明邮件发出了吗?没有,raw只是格式化字节。信封地址、SMTP认证、传输失败和最终投递由另一模块管理。

零字节附件是否一定失败?不是,MIME可以表达空部件,是否允许空附件由业务合同决定。

是否需要手动设置传输编码?普通场景先用默认值。若确有特殊需求,应确保声明与写入方式一致,避免双重编码。

技术依据:官方mail API、mail Writer实现、底层Writer实现。周报场景、限额与构建函数为本文原创设计。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go go-imap 的 Unselect 与带删除标记的退出有什么区别Go go-imap 的 Unselect 与带删除标记的退出有什么区别
上一篇
Go go-imap 的 Unselect 与带删除标记的退出有什么区别
Go go-message 写入邮件后为什么要先关闭附件再关闭总写入器
下一篇
Go go-message 写入邮件后为什么要先关闭附件再关闭总写入器
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    799次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    792次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    878次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    848次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    680次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码