疯狂的狮子Li
2023-03-12 662df23d4ddb9260670cb30b88c5eaaffa2a9e36
fix 修复 hutool MailUtils 未适配 jakarta 问题
已添加2个文件
594 ■■■■■ 文件已修改
ruoyi-common/ruoyi-common-mail/src/main/java/com/ruoyi/common/mail/utils/InternalMailUtil.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/ruoyi-common-mail/src/main/java/com/ruoyi/common/mail/utils/Mail.java 486 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/ruoyi-common-mail/src/main/java/com/ruoyi/common/mail/utils/InternalMailUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
package com.ruoyi.common.mail.utils;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.extra.mail.MailException;
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 InternalMailUtil {
    /**
     * å°†å¤šä¸ªå­—符串邮件地址转为{@link InternetAddress}列表<br>
     * å•个字符串地址可以是多个地址合并的字符串
     *
     * @param addrStrs åœ°å€æ•°ç»„
     * @param charset ç¼–码(主要用于中文用户名的编码)
     * @return åœ°å€æ•°ç»„
     * @since 4.0.3
     */
    public static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) {
        final List<InternetAddress> 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];
    }
    /**
     * å°†ä¸€ä¸ªåœ°å€å­—符串解析为多个地址<br>
     * åœ°å€é—´ä½¿ç”¨" "、","、";"分隔
     *
     * @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;
    }
    /**
     * ç¼–码中文字符<br>
     * ç¼–码失败返回原字符串
     *
     * @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;
    }
}
ruoyi-common/ruoyi-common-mail/src/main/java/com/ruoyi/common/mail/utils/Mail.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,486 @@
package com.ruoyi.common.mail.utils;
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 cn.hutool.extra.mail.*;
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 Mail implements Builder<MimeMessage> {
    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 Mail create(MailAccount mailAccount) {
        return new Mail(mailAccount);
    }
    /**
     * åˆ›å»ºé‚®ä»¶å®¢æˆ·ç«¯ï¼Œä½¿ç”¨å…¨å±€é‚®ä»¶å¸æˆ·
     *
     * @return Mail
     */
    public static Mail create() {
        return new Mail();
    }
    // --------------------------------------------------------------- Constructor start
    /**
     * æž„造,使用全局邮件帐户
     */
    public Mail() {
        this(GlobalMailAccount.INSTANCE.getAccount());
    }
    /**
     * æž„造
     *
     * @param mailAccount é‚®ä»¶å¸æˆ·ï¼Œå¦‚果为null使用默认配置文件的全局邮件配置
     */
    public Mail(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 Mail to(String... tos) {
        return setTos(tos);
    }
    /**
     * è®¾ç½®å¤šä¸ªæ”¶ä»¶äºº
     *
     * @param tos æ”¶ä»¶äººåˆ—表
     * @return this
     */
    public Mail setTos(String... tos) {
        this.tos = tos;
        return this;
    }
    /**
     * è®¾ç½®å¤šä¸ªæŠ„送人(carbon copy)
     *
     * @param ccs æŠ„送人列表
     * @return this
     * @since 4.0.3
     */
    public Mail setCcs(String... ccs) {
        this.ccs = ccs;
        return this;
    }
    /**
     * è®¾ç½®å¤šä¸ªå¯†é€äººï¼ˆblind carbon copy)
     *
     * @param bccs å¯†é€äººåˆ—表
     * @return this
     * @since 4.0.3
     */
    public Mail setBccs(String... bccs) {
        this.bccs = bccs;
        return this;
    }
    /**
     * è®¾ç½®å¤šä¸ªå›žå¤åœ°å€(reply-to)
     *
     * @param reply å›žå¤åœ°å€(reply-to)列表
     * @return this
     * @since 4.6.0
     */
    public Mail setReply(String... reply) {
        this.reply = reply;
        return this;
    }
    /**
     * è®¾ç½®æ ‡é¢˜
     *
     * @param title æ ‡é¢˜
     * @return this
     */
    public Mail setTitle(String title) {
        this.title = title;
        return this;
    }
    /**
     * è®¾ç½®æ­£æ–‡<br>
     * æ­£æ–‡å¯ä»¥æ˜¯æ™®é€šæ–‡æœ¬ä¹Ÿå¯ä»¥æ˜¯HTML(默认普通文本),可以通过调用{@link #setHtml(boolean)} è®¾ç½®æ˜¯å¦ä¸ºHTML
     *
     * @param content æ­£æ–‡
     * @return this
     */
    public Mail setContent(String content) {
        this.content = content;
        return this;
    }
    /**
     * è®¾ç½®æ˜¯å¦æ˜¯HTML
     *
     * @param isHtml æ˜¯å¦ä¸ºHTML
     * @return this
     */
    public Mail setHtml(boolean isHtml) {
        this.isHtml = isHtml;
        return this;
    }
    /**
     * è®¾ç½®æ­£æ–‡
     *
     * @param content æ­£æ–‡å†…容
     * @param isHtml  æ˜¯å¦ä¸ºHTML
     * @return this
     */
    public Mail setContent(String content, boolean isHtml) {
        setContent(content);
        return setHtml(isHtml);
    }
    /**
     * è®¾ç½®æ–‡ä»¶ç±»åž‹é™„件,文件可以是图片文件,此时自动设置cid(正文中引用图片),默认cid为文件名
     *
     * @param files é™„件文件列表
     * @return this
     */
    public Mail 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 Mail 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 Mail 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 Mail 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 Mail 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 Mail setCharset(Charset charset) {
        this.mailAccount.setCharset(charset);
        return this;
    }
    /**
     * è®¾ç½®æ˜¯å¦ä½¿ç”¨å…¨å±€ä¼šè¯ï¼Œé»˜è®¤ä¸ºtrue
     *
     * @param isUseGlobalSession æ˜¯å¦ä½¿ç”¨å…¨å±€ä¼šè¯ï¼Œé»˜è®¤ä¸ºtrue
     * @return this
     * @since 4.0.2
     */
    public Mail setUseGlobalSession(boolean isUseGlobalSession) {
        this.useGlobalSession = isUseGlobalSession;
        return this;
    }
    /**
     * è®¾ç½®debug输出位置,可以自定义debug日志
     *
     * @param debugOutput debug输出位置
     * @return this
     * @since 5.5.6
     */
    public Mail 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(InternalMailUtil.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, InternalMailUtil.parseAddressFromStrs(this.tos, charset));
        // æŠ„送人
        if (ArrayUtil.isNotEmpty(this.ccs)) {
            msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));
        }
        // å¯†é€äºº
        if (ArrayUtil.isNotEmpty(this.bccs)) {
            msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));
        }
        // å›žå¤åœ°å€(reply-to)
        if (ArrayUtil.isNotEmpty(this.reply)) {
            msg.setReplyTo(InternalMailUtil.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;
    }
    /**
     * èŽ·å–é»˜è®¤é‚®ä»¶ä¼šè¯<br>
     * å¦‚果为全局单例的会话,则全局只允许一个邮件帐号,否则每次发送邮件会新建一个新的会话
     *
     * @return é‚®ä»¶ä¼šè¯ {@link Session}
     */
    private Session getSession() {
        final Session session = MailUtils.getSession(this.mailAccount, this.useGlobalSession);
        if (null != this.debugOutput) {
            session.setDebugOut(debugOutput);
        }
        return session;
    }
    // --------------------------------------------------------------- Private method end
}