diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8156440ea..8e102cda1 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
### 🐣新特性
* 【core 】 Converter转换规则变更,空对象、空值转为Bean时,创建默认对象,而非null(issue#3649@Github)
* 【core 】 UrlQuery增加remove方法
+* 【extra 】 增加JakartaMailUtil,支持新包名的mail
### 🐞Bug修复
* 【core 】 修复因RFC3986理解有误导致的UrlPath处理冒号转义问题(issue#IAAE88@Gitee)
diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml
index a94cd21f2..58fd4f36c 100755
--- a/hutool-extra/pom.xml
+++ b/hutool-extra/pom.xml
@@ -142,6 +142,19 @@
compile
true
+
+ jakarta.mail
+ jakarta.mail-api
+ 2.1.2
+ compile
+ true
+
+
+ org.eclipse.angus
+ jakarta.mail
+ 2.0.2
+ test
+
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaInternalMailUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaInternalMailUtil.java
new file mode 100644
index 000000000..28df9aacf
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaInternalMailUtil.java
@@ -0,0 +1,107 @@
+package cn.hutool.extra.mail;
+
+import cn.hutool.core.util.ArrayUtil;
+
+import jakarta.mail.internet.AddressException;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeUtility;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 邮件内部工具类
+ * @author looly
+ * @since 3.2.3
+ */
+public class JakartaInternalMailUtil {
+
+ /**
+ * 将多个字符串邮件地址转为{@link InternetAddress}列表
+ * 单个字符串地址可以是多个地址合并的字符串
+ *
+ * @param addrStrs 地址数组
+ * @param charset 编码(主要用于中文用户名的编码)
+ * @return 地址数组
+ * @since 4.0.3
+ */
+ public static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) {
+ final List resultList = new ArrayList<>(addrStrs.length);
+ InternetAddress[] addrs;
+ for (String addrStr : addrStrs) {
+ addrs = parseAddress(addrStr, charset);
+ if (ArrayUtil.isNotEmpty(addrs)) {
+ Collections.addAll(resultList, addrs);
+ }
+ }
+ return resultList.toArray(new InternetAddress[0]);
+ }
+
+ /**
+ * 解析第一个地址
+ *
+ * @param address 地址字符串
+ * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
+ * @return 地址列表
+ */
+ public static InternetAddress parseFirstAddress(String address, Charset charset) {
+ final InternetAddress[] internetAddresses = parseAddress(address, charset);
+ if (ArrayUtil.isEmpty(internetAddresses)) {
+ try {
+ return new InternetAddress(address);
+ } catch (AddressException e) {
+ throw new MailException(e);
+ }
+ }
+ return internetAddresses[0];
+ }
+
+ /**
+ * 将一个地址字符串解析为多个地址
+ * 地址间使用" "、","、";"分隔
+ *
+ * @param address 地址字符串
+ * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
+ * @return 地址列表
+ */
+ public static InternetAddress[] parseAddress(String address, Charset charset) {
+ InternetAddress[] addresses;
+ try {
+ addresses = InternetAddress.parse(address);
+ } catch (AddressException e) {
+ throw new MailException(e);
+ }
+ //编码用户名
+ if (ArrayUtil.isNotEmpty(addresses)) {
+ final String charsetStr = null == charset ? null : charset.name();
+ for (InternetAddress internetAddress : addresses) {
+ try {
+ internetAddress.setPersonal(internetAddress.getPersonal(), charsetStr);
+ } catch (UnsupportedEncodingException e) {
+ throw new MailException(e);
+ }
+ }
+ }
+
+ return addresses;
+ }
+
+ /**
+ * 编码中文字符
+ * 编码失败返回原字符串
+ *
+ * @param text 被编码的文本
+ * @param charset 编码
+ * @return 编码后的结果
+ */
+ public static String encodeText(String text, Charset charset) {
+ try {
+ return MimeUtility.encodeText(text, charset.name(), null);
+ } catch (UnsupportedEncodingException e) {
+ // ignore
+ }
+ return text;
+ }
+}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaMail.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaMail.java
new file mode 100644
index 000000000..094ddcf8c
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaMail.java
@@ -0,0 +1,485 @@
+package cn.hutool.extra.mail;
+
+import cn.hutool.core.builder.Builder;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+
+import jakarta.activation.DataHandler;
+import jakarta.activation.DataSource;
+import jakarta.activation.FileDataSource;
+import jakarta.activation.FileTypeMap;
+import jakarta.mail.*;
+import jakarta.mail.internet.MimeBodyPart;
+import jakarta.mail.internet.MimeMessage;
+import jakarta.mail.internet.MimeMultipart;
+import jakarta.mail.internet.MimeUtility;
+import jakarta.mail.util.ByteArrayDataSource;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.util.Date;
+
+/**
+ * 邮件发送客户端
+ *
+ * @author looly
+ * @since 3.2.0
+ */
+public class JakartaMail implements Builder {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 邮箱帐户信息以及一些客户端配置信息
+ */
+ private final MailAccount mailAccount;
+ /**
+ * 收件人列表
+ */
+ private String[] tos;
+ /**
+ * 抄送人列表(carbon copy)
+ */
+ private String[] ccs;
+ /**
+ * 密送人列表(blind carbon copy)
+ */
+ private String[] bccs;
+ /**
+ * 回复地址(reply-to)
+ */
+ private String[] reply;
+ /**
+ * 标题
+ */
+ private String title;
+ /**
+ * 内容
+ */
+ private String content;
+ /**
+ * 是否为HTML
+ */
+ private boolean isHtml;
+ /**
+ * 正文、附件和图片的混合部分
+ */
+ private final Multipart multipart = new MimeMultipart();
+ /**
+ * 是否使用全局会话,默认为false
+ */
+ private boolean useGlobalSession = false;
+
+ /**
+ * debug输出位置,可以自定义debug日志
+ */
+ private PrintStream debugOutput;
+
+ /**
+ * 创建邮件客户端
+ *
+ * @param mailAccount 邮件帐号
+ * @return Mail
+ */
+ public static JakartaMail create(MailAccount mailAccount) {
+ return new JakartaMail(mailAccount);
+ }
+
+ /**
+ * 创建邮件客户端,使用全局邮件帐户
+ *
+ * @return Mail
+ */
+ public static JakartaMail create() {
+ return new JakartaMail();
+ }
+
+ // --------------------------------------------------------------- Constructor start
+
+ /**
+ * 构造,使用全局邮件帐户
+ */
+ public JakartaMail() {
+ this(GlobalMailAccount.INSTANCE.getAccount());
+ }
+
+ /**
+ * 构造
+ *
+ * @param mailAccount 邮件帐户,如果为null使用默认配置文件的全局邮件配置
+ */
+ public JakartaMail(MailAccount mailAccount) {
+ mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
+ this.mailAccount = mailAccount.defaultIfEmpty();
+ }
+ // --------------------------------------------------------------- Constructor end
+
+ // --------------------------------------------------------------- Getters and Setters start
+
+ /**
+ * 设置收件人
+ *
+ * @param tos 收件人列表
+ * @return this
+ * @see #setTos(String...)
+ */
+ public JakartaMail to(String... tos) {
+ return setTos(tos);
+ }
+
+ /**
+ * 设置多个收件人
+ *
+ * @param tos 收件人列表
+ * @return this
+ */
+ public JakartaMail setTos(String... tos) {
+ this.tos = tos;
+ return this;
+ }
+
+ /**
+ * 设置多个抄送人(carbon copy)
+ *
+ * @param ccs 抄送人列表
+ * @return this
+ * @since 4.0.3
+ */
+ public JakartaMail setCcs(String... ccs) {
+ this.ccs = ccs;
+ return this;
+ }
+
+ /**
+ * 设置多个密送人(blind carbon copy)
+ *
+ * @param bccs 密送人列表
+ * @return this
+ * @since 4.0.3
+ */
+ public JakartaMail setBccs(String... bccs) {
+ this.bccs = bccs;
+ return this;
+ }
+
+ /**
+ * 设置多个回复地址(reply-to)
+ *
+ * @param reply 回复地址(reply-to)列表
+ * @return this
+ * @since 4.6.0
+ */
+ public JakartaMail setReply(String... reply) {
+ this.reply = reply;
+ return this;
+ }
+
+ /**
+ * 设置标题
+ *
+ * @param title 标题
+ * @return this
+ */
+ public JakartaMail setTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * 设置正文
+ * 正文可以是普通文本也可以是HTML(默认普通文本),可以通过调用{@link #setHtml(boolean)} 设置是否为HTML
+ *
+ * @param content 正文
+ * @return this
+ */
+ public JakartaMail setContent(String content) {
+ this.content = content;
+ return this;
+ }
+
+ /**
+ * 设置是否是HTML
+ *
+ * @param isHtml 是否为HTML
+ * @return this
+ */
+ public JakartaMail setHtml(boolean isHtml) {
+ this.isHtml = isHtml;
+ return this;
+ }
+
+ /**
+ * 设置正文
+ *
+ * @param content 正文内容
+ * @param isHtml 是否为HTML
+ * @return this
+ */
+ public JakartaMail setContent(String content, boolean isHtml) {
+ setContent(content);
+ return setHtml(isHtml);
+ }
+
+ /**
+ * 设置文件类型附件,文件可以是图片文件,此时自动设置cid(正文中引用图片),默认cid为文件名
+ *
+ * @param files 附件文件列表
+ * @return this
+ */
+ public JakartaMail setFiles(File... files) {
+ if (ArrayUtil.isEmpty(files)) {
+ return this;
+ }
+
+ final DataSource[] attachments = new DataSource[files.length];
+ for (int i = 0; i < files.length; i++) {
+ attachments[i] = new FileDataSource(files[i]);
+ }
+ return setAttachments(attachments);
+ }
+
+ /**
+ * 增加附件或图片,附件使用{@link DataSource} 形式表示,可以使用{@link FileDataSource}包装文件表示文件附件
+ *
+ * @param attachments 附件列表
+ * @return this
+ * @since 4.0.9
+ */
+ public JakartaMail setAttachments(DataSource... attachments) {
+ if (ArrayUtil.isNotEmpty(attachments)) {
+ final Charset charset = this.mailAccount.getCharset();
+ MimeBodyPart bodyPart;
+ String nameEncoded;
+ try {
+ for (DataSource attachment : attachments) {
+ bodyPart = new MimeBodyPart();
+ bodyPart.setDataHandler(new DataHandler(attachment));
+ nameEncoded = attachment.getName();
+ if (this.mailAccount.isEncodefilename()) {
+ nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);
+ }
+ // 普通附件文件名
+ bodyPart.setFileName(nameEncoded);
+ if (StrUtil.startWith(attachment.getContentType(), "image/")) {
+ // 图片附件,用于正文中引用图片
+ bodyPart.setContentID(nameEncoded);
+ }
+ this.multipart.addBodyPart(bodyPart);
+ }
+ } catch (MessagingException e) {
+ throw new MailException(e);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * 增加图片,图片的键对应到邮件模板中的占位字符串,图片类型默认为"image/jpeg"
+ *
+ * @param cid 图片与占位符,占位符格式为cid:${cid}
+ * @param imageStream 图片文件
+ * @return this
+ * @since 4.6.3
+ */
+ public JakartaMail addImage(String cid, InputStream imageStream) {
+ return addImage(cid, imageStream, null);
+ }
+
+ /**
+ * 增加图片,图片的键对应到邮件模板中的占位字符串
+ *
+ * @param cid 图片与占位符,占位符格式为cid:${cid}
+ * @param imageStream 图片流,不关闭
+ * @param contentType 图片类型,null赋值默认的"image/jpeg"
+ * @return this
+ * @since 4.6.3
+ */
+ public JakartaMail addImage(String cid, InputStream imageStream, String contentType) {
+ ByteArrayDataSource imgSource;
+ try {
+ imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ imgSource.setName(cid);
+ return setAttachments(imgSource);
+ }
+
+ /**
+ * 增加图片,图片的键对应到邮件模板中的占位字符串
+ *
+ * @param cid 图片与占位符,占位符格式为cid:${cid}
+ * @param imageFile 图片文件
+ * @return this
+ * @since 4.6.3
+ */
+ public JakartaMail addImage(String cid, File imageFile) {
+ InputStream in = null;
+ try {
+ in = FileUtil.getInputStream(imageFile);
+ return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
+ } finally {
+ IoUtil.close(in);
+ }
+ }
+
+ /**
+ * 设置字符集编码
+ *
+ * @param charset 字符集编码
+ * @return this
+ * @see MailAccount#setCharset(Charset)
+ */
+ public JakartaMail setCharset(Charset charset) {
+ this.mailAccount.setCharset(charset);
+ return this;
+ }
+
+ /**
+ * 设置是否使用全局会话,默认为true
+ *
+ * @param isUseGlobalSession 是否使用全局会话,默认为true
+ * @return this
+ * @since 4.0.2
+ */
+ public JakartaMail setUseGlobalSession(boolean isUseGlobalSession) {
+ this.useGlobalSession = isUseGlobalSession;
+ return this;
+ }
+
+ /**
+ * 设置debug输出位置,可以自定义debug日志
+ *
+ * @param debugOutput debug输出位置
+ * @return this
+ * @since 5.5.6
+ */
+ public JakartaMail setDebugOutput(PrintStream debugOutput) {
+ this.debugOutput = debugOutput;
+ return this;
+ }
+ // --------------------------------------------------------------- Getters and Setters end
+
+ @Override
+ public MimeMessage build() {
+ try {
+ return buildMsg();
+ } catch (MessagingException e) {
+ throw new MailException(e);
+ }
+ }
+
+ /**
+ * 发送
+ *
+ * @return message-id
+ * @throws MailException 邮件发送异常
+ */
+ public String send() throws MailException {
+ try {
+ return doSend();
+ } catch (MessagingException e) {
+ if (e instanceof SendFailedException) {
+ // 当地址无效时,显示更加详细的无效地址信息
+ final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
+ final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
+ throw new MailException(msg, e);
+ }
+ throw new MailException(e);
+ }
+ }
+
+ // --------------------------------------------------------------- Private method start
+
+ /**
+ * 执行发送
+ *
+ * @return message-id
+ * @throws MessagingException 发送异常
+ */
+ private String doSend() throws MessagingException {
+ final MimeMessage mimeMessage = buildMsg();
+ Transport.send(mimeMessage);
+ return mimeMessage.getMessageID();
+ }
+
+ /**
+ * 构建消息
+ *
+ * @return {@link MimeMessage}消息
+ * @throws MessagingException 消息异常
+ */
+ private MimeMessage buildMsg() throws MessagingException {
+ final Charset charset = this.mailAccount.getCharset();
+ final MimeMessage msg = new MimeMessage(getSession());
+ // 发件人
+ final String from = this.mailAccount.getFrom();
+ if (StrUtil.isEmpty(from)) {
+ // 用户未提供发送方,则从Session中自动获取
+ msg.setFrom();
+ } else {
+ msg.setFrom(JakartaInternalMailUtil.parseFirstAddress(from, charset));
+ }
+ // 标题
+ msg.setSubject(this.title, (null == charset) ? null : charset.name());
+ // 发送时间
+ msg.setSentDate(new Date());
+ // 内容和附件
+ msg.setContent(buildContent(charset));
+ // 收件人
+ msg.setRecipients(MimeMessage.RecipientType.TO, JakartaInternalMailUtil.parseAddressFromStrs(this.tos, charset));
+ // 抄送人
+ if (ArrayUtil.isNotEmpty(this.ccs)) {
+ msg.setRecipients(MimeMessage.RecipientType.CC, JakartaInternalMailUtil.parseAddressFromStrs(this.ccs, charset));
+ }
+ // 密送人
+ if (ArrayUtil.isNotEmpty(this.bccs)) {
+ msg.setRecipients(MimeMessage.RecipientType.BCC, JakartaInternalMailUtil.parseAddressFromStrs(this.bccs, charset));
+ }
+ // 回复地址(reply-to)
+ if (ArrayUtil.isNotEmpty(this.reply)) {
+ msg.setReplyTo(JakartaInternalMailUtil.parseAddressFromStrs(this.reply, charset));
+ }
+
+ return msg;
+ }
+
+ /**
+ * 构建邮件信息主体
+ *
+ * @param charset 编码,{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()}
+ * @return 邮件信息主体
+ * @throws MessagingException 消息异常
+ */
+ private Multipart buildContent(Charset charset) throws MessagingException {
+ final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
+ // 正文
+ final MimeBodyPart body = new MimeBodyPart();
+ body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr));
+ this.multipart.addBodyPart(body);
+
+ return this.multipart;
+ }
+
+ /**
+ * 获取默认邮件会话
+ * 如果为全局单例的会话,则全局只允许一个邮件帐号,否则每次发送邮件会新建一个新的会话
+ *
+ * @return 邮件会话 {@link Session}
+ */
+ private Session getSession() {
+ final Session session = JakartaMailUtil.getSession(this.mailAccount, this.useGlobalSession);
+
+ if (null != this.debugOutput) {
+ session.setDebugOut(debugOutput);
+ }
+
+ return session;
+ }
+ // --------------------------------------------------------------- Private method end
+}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaMailUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaMailUtil.java
new file mode 100644
index 000000000..d41cce22a
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaMailUtil.java
@@ -0,0 +1,441 @@
+package cn.hutool.extra.mail;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.CharUtil;
+import cn.hutool.core.util.StrUtil;
+
+import jakarta.mail.Authenticator;
+import jakarta.mail.Session;
+import java.io.File;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * 邮件工具类,基于jakarta.mail封装
+ *
+ * @author looly
+ * @since 5.8.30
+ */
+public class JakartaMailUtil {
+ /**
+ * 使用配置文件中设置的账户发送文本邮件,发送给单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendText(String to, String subject, String content, File... files) {
+ return send(to, subject, content, false, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(String to, String subject, String content, File... files) {
+ return send(to, subject, content, true, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(String to, String subject, String content, boolean isHtml, File... files) {
+ return send(splitAddress(to), subject, content, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人、抄送人、密送人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param cc 抄送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param bcc 密送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(String to, String cc, String bcc, String subject, String content, boolean isHtml, File... files) {
+ return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送文本邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String sendText(Collection tos, String subject, String content, File... files) {
+ return send(tos, subject, content, false, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送HTML邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(Collection tos, String subject, String content, File... files) {
+ return send(tos, subject, content, true, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(Collection tos, String subject, String content, boolean isHtml, File... files) {
+ return send(tos, null, null, subject, content, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(Collection tos, Collection ccs, Collection bccs, String subject, String content, boolean isHtml, File... files) {
+ return send(GlobalMailAccount.INSTANCE.getAccount(), true, tos, ccs, bccs, subject, content, null, isHtml, files);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件认证对象
+ * @param to 收件人,多个收件人逗号或者分号隔开
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String send(MailAccount mailAccount, String to, String subject, String content, boolean isHtml, File... files) {
+ return send(mailAccount, splitAddress(to), subject, content, isHtml, files);
+ }
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(MailAccount mailAccount, Collection tos, String subject, String content, boolean isHtml, File... files) {
+ return send(mailAccount, tos, null, null, subject, content, isHtml, files);
+ }
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(MailAccount mailAccount, Collection tos, Collection ccs, Collection bccs, String subject, String content, boolean isHtml, File... files) {
+ return send(mailAccount, false, tos, ccs, bccs, subject, content, null, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(String to, String subject, String content, Map imageMap, File... files) {
+ return send(to, subject, content, imageMap, true, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(String to, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(splitAddress(to), subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人、抄送人、密送人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param cc 抄送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param bcc 密送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(String to, String cc, String bcc, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送HTML邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(Collection tos, String subject, String content, Map imageMap, File... files) {
+ return send(tos, subject, content, imageMap, true, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(Collection tos, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(tos, null, null, subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(Collection tos, Collection ccs, Collection bccs, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(GlobalMailAccount.INSTANCE.getAccount(), true, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件认证对象
+ * @param to 收件人,多个收件人逗号或者分号隔开
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String send(MailAccount mailAccount, String to, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(mailAccount, splitAddress(to), subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.6.3
+ */
+ public static String send(MailAccount mailAccount, Collection tos, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(mailAccount, tos, null, null, subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.6.3
+ */
+ public static String send(MailAccount mailAccount, Collection tos, Collection ccs, Collection bccs, String subject, String content, Map imageMap,
+ boolean isHtml, File... files) {
+ return send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 根据配置文件,获取邮件客户端会话
+ *
+ * @param mailAccount 邮件账户配置
+ * @param isSingleton 是否单例(全局共享会话)
+ * @return {@link Session}
+ * @since 5.5.7
+ */
+ public static Session getSession(MailAccount mailAccount, boolean isSingleton) {
+ Authenticator authenticator = null;
+ if (mailAccount.isAuth()) {
+ authenticator = new JakartaUserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
+ }
+
+ return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
+ : Session.getInstance(mailAccount.getSmtpProps(), authenticator);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------ Private method start
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param useGlobalSession 是否全局共享Session
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:${cid}
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.6.3
+ */
+ private static String send(MailAccount mailAccount, boolean useGlobalSession, Collection tos, Collection ccs, Collection bccs, String subject, String content,
+ Map imageMap, boolean isHtml, File... files) {
+ final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession);
+
+ // 可选抄送人
+ if (CollUtil.isNotEmpty(ccs)) {
+ mail.setCcs(ccs.toArray(new String[0]));
+ }
+ // 可选密送人
+ if (CollUtil.isNotEmpty(bccs)) {
+ mail.setBccs(bccs.toArray(new String[0]));
+ }
+
+ mail.setTos(tos.toArray(new String[0]));
+ mail.setTitle(subject);
+ mail.setContent(content);
+ mail.setHtml(isHtml);
+ mail.setFiles(files);
+
+ // 图片
+ if (MapUtil.isNotEmpty(imageMap)) {
+ for (Entry entry : imageMap.entrySet()) {
+ mail.addImage(entry.getKey(), entry.getValue());
+ // 关闭流
+ IoUtil.close(entry.getValue());
+ }
+ }
+
+ return mail.send();
+ }
+
+ /**
+ * 将多个联系人转为列表,分隔符为逗号或者分号
+ *
+ * @param addresses 多个联系人,如果为空返回null
+ * @return 联系人列表
+ */
+ private static List splitAddress(String addresses) {
+ if (StrUtil.isBlank(addresses)) {
+ return null;
+ }
+
+ List result;
+ if (StrUtil.contains(addresses, CharUtil.COMMA)) {
+ result = StrUtil.splitTrim(addresses, CharUtil.COMMA);
+ } else if (StrUtil.contains(addresses, ';')) {
+ result = StrUtil.splitTrim(addresses, ';');
+ } else {
+ result = CollUtil.newArrayList(addresses);
+ }
+ return result;
+ }
+ // ------------------------------------------------------------------------------------------------------------------------ Private method end
+}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaUserPassAuthenticator.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaUserPassAuthenticator.java
new file mode 100644
index 000000000..ae2d8d318
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/JakartaUserPassAuthenticator.java
@@ -0,0 +1,33 @@
+package cn.hutool.extra.mail;
+
+import jakarta.mail.Authenticator;
+import jakarta.mail.PasswordAuthentication;
+
+/**
+ * jakarta用户名密码验证器
+ *
+ * @author looly
+ * @since 5.8.30
+ */
+public class JakartaUserPassAuthenticator extends Authenticator {
+
+ private final String user;
+ private final String pass;
+
+ /**
+ * 构造
+ *
+ * @param user 用户名
+ * @param pass 密码
+ */
+ public JakartaUserPassAuthenticator(String user, String pass) {
+ this.user = user;
+ this.pass = pass;
+ }
+
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(this.user, this.pass);
+ }
+
+}
diff --git a/hutool-extra/src/test/java/cn/hutool/extra/mail/JakartaMailTest.java b/hutool-extra/src/test/java/cn/hutool/extra/mail/JakartaMailTest.java
new file mode 100644
index 000000000..b70cfee15
--- /dev/null
+++ b/hutool-extra/src/test/java/cn/hutool/extra/mail/JakartaMailTest.java
@@ -0,0 +1,69 @@
+package cn.hutool.extra.mail;
+
+import cn.hutool.core.io.FileUtil;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * 邮件发送测试
+ * @author looly
+ *
+ */
+public class JakartaMailTest {
+
+ @Test
+ @Ignore
+ public void sendWithFileTest() {
+ JakartaMailUtil.send("hutool@foxmail.com", "测试", "邮件来自Hutool测试
", true, FileUtil.file("d:/测试附件文本.txt"));
+ }
+
+ @Test
+ @Ignore
+ public void sendWithLongNameFileTest() {
+ //附件名长度大于60时的测试
+ JakartaMailUtil.send("hutool@foxmail.com", "测试", "邮件来自Hutool测试
", true, FileUtil.file("d:/6-LongLong一阶段平台建设周报2018.3.12-3.16.xlsx"));
+ }
+
+ @Test
+ @Ignore
+ public void sendWithImageTest() {
+ Map map = new HashMap<>();
+ map.put("testImage", FileUtil.getInputStream("f:/test/me.png"));
+ JakartaMailUtil.sendHtml("hutool@foxmail.com", "测试", "邮件来自Hutool测试
", map);
+ }
+
+ @Test
+ @Ignore
+ public void sendHtmlTest() {
+ JakartaMailUtil.send("hutool@foxmail.com", "测试", "邮件来自Hutool测试
", true);
+ }
+
+ @Test
+ @Ignore
+ public void sendByAccountTest() {
+ MailAccount account = new MailAccount();
+ account.setHost("smtp.yeah.net");
+ account.setPort(465);
+ account.setSslEnable(true);
+ account.setFrom("hutool@yeah.net");
+ account.setUser("hutool");
+ account.setPass("q1w2e3");
+ JakartaMailUtil.send(account, "hutool@foxmail.com", "测试", "邮件来自Hutool测试
", true);
+ }
+
+ @Test
+ public void mailAccountTest() {
+ MailAccount account = new MailAccount();
+ account.setFrom("hutool@yeah.net");
+ account.setDebug(true);
+ account.defaultIfEmpty();
+ Properties props = account.getSmtpProps();
+ Assert.assertEquals("true", props.getProperty("mail.debug"));
+ }
+}