commons-email 发送HTML邮件(结合freemar
需求:批量发邮件给用户,邮件是html的,有个按钮点了可以下载附件或html之类的
想法:java基本的发邮件的工具是JavaMail,而我用的是alpha的comnoms-email。然后呢,html的内容也不少,不可能用StringBuilder去拼一个字符串吧,所以我就使用freemarker模板,去生成html。后面又说邮件要分状态,某个状态下是下载附件,这个是已经上传到阿里oss上了,直接打开那个oss地址就行;但某个状态是要本地生成一个html,然后上传到oss上,然后拿回调的oss地址去下载。很6的是这个本地生成的html其实是已经有了的,但是它是在前端Vue项目里的,所以说可以复用。但是呢,我还真没试过在freemaker里去搞vue,有这功夫谁会在模板引擎里用,直接前后台分离啊歪。好吧,需求如此,开干。
首先是,集成下所需的jar包:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-email</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
然后,实现发邮件的功能,同时生成html邮件。使用Juint运行commons-email官方示例。
@Test
public void test() throws EmailException {
HtmlEmail email = new HtmlEmail();
// hostName为你发送邮件的账号的smtp服务器,一般每种邮箱都有特定的smtp服务器
// 比如:smtp.163.com是163邮箱的,端口25,身份认证是:账号和登录密码
// smtp.qq.com是qq邮箱的,端口465,身份认证是:账号和QQ邮箱授权码
email.setHostName("smtp.163.com");
email.setSmtpPort(25);
// 账号和密码
email.setAuthentication("***@163.com", "***");
// 将要发送邮件的接受人和称呼
email.addTo("**@qq.com", "weic");
// 发送人和称呼
email.setFrom("*@163.com", "Me");
// 邮件主题
email.setSubject("Test email with inline image");
// 字符编码
email.setCharset("UTF-8");
// embed the image and get the content id
URL url = new URL("http://www.apache.org/images/asf_logo_wide.gif");
String cid = email.embed(url, "Apache logo");
// set the html message
email.setHtmlMsg("<html>The apache logo - <img src=\"cid:"+cid+"\"></html>");
// set the alternative message
email.setTextMsg("你的email不支持html格式");
// send the email
email.send();
}
运行下,可以看看你是否接收到邮件
官方实例演示邮件.png
现在思考下一个需求点,邮件不再是一个简单的html页面,可能需要一些花里胡哨的操作。那可以使用freemarker模板引擎生成对应的html,总好过自己去搞html字符串。
首先,在resources文件下新建一个email/template文件,然后将我们用到的ftl模板放入。需要注意的是如果我们是在test下跑的junit,需要在test中java的同级目录新建一个resources文件,然后通过Mark将其标记为资源文件,之后重复resources的流程。
IDEA设置文件夹为资源文件.png
index.ftl
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World!</title>
</head>
<body>
<div class="container">
<div class="header">
<div>
<span>用于测试的Html邮件</span>
</div>
</div>
<div class="content">
<div class="card">
<div class="card-head">
<div class="card-head-left">
<span style="margin-right: 10px">${name}的名片</span>
</div>
<div class="card-head-right">
<a style="text-decoration: none;" href="${downloadUrl}">${btnText}</a>
</div>
</div>
<div class="card-content">
<div class="card-content-avatar">
<p></p>
</div>
<div class="card-content-text">
<div class="item">
<p class="p-text">${name}</p>
<#--<p class="p-text">${sex}</p>-->
<#--<p class="p-text">${age}岁</p>-->
</div>
<div class="item">
<p class="p-text">电话:</p>
<p class="p-text">10086</p>
</div>
<div class="item">
<p class="p-text">邮箱:</p>
<p class="p-text">****@**.com</p>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<style>
.container{
/* Safari */
display: -webkit-flex;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
.header{
display: flex;
flex-flow: row;
align-items: center;
justify-content: flex-start;
width: 744px;
margin-bottom: 20px;
}
.grey-mini-text{
font-size: 12px;
color: grey;
}
.content{
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
width: 774px;
}
.contant-span{
margin-bottom: 20px;
}
.card{
width: 744px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
text-align: center;
float: left;
margin-right: 10px;
padding: 5px;
padding-top: 15px;
border-radius: 10px;
}
.card-head{
display: flex;
flex-flow: row;
align-items: center;
padding: 0 20px;
}
.card-head-left{
flex: 1;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
}
.card-head-right{
flex: 1;
display: flex;
flex-direction: row;
align-items: flex-end;
justify-content: flex-end;
}
.card-head-right a{
background: -webkit-linear-gradient(top,#fd3608 0%,#fd3608 90%,#fd3608 100%);
border: 1px solid #fd3608;
border-radius: 10px;
color: white;
padding: 12px 35px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 18px;
cursor: pointer;
float: left;
}
.card-head-right a:hover {
background-color: #fd3608;
}
.card-head-right a:active {
background-color: #fd3608;
}
.card-head-left span{
color: grey;
}
.card-content{
margin-top: 10px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.card-content-avatar{
flex: 1;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.card-content-avatar p{
width: 100px;
height: 100px;
border-radius: 50px;
background: grey url("") no-repeat center;
background-size: 100px;
}
.card-content-text{
flex: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.card-content-text .item{
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.p-text{
margin-right: 5px;
}
</style>
juint test方法
HtmlEmail email = new HtmlEmail();
// hostName为你发送邮件的账号的smtp服务器,一般每种邮箱都有特定的smtp服务器
// 比如:smtp.163.com是163邮箱的,端口25,身份认证是:账号和登录密码
// smtp.qq.com是qq邮箱的,端口465,身份认证是:账号和QQ邮箱授权码
email.setHostName("smtp.163.com");
email.setSmtpPort(25);
// 账号和密码
email.setAuthentication("***@163.com", "***");
// 将要发送邮件的接受人和称呼
email.addTo("**@qq.com", "weic");
// 发送人和称呼
email.setFrom("*@163.com", "Me");
// 邮件主题
email.setSubject("Test email with inline image");
// 字符编码
email.setCharset("UTF-8");
// 开启ssl
email.setSSLCheckServerIdentity(true)
// 读取SendEmail的根目录,这里就是target的目录
String baseURL = Objects.requireNonNull(SendEmail.class.getClassLoader().getResource("")).getPath();
StringWriter html = new StringWriter();
try {
// freemarker指定版本
Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
//指定对象模板存放的位置
cfg.setDirectoryForTemplateLoading(new File(baseURL+"/email/template"));
//设置字符集
cfg.setDefaultEncoding("utf-8");
// 通过map设置参数
Map<String, Object> rootMap = new HashMap<>(20);
rootMap.put("logo", "http://www.apache.org/images/asf_logo_wide.gif");
rootMap.put("name", "Hello World!");
rootMap.put("btnText", "点击一下");
rootMap.put("downloadUrl", "");
// 读取模板
Template template = cfg.getTemplate("index.ftl");
// 写入参数
template.process(rootMap, html);
} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
}
// set the html message
email.setHtmlMsg(html.toString());
// set the alternative message
email.setTextMsg("你的email不支持html格式");
// send the email
email.send();
看下效果:
邮件一.png
在代码中我们有一个downloadUrl参数,他的作用是点击下载一个html,但是呢,这个html需要本地去生成,然后上传到oss上。也就是说我们需要oss回调的地址,作为download的地址。
...
String baseURL = Objects.requireNonNull(SendEmail.class.getClassLoader().getResource("")).getPath();
// 在本地生成一个html
File afile = new File(baseURL + "test.html");
try {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
//指定对象模板存放的位置
cfg.setDirectoryForTemplateLoading(new File(baseURL+"/email/template"));
//设置字符集
cfg.setDefaultEncoding("utf-8");
Template template = cfg.getTemplate("resume.ftl");
// 为resume.ftl模板写入数据
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(afile), "UTF-8"));
template.process(result, out);
out.flush();
out.close();
// 通过oss上传html
OSS ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
// 通过雪花算法生成唯一文件名
String fileName = Snowflake.generateId() + ".html";
PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, fileName , afile);
ossClient.putObject(putObjectRequest);
ossClient.shutdown();
// 获取地址,注:oss的相关配置应提前准备好,webHost地址为oss地址前缀
onlinePath = webHost + fileName;
// 如果文件存在删除
if (afile.exists()) {
afile.delete();
}
} catch (Exception e) {
e.printStackTrace();
// 如果文件存在删除
if (afile.exists()) {
afile.delete();
}
}
...
resume.ftl
!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- import CSS -->
<link rel = "stylesheet" href = "https://unpkg.com/element-ui/lib/theme-chalk/index.css" >
</head>
<body>
<div id="app">
</div>
</body>
<!-- import Vue before Element -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
// 将传入的参数放到window
window.baseInfoData = '${baseInfo}';
window.intentionInfoData = '${intentionInfo}';
// 收到触发vue初始化事件
window.onload = function(){
vm.initInfo();
}
let vm = new Vue({
el: '#app',
data: function () {
return {
baseInfoData: [
{
name1: '姓名',
value1: '',
name2: '性别',
value2: ''
},
{
name1: '出生日期',
value1: '',
name2: '户籍所在地',
value2: ''
}
]
};
},
methods: {
initInfo: function () {
// 将对象转成json,js手动转回来
let baseInfo = JSON.parse(window.baseInfoData);
}
}
});
具体的resume.ftl这里就不给出了,毕竟是别人公司直接复制过来的代码。这里只说下遇到的问题和解决方法。
问题
一、在freemarker作为模板写入图片,或者说使用email里email.embed()方法会使得图片变为附件,所以最后还是决定使用oss地址来显示图片。
二、如果收信人第一次收到邮件,没有信任这个邮件,会导致图片显示不出来。用户查看邮件的时候可以信任发邮件方。暂没有什么解决方法。
三、resume.ftl模板是从vue中复制过来的,使用了CDN去引用vue和element-ui,如果将数据直接通过#{}引用到data里,并不会触发双向绑定,同时也不会触发mounted方法去初始化vue。如果你是将一个对象通过map注入,那么他在页面里就变成了一个字符串,真真的字符串无法使用。。。解决方法:将变量赋值给window里,这样保证不会没有,然后通过window.onload方法手动调用初始化方法。对象就通过json转成字符串,然后在js里转回来
四、如果你部署的服务器开启了ssl,必须在;发邮件的代码里手动开启下ssl配置,否则发不出邮件。。。当然官方推荐使用ssl或STARTTLS 去发邮件
Email.setSSLCheckServerIdentity(true)
四、邮件不支持js等一些特殊标签及在div里加click点击事件无效;解决方法:使用a标签,设置好样式,可以实现同一效果。
贴出commons-email官方地址:http://commons.apache.org/proper/commons-email/userguide.html
官方文档是最给力的!
我算是发现了,我啊戒不了游戏,也戒不了小说。像苦行僧一样无欲无求直奔技术的前路,对我来说是不现实的。。。那好吧,转化思路,游戏、打,小说、看,但是呢,我们做个标准,每周写一个博客(可以记录工作、学习的笔记);每个月找一本工作相关书籍,看完他,每周看一章;每周逛逛Leetcode的探索章节,写几道题。时间只能中午挤出来,上班任务完成后摸鱼出来,外加视情况而定的业余时间抽出了。其他的想干嘛干嘛,他**,逼不了就逼不了,相反效果都出来了,再逼估计都快得抑郁症了。。。