首页 / 资讯中心 / 文章详情

Java 发送邮件带附件:从 JavaMail 到 Jakarta Mail 的选型与落地

Java 发送邮件带附件:从 JavaMail 到 Jakarta Mail 的选型与落地 ★ FEATURED ARTICLE
简介这是一份面向Java开发者、企业级应用开发人员及自动化流程设计者的邮件发送实战资源聚焦于通过JavaMail API实现带正文与附件的邮件发送功能。资源包共41个文件以9个java源码、11个class编译文件、7个jar依赖库为主另含xml配置、log日志及html页面等辅助文件压缩包约1.33MB结构完整可直接导入运行。已有816人学习下载适合需要快速掌握SMTP配置、Session会话管理、MimeMessage与Multipart组合、附件添加及Transport发送等核心知识点的初中级开发者。通过研读源码读者可理解邮件正文与附件混合发送的完整实现逻辑掌握认证配置、异常处理与调试模式开启等实用技巧并参考其目录组织方式构建自己的邮件发送模块也可在此基础上扩展HTML正文、抄送密送及邮件头定制等进阶功能。1. Java 发送邮件带附件从 JavaMail 到 Jakarta Mail 的选型与落地很多项目里都有这样一个需求系统跑完批处理要把结果报表发给运营用户注册成功要发一封带 PDF 的欢迎信监控告警触发要把日志文件当附件甩出去。这些场景用 Java 实现绕不开的就是邮件协议和附件编码。我见过不少团队在这件事上翻车——本地跑得好好的一上生产附件就乱码或者被邮件服务商直接拒收。问题往往不在业务代码而在对 MIME 结构和 SMTP 交互的理解。这篇笔记拆的是一个「Java 发送邮件内容附件」的完整实现资源覆盖纯文本、HTML 正文、多附件、中文文件名、SSL/TLS 连接这些实际会遇到的点。适合正在做通知模块的后端也适合想搞清楚邮件底层结构的人。下面从依赖选型开始一步步把可复现的代码和参数讲透。2. 依赖选型与 MIME 结构为什么你的附件会变成一串乱码2.1 JavaMail 与 Jakarta Mail 的版本分水岭Java 发邮件这件事底层标准是 SMTP、POP3、IMAP但 Java 侧的操作接口经历过一次命名空间迁移。早期用的是javax.mail对应 artifact 是javax.mail:javax.mail-api加com.sun.mail:javax.mail实现。从 Jakarta EE 9 开始包名整体迁移到jakarta.mailartifact 变成jakarta.mail:jakarta.mail-api和org.eclipse.angus:angus-mailAngus Mail 是 Jakarta Mail 的参考实现接替了原来的 Sun 实现。选型上有个硬边界如果你的项目还在 JDK 8 且短期内不升级用javax.mail最稳社区资料多坑都被踩平了。如果项目已经上了 JDK 17 或 Spring Boot 3.x那必须用jakarta.mail因为 Spring 的JavaMailSender在 6.x 里已经切到 jakarta 命名空间混用会直接ClassNotFoundException。我一般会先看项目里 Spring Boot 的版本2.x 配 javax3.x 配 jakarta不要试图在一个工程里同时引两套。Maven 依赖这样写以 jakarta 为例dependency groupIdjakarta.mail/groupId artifactIdjakarta.mail-api/artifactId version2.1.3/version /dependency dependency groupIdorg.eclipse.angus/groupId artifactIdangus-mail/artifactId version2.0.3/version scoperuntime/scope /dependencyjakarta.mail-api只提供接口angus-mail是运行时实现scope 设成 runtime 就够了编译期不需要。版本号不要照抄去中央仓库看当前稳定版我这里写的是写稿时的常见版本。2.2 MIME 多部分结构附件不是「贴上去」的很多人以为发附件就是调一个setAttachment方法实际上 JavaMail 里没有这种 API。附件的本质是把邮件体构造成一个Multipart容器里面装若干BodyPart第一个是正文纯文本或 HTML后面每个附件是一个独立的BodyPart通过DataHandler包装文件流并设置Content-Disposition: attachment; filename...。如果正文和附件没有正确分层就会出现两种典型故障一是附件内容被当成正文显示出来满屏 base64 乱码二是附件能收到但文件名变成?UTF-8?B?...?这种编码串。前者是Multipart的 subtype 用错了后者是文件名没有做 RFC 2231 编码。理解这个结构后面写代码就是填空。常见的 MIME 类型对应关系场景Multipart subtype正文 BodyPart 类型纯文本 附件mixedtext/plainHTML 附件mixedtext/htmlHTML 内嵌图片 附件related 嵌套在 mixed 内text/html image/*纯文本 HTML 双版本alternativetext/plain text/html大部分「正文附件」的需求用mixed就够了。只有正文里要内嵌图片比如邮件签名里的 logo才需要related而且通常是mixed里套一个related结构别搞反。2.3 会话与连接参数别把密码硬编码进代码Session对象承载了 SMTP 服务器地址、端口、认证信息。生产环境绝对不要把授权码写在代码里常见做法是走配置中心或环境变量。下面是一个典型的 Session 构建Properties props new Properties(); props.put(mail.smtp.host, smtp.example.com); props.put(mail.smtp.port, 465); props.put(mail.smtp.auth, true); props.put(mail.smtp.ssl.enable, true); // 465 端口用隐式 SSL props.put(mail.smtp.connectiontimeout, 5000); // 连接超时毫秒 props.put(mail.smtp.timeout, 5000); // 读超时 props.put(mail.smtp.writetimeout, 5000); // 写超时 Session session Session.getInstance(props, new Authenticator() { Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(user, password); } });参数说明465 端口是 SMTPS走mail.smtp.ssl.enabletrue587 端口是 STARTTLS走mail.smtp.starttls.enabletrue两者不能混。超时三个参数建议都设默认是无限等待网络抖动时线程会一直挂住。Session.getInstance和getDefaultInstance的区别在于后者会复用多账号场景下容易串我一般用getInstance。3. 正文与附件组装一份能直接抄的发送代码3.1 构建 MimeMessage 的完整流程下面这段代码覆盖了 HTML 正文加两个附件的场景中文文件名做了编码处理。可以直接拿去改服务器地址和账号。import jakarta.mail.*; import jakarta.mail.internet.*; import jakarta.activation.DataHandler; import jakarta.activation.FileDataSource; import java.io.File; import java.util.Properties; public class MailSender { public void sendWithAttachments(String host, String user, String pass, String to, String subject, String htmlBody, File[] attachments) throws Exception { Properties props new Properties(); props.put(mail.smtp.host, host); props.put(mail.smtp.port, 465); props.put(mail.smtp.auth, true); props.put(mail.smtp.ssl.enable, true); props.put(mail.smtp.connectiontimeout, 5000); props.put(mail.smtp.timeout, 5000); Session session Session.getInstance(props, new Authenticator() { Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(user, pass); } }); MimeMessage message new MimeMessage(session); message.setFrom(new InternetAddress(user)); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setSubject(subject, UTF-8); // 正文部分 MimeBodyPart bodyPart new MimeBodyPart(); bodyPart.setContent(htmlBody, text/html; charsetUTF-8); // 整个邮件是一个 mixed 容器 MimeMultipart multipart new MimeMultipart(mixed); multipart.addBodyPart(bodyPart); // 逐个追加附件 for (File file : attachments) { MimeBodyPart attachPart new MimeBodyPart(); FileDataSource source new FileDataSource(file); attachPart.setDataHandler(new DataHandler(source)); // 用 MimeUtility 编码中文文件名避免收件端乱码 attachPart.setFileName(MimeUtility.encodeText(file.getName(), UTF-8, B)); multipart.addBodyPart(attachPart); } message.setContent(multipart); Transport.send(message); } }逻辑说明先建正文MimeBodyPartsetContent第二个参数必须带 charset否则中文正文在部分客户端会乱。然后建MimeMultipart(mixed)先加正文再加附件顺序不能反——正文必须是第一个 part否则某些客户端会把第一个附件当正文预览。附件用FileDataSource包装setFileName外面套MimeUtility.encodeText这是中文文件名不乱码的关键。最后Transport.send内部会自动 connect、send、close如果要做连接复用可以手动Transport.connect后循环发。参数上要注意setSubject也要指定 UTF-8不然标题里的中文同样会乱。InternetAddress.parse支持逗号分隔的多个收件人但生产环境建议对地址做校验非法地址会抛AddressException。3.2 附件大小与流式处理上面用的是FileDataSource文件会整个读进内存再编码。附件超过 10MB 时堆内存压力明显尤其是并发发送场景。常见做法是改用ByteArrayDataSource配合流式读取或者直接对文件流做分块编码。但要注意SMTP 协议本身对单封邮件大小有限制多数服务商在 25MB 到 50MB 之间超过会被拒。我的经验是单封邮件附件总量控制在 10MB 以内大文件走对象存储生成临时链接把链接放正文里而不是硬塞附件。如果确实要发大附件可以设置mail.smtp.allow8bitmimetrue减少 base64 膨胀base64 会让体积增大约 33%但需要服务端支持 8BITMIME 扩展。不确定的话就保持默认。3.3 抄送、密送与回复地址实际业务里经常要抄送主管、密送归档邮箱。MimeMessage提供了对应的 RecipientTypemessage.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setRecipients(Message.RecipientType.CC, InternetAddress.parse(cc)); message.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(bcc)); message.setReplyTo(InternetAddress.parse(replyTo));CC 和 BCC 的区别在于 BCC 不会出现在邮件头里适合归档或不想让收件人互相看到地址的场景。setReplyTo用于设置回复地址和 From 可以不同比如系统发信用 noreply 发件但回复指向客服邮箱。这几个方法都支持多个地址用逗号分隔的字符串传进去即可。4. 避坑与排查附件乱码、被拒收、超时的真实原因4.1 中文附件名变成编码串现象收件人收到的附件文件名显示为?UTF-8?B?5rWL6KV?这样的字符串而不是中文。原因setFileName直接传了原始中文名没有做 RFC 2231 或 RFC 2047 编码。不同邮件客户端对未编码文件名的解析行为不一致有的能猜对有的直接显示原始字节。解决统一用MimeUtility.encodeText(fileName, UTF-8, B)包一层。B 表示 base64 编码也可以用 Q 表示 quoted-printable中文场景下 B 更稳。注意编码后的文件名长度会增加超长文件名可能被截断建议业务侧限制文件名在 100 字符以内。4.2 邮件被服务商拒收或进垃圾箱现象代码没报错Transport.send正常返回但收件人没收到或者进了垃圾邮件文件夹。原因常见有三类。一是发件人地址和认证账号不一致被 SPF 校验拦截二是邮件主题或正文含敏感词、大量链接触发内容过滤三是发送频率过高被判定为垃圾邮件行为。解决发件人地址必须和 SMTP 认证账号的域名一致或者该域名在服务商处配置了 SPF 记录。批量发送时加间隔比如每封之间 sleep 200 到 500 毫秒。正文里避免短链接和夸张营销词。如果是企业邮箱找管理员确认发信额度很多服务商对单账号有每小时上限。4.3 连接超时或 SSL 握手失败现象抛MessagingException: Could not connect to SMTP host或SSLHandshakeException。原因端口和加密方式不匹配是最常见的。465 端口必须用ssl.enabletrue587 端口必须用starttls.enabletrue两个都开或都不开都会失败。另外 JDK 版本过低时TLS 1.2 以下的协议被服务端禁用也会握手失败。解决先确认服务商要求的端口和加密方式然后检查 JDK 版本JDK 8 早期版本需要手动开启 TLS 1.2。超时参数一定要设否则网络不通时线程会永久阻塞。排查时可以把mail.debug设为 true控制台会打印完整的 SMTP 交互过程能看到卡在哪一步。4.4 附件收到但打不开现象附件能下载但打开提示文件损坏或者大小变成 0。原因一是发送时文件流被提前关闭FileDataSource在Transport.send之前文件被删除或移动二是编码方式和服务端不兼容8bit 和 base64 混用导致内容截断。解决确保发送期间文件不被其他线程改动必要时先复制到临时目录。不要手动设置Content-Transfer-Encoding让 JavaMail 根据内容自动选择。如果必须指定统一用 base64。4.5 正文里的 HTML 被当成纯文本显示现象收件人看到的是h1标题/h1这样的源码而不是渲染后的样式。原因setContent的第二个参数没写text/html或者写成了text/plain。解决正文 part 必须显式声明text/html; charsetUTF-8。如果同时要兼容不支持 HTML 的老客户端可以用alternative容器装纯文本和 HTML 两个版本客户端会自动选支持的版本显示。5. 进阶技巧连接复用、异步发送与发送结果校验5.1 连接复用降低握手开销Transport.send每次都会新建连接、认证、发送、关闭。批量发信时这个开销很可观。可以手动管理 TransportTransport transport session.getTransport(smtp); transport.connect(host, user, pass); try { for (MimeMessage msg : messages) { transport.sendMessage(msg, msg.getAllRecipients()); } } finally { transport.close(); }这样一次连接发多封认证只做一次。注意sendMessage的第二个参数是收件人数组必须显式传否则不会发送。连接复用时要处理异常某封失败后连接可能已断开需要重连。5.2 异步发送与线程池邮件发送是 IO 密集型操作放在主流程里会拖慢接口响应。常见做法是丢到线程池异步执行配合重试。但要注意线程池大小SMTP 服务商通常对并发连接有限制一般 5 到 10 个线程足够。重试策略上认证失败不要重试网络超时可以重试 2 到 3 次间隔递增。5.3 发送结果校验Transport.send不抛异常只代表服务端接受了邮件不代表对方一定收到。要确认投递结果只能靠退信NDR或者查服务商的发送日志。如果业务要求强一致可以在发送后记录 message-id通过 IMAP 去查已发送文件夹但这会引入额外复杂度。多数场景下捕获MessagingException并记录日志就够了。5.4 一个容易忽略的细节邮件大小限制不同服务商对单封邮件大小限制不同有的是 25MB有的是 50MB。附件 base64 编码后体积会膨胀约三分之一所以实际能发的原始文件比限制值要小。我一般会在发送前算一下所有附件总大小超过阈值就拒绝并提示走其他通道。这个校验放在业务层比等 SMTP 报错再处理要友好得多。从那以后我每次写邮件发送模块都会先把 MIME 结构在草稿纸上画一遍确认正文和附件的层级关系再动手写代码。中文文件名编码和超时参数这两项我强制自己每次都检查一遍因为这两个坑踩一次就要花半天排查。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站