Spring Boot 邮件发送完整指南:带附件、内嵌图片与中文乱码根治方案

本文基于 Spring Boot 2.x + JDK 1.8 + Jakarta Mail 1.6.2,提供生产环境可用的完整邮件发送解决方案,涵盖附件发送、内嵌图片、中文乱码处理等核心场景。


一、技术选型与依赖配置

1.1 Maven 依赖配置(JDK 1.8 兼容)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xxx</groupId>
    <artifactId>springboot-email-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>springboot-email-demo</name>
    <description>Spring Boot 邮件发送完整解决方案</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Mail Starter(Jakarta Mail 1.6.2,JDK 1.8 兼容) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <!-- Spring Boot Web(可选,用于提供接口) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Lombok(简化代码,可选) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

重要说明

  • Spring Boot 2.7.18 内置 Jakarta Mail 1.6.2,完美兼容 JDK 1.8
  • 如果使用 Spring Boot 3.x,需要 JDK 17+,且邮件API包名从 javax.mail 变为 jakarta.mail

1.2 application.yml 配置

yaml 复制代码
spring:
  mail:
    # SMTP服务器配置
    host: smtp.qq.com
    port: 587
    username: your_email@qq.com
    password: your_smtp_authorization_code  # 注意:是授权码,不是邮箱密码
    
    # 编码配置(根治中文乱码)
    default-encoding: UTF-8
    
    # 协议配置
    protocol: smtp
    
    # TLS加密配置
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true
          ssl:
            protocols: TLSv1.2
          connectiontimeout: 10000
          timeout: 10000
          writetimeout: 10000

# 服务器端口
server:
  port: 8080

常用SMTP服务器配置参考

邮箱服务商 SMTP地址 端口 认证方式
QQ邮箱 smtp.qq.com 587 (TLS) / 465 (SSL) 授权码
163邮箱 smtp.163.com 465 (SSL) / 25 授权码
Gmail smtp.gmail.com 587 (TLS) 应用专用密码
Outlook smtp.office365.com 587 (TLS) 邮箱密码或应用密码

二、核心工具类实现

2.1 邮件发送工具类

java 复制代码
package com.xxx.email.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

/**
 * 邮件发送工具类
 * <p>
 * 支持功能:
 * 1. 纯文本邮件发送
 * 2. HTML邮件发送
 * 3. 带附件邮件发送(支持中文附件名)
 * 4. 内嵌图片邮件发送(图片显示在正文中)
 * 5. 混合场景:同时包含内嵌图片和附件
 * </p>
 *
 * @author auth
 * @since 2026-02-13
 */
@Slf4j
@Component
public class EmailUtil {

    @Autowired
    private JavaMailSender mailSender;

    /**
     * 发送纯文本邮件
     *
     * @param from    发件人邮箱
     * @param to      收件人邮箱(支持多个,用逗号分隔)
     * @param subject 邮件主题
     * @param content 邮件内容
     */
    public void sendSimpleEmail(String from, String to, String subject, String content) {
        try {
            SimpleMailMessage message = new SimpleMailMessage();
            message.setFrom(from);
            message.setTo(to.split(","));
            message.setSubject(subject);
            message.setText(content);

            mailSender.send(message);
            log.info("纯文本邮件发送成功!收件人:{}, 主题:{}", to, subject);
        } catch (Exception e) {
            log.error("纯文本邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
            throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
        }
    }


    /**
     *  发送纯文本带附件邮件
     * @param from          发件人邮箱
     * @param to            收件人邮箱(支持多个,用逗号分隔)
     * @param subject       邮件主题
     * @param content       邮件主题
     * @param attachments   附件文件列表
     */
    public void sendSimpleEmailAttachments(String from, String to, String subject, String content, List<File> attachments) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            // 第二个参数true表示创建multipart消息
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            helper.setFrom(from);
            helper.setTo(to.split(","));
            helper.setSubject(subject);
            helper.setText(content);

            // 添加附件(自动处理中文文件名)
            if (attachments != null && !attachments.isEmpty()) {
                for (File file : attachments) {
                    if (file != null && file.exists()) {
                        helper.addAttachment(encodeFileName(file.getName()), file);
                        log.debug("添加附件:{}", file.getName());
                    } else {
                        log.warn("附件文件不存在,跳过:{}", file != null ? file.getName() : "null");
                    }
                }
            }

            mailSender.send(message);
            log.info("纯文本带附件邮件发送成功!收件人:{}, 主题:{}, 附件数量:{}", to, subject,
                    attachments != null ? attachments.size() : 0);
        } catch (Exception e) {
            log.error("纯文本带附件邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
            throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
        }
    }


    /**
     * 发送HTML邮件
     *
     * @param from         发件人邮箱
     * @param to           收件人邮箱
     * @param subject      邮件主题
     * @param htmlContent  HTML格式的邮件内容
     */
    public void sendHtmlEmail(String from, String to, String subject, String htmlContent) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

            helper.setFrom(from);
            helper.setTo(to.split(","));
            helper.setSubject(encodeText(subject));
            helper.setText(htmlContent, true);

            mailSender.send(message);
            log.info("HTML邮件发送成功!收件人:{}, 主题:{}", to, subject);
        } catch (Exception e) {
            log.error("HTML邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
            throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
        }
    }

    /**
     * 发送带附件的邮件(支持中文附件名)
     *
     * @param from         发件人邮箱
     * @param to           收件人邮箱
     * @param subject      邮件主题
     * @param htmlContent  HTML格式的邮件内容
     * @param attachments  附件文件列表
     */
    public void sendEmailWithAttachments(String from, String to, String subject,
                                         String htmlContent, List<File> attachments) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            // 第二个参数true表示创建multipart消息
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

            helper.setFrom(from);
            helper.setTo(to.split(","));
            helper.setSubject(encodeText(subject));
            helper.setText(htmlContent, true);

            // 添加附件(自动处理中文文件名)
            if (attachments != null && !attachments.isEmpty()) {
                for (File file : attachments) {
                    if (file != null && file.exists()) {
                        helper.addAttachment(encodeFileName(file.getName()), file);
                        log.debug("添加附件:{}", file.getName());
                    } else {
                        log.warn("附件文件不存在,跳过:{}", file != null ? file.getName() : "null");
                    }
                }
            }

            mailSender.send(message);
            log.info("带附件邮件发送成功!收件人:{}, 主题:{}, 附件数量:{}", to, subject, 
                    attachments != null ? attachments.size() : 0);
        } catch (Exception e) {
            log.error("带附件邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
            throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
        }
    }

    /**
     * 发送带内嵌图片的邮件(图片显示在正文中)
     *
     * @param from         发件人邮箱
     * @param to           收件人邮箱
     * @param subject      邮件主题
     * @param htmlContent  HTML格式的邮件内容(通过cid:xxx引用图片)
     * @param inlineImages 内嵌图片列表
     */
    public void sendEmailWithInlineImages(String from, String to, String subject,
                                          String htmlContent, List<InlineImage> inlineImages) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            // 必须使用true创建multipart消息
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

            helper.setFrom(from);
            helper.setTo(to.split(","));
            helper.setSubject(encodeText(subject));
            helper.setText(htmlContent, true);

            // 添加内嵌图片
            if (inlineImages != null && !inlineImages.isEmpty()) {
                for (InlineImage inlineImage : inlineImages) {
                    if (inlineImage != null && inlineImage.getFile() != null && inlineImage.getFile().exists()) {
                        DataSource dataSource = new FileDataSource(inlineImage.getFile());
                        helper.addInline(inlineImage.getContentId(), dataSource);
                        log.debug("添加内嵌图片:CID={}, 文件名:{}", 
                                inlineImage.getContentId(), inlineImage.getFile().getName());
                    } else {
                        log.warn("内嵌图片文件不存在,跳过:CID={}", 
                                inlineImage != null ? inlineImage.getContentId() : "null");
                    }
                }
            }

            mailSender.send(message);
            log.info("内嵌图片邮件发送成功!收件人:{}, 主题:{}, 图片数量:{}", to, subject,
                    inlineImages != null ? inlineImages.size() : 0);
        } catch (Exception e) {
            log.error("内嵌图片邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
            throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
        }
    }

    /**
     * 发送复杂邮件(同时包含内嵌图片和附件)
     *
     * @param from         发件人邮箱
     * @param to           收件人邮箱
     * @param subject      邮件主题
     * @param htmlContent  HTML格式的邮件内容
     * @param inlineImages 内嵌图片列表
     * @param attachments  附件文件列表
     */
    public void sendComplexEmail(String from, String to, String subject,
                                 String htmlContent, List<InlineImage> inlineImages,
                                 List<File> attachments) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

            helper.setFrom(from);
            helper.setTo(to.split(","));
            helper.setSubject(encodeText(subject));
            helper.setText(htmlContent, true);

            // 添加内嵌图片
            if (inlineImages != null && !inlineImages.isEmpty()) {
                for (InlineImage inlineImage : inlineImages) {
                    if (inlineImage != null && inlineImage.getFile() != null && inlineImage.getFile().exists()) {
                        DataSource dataSource = new FileDataSource(inlineImage.getFile());
                        helper.addInline(inlineImage.getContentId(), dataSource);
                        log.debug("添加内嵌图片:CID={}, 文件名:{}", 
                                inlineImage.getContentId(), inlineImage.getFile().getName());
                    }
                }
            }

            // 添加附件
            if (attachments != null && !attachments.isEmpty()) {
                for (File file : attachments) {
                    if (file != null && file.exists()) {
                        helper.addAttachment(encodeFileName(file.getName()), file);
                        log.debug("添加附件:{}", file.getName());
                    }
                }
            }

            mailSender.send(message);
            log.info("复杂邮件发送成功!收件人:{}, 主题:{}, 图片数量:{}, 附件数量:{}", 
                    to, subject, inlineImages != null ? inlineImages.size() : 0,
                    attachments != null ? attachments.size() : 0);
        } catch (Exception e) {
            log.error("复杂邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
            throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
        }
    }

    /**
     * 编码文本(处理中文乱码)
     * 使用RFC 2047标准编码
     *
     * @param text 原始文本
     * @return 编码后的文本
     */
    private String encodeText(String text) {
        if (text == null || text.isEmpty()) {
            return text;
        }
        try {
            // 使用Base64编码(B编码),UTF-8字符集
            return MimeUtility.encodeText(text, "UTF-8", "B");
        } catch (UnsupportedEncodingException e) {
            log.warn("文本编码失败,返回原始文本:{}", text, e);
            return text;
        }
    }

    /**
     * 编码文件名(处理中文附件名乱码)
     *
     * @param fileName 原始文件名
     * @return 编码后的文件名
     */
    private String encodeFileName(String fileName) {
        if (fileName == null || fileName.isEmpty()) {
            return fileName;
        }
        try {
            // 使用Base64编码(B编码),UTF-8字符集
            return MimeUtility.encodeText(fileName, "UTF-8", "B");
        } catch (UnsupportedEncodingException e) {
            log.warn("文件名编码失败,返回原始文件名:{}", fileName, e);
            return fileName;
        }
    }

    /**
     * 内嵌图片包装类
     */
    public static class InlineImage {
        /**
         * 图片文件
         */
        private File file;

        /**
         * Content-ID(用于HTML中通过cid:引用)
         */
        private String contentId;

        public InlineImage(File file, String contentId) {
            this.file = file;
            this.contentId = contentId;
        }

        public File getFile() {
            return file;
        }

        public void setFile(File file) {
            this.file = file;
        }

        public String getContentId() {
            return contentId;
        }

        public void setContentId(String contentId) {
            this.contentId = contentId;
        }
    }
}

工具类设计亮点

  1. 完整的日志记录:成功/失败均有详细日志,便于排查问题
  2. 异常处理:所有异常统一捕获并记录,重新抛出为运行时异常
  3. 中文乱码根治 :通过 MimeUtility.encodeText() 统一处理主题和附件名
  4. 参数校验:对文件存在性、null值进行校验
  5. 支持多种场景:纯文本、HTML、附件、内嵌图片、混合场景

2.2 Spring Boot 启动类

java 复制代码
package com.xxx.email;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class EmailApplication {

    public static void main(String[] args) {
        SpringApplication.run(EmailApplication.class, args);
        System.out.println("========================================");
        System.out.println("邮件服务启动成功!");
        System.out.println("========================================");
    }
}

三、使用示例

3.1 测试控制器

java 复制代码
package com.xxx.email.controller;

import com.example.email.util.EmailUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 * 邮件发送测试控制器
 */
@RestController
@RequestMapping("/email")
public class EmailController {

    @Autowired
    private EmailUtil emailUtil;

    @Value("${spring.mail.username}")
    private String fromEmail;

    /**
     * 测试1:发送纯文本邮件
     */
    @GetMapping("/sendSimple")
    public String sendSimpleEmail() {
        try {
            String to = "recipient@example.com";
            String subject = "测试纯文本邮件";
            String content = "你好!\n\n这是一封测试邮件。\n\n祝好!";

            emailUtil.sendSimpleEmail(fromEmail, to, subject, content);

            return "纯文本邮件发送成功!";
        } catch (Exception e) {
            return "发送失败:" + e.getMessage();
        }
    }

    /**
     * 测试2:发送HTML邮件
     */
    @GetMapping("/sendHtml")
    public String sendHtmlEmail() {
        try {
            String to = "recipient@example.com";
            String subject = "测试HTML邮件";

            String htmlContent =
                    "<html>" +
                    "<head>" +
                    "  <style>" +
                    "    body { font-family: Arial, sans-serif; padding: 20px; }" +
                    "    .header { background: #4CAF50; color: white; padding: 20px; text-align: center; }" +
                    "    .content { margin-top: 20px; line-height: 1.6; }" +
                    "  </style>" +
                    "</head>" +
                    "<body>" +
                    "  <div class='header'>" +
                    "    <h1>欢迎使用我们的服务</h1>" +
                    "  </div>" +
                    "  <div class='content'>" +
                    "    <p>亲爱的用户:</p>" +
                    "    <p>这是一封<b>HTML格式</b>的测试邮件。</p>" +
                    "    <p>感谢您的使用!</p>" +
                    "  </div>" +
                    "</body>" +
                    "</html>";

            emailUtil.sendHtmlEmail(fromEmail, to, subject, htmlContent);

            return "HTML邮件发送成功!";
        } catch (Exception e) {
            return "发送失败:" + e.getMessage();
        }
    }

    /**
     * 测试3:发送带附件的邮件(支持中文附件名)
     */
    @GetMapping("/sendWithAttachments")
    public String sendEmailWithAttachments() {
        try {
            String to = "recipient@example.com";
            String subject = "测试带附件的邮件";

            String htmlContent =
                    "<html>" +
                    "<body>" +
                    "  <h2>附件测试邮件</h2>" +
                    "  <p>你好!</p>" +
                    "  <p>这是一封带附件的测试邮件,附件包含中文名称。</p>" +
                    "  <p>请查收附件。</p>" +
                    "</body>" +
                    "</html>";

            // 准备附件(包含中文文件名)
            List<File> attachments = new ArrayList<>();
            attachments.add(new File("D:/test/中文文档.docx"));
            attachments.add(new File("D:/test/数据报表.xlsx"));

            emailUtil.sendEmailWithAttachments(fromEmail, to, subject, htmlContent, attachments);

            return "带附件邮件发送成功!";
        } catch (Exception e) {
            return "发送失败:" + e.getMessage();
        }
    }

    /**
     * 测试4:发送带内嵌图片的邮件(图片显示在正文中)
     */
    @GetMapping("/sendWithInlineImages")
    public String sendEmailWithInlineImages() {
        try {
            String to = "recipient@example.com";
            String subject = "测试内嵌图片邮件";

            // HTML正文:通过cid:引用图片
            String htmlContent =
                    "<html>" +
                    "<head>" +
                    "  <style>" +
                    "    body { font-family: Arial, sans-serif; padding: 20px; }" +
                    "    .header { background: #4CAF50; color: white; padding: 20px; text-align: center; }" +
                    "    .content { margin-top: 20px; line-height: 1.6; }" +
                    "    img { max-width: 100%; height: auto; display: block; margin: 20px auto; }" +
                    "  </style>" +
                    "</head>" +
                    "<body>" +
                    "  <div class='header'>" +
                    "    <h1>内嵌图片测试</h1>" +
                    "  </div>" +
                    "  <div class='content'>" +
                    "    <p>亲爱的用户:</p>" +
                    "    <p>这是一封带内嵌图片的测试邮件。</p>" +
                    "    <p>以下是公司Logo:</p>" +
                    "    <!-- 通过cid:logo引用内嵌图片 -->" +
                    "    <img src='cid:logo' alt='Logo'>" +
                    "    <p>以下是产品示意图:</p>" +
                    "    <!-- 通过cid:product引用内嵌图片 -->" +
                    "    <img src='cid:product' alt='产品示意图'>" +
                    "    <p>图片直接显示在邮件正文中,无需下载。</p>" +
                    "  </div>" +
                    "</body>" +
                    "</html>";

            // 准备内嵌图片(File + Content-ID)
            List<EmailUtil.InlineImage> inlineImages = new ArrayList<>();
            inlineImages.add(new EmailUtil.InlineImage(new File("D:/test/logo.png"), "logo"));
            inlineImages.add(new EmailUtil.InlineImage(new File("D:/test/product.png"), "product"));

            emailUtil.sendEmailWithInlineImages(fromEmail, to, subject, htmlContent, inlineImages);

            return "内嵌图片邮件发送成功!";
        } catch (Exception e) {
            return "发送失败:" + e.getMessage();
        }
    }

    /**
     * 测试5:发送复杂邮件(同时包含内嵌图片和附件)
     */
    @GetMapping("/sendComplex")
    public String sendComplexEmail() {
        try {
            String to = "recipient@example.com";
            String subject = "测试复杂邮件(内嵌图片 + 附件)";

            // HTML正文
            String htmlContent =
                    "<html>" +
                    "<head>" +
                    "  <style>" +
                    "    body { font-family: Arial, sans-serif; padding: 20px; }" +
                    "    .header { background: #4CAF50; color: white; padding: 20px; text-align: center; }" +
                    "    .content { margin-top: 20px; line-height: 1.6; }" +
                    "    img { max-width: 100%; height: auto; display: block; margin: 20px auto; }" +
                    "  </style>" +
                    "</head>" +
                    "<body>" +
                    "  <div class='header'>" +
                    "    <h1>复杂邮件测试</h1>" +
                    "  </div>" +
                    "  <div class='content'>" +
                    "    <p>亲爱的用户:</p>" +
                    "    <p>这封邮件同时包含:</p>" +
                    "    <ul>" +
                    "      <li>内嵌图片(直接显示在正文中)</li>" +
                    "      <li>附件(需要下载查看)</li>" +
                    "    </ul>" +
                    "    <p>以下是内嵌图片:</p>" +
                    "    <img src='cid:logo' alt='Logo'>" +
                    "    <p>请查收附件文件。</p>" +
                    "  </div>" +
                    "</body>" +
                    "</html>";

            // 内嵌图片
            List<EmailUtil.InlineImage> inlineImages = new ArrayList<>();
            inlineImages.add(new EmailUtil.InlineImage(new File("D:/test/logo.png"), "logo"));

            // 附件
            List<File> attachments = new ArrayList<>();
            attachments.add(new File("D:/test/中文文档.docx"));
            attachments.add(new File("D:/test/数据报表.xlsx"));

            emailUtil.sendComplexEmail(fromEmail, to, subject, htmlContent, inlineImages, attachments);

            return "复杂邮件发送成功!";
        } catch (Exception e) {
            return "发送失败:" + e.getMessage();
        }
    }
}

3.2 单元测试

java 复制代码
package com.xxx.email;

import com.example.email.util.EmailUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

@SpringBootTest
class EmailApplicationTests {

    @Autowired
    private EmailUtil emailUtil;

    @Value("${spring.mail.username}")
    private String fromEmail;

    /**
     * 测试纯文本邮件
     */
    @Test
    void testSendSimpleEmail() {
        String to = "recipient@example.com";
        String subject = "【测试】纯文本邮件";
        String content = "你好!\n\n这是一封测试邮件。\n\n祝好!";

        emailUtil.sendSimpleEmail(fromEmail, to, subject, content);
    }

    /**
     * 测试HTML邮件
     */
    @Test
    void testSendHtmlEmail() {
        String to = "recipient@example.com";
        String subject = "【测试】HTML邮件";

        String htmlContent =
                "<html>" +
                "<body>" +
                "  <h2>HTML邮件测试</h2>" +
                "  <p>这是一封<b>HTML格式</b>的测试邮件。</p>" +
                "</body>" +
                "</html>";

        emailUtil.sendHtmlEmail(fromEmail, to, subject, htmlContent);
    }

    /**
     * 测试带附件的邮件
     */
    @Test
    void testSendEmailWithAttachments() {
        String to = "recipient@example.com";
        String subject = "【测试】带附件的邮件";

        String htmlContent =
                "<html>" +
                "<body>" +
                "  <h2>附件测试</h2>" +
                "  <p>请查收附件。</p>" +
                "</body>" +
                "</html>";

        List<File> attachments = new ArrayList<>();
        attachments.add(new File("D:/test/测试文档.txt"));

        emailUtil.sendEmailWithAttachments(fromEmail, to, subject, htmlContent, attachments);
    }

    /**
     * 测试内嵌图片邮件
     */
    @Test
    void testSendEmailWithInlineImages() {
        String to = "recipient@example.com";
        String subject = "【测试】内嵌图片邮件";

        String htmlContent =
                "<html>" +
                "<body>" +
                "  <h2>内嵌图片测试</h2>" +
                "  <img src='cid:test' alt='测试图片'>" +
                "</body>" +
                "</html>";

        List<EmailUtil.InlineImage> inlineImages = new ArrayList<>();
        inlineImages.add(new EmailUtil.InlineImage(new File("D:/test/test.png"), "test"));

        emailUtil.sendEmailWithInlineImages(fromEmail, to, subject, htmlContent, inlineImages);
    }
}

四、关键技术与原理

4.1 中文乱码的根源与解决方案

乱码产生的原因

邮件协议(SMTP)基于文本传输,只支持7位ASCII字符。当传输中文等非ASCII字符时,必须进行编码。

解决方案

乱码类型 根本原因 解决方案
附件文件名乱码 文件名未按MIME标准编码(RFC 2047) 使用 MimeUtility.encodeText(filename, "UTF-8", "B")
邮件正文乱码 正文未指定charset或使用了非UTF-8编码 MimeMessageHelper 构造函数中指定 UTF-8
主题乱码 主题未进行MIME编码 使用 MimeUtility.encodeText(subject, "UTF-8", "B")

编码方式说明

java 复制代码
MimeUtility.encodeText(text, charset, encoding)
  • charset : 必须使用 "UTF-8",确保能处理所有Unicode字符
  • encoding :
    • "B" - Base64编码(推荐,兼容性最好)
    • "Q" - Quoted-Printable编码(适用于ASCII字符较多的场景)

生产环境建议始终使用 "B"(Base64)编码,因为各邮件客户端对Base64的支持最稳定。

4.2 内嵌图片的技术原理

Multipart 模式选择

java 复制代码
// 错误:使用默认的mixed模式(附件模式)
MimeMultipart multipart = new MimeMultipart();

// 正确:使用related模式(内嵌资源模式)
MimeMultipart multipart = new MimeMultipart("related");
Multipart类型 用途 说明
mixed 混合模式 用于独立的附件(如Word、PDF)
related 相关模式 用于内嵌资源(图片、CSS、字体)
alternative 替代模式 用于同一内容的多版本(纯文本 + HTML)

Content-ID(CID)设置规则

java 复制代码
// 1. 在Java代码中设置Content-ID
helper.addInline("logo", dataSource);

// 2. 在HTML中引用(注意:必须加上cid:前缀)
<img src="cid:logo" alt="Logo">

重要规则

  • Content-ID 建议使用字母、数字、下划线,避免使用中文或特殊字符
  • HTML引用时使用 cid: 前缀
  • 同一邮件中的CID必须唯一,否则会显示错误或无法加载

4.3 Spring Boot Mail 自动配置原理

Spring Boot 通过 org.springframework.boot.autoconfigure.mail.MailAutoConfiguration 自动配置 JavaMailSender

  1. 自动检测配置 :从 application.yml 中读取 spring.mail.* 配置
  2. 创建 Session :根据配置创建 JavaMailSender
  3. 注入 Bean :将 JavaMailSender 注入到Spring容器中

开发者只需

  1. 配置 spring.mail.* 属性
  2. 注入 JavaMailSenderJavaMailSenderImpl
  3. 使用 MimeMessageHelper 简化邮件构建

五、常见问题与解决方案

5.1 认证失败

问题

复制代码
AuthenticationFailedException: 535 Error: authentication failed

原因

  • 使用了邮箱密码而非授权码
  • 授权码已过期或被重置
  • SMTP服务未开启

解决方案

  1. 登录邮箱设置,开启SMTP服务
  2. 生成新的授权码
  3. application.yml 中使用授权码

获取授权码示例(QQ邮箱)

  1. 登录QQ邮箱 → 设置 → 账户
  2. 找到"POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务"
  3. 开启"IMAP/SMTP服务"
  4. 按提示发送短信获取授权码

5.2 连接超时

问题

复制代码
MailConnectException: Couldn't connect to host, port: smtp.qq.com

原因

  • 端口配置错误
  • 网络问题
  • 防火墙拦截

解决方案

  1. 检查端口配置:

    • TLS加密:端口 587
    • SSL加密:端口 465
  2. 检查网络连接

  3. 配置超时时间:

    yaml 复制代码
    spring:
      mail:
        properties:
          mail:
            smtp:
              connectiontimeout: 10000
              timeout: 10000
              writetimeout: 10000

5.3 附件文件名乱码

问题:附件文件名显示为乱码

解决方案

使用 MimeUtility.encodeText() 编码文件名:

java 复制代码
private String encodeFileName(String fileName) {
    try {
        return MimeUtility.encodeText(fileName, "UTF-8", "B");
    } catch (UnsupportedEncodingException e) {
        return fileName;
    }
}

5.4 内嵌图片显示为红叉

问题:图片显示为红叉或无法加载

可能原因及解决方案

可能原因 解决方案
CID不匹配 检查HTML中的 cid: 与Java中的 addInline() 的参数是否一致
图片文件不存在 使用 file.exists() 验证文件是否存在
图片格式不支持 转换为PNG、JPG或GIF
图片过大 压缩图片(建议单张不超过500KB)

5.5 Outlook 中内嵌图片显示为附件

问题:内嵌图片在Outlook中显示为附件而非正文

解决方案

  1. 确保设置了正确的Multipart模式

  2. 设置文件名:

    java 复制代码
    helper.addInline("logo", dataSource);

5.6 邮件发送慢

问题:邮件发送耗时较长

解决方案

  1. 使用异步发送(推荐):
java 复制代码
@Service
public class AsyncEmailService {

    @Autowired
    private EmailUtil emailUtil;

    @Async("emailTaskExecutor")
    public void sendEmailAsync(String from, String to, String subject, String content) {
        emailUtil.sendHtmlEmail(from, to, subject, content);
    }
}
  1. 配置线程池:
java 复制代码
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("emailTaskExecutor")
    public TaskExecutor emailTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("Email-Async-");
        executor.initialize();
        return executor;
    }
}

六、最佳实践与优化建议

6.1 邮件发送建议

  1. 控制附件大小:单个附件不超过10MB,总大小不超过25MB(大多数邮箱的限制)
  2. 图片优化
    • 使用PNG或JPG格式
    • 控制单张图片大小在500KB以内
    • 使用合适的分辨率(72-150 DPI)
  3. HTML规范
    • 使用内联CSS样式
    • 避免使用JavaScript
    • 使用table布局提升兼容性
  4. 多客户端测试:在Gmail、Outlook、QQ邮箱等主流客户端中测试显示效果

6.2 异常处理建议

  1. 统一异常处理
java 复制代码
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception e) {
        log.error("系统异常", e);
        return ResponseEntity.status(500).body("系统异常:" + e.getMessage());
    }
}
  1. 重试机制:对于网络异常,可以添加重试逻辑

6.3 安全建议

  1. 敏感信息加密 :使用 application-{profile}.yml 区分环境,敏感信息加密存储
  2. 限制发送频率:防止被滥用
  3. 添加DKIM签名:提升邮件信誉度(可选)

6.4 监控建议

  1. 记录发送日志:记录发送状态、耗时、失败原因
  2. 监控指标
    • 发送成功率
    • 平均发送耗时
    • 失败原因分布
  3. 告警机制:发送失败率超过阈值时触发告警

6.5 生产环境配置示例

yaml 复制代码
spring:
  profiles:
    active: prod
  mail:
    host: ${SMTP_HOST:smtp.qq.com}
    port: ${SMTP_PORT:587}
    username: ${SMTP_USERNAME}
    password: ${SMTP_PASSWORD}
    default-encoding: UTF-8
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
          connectiontimeout: 15000
          timeout: 15000
          writetimeout: 15000

# 日志配置
logging:
  level:
    org.springframework.mail: DEBUG
  file:
    name: logs/email-service.log

七、总结

本文提供了一个完整的、生产级可用的 Spring Boot 邮件发送解决方案,核心要点:

  1. 技术选型:Spring Boot 2.7.18 + Jakarta Mail 1.6.2,兼容 JDK 1.8
  2. 核心工具类EmailUtil 提供了完整的邮件发送功能,包括:
    • 纯文本邮件
    • HTML邮件
    • 带附件邮件(支持中文附件名)
    • 内嵌图片邮件
    • 复杂邮件(内嵌图片 + 附件)
  3. 中文乱码根治 :使用 MimeUtility.encodeText() 统一处理
  4. 内嵌图片原理:通过 Content-ID 建立 HTML 与图片的映射关系
  5. 异常处理:完整的日志记录和异常处理机制
  6. 最佳实践:异步发送、监控告警、安全配置等
相关推荐
sheji70091 小时前
Springboot家教平台中心系统53754--(程序+源码+数据库+调试部署+开发环境)
java·数据库·spring boot·后端·spring·旅游
VXbishe1 小时前
基于web的校园失物招领管理系统-计算机毕设 附源码 24150
javascript·vue.js·spring boot·python·node.js·php·html5
QQ 31316378901 小时前
文华支撑压力画线主图指标公式源码
java
星火开发设计2 小时前
类模板:实现通用数据结构的基础
java·开发语言·数据结构·c++·html·知识
小宋10212 小时前
Java 数据库访问 vs Python 数据库访问:JDBC vs ORM
java·数据库·python
君爱学习2 小时前
MySQL 分布式锁实现方案
java
xuzhiqiang07242 小时前
【wiki知识库】07.用户管理后端SpringBoot部分
spring boot·后端·状态模式
努力学编程呀(๑•ี_เ•ี๑)2 小时前
【405】Not Allowed
java·vue.js·nginx·node.js
未既2 小时前
docker & docker-compose离线部署步骤
java·docker