commons-email 发送HTML邮件(结合freemarker生成html)

需求:批量发邮件给用户,邮件是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的探索章节,写几道题。时间只能中午挤出来,上班任务完成后摸鱼出来,外加视情况而定的业余时间抽出了。其他的想干嘛干嘛,他**,逼不了就逼不了,相反效果都出来了,再逼估计都快得抑郁症了。。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容