1. 项目概述
1.1 这个需求背后的真实场景
说到Java Web发邮件,很多刚入行的同学第一反应是拿个Spring Boot项目,加个starter,填几个配置,一个JavaMailSender注入进来就能跑了。但如果你还在学原生Servlet、JSP的老一套技术栈,或者你所在项目有"尽量不引入框架依赖"的约束,那"原生Java Web实现邮件发送"这个需求就会突然变得不那么轻松。
我这次做的东西就是在这个背景下展开的:一个纯Servlet + JavaMail API实现的邮件发送功能,页面提交表单,后端接到请求后通过SMTP协议把邮件发出去。没有Spring,没有Maven插件,没有一堆自动配置,每一步都手动拼起来。这种做法的价值在于,它能让你真正理解一封邮件从浏览器表单到对方收件箱之间到底经历了哪些环节,而不是被框架的黑盒隐藏起来。
这套代码适合谁参考?三类人:第一类是正在学Java Web、想搞明白Servlet如何承接请求并完成任务的同学;第二类是在校做课程设计、需要一个完整可演示的邮件发送模块的开发者;第三类是工作中遇到了"不能用框架、必须原生实现"这种约束,需要快速落地的工程师。看完这篇文章,你能拿到可以直接跑起来的源码,也能明白文件编码问题、SMTP认证失败、端口不通这些坑是怎么踩出来的。
1.2 准备聊哪些内容
文章会分为这样几个部分:先说邮件发送涉及的基础知识——SMTP协议是什么、JavaMail API和它的依赖关系;然后讲我如何规划项目结构和关键配置;接着给出完整源码,逐段拆解发送逻辑;再聊Servlet如何接入Http请求,以及JSP页面上的表单细节;最后把实际开发中遇到的典型问题和排查思路整理成清单。每个段落都会附带我在动手过程中的实际操作记录和经验体会,不是纯理论,都是可复现的步骤。
2. 邮件发送的核心机制与JavaMail选型
2.1 SMTP协议到底是干什么的
邮件发送这件事,本质上就是你的程序作为邮件客户端,把一封构造好的邮件"扔"给一台SMTP服务器,再由这台服务器负责把邮件投递到收件人的邮件服务器上。整个过程遵循SMTP协议——Simple Mail Transfer Protocol,简单邮件传输协议。它定义了两台服务器之间、客户端与服务器之间的对话规则。
你可以把SMTP服务器想象成一个快递中转站。你写好了包裹(邮件),填好了收件人地址(收件邮箱),然后把它交给中转站(SMTP服务器),中转站根据地址去投递。如果收件人邮箱恰好也在同一家中转站的管辖范围,那它直接投进对方信箱;如果不在,它会转交给下一个中转站。发邮件这个动作,从用户视角看只是点了"发送"按钮,但背后经历了至少一次TCP连接、一次身份认证(通常需要)、一次邮件数据传输。
第三方免费邮箱提供的SMTP服务器,通常会在服务器上开一个认证机制。你用自己的邮箱账号去登录对方服务器,通过验证之后才允许发信。这也是为什么实际开发中你几乎不可能只写代码就能把邮件发出去——你必须有一个真实的、开启了SMTP服务权限的邮箱账户。大多数主流邮箱厂商默认不开放这个功能,需要你手动在设置里开启SMTP/POP3/IMAP服务,然后生成一个"授权码"而非直接使用登录密码。这个细节非常关键,我第一次测试时用账号密码直接认证,服务器返回535错误,查了半天才意识到要用授权码。
2.2 JavaMail API的结构与依赖
Java原生发邮件依赖的核心API是javax.mail包,这是JavaEE规范中JavaMail的一部分。它不是JDK自带的,需要额外引入两个JAR包:
mail.jar:提供Session、Message、Transport等主要类activation.jar:处理MIME类型数据的辅助包,比如附件、图片内嵌这类二进制的数据
如果你的项目用Maven管理依赖,引入这两个坐标就行;但如果是原生Servlet项目、没有Maven,那你得手动把JAR包放在WEB-INF/lib目录下。我在开发时使用的是最传统的手动部署方式——自己在本地下载了mail.jar和activation.jar,放在lib目录,然后让Tomcat在启动时加载它们。这种方式的好处是完全可控,不用依赖网络构件仓库,对课程设计或者内网开发环境非常友好。
JavaMail API工作的核心对象有三个:
Session:整个邮件会话的配置中心,持有服务器地址、端口、认证开关、网络超时这些参数MimeMessage:代表一封邮件,负责组装发件人、收件人、主题、正文、附件Transport:真正把MimeMessage发给SMTP服务器并完成数据传输的通道
这三者的关系可以简化理解为一个类似快递流程:你(Session)约好了快递公司(SMTP服务器)的规矩,填好快递单(MimeMessage),然后交给快递员(Transport)带走。在后面的代码部分,你就能看到这三者依次登场。
2.3 为什么不用第三方SDK而自己写
现在很多云服务商提供了HTTP接口发邮件的SDK,比如通过API密钥调用发信接口,代码量更小、送达率也更高。那为什么我这次坚持用原生JavaMail?原因很简单:题目要求是"原生JavaWeb实现",这是技术学习路径上必须走的一步。用云服务商的SDK,屏蔽了底层的SMTP交互,学习价值大打折扣。而手工写JavaMail代码,你能看清:
- SMTP服务器的连接是Socket层面的TCP通信
- 邮件内容本质上是MIME格式的文本数据
- 附件是多段MIME结构拼接起来的
这些底层认知,对于将来调试复杂的邮件问题——比如附件格式错乱、邮件被识别为垃圾内容——会提供很大的帮助。当然,如果项目上线到生产环境,我的建议还是优先用成熟的邮件服务商或者企业内部邮件服务器,原生JavaMail更适合学习和可控的自建场景。后者在投递成功率、反垃圾评分上,靠的是背后整个基础设施的积累。
3. 项目结构和基础配置准备
3.1 项目目录怎么组织
我这次的项目结构完全按照Servlet规范的标准布局来,没有多余的花架子。核心文件有这些:
mail-demo/ ├── src/ │ └── com/example/mail/ │ ├── MailSender.java // 邮件发送核心逻辑 │ ├── SendMailServlet.java // 接收表单请求的Servlet │ └── MailConfig.java // 邮件配置常量(服务器、账号、授权码) ├── web/ │ ├── WEB-INF/ │ │ ├── lib/ │ │ │ ├── mail.jar │ │ │ └── activation.jar │ │ └── web.xml │ └── sendMail.jsp // 发送页面这个结构是手动建出来的,没有用IDE的Web项目向导。之所以手动搭,是因为很多初学者在IDE里点"新建Dynamic Web Project"时,会面临各种版本兼容的选项错误,反而不如自己动手创建目录、自己写web.xml来得清晰。当然,你在IDEA里用Project Structure新建Web项目也是可以的,只要保证最终部署出来的目录结构满足Servlet规范即可。
还有一点要提前说清楚:WEB-INF目录下的东西是Web容器保护的,外部不能通过URL直接访问。所以mail.jar和activation.jar放在WEB-INF/lib下能被Tomcat自动加载,而用户无法通过http://ip:8080/mail-demo/mail.jar下载到它。这个机制是Servlet规范定义的,保证依赖库的私密性。
3.2 需要的JAR包怎么获取
前面提到的两个JAR包,获取途径很常规。mail.jar可以在Oracle官网的JavaMail页面下载,也可以从Maven中央仓库直接下载jar包文件。activation.jar同样可以在Maven中央仓库获取,搜索javax.activation即可。需要注意的是版本问题:JavaMail版本建议选1.4.7,这是公认稳定且资料多的版本;activation建议选1.1.1。高版本的jakarta.mail是另一套命名空间(jakarta.mail包),跟传统的javax.mail不兼容,如果项目还是老式Servlet容器(Tomcat 8/9),那用javax.mail的命名空间就够了,不用追新。
如果你用的是Maven,直接在pom.xml中配置:
<dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency>这里有一个容易踩的坑:不同版本的mail.jar依赖的activation.jar版本可能不同,如果不匹配,运行时会出现ClassNotFoundException: com.sun.activation.registries.MailcapFile这类错误。我自己的做法是干脆两个JAR都从同一个来源下载(比如Maven仓库中的配套版本),避免版本错位。
3.3 关键配置参数解释
在做邮件发送之前,先要摸清你选择的邮箱服务商提供的基础参数。不同的邮件服务商,SMTP服务器地址和端口可能不一样,常见的配置模式有:
- 普通SSL加密模式:
smtp.xxx.com,端口465,需要开启SSL - 升级TLS模式:
smtp.xxx.com,端口587,需要STARTTLS
我实际操作时优先使用465端口+SSL模式,因为587端口在有些服务器环境下会被运营商封锁或者邮件服务商默认不允许非加密连接。465这种SSL直连模式更省事,只需要在Properties设置mail.smtp.ssl.enable=true即可。
除了服务器和端口,还要准备好:
- 你的完整邮箱地址:作为发件人地址
- 授权码:在邮箱设置中开启SMTP后生成的一串独立密码
- 收件人邮箱:测试时最好用另一个不同域名下的邮箱,这样能验证跨服务器投递是否正常
我给项目加了一个MailConfig类来统一存放这些常量,避免在代码里到处写死。这个问题虽然简单,但做得好不好,直接影响后面维护的体验——我见过不少项目把邮箱密码散落在七八个类里,改一次授权码要全局搜索,非常痛苦。
4. 核心代码实现详解
4.1 MailSender类——邮件发送的骨架
先给出这个类的完整代码,这是整个项目中最核心的部分:
package com.example.mail; import java.util.Properties; import javax.mail.Authenticator; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.PasswordAuthentication; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; public class MailSender { public static void send(String to, String subject, String content) throws MessagingException { Properties props = new Properties(); // SMTP服务器地址 props.put("mail.smtp.host", MailConfig.SMTP_HOST); // 端口 props.put("mail.smtp.port", MailConfig.SMTP_PORT); // 是否开启认证 props.put("mail.smtp.auth", "true"); // 是否开启SSL props.put("mail.smtp.ssl.enable", "true"); // 设置超时时间,避免服务器无响应时客户端卡死 props.put("mail.smtp.connectiontimeout", "10000"); props.put("mail.smtp.timeout", "10000"); Session session = Session.getInstance(props, new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(MailConfig.USERNAME, MailConfig.AUTH_CODE); } }); // 打开调试模式,方便观察SMTP协议交互过程 session.setDebug(true); MimeMessage message = new MimeMessage(session); message.setFrom(new InternetAddress(MailConfig.USERNAME)); message.addRecipient(Message.RecipientType.TO, new InternetAddress(to)); message.setSubject(subject, "UTF-8"); message.setContent(content, "text/html;charset=UTF-8"); Transport.send(message); } }这段代码里值得逐行解释的地方不少。先看Session.getInstance(props, authenticator)这一行,props里配置了连接SMTP的所有参数,而Authenticator是JavaMail用来获取认证凭据的接口。注意看,我在这里返回的不是邮箱的登录密码,而是AUTH_CODE授权码。这个设计是JavaMail API的标准用法——通过PasswordAuthentication封装用户名和密码/授权码,Transport在连接服务器时会自动使用这组凭据做认证。
再看message.setSubject(subject, "UTF-8")。这个方法的重载形式很多,如果你直接调用setSubject(subject),主题会按默认编码处理,中文大概率变成乱码。我特意指定了UTF-8,确保中英文都能正确编码。同理,正文通过setContent(content, "text/html;charset=UTF-8")设置,告诉收件人的邮件客户端"这是一封HTML格式、UTF-8编码的邮件"。如果你想要纯文本格式,把第二个参数改成text/plain;charset=UTF-8就行。
还有一个容易被忽略的细节:session.setDebug(true)。这行代码平时没存在感,但当你遇到"邮件发不出去"这种问题需要排查时,它会帮你打印出客户端和SMTP服务器之间的完整对话过程——包括DNS解析、TCP连接、EHLO指令、AUTH LOGIN认证、MAIL FROM指令、RCPT TO指令、DATA指令,以及服务器的每个响应码。我第一次排查认证失败问题时,就是依靠这个输出看到了服务器返回的错误码,才定位到授权码的问题。上线部署时,记得把setDebug改成false,否则会产生大量日志输出。
4.2 MailConfig类——统一管理配置
有经验的人写代码,不会把配置参数分散在业务代码里。我单独抽了个MailConfig类:
package com.example.mail; public class MailConfig { // SMTP服务器地址 public static final String SMTP_HOST = "smtp.yourmailservice.com"; // 端口,SSL加密连接常用465 public static final String SMTP_PORT = "465"; // 发件人邮箱地址 public static final String USERNAME = "your_account@example.com"; // 授权码(不是登录密码) public static final String AUTH_CODE = "xxxxxxxxxxxxxx"; }这个类的作用不言自明:当你的部署环境切换时,比如从测试邮箱切换到公司内部邮件服务器,只需要改这一个文件;当授权码过期需要替换时,也只需要改这一个地方。这里有个建议,如果你要把项目推到Git仓库上,务必在.gitignore中排除这个文件或者在提交前将真实授权码替换成占位符——否则授权码泄露,别人就能用你的邮箱发邮件,轻则被当作垃圾邮件源,重则影响邮箱安全。
4.3 SendMailServlet——接收请求的核心入口
Servlet在Java Web开发中扮演的角色,就是接受HTTP请求、执行业务逻辑、返回响应视图。我这里创建了一个SendMailServlet,只负责接收页面表单的POST请求并调用MailSender:
package com.example.mail; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/sendMail") public class SendMailServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); String to = request.getParameter("to"); String subject = request.getParameter("subject"); String content = request.getParameter("content"); if (to == null || to.trim().isEmpty() || subject == null || subject.trim().isEmpty()) { request.setAttribute("message", "收件人、主题不能为空"); request.getRequestDispatcher("sendMail.jsp").forward(request, response); return; } try { MailSender.send(to.trim(), subject.trim(), content); request.setAttribute("message", "邮件发送成功!"); } catch (Exception e) { e.printStackTrace(); request.setAttribute("message", "邮件发送失败:" + e.getMessage()); } request.getRequestDispatcher("sendMail.jsp").forward(request, response); } }注意编码问题:我在doPost第一行就设置了request.setCharacterEncoding("UTF-8")。这个调用必须在读取任何请求参数之前执行,否则Tomcat会按默认的ISO-8859-1来解码POST请求体,中文表单提交就会出现乱码。这个细节,初学者特别容易踩坑——我见过很多人在页面已经设置了UTF-8,但后台收到的参数依然是乱码的情况,原因就是漏了这行。
另外我做了基本的非空校验,只校验了收件人和主题,没对邮箱格式做正则校验。正常开发中,收件人格式校验是很基本的——邮箱里注入垃圾内容导致发信失败事小,如果将来做批量发送功能,格式错误可能引发异常退信。后面在升级篇中我会展示一个简单的验证方式。
4.4 sendMail.jsp——前端提交界面
最后需要一个交互页面。我这里用JSP实现,通过简单的HTML表单提交数据:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>邮件发送</title> <style> body { font-family: "Microsoft YaHei", sans-serif; background: #f5f5f5; } .container { width: 600px; margin: 50px auto; background: #fff; padding: 30px; border-radius: 8px; } label { display: block; margin-top: 15px; font-weight: bold; } input[type="text"], textarea { width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ccc; border-radius: 4px; } textarea { height: 120px; } button { margin-top: 20px; padding: 10px 30px; background: #409eff; color: #fff; border: none; border-radius: 4px; cursor: pointer; } </style> </head> <body> <div class="container"> <h2>发送邮件</h2> <p style="color: red;">${message}</p> <form action="sendMail" method="post"> <label>收件人邮箱</label> <input type="text" name="to" placeholder="someone@example.com"> <label>邮件主题</label> <input type="text" name="subject" placeholder="邮件主题"> <label>邮件内容</label> <textarea name="content" placeholder="请输入邮件正文内容"></textarea> <button type="submit">发送</button> </form> </div> </body> </html>表单的提交方式必须是post,因为邮件发送会改变服务器状态,而且邮件主题和正文可能包含较长的内容,POST在数据大小限制和语义上都更合适。表单的action指向sendMail,这个映射在Servlet代码中是通过@WebServlet("/sendMail")注解定义的——如果用低版本的Servlet容器,注解可能不支持,你需要再去web.xml里声明Servlet映射,我这次使用的是Servlet 3.0+,直接走注解。
页面上用了${message}来显示后端返回的结果,这要求JSP默认支持EL表达式。大部分Tomcat配置都是支持的,如果显示原样${message},那是开了忽略EL的配置,可以在JSP顶部加<%@ page isELIgnored="false" %>解决。
5. 部署运行与完整验证流程
5.1 部署到Tomcat
部署过程我是这样操作的:先确保本地已经安装了Tomcat 8或9,然后把整个项目导出为WAR包或者直接把项目目录丢到Tomcat的webapps目录下。如果你用的是IDEA,可以在Run Configuration里配置Tomcat Server,把项目部署到Tomcat后点击运行。
启动过程中有几点要特别注意:
- Tomcat启动时会在日志中打印部署信息,如果
WEB-INF/lib下的JAR包没有正确加载,你会看到ClassNotFoundException异常,那多半是lib目录路径不对或者JAR包缺失 - 如果出现端口占用,检查是否有其他Tomcat实例在运行,可以在
conf/server.xml中改端口 - 浏览器访问路径通常是
http://localhost:8080/mail-demo/sendMail.jsp,这里mail-demo是你的上下文路径,也就是WAR包名或项目名
启动完成后,页面能正常显示,就可以开始测试发送了。
5.2 功能验证步骤
我的验证流程是这样的:开两个邮箱,一个作为发件方,一个作为收件方,两个邮箱都用不同域名,这样才能检验跨服务器投递。然后我在页面上分别填写:
- 收件人:
recipient@otherdomain.com - 主题:
JavaMail原生发送测试 - 内容:
<h2>这是HTML内容</h2><p>这是一封来自原生JavaWeb项目的邮件。</p>
点击发送后,如果页面显示"邮件发送成功",再去看收件方邮箱是否收到。为了确认整个链路完整,我在MailSender中开着setDebug(true),控制台会打印出一大段SMTP协议交互日志。关键的日志片段类似:
DEBUG SMTP: Connected to host: smtp.xxx.com, port: 465 DEBUG SMTP: AUTH LOGIN command mixed with SSL DEBUG SMTP: Use 8bit NTLM DEBUG SMTP: AUTH LOGIN succeeded DEBUG SMTP: MAIL FROM:<your_account@example.com> DEBUG SMTP: RCPT TO:<recipient@otherdomain.com> DEBUG SMTP: DATA当看到AUTH LOGIN succeeded和RCPT TO成功返回时,基本可以判断邮件已经进入投递流程。等收件方邮箱收到邮件,说明整个链路是通的。
5.3 控制台日志怎么看
很多同学面对一大段Debug日志会发懵,我在这里简单说一下怎么看。SMTP协议的命令都是有迹可循的:
220开头的行:服务器打招呼,说明TCP连接成功250开头的行:请求被接受,比如EHLO、MAIL FROM、RCPT TO成功334开头的行:服务器要求提供认证信息(用户名/密码)235开头的行:认证成功535开头的行:认证失败,常见原因是授权码错误554开头的行:发送被拒绝,可能是内容触发反垃圾策略
把这几个状态码记住,绝大多数邮件发送问题都能在日志中直接定位。我也是用这个方法调通了整个项目,后面要分享的几个坑也全都靠日志才排查清楚。
6. 常见问题与排查技巧实录
6.1 授权码错误的经典报错
第一次测试时,我在发件人配置中填了真正的邮箱登录密码,结果控制台打出如下错误:
javax.mail.AuthenticationFailedException: 535 Error: authentication failed排查步骤很简单:先确认邮箱是否已开启SMTP服务,再确认授权码是否有效。多数邮箱开启SMTP服务后,会明确提示"授权码仅显示一次", 需要妥善保存;如果遗忘了,只能重新生成。把配置里的AUTH_CODE换成新授权码,问题就解决了。
这是一个很多人会犯的错误,因为登录密码和授权码是两个不同的东西,登录密码只能在网页Web端使用,授权码是专门提供给第三方客户端使用的。所以拿到一个"发不出去邮件"的报错,先别慌着改代码,先检查认证凭据。
6.2 端口不通的排查记录
另一次测试中,我配置了SMTP端口为25(这是SMTP协议的默认端口),但在部分网络环境下,25端口会被ISP封锁,日志显示连接超时:
java.net.ConnectException: Connection timed out: connect排查方法是:先用telnet smtp.xxx.com 25测试端口连通性。如果telnet连接不了,果断换成465或587端口。服务器防火墙规则、云服务商安全组这些网络层面因素,都可能导致端口不通。实际使用时,我用465端口一切正常,25端口则出现超时。
这里有个经验:如果你部署在云服务器上,还要检查安全组规则是否放行了对应出方向端口。大部分云厂商默认放行所有出方向端口,但在部分企业内网环境,需要向网络管理员申请开通。
6.3 中文乱码问题根因分析
中文乱码是另一个高频问题。问题出现的现象是:收件方收到邮件后,主题显示问号或乱码,正文中的中文也成了乱码。根因在于编码设置不一致。处理方法就是我前面代码中体现的:
- JSP页面声明UTF-8,表单提交前使用UTF-8编码
- Servlet中调用
request.setCharacterEncoding("UTF-8"),在读取参数之前执行 setSubject指定"UTF-8"参数setContent声明text/html;charset=UTF-8
四个环节缺一不可。我在JSP页面里还特意给表单加了accept-charset="UTF-8"属性,虽然现代浏览器默认会按页面编码提交,但多这一层保险总没有坏处。另外一个隐蔽的坑是,如果你的项目部署在Windows服务器上,系统默认字符集是GBK,某些文件的编码如果被IDE用系统默认字符集保存了,JSP编译出来就会出乱码。这种情况可以在server.xml中的Connector标签添加URIEncoding="UTF-8"属性,确保GET请求参数的编码也正确。
6.4 频繁发送触发限流策略
测试时如果短时间内连续发送多封邮件,可能会触发邮箱服务商的反垃圾策略。服务器会返回类似这样的错误:
554 Transaction failed这时不能继续重试,否则可能被封禁发信权限。正确做法是放慢发送频率,每次间隔至少一两分钟;如果是正式项目需要批量发送,建议设计异步队列,控制发送速率和日发送量上限。个人邮箱的日发送配额通常有限,不建议拿个人邮箱做大规模生产发信。
6.5 常见错误码速查表
为了方便排查,我把一些常见问题整理成了表格:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 535认证失败 | 授权码错误 | 重新生成授权码并更新配置 |
| Connection timed out | 端口不通、防火墙拦截 | telnet测试端口,更换465端口 |
| Could not connect to SMTP host | 服务器地址配置错误 | 检查邮箱服务商SMTP地址 |
| 550 Mailbox not found | 收件人地址不存在 | 确认收件人邮箱拼写 |
| 554 Transaction failed | 内容触发反垃圾策略 | 降低发送频率,检查邮件内容 |
| 主题显示乱码 | setSubject未指定编码 | 使用setSubject(subject, "UTF-8") |
| 页面表单中文乱码 | Servlet未调用setCharacterEncoding | 在读取参数前设置UTF-8 |
这张表可以贴在你项目的README里,后续维护时候查起来很方便。
7. 进阶扩展与个人经验总结
7.1 如何加入附件支持
现在实现的版本只支持纯文本和HTML内容。如果业务需要支持附件,你需要在原来的MimeMessage基础上使用MimeMultipart和MimeBodyPart构造复合消息。
MimeMultipart multipart = new MimeMultipart(); // 正文部分 MimeBodyPart textPart = new MimeBodyPart(); textPart.setContent(content, "text/html;charset=UTF-8"); multipart.addBodyPart(textPart); // 附件部分 MimeBodyPart filePart = new MimeBodyPart(); DataSource source = new FileDataSource(filePath); filePart.setDataHandler(new DataHandler(source)); filePart.setFileName(MimeUtility.encodeText(fileName, "UTF-8", "B")); multipart.addBodyPart(filePart); message.setContent(multipart);这里有个关键点:setFileName需要进行MIME编码处理,否则中文文件名在收件人那里会变成乱码。MimeUtility.encodeText帮我们把中文转换为符合MIME规范的编码。如果不做这步,收件人下载附件时文件名就会显示成乱码——这个问题我实际测试时遇到过,填入测试报告.pdf后收件人看到的是=?UTF-8?B?...?=这样的长串,令人头大。
7.2 支持多收件人与抄送
实际业务中,一封邮件可能需要发给多个收件人。JavaMail API支持在一条消息上添加多个收件人:
message.addRecipients(Message.RecipientType.TO, InternetAddress.parse("a@example.com,b@example.com"));这里用的是addRecipients(复数),可以一次解析多个地址,用逗号分隔。抄送则是:
message.addRecipient(Message.RecipientType.CC, new InternetAddress("cc@example.com"));7.3 开发中的三点体会
最后分享几个我在整个实现过程中的体会,这些不是代码层面的知识,但我觉得比代码更重要。
第一,原生实现的最大价值不是"不用框架",而是让你理解框架帮你做了什么。当你亲手配置Session、看着Authenticator的调用、阅读SMTP协议的调试日志,你才真正理解为什么Spring Boot里只要填几行配置邮件就能发出去——因为框架替你封装了这一整条链路。有了这个理解,再用框架时你排查问题会精准很多。
第二,很多"奇怪"的问题,根源都是编码不一致。我做这个项目时,中文乱码问题前前后后折腾了大半天,从页面排查到Servlet再到JavaMail的设置,最后发现是JSP文件本身的编码被IDE存成了GBK。所以项目创建之初就统一设置项目编码为UTF-8,文件编码为UTF-8,连接数据库、写页面、返回响应都明确指定UTF-8,能避免后续大量扯皮。
第三,调试信息是好朋友,别嫌它烦。setDebug(true)打出的日志虽然长,但它是你能看到的、客户端和服务器之间的真实对话记录。遇到问题时先看日志再说,能省掉大量盲目试错的时间。正式上线时关掉它,否则日志文件会快速膨胀。
这个项目的后续扩展方向其实不少:比如把发件配置移动到数据库表,做成后台可以动态管理的功能;比如加入发送记录表,记录每次发送的时间、收件人、状态;再比如做一个定时任务,定期处理队列中的待发邮件。每一步扩展,都是对JavaWeb技术栈更深入的一次实践。