JavaMail API 提供平台無關、協議無關的郵件發送框架。核心類別:Session (環境)、Message (郵件)、Transport (傳輸)、Store (接收)。需引入 javax.mail:mail 或 Spring Boot spring-boot-starter-mail。
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
@Data
@Builder
public class MailProperties {
private String host; // SMTP 伺服器
private int port = 465; // SSL 埠口 (587 for STARTTLS)
private String username; // 發信帳號
private String password; // 授權碼/密碼
private String from; // 發件人
private String protocol = "smtp";
private boolean sslEnable = true;
private boolean starttlsEnable = false;
private int timeout = 10000; // 連線逾時 ms
private String[] defaultTo; // 預設收件人
}
public class MimeMessageBuilder {
private final Session session;
private final MailProperties props;
public MimeMessageBuilder(Session session, MailProperties props) {
this.session = session;
this.props = props;
}
public MimeMessage buildSimple(String to, String subject, String htmlContent)
throws MessagingException {
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(props.getFrom()));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
message.setSubject(subject, "UTF-8");
message.setContent(htmlContent, "text/html;charset=UTF-8");
message.setSentDate(new Date());
return message;
}
public MimeMessage buildWithAttachments(String to, String subject, String htmlContent,
List attachments, Map inlineImages)
throws MessagingException {
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(props.getFrom()));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
message.setSubject(subject, "UTF-8");
message.setSentDate(new Date());
// 根容器:mixed (附件 + related)
Multipart root = new MimeMultipart("mixed");
// 關聯容器:related (HTML + 內嵌圖片)
Multipart related = new MimeMultipart("related");
// HTML 內容部分
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlContent, "text/html;charset=UTF-8");
related.addBodyPart(htmlPart);
// 內嵌圖片:cid: 引用
if (inlineImages != null) {
for (Map.Entry entry : inlineImages.entrySet()) {
MimeBodyPart imgPart = new MimeBodyPart();
imgPart.attachFile(entry.getValue());
imgPart.setContentID("<" + entry.getKey() + ">"); //
imgPart.setDisposition(MimeBodyPart.INLINE);
related.addBodyPart(imgPart);
}
}
// 將 related 包裝進 mixed
MimeBodyPart relatedWrapper = new MimeBodyPart();
relatedWrapper.setContent(related);
root.addBodyPart(relatedWrapper);
// 附件部分
if (attachments != null) {
for (File file : attachments) {
MimeBodyPart attPart = new MimeBodyPart();
attPart.attachFile(file);
attPart.setFileName(MimeUtility.encodeText(file.getName(), "UTF-8", "B"));
root.addBodyPart(attPart);
}
}
message.setContent(root);
return message;
}
}
@Service
public class MailService {
private final MailProperties props;
private final Session session;
public MailService(MailProperties props) {
this.props = props;
Properties javaMailProps = new Properties();
javaMailProps.put("mail.smtp.host", props.getHost());
javaMailProps.put("mail.smtp.port", props.getPort());
javaMailProps.put("mail.smtp.auth", "true");
javaMailProps.put("mail.smtp.timeout", props.getTimeout());
javaMailProps.put("mail.smtp.connectiontimeout", props.getTimeout());
if (props.isSslEnable()) {
javaMailProps.put("mail.smtp.ssl.enable", "true");
javaMailProps.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
javaMailProps.put("mail.smtp.socketFactory.port", props.getPort());
}
if (props.isStarttlsEnable()) {
javaMailProps.put("mail.smtp.starttls.enable", "true");
}
this.session = Session.getInstance(javaMailProps, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(props.getUsername(), props.getPassword());
}
});
// 除錯模式
// session.setDebug(true);
}
public void sendSimple(String to, String subject, String htmlContent) {
send(Collections.singletonList(to), subject, htmlContent, null, null);
}
public void sendWithAttachments(String to, String subject, String htmlContent,
List attachments, Map inlineImages) {
send(Collections.singletonList(to), subject, htmlContent, attachments, inlineImages);
}
private void send(List toList, String subject, String htmlContent,
List attachments, Map inlineImages) {
try {
MimeMessageBuilder builder = new MimeMessageBuilder(session, props);
MimeMessage message;
if (attachments != null || inlineImages != null) {
message = builder.buildWithAttachments(
String.join(",", toList), subject, htmlContent, attachments, inlineImages);
} else {
message = builder.buildSimple(String.join(",", toList), subject, htmlContent);
}
Transport.send(message);
log.info("郵件發送成功: {} -> {}", props.getFrom(), toList);
} catch (MessagingException e) {
log.error("郵件發送失敗", e);
throw new MailSendException("發送失敗: " + e.getMessage(), e);
}
}
}
| 服務商 | SMTP 主機 | SSL 埠口 | STARTTLS 埠口 | 備註 |
|---|---|---|---|---|
| Gmail | smtp.gmail.com | 465 | 587 | 需開啟「應用程式密碼」 |
| Outlook/Hotmail | smtp.office365.com | 465 | 587 | 企業帳號可能不同 |
| QQ/騰訊企業郵 | smtp.qq.com / smtp.exmail.qq.com | 465 | 587 | 需開啟 SMTP 服務取得授權碼 |
| 163/126 | smtp.163.com | 465/994 | 587 | 需客戶端授權密碼 |
| 新浪 | smtp.sina.com | 465 | 587 | 免費帳號有發送限制 |
spring:
mail:
host: smtp.gmail.com
port: 465
username: your@gmail.com
password: 16位應用程式密碼
protocol: smtp
properties:
mail:
smtp:
ssl:
enable: true
auth: true
timeout: 10000
connectiontimeout: 10000
debug: false
@Service
@RequiredArgsConstructor
public class SpringMailService {
private final JavaMailSender mailSender;
public void sendHtml(String to, String subject, String html) {
MimeMessage message = mailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom("your@gmail.com");
helper.setTo(to);
helper.setSubject(subject);
helper.setText(html, true); // true = HTML
// 附件
// helper.addAttachment("report.pdf", new File("report.pdf"));
// 內嵌圖片
// helper.addInline("logo", new ClassPathResource("static/logo.png"));
mailSender.send(message);
} catch (MessagingException e) {
throw new MailException("發送失敗", e);
}
}
}
MimeUtility.encodeText(filename, "UTF-8", "B")cid:contentId 並放在 multipart/related 中Transport 連線、非同步佇列、批次發送、專用郵件閘道 (SendGrid、Amazon SES)List-Unsubscribe 標頭,符合反垃圾郵件法規