Java实现带附件邮件发送的完整指南
1. 为什么需要发送带附件的邮件?
在日常开发中,邮件功能是系统通知、业务流转的重要通道。而带附件的邮件发送能力,更是许多业务场景的刚需:
- 电商平台需要给用户发送电子发票
- OA系统需要传递合同文档
- 监控系统需要发送日志文件
- 报表系统需要定期推送Excel数据
传统的纯文本邮件已经无法满足这些需求。JavaMail API配合JAF(JavaBeans Activation Framework)提供了完整的解决方案,可以支持各种类型的附件处理。
注意:虽然现在很多企业改用企业微信等IM工具,但邮件依然是正式业务通知的首选渠道,因其具有法律效力且可追溯。
2. 环境准备与依赖配置
2.1 必备依赖
在Maven项目中,需要添加以下依赖:
<dependency> <groupId>javax.mail</groupId> <artifactId>javax.mail-api</artifactId> <version>1.6.2</version> </dependency> <dependency> <groupId>com.sun.mail</groupId> <artifactId>javax.mail</artifactId> <version>1.6.2</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency>为什么需要这三个依赖?
javax.mail-api:提供了JavaMail的核心接口javax.mail:Sun/Oracle的实现类activation:处理附件类型识别的JAF框架
2.2 SMTP服务器配置
发送邮件需要SMTP服务器支持。常见配置:
# 以QQ邮箱为例 mail.smtp.host=smtp.qq.com mail.smtp.port=465 mail.smtp.ssl.enable=true mail.smtp.auth=true mail.user=your_email@qq.com mail.password=your_authorization_code重要提示:不要直接使用邮箱密码,而应该使用"授权码"。各大邮箱服务商都提供了获取授权码的方式。
3. 核心实现步骤
3.1 创建邮件会话
Properties props = new Properties(); props.put("mail.smtp.host", host); props.put("mail.smtp.port", port); props.put("mail.smtp.ssl.enable", "true"); props.put("mail.smtp.auth", "true"); Session session = Session.getInstance(props, new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } });3.2 构建复杂邮件
带附件的邮件属于MIME(Multipurpose Internet Mail Extensions)类型:
MimeMessage message = new MimeMessage(session); // 设置发件人 message.setFrom(new InternetAddress(from)); // 设置收件人 message.setRecipient(Message.RecipientType.TO, new InternetAddress(to)); // 设置主题 message.setSubject("带附件的测试邮件"); // 创建邮件正文和附件的组合容器 MimeMultipart multipart = new MimeMultipart(); // 添加正文部分 MimeBodyPart textPart = new MimeBodyPart(); textPart.setText("这是一封带附件的测试邮件"); multipart.addBodyPart(textPart); // 添加附件部分 MimeBodyPart attachPart = new MimeBodyPart(); attachPart.attachFile(new File("test.pdf")); multipart.addBodyPart(attachPart); // 设置邮件内容 message.setContent(multipart);3.3 发送邮件
Transport.send(message);4. 高级功能与常见问题
4.1 支持多种附件类型
可以同时添加多个不同类型的附件:
// 添加Excel附件 MimeBodyPart excelPart = new MimeBodyPart(); excelPart.attachFile(new File("report.xlsx")); excelPart.setFileName("月度报表.xlsx"); multipart.addBodyPart(excelPart); // 添加图片附件 MimeBodyPart imagePart = new MimeBodyPart(); imagePart.attachFile(new File("photo.jpg")); imagePart.setFileName("产品照片.jpg"); multipart.addBodyPart(imagePart);4.2 常见问题排查
认证失败:
- 检查是否使用了授权码而非密码
- 确认SMTP服务是否开启
- 检查网络是否能够访问SMTP服务器
附件大小限制:
- 大多数邮件服务器限制附件大小(通常25MB)
- 解决方案:使用云存储链接替代大附件
中文乱码:
message.setSubject(MimeUtility.encodeText("中文主题", "UTF-8", "B")); textPart.setText(MimeUtility.encodeText("中文内容", "UTF-8", "B"));发送缓慢:
- 大附件建议使用异步发送
- 可以压缩附件减少体积
4.3 安全性考虑
- 不要硬编码邮箱凭证,应该使用配置中心或环境变量
- 对附件进行病毒扫描
- 限制附件类型,防止上传可执行文件
- 使用TLS加密传输
5. 完整代码示例
import javax.mail.*; import javax.mail.internet.*; import javax.activation.*; import java.util.*; public class EmailWithAttachmentSender { private String host; private int port; private String username; private String password; public EmailWithAttachmentSender(String host, int port, String username, String password) { this.host = host; this.port = port; this.username = username; this.password = password; } public void send(String from, String to, String subject, String text, List<File> attachments) throws Exception { Properties props = new Properties(); props.put("mail.smtp.host", host); props.put("mail.smtp.port", port); props.put("mail.smtp.ssl.enable", "true"); props.put("mail.smtp.auth", "true"); Session session = Session.getInstance(props, new Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); MimeMessage message = new MimeMessage(session); message.setFrom(new InternetAddress(from)); message.setRecipient(Message.RecipientType.TO, new InternetAddress(to)); message.setSubject(subject); MimeMultipart multipart = new MimeMultipart(); // 正文部分 MimeBodyPart textPart = new MimeBodyPart(); textPart.setText(text); multipart.addBodyPart(textPart); // 附件部分 for (File file : attachments) { MimeBodyPart attachPart = new MimeBodyPart(); attachPart.attachFile(file); attachPart.setFileName(MimeUtility.encodeText(file.getName())); multipart.addBodyPart(attachPart); } message.setContent(multipart); Transport.send(message); } public static void main(String[] args) throws Exception { EmailWithAttachmentSender sender = new EmailWithAttachmentSender( "smtp.qq.com", 465, "your_email@qq.com", "your_authorization_code" ); List<File> attachments = new ArrayList<>(); attachments.add(new File("test.pdf")); attachments.add(new File("report.xlsx")); sender.send( "your_email@qq.com", "recipient@example.com", "测试带附件邮件", "请查收附件", attachments ); } }6. 性能优化建议
连接池:频繁发送邮件时,使用连接池管理SMTP连接
// 使用Apache Commons Email提供的连接池 SmtpConnectionPool pool = new SmtpConnectionPool();异步发送:避免阻塞主线程
ExecutorService executor = Executors.newFixedThreadPool(5); executor.submit(() -> { try { Transport.send(message); } catch (Exception e) { logger.error("发送邮件失败", e); } });附件压缩:对大文件先压缩再发送
File compressedFile = compressFile(originalFile); attachPart.attachFile(compressedFile);批量发送:使用BCC密送功能批量发送
message.setRecipients(Message.RecipientType.BCC, addresses);错误重试:网络波动时自动重试
int retry = 3; while (retry-- > 0) { try { Transport.send(message); break; } catch (Exception e) { if (retry == 0) throw e; Thread.sleep(5000); } }
7. 实际业务场景扩展
7.1 邮件模板引擎
对于固定格式的业务邮件(如订单确认、密码重置),可以使用模板引擎:
// 使用Freemarker Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); cfg.setClassForTemplateLoading(this.getClass(), "/templates"); Template template = cfg.getTemplate("order-confirmation.ftl"); Map<String, Object> data = new HashMap<>(); data.put("orderNo", "2023123456"); data.put("items", items); StringWriter writer = new StringWriter(); template.process(data, writer); textPart.setText(writer.toString(), "UTF-8", "html");7.2 邮件追踪
通过嵌入追踪像素监控邮件打开情况:
<img src="https://yourdomain.com/track?mailId=12345" width="1" height="1">7.3 合规性检查
对发送的邮件内容进行合规检查:
- 敏感词过滤
- 附件类型检查
- 发送频率限制
public boolean checkContentSafety(String content) { // 实现敏感词检查逻辑 return !content.contains("违规词"); }7.4 与Spring集成
在Spring Boot项目中,可以使用更简洁的方式:
@Configuration public class MailConfig { @Bean public JavaMailSender mailSender() { JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setHost("smtp.qq.com"); sender.setPort(465); sender.setUsername("your_email@qq.com"); sender.setPassword("your_authorization_code"); Properties props = sender.getJavaMailProperties(); props.put("mail.transport.protocol", "smtp"); props.put("mail.smtp.auth", "true"); props.put("mail.smtp.ssl.enable", "true"); return sender; } } @Service public class MailService { @Autowired private JavaMailSender mailSender; public void sendWithAttachments(String to, String subject, String text, List<File> attachments) { try { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setTo(to); helper.setSubject(subject); helper.setText(text); for (File file : attachments) { helper.addAttachment(file.getName(), file); } mailSender.send(message); } catch (Exception e) { throw new RuntimeException("发送邮件失败", e); } } }8. 测试与调试技巧
8.1 使用测试SMTP服务器
开发阶段可以使用假的SMTP服务器进行测试:
- MailHog
- FakeSMTP
- GreenMail
// 使用GreenMail进行测试 @Before public void setup() { GreenMail greenMail = new GreenMail(); greenMail.start(); } @Test public void testEmailSending() throws Exception { // 发送测试邮件 sendTestEmail(); // 验证邮件是否收到 MimeMessage[] messages = greenMail.getReceivedMessages(); assertEquals(1, messages.length); MimeMessage msg = messages[0]; assertEquals("测试主题", msg.getSubject()); }8.2 日志记录
记录详细的发送日志:
session.setDebug(true); // 开启调试日志8.3 邮件内容预览
在发送前可以先输出邮件内容检查:
ByteArrayOutputStream os = new ByteArrayOutputStream(); message.writeTo(os); System.out.println(os.toString());8.4 异常处理
完善的异常处理机制:
try { Transport.send(message); } catch (AuthenticationFailedException e) { logger.error("认证失败,请检查用户名和授权码", e); } catch (MessagingException e) { logger.error("邮件发送失败", e); if (e.getCause() instanceof ConnectException) { logger.error("无法连接到SMTP服务器,请检查网络"); } } catch (Exception e) { logger.error("未知错误", e); }9. 替代方案比较
9.1 第三方邮件服务
对于大规模发送需求,可以考虑:
- SendGrid
- Mailgun
- Amazon SES
优势:
- 更高的发送限额
- 更好的送达率
- 丰富的统计功能
9.2 命令行工具
对于简单需求,可以直接调用系统命令:
Runtime.getRuntime().exec("mail -s \"Subject\" -a file.txt user@example.com < body.txt");缺点:
- 依赖系统环境
- 功能有限
- 安全性差
9.3 其他Java库
- Apache Commons Email:更简洁的API
- SimpleJavaMail:更现代的接口
- Jodd Mail:轻量级替代方案
比较表:
| 特性 | JavaMail | Commons Email | SimpleJavaMail |
|---|---|---|---|
| 学习曲线 | 陡峭 | 中等 | 平缓 |
| 功能完整性 | 完整 | 完整 | 完整 |
| 社区支持 | 强 | 强 | 中等 |
| 配置复杂度 | 高 | 中 | 低 |
| 适合场景 | 复杂需求 | 一般需求 | 快速开发 |
10. 实战经验分享
在实际项目中,我总结了以下经验教训:
附件命名问题:
- 中文文件名必须编码:
MimeUtility.encodeText(filename) - 避免特殊字符:
<>:"/\|?*等
- 中文文件名必须编码:
内存管理:
- 大附件应该使用
FileDataSource而不是加载到内存
attachPart.setDataHandler(new DataHandler(new FileDataSource(file)));- 大附件应该使用
超时设置:
mail.smtp.timeout=5000 mail.smtp.writetimeout=5000 mail.smtp.connectiontimeout=5000代理设置: 如果需要通过代理发送:
props.put("mail.smtp.socks.host", "proxy.example.com"); props.put("mail.smtp.socks.port", "1080");发送状态回调:
Transport.send(message, new TransportListener() { public void messageDelivered(TransportEvent e) { logger.info("邮件已送达"); } public void messageNotDelivered(TransportEvent e) { logger.warn("邮件未送达"); } public void messagePartiallyDelivered(TransportEvent e) { logger.warn("邮件部分送达"); } });内容安全:
- 对HTML内容进行净化,防止XSS
- 扫描附件中的恶意内容
国际化支持:
message.setHeader("Content-Type", "text/plain; charset=UTF-8"); message.setHeader("Content-Transfer-Encoding", "base64");邮件优先级:
message.setHeader("X-Priority", "1"); // 1=最高, 3=普通, 5=最低阅读回执:
message.setHeader("Disposition-Notification-To", "return@example.com");定时发送:
message.setHeader("X-Delay", "3600"); // 延迟1小时发送
这些经验都是从实际项目中积累的,很多在官方文档中并没有明确说明,但能显著提高邮件功能的可靠性和用户体验。