diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e9a64c60..a3122992b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ ### 新特性 * 【core】 改进CollUtil.zip逻辑,减少内存复制(issue#I10T01@Gitee) -* 【extra】 邮件支持图片(pr#495@Github) +* 【extra】 邮件增加图片支持(pr#495@Github) ### Bug修复 * 【http】 修复HttpRquest中body方法长度计算问题(issue#I10UPG@Gitee) diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java index ea24db971..60fdcf8a8 100644 --- a/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java @@ -5,13 +5,12 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.util.Date; -import java.util.Map; import javax.activation.DataHandler; import javax.activation.DataSource; import javax.activation.FileDataSource; +import javax.activation.FileTypeMap; import javax.mail.Authenticator; -import javax.mail.BodyPart; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.Session; @@ -21,8 +20,11 @@ import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import javax.mail.util.ByteArrayDataSource; +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; /** @@ -49,10 +51,8 @@ public class Mail { private String content; /** 是否为HTML */ private boolean isHtml; - /** 附件列表 */ - private DataSource[] attachments; - /** 图片列表 */ - private Map imageMap; + /** 正文、附件和图片的混合部分 */ + private Multipart multipart = new MimeMultipart(); /** 是否使用全局会话,默认为false */ private boolean useGlobalSession = false; @@ -165,7 +165,8 @@ public class Mail { } /** - * 设置正文 + * 设置正文
+ * 正文可以是普通文本也可以是HTML(默认普通文本),可以通过调用{@link #setHtml(boolean)} 设置是否为HTML * * @param content 正文 * @return this @@ -185,9 +186,21 @@ public class Mail { 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 @@ -205,7 +218,7 @@ public class Mail { } /** - * 设置附件,附件使用{@link DataSource} 形式表示,可以使用{@link FileDataSource}包装文件表示文件附件 + * 增加附件或图片,附件使用{@link DataSource} 形式表示,可以使用{@link FileDataSource}包装文件表示文件附件 * * @param attachments 附件列表 * @return this @@ -213,21 +226,74 @@ public class Mail { */ public Mail setAttachments(DataSource... attachments) { if (ArrayUtil.isNotEmpty(attachments)) { - this.attachments = 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 = InternalMailUtil.encodeText(attachment.getName(), 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 图片文件 + * @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" + * @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 imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER + * @param cid 图片与占位符,占位符格式为cid:${cid} + * @param imageFile 图片文件 + * @since 4.6.3 */ - public Mail setImageMap(Map imageMap) { - if (imageMap != null && imageMap.size() > 0) { - this.imageMap = imageMap; + 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); } - return this; } /** @@ -330,43 +396,12 @@ public class Mail { * @throws MessagingException 消息异常 */ private Multipart buildContent(Charset charset) throws MessagingException { - final Multipart mainPart = new MimeMultipart(); - // 正文 - final BodyPart body = new MimeBodyPart(); + final MimeBodyPart body = new MimeBodyPart(); body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charset)); - mainPart.addBodyPart(body); + this.multipart.addBodyPart(body); - // 附件 - if (ArrayUtil.isNotEmpty(this.attachments)) { - BodyPart bodyPart; - for (DataSource attachment : attachments) { - bodyPart = new MimeBodyPart(); - bodyPart.setDataHandler(new DataHandler(attachment)); - bodyPart.setFileName(InternalMailUtil.encodeText(attachment.getName(), charset)); - mainPart.addBodyPart(bodyPart); - } - } - - // 图片 - for (Map.Entry entry : imageMap.entrySet()) { - MimeBodyPart imgBodyPart = new MimeBodyPart(); - DataSource ds; - try { - ds = new ByteArrayDataSource(entry.getValue(), "image/jpeg"); - IoUtil.close(entry.getValue()); - } catch (IOException e) { - throw new MailException(e); - } - - imgBodyPart.setDataHandler(new DataHandler(ds)); - // imgBodyPart.setHeader("Content-ID", String.format("<%s>", entry.getKey())); - imgBodyPart.setContentID(entry.getKey()); - // add it - mainPart.addBodyPart(imgBodyPart); - } - - return mainPart; + return this.multipart; } /** diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/MailUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/MailUtil.java index 25481863b..d4fe5f873 100644 --- a/hutool-extra/src/main/java/cn/hutool/extra/mail/MailUtil.java +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/MailUtil.java @@ -5,8 +5,11 @@ import java.io.InputStream; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; /** @@ -129,7 +132,7 @@ public class MailUtil { send(GlobalMailAccount.INSTANCE.getAccount(), true, tos, ccs, bccs, subject, content, null, isHtml, files); } - //------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount + // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount /** * 发送邮件给多人 * @@ -175,7 +178,7 @@ public class MailUtil { public static void send(MailAccount mailAccount, Collection tos, Collection ccs, Collection bccs, String subject, String content, boolean isHtml, File... files) { send(mailAccount, false, tos, ccs, bccs, subject, content, null, isHtml, files); } - + /** * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人
* 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔 @@ -205,7 +208,7 @@ public class MailUtil { public static void send(String to, String subject, String content, Map imageMap, boolean isHtml, File... files) { send(splitAddress(to), subject, content, imageMap, isHtml, files); } - + /** * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
* 多个收件人、抄送人、密送人可以使用逗号“,”分隔,也可以通过分号“;”分隔 @@ -223,7 +226,7 @@ public class MailUtil { public static void send(String to, String cc, String bcc, String subject, String content, Map imageMap, boolean isHtml, File... files) { send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, imageMap, isHtml, files); } - + /** * 使用配置文件中设置的账户发送HTML邮件,发送给多人 * @@ -251,7 +254,7 @@ public class MailUtil { public static void send(Collection tos, String subject, String content, Map imageMap, boolean isHtml, File... files) { send(tos, null, null, subject, content, imageMap, isHtml, files); } - + /** * 使用配置文件中设置的账户发送邮件,发送给多人 * @@ -268,8 +271,8 @@ public class MailUtil { public static void send(Collection tos, Collection ccs, Collection bccs, String subject, String content, Map imageMap, boolean isHtml, File... files) { send(GlobalMailAccount.INSTANCE.getAccount(), true, tos, ccs, bccs, subject, content, imageMap, isHtml, files); } - - //------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount + + // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount /** * 发送邮件给多人 * @@ -296,11 +299,12 @@ public class MailUtil { * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param isHtml 是否为HTML格式 * @param files 附件列表 + * @since 4.6.3 */ public static void send(MailAccount mailAccount, Collection tos, String subject, String content, Map imageMap, boolean isHtml, File... files) { send(mailAccount, tos, null, null, subject, content, imageMap, isHtml, files); } - + /** * 发送邮件给多人 * @@ -313,13 +317,14 @@ public class MailUtil { * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER * @param isHtml 是否为HTML格式 * @param files 附件列表 - * @since 4.0.3 + * @since 4.6.3 */ - public static void send(MailAccount mailAccount, Collection tos, Collection ccs, Collection bccs, String subject, String content, Map imageMap, boolean isHtml, File... files) { + public static void send(MailAccount mailAccount, Collection tos, Collection ccs, Collection bccs, String subject, String content, Map imageMap, + boolean isHtml, File... files) { send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files); } - - //------------------------------------------------------------------------------------------------------------------------ Private method start + + // ------------------------------------------------------------------------------------------------------------------------ Private method start /** * 发送邮件给多人 * @@ -330,53 +335,62 @@ public class MailUtil { * @param bccs 密送人列表,可以为null或空 * @param subject 标题 * @param content 正文 - * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER + * @param imageMap 图片与占位符,占位符格式为cid:${cid} * @param isHtml 是否为HTML格式 * @param files 附件列表 - * @since 4.0.3 + * @since 4.6.3 */ - private static void send(MailAccount mailAccount, boolean useGlobalSession, Collection tos, Collection ccs, Collection bccs, String subject, String content, Map imageMap, boolean isHtml, File... files) { + private static void 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)) { + + // 可选抄送人 + if (CollUtil.isNotEmpty(ccs)) { mail.setCcs(ccs.toArray(new String[ccs.size()])); } - //可选密送人 - if(CollUtil.isNotEmpty(bccs)) { + // 可选密送人 + if (CollUtil.isNotEmpty(bccs)) { mail.setBccs(bccs.toArray(new String[bccs.size()])); } - + mail.setTos(tos.toArray(new String[tos.size()])); mail.setTitle(subject); mail.setContent(content); mail.setHtml(isHtml); mail.setFiles(files); - mail.setImageMap(imageMap); + // 图片 + if(MapUtil.isNotEmpty(imageMap)) { + for (Entry entry : imageMap.entrySet()) { + mail.addImage(entry.getKey(), entry.getValue()); + // 关闭流 + IoUtil.close(entry.getValue()); + } + } + mail.send(); } - + /** * 将多个联系人转为列表,分隔符为逗号或者分号 * * @param addresses 多个联系人,如果为空返回null * @return 联系人列表 */ - private static List splitAddress(String addresses){ - if(StrUtil.isBlank(addresses)) { + private static List splitAddress(String addresses) { + if (StrUtil.isBlank(addresses)) { return null; } - + List result; - if(StrUtil.contains(addresses, ',')) { + if (StrUtil.contains(addresses, ',')) { result = StrUtil.splitTrim(addresses, ','); - }else if(StrUtil.contains(addresses, ';')) { + } else if (StrUtil.contains(addresses, ';')) { result = StrUtil.splitTrim(addresses, ';'); - }else { + } else { result = CollUtil.newArrayList(addresses); } return result; } - //------------------------------------------------------------------------------------------------------------------------ Private method end + // ------------------------------------------------------------------------------------------------------------------------ Private method end } diff --git a/hutool-extra/src/test/java/cn/hutool/extra/mail/MailTest.java b/hutool-extra/src/test/java/cn/hutool/extra/mail/MailTest.java index 8f05eaf87..998bed40f 100644 --- a/hutool-extra/src/test/java/cn/hutool/extra/mail/MailTest.java +++ b/hutool-extra/src/test/java/cn/hutool/extra/mail/MailTest.java @@ -1,5 +1,8 @@ package cn.hutool.extra.mail; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; import java.util.Properties; import org.junit.Assert; @@ -17,17 +20,25 @@ public class MailTest { @Test @Ignore - public void sendTest() { + public void sendWithFileTest() { MailUtil.send("hutool@foxmail.com", "测试", "

邮件来自Hutool测试

", true, FileUtil.file("d:/测试附件文本.txt")); } @Test @Ignore - public void sendTest2() { + public void sendWithLongNameFileTest() { //附件名长度大于60时的测试 MailUtil.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")); + MailUtil.sendHtml("hutool@foxmail.com", "测试", "

邮件来自Hutool测试

", map); + } + @Test @Ignore public void sendHtmlTest() {