在使用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
标签+绝对定位控制位置的方式,来实现需求要达到的效果。
完整代码如下:
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;
}
}