在使用java进行邮件发送开发时遇到的坑

为了方便开发,引入了Hutool在新窗口打开工具类,使用MailUtil在新窗口打开相关工具进行邮件发送。

1. 邮件内容带图片

方案1(不推荐)

使用html格式的邮件内容,图片使用base64编码,如下:

<img src="data:image/png;base64,图片base64编码"/>

在开发时,将模板中图片base64编码替换为占位符,使用Freemarker进行渲染,将占位符替换为图片base64编码。

然后调用MailUtil.sendHtml方法进行发送,代码如下:

String to = "123@demo.com";
String subject = "测试邮件";
String content = "邮件内容";
MailUtil.sendHtml(to, subject, content);

注意

坑:兼容性不好,使用此方法发送邮件,在邮件客户端(例如foxmail,其他未测试)图片可正常显示,在网页版邮箱(例如IBM iNotes ,其他未测试)图片显示不正常。

方案2(推荐)

使用html格式的邮件内容,图片使用cid,如下:

<img src="cid:图片cid"/>

在开发时,将图片cid与图片文件流进行映射,相关代码如下:

Map<String, InputStream> imageMap = new HashMap<>();
InputStream image = new FileInputStream("image.png");
imageMap.put("图片cid", image);
MailUtil.send(account, tos, ccs, null, title, content, imageMap, true);

注意

坑1:附件列表会额外显示正文中的图片(具体哪些网页版或客户端会遇到未进行测试,如果遇到的小伙伴可以尝试此方式),经过排查,Hutool 工具类在添加图片时会给附件设置FileName,设置后,会在附件列表显示。

修改方法:

在核心类Mail中,修改setAttachments方法,增加boolean inline参数,是否为内联附件,对于内联附件,不设置FileName,代码如下:

public Mail setAttachments(boolean inline, DataSource... attachments) {
    // ···
                if (!inline) {
                    bodyPart.setFileName(nameEncoded);
                }
    // ···
}

同时,需要修改调用此方法的代码,方法addImage中的调用传入true,其他调用传入false,标识图片为内联附件,不在附件列表中显示。

注意

坑2:博主在集成时使用的邮箱是IBM iNotes,在测试过程中发现,邮件客户端可正常显示,但是在网页版会多显示一个图片,经过排查发现, Hutool工具类在处理邮件内容时,处理逻辑为先添加图片,后添加正文内容,导致多个图片显示。

修改方法:

在核心类Mail中,增加类变量存储附件,先不添加附件到内容中,

private final List<MimeBodyPart> attachments = new ArrayList<>();

public Mail setAttachments(boolean inline, DataSource... attachments) {
    // ···
                // 先不添加到multipart中,先存储到attachments变量中,最后统一添加
                // this.multipart.addBodyPart(bodyPart);
                this.attachments.add(bodyPart);
    // ···
}
private Multipart buildContent(Charset charset) throws MessagingException {
    String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
    MimeBodyPart body = new MimeBodyPart();
    body.setContent(this.content, StrUtil.format("text/{}; charset={}", this.isHtml ? "html" : "plain", charsetStr));
    this.multipart.addBodyPart(body);
    // 添加完正文内容后,将变量中附件添加到multipart中
    if (CollectionUtil.isNotEmpty(this.attachments)) {
        for (MimeBodyPart attachment : this.attachments) {
            this.multipart.addBodyPart(attachment);
        }
    }
    return this.multipart;
}

注意

坑3:有些小伙伴在发送邮件时,可能页面要求比较复杂,需要用到背景图之类的,一般写法可能是使用background-image: url('cid:图片cid') ,但是可能在某些邮箱中(比如盈世)会出现图片出现在附件列表中的情况,经过排查发现背景图样式的不会被邮箱判定为引用,从而导致显示在附件列表中。

修改方法:

在模板中,除了写background-image: url('cid:图片cid')外,额外添加一个<img src="cid:图片cid" style="display: none;"/> ,来让邮箱识别为图片已被应用,从而解决图片显示在附件列表中的问题。

或者调整邮件模板样式,不使用背景图,而是使用img标签+绝对定位控制位置的方式,来实现需求要达到的效果。

在不修改jar包源码的情况下替换jar包中方法的逻辑

完整代码如下:

package cn.hutool.extra.mail;

import cn.hutool.core.builder.Builder;
import cn.hutool.core.collection.CollectionUtil;
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 javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.activation.FileTypeMap;
import javax.mail.*;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMessage.RecipientType;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import javax.mail.util.ByteArrayDataSource;
import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class Mail implements Builder<MimeMessage> {
    private static final long serialVersionUID = 1L;
    private final MailAccount mailAccount;
    private String[] tos;
    private String[] ccs;
    private String[] bccs;
    private String[] reply;
    private String title;
    private String content;
    private boolean isHtml;
    private final Multipart multipart;
    private boolean useGlobalSession;
    private PrintStream debugOutput;
    private final List<MimeBodyPart> attachments = new ArrayList<>();

    public static Mail create(MailAccount mailAccount) {
        return new Mail(mailAccount);
    }

    public static Mail create() {
        return new Mail();
    }

    public Mail() {
        this(GlobalMailAccount.INSTANCE.getAccount());
    }

    public Mail(MailAccount mailAccount) {
        this.multipart = new MimeMultipart();
        this.useGlobalSession = false;
        mailAccount = null != mailAccount ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
        this.mailAccount = mailAccount.defaultIfEmpty();
    }

    public Mail to(String... tos) {
        return this.setTos(tos);
    }

    public Mail setTos(String... tos) {
        this.tos = tos;
        return this;
    }

    public Mail setCcs(String... ccs) {
        this.ccs = ccs;
        return this;
    }

    public Mail setBccs(String... bccs) {
        this.bccs = bccs;
        return this;
    }

    public Mail setReply(String... reply) {
        this.reply = reply;
        return this;
    }

    public Mail setTitle(String title) {
        this.title = title;
        return this;
    }

    public Mail setContent(String content) {
        this.content = content;
        return this;
    }

    public Mail setHtml(boolean isHtml) {
        this.isHtml = isHtml;
        return this;
    }

    public Mail setContent(String content, boolean isHtml) {
        this.setContent(content);
        return this.setHtml(isHtml);
    }

    public Mail setFiles(File... files) {
        if (ArrayUtil.isEmpty(files)) {
            return this;
        } else {
            DataSource[] attachments = new DataSource[files.length];

            for (int i = 0; i < files.length; ++i) {
                attachments[i] = new FileDataSource(files[i]);
            }

            return this.setAttachments(attachments);
        }
    }

    public Mail setAttachments(DataSource... attachments) {
        return setAttachments(false, attachments);
    }

    public Mail setAttachments(boolean inline, DataSource... attachments) {
        if (ArrayUtil.isNotEmpty(attachments)) {
            Charset charset = this.mailAccount.getCharset();

            try {

                for (DataSource attachment : attachments) {
                    MimeBodyPart bodyPart = new MimeBodyPart();
                    bodyPart.setDataHandler(new DataHandler(attachment));
                    String nameEncoded = attachment.getName();
                    if (this.mailAccount.isEncodefilename()) {
                        nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);
                    }
                    if (!inline) {
                        bodyPart.setFileName(nameEncoded);
                    }
                    if (StrUtil.startWith(attachment.getContentType(), "image/")) {
                        bodyPart.setContentID(nameEncoded);
                    }
                    this.attachments.add(bodyPart);
//                    this.multipart.addBodyPart(bodyPart);
                }
            } catch (MessagingException var9) {
                throw new MailException(var9);
            }
        }

        return this;
    }

    public Mail addImage(String cid, InputStream imageStream) {
        return this.addImage(cid, imageStream, null);
    }

    public Mail addImage(String cid, InputStream imageStream, String contentType) {
        ByteArrayDataSource imgSource;
        try {
            imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
        } catch (IOException var6) {
            throw new IORuntimeException(var6);
        }

        imgSource.setName(cid);
        return this.setAttachments(true, imgSource);
    }

    public Mail addImage(String cid, File imageFile) {
        BufferedInputStream in = null;

        Mail var4;
        try {
            in = FileUtil.getInputStream(imageFile);
            var4 = this.addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
        } finally {
            IoUtil.close(in);
        }

        return var4;
    }

    public Mail setCharset(Charset charset) {
        this.mailAccount.setCharset(charset);
        return this;
    }

    public Mail setUseGlobalSession(boolean isUseGlobalSession) {
        this.useGlobalSession = isUseGlobalSession;
        return this;
    }

    public Mail setDebugOutput(PrintStream debugOutput) {
        this.debugOutput = debugOutput;
        return this;
    }

    public MimeMessage build() {
        try {
            return this.buildMsg();
        } catch (MessagingException var2) {
            throw new MailException(var2);
        }
    }

    public String send() throws MailException {
        try {
            return this.doSend();
        } catch (MessagingException var4) {
            if (var4 instanceof SendFailedException) {
                Address[] invalidAddresses = ((SendFailedException) var4).getInvalidAddresses();
                String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
                throw new MailException(msg, var4);
            } else {
                throw new MailException(var4);
            }
        }
    }

    private String doSend() throws MessagingException {
        MimeMessage mimeMessage = this.buildMsg();
        Transport.send(mimeMessage);
        return mimeMessage.getMessageID();
    }

    private MimeMessage buildMsg() throws MessagingException {
        Charset charset = this.mailAccount.getCharset();
        MimeMessage msg = new MimeMessage(this.getSession());
        String from = this.mailAccount.getFrom();
        if (StrUtil.isEmpty(from)) {
            msg.setFrom();
        } else {
            msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset));
        }

        msg.setSubject(this.title, null == charset ? null : charset.name());
        msg.setSentDate(new Date());
        msg.setContent(this.buildContent(charset));
        msg.setRecipients(RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset));
        if (ArrayUtil.isNotEmpty(this.ccs)) {
            msg.setRecipients(RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));
        }

        if (ArrayUtil.isNotEmpty(this.bccs)) {
            msg.setRecipients(RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));
        }

        if (ArrayUtil.isNotEmpty(this.reply)) {
            msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset));
        }

        return msg;
    }

    private Multipart buildContent(Charset charset) throws MessagingException {
        String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
        MimeBodyPart body = new MimeBodyPart();
        body.setContent(this.content, StrUtil.format("text/{}; charset={}", this.isHtml ? "html" : "plain", charsetStr));
        this.multipart.addBodyPart(body);
        if (CollectionUtil.isNotEmpty(this.attachments)) {
            for (MimeBodyPart attachment : this.attachments) {
                this.multipart.addBodyPart(attachment);
            }
        }
        return this.multipart;
    }

    private Session getSession() {
        Session session = MailUtil.getSession(this.mailAccount, this.useGlobalSession);
        if (null != this.debugOutput) {
            session.setDebugOut(this.debugOutput);
        }

        return session;
    }
}