使用Velocity做爲郵件的模板

Velocity 是一個基於Java的模板引擎。它容許任何人使用一種簡單但強大的模板語言去引用Java代碼中定義的對象。css

Velocity的基本經常使用語法:https://www.cnblogs.com/xiohao/p/5788932.htmlhtml

最近在作ESL的郵件報警功能,郵件內容包含兩個表格,分別填充兩種報警內容,須要根據系統的語言設置顯示不同的表頭。前端

核心作法:java

package com.zk.mail;

import lombok.extern.slf4j.Slf4j;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.springframework.stereotype.Component;
import org.springframework.ui.velocity.VelocityEngineUtils;
import org.springframework.util.StringUtils;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.util.ByteArrayDataSource;
import java.io.*;
import java.util.*;

@Slf4j
@Component(value = "mailUtils")
public class MailUtils {

    public static final String HTML_CONTENT = "text/html;charset=UTF-8";
    public static final String ATTACHMENT_CONTENT = "text/plain;charset=gb2312";

    private static VelocityEngine velocityEngine = new VelocityEngine();

    static {
        Properties properties = new Properties();
        String basePath = "src/main/resources/mailTemplate/";
        // 設置模板的路徑
        properties.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, basePath);
        // 初始花velocity 讓設置的路徑生效
        velocityEngine.init(properties);
    }

    public <T extends List> void sendEmail(T t1, T t2, String title, String[] to, String[] bcc, String templateName, EmailServerConfig config, Map<String, String> content) {
        Map map = new HashMap();
        map.put("priceTagDatas", t1);
        map.put("ApDatas", t2);
        map.put("merchantName", content.get("merchantName"));
        map.put("storeName", content.get("storeName"));
        map.put("alarmStartTime", content.get("alarmStartTime"));
        map.put("alarmEndTime", content.get("alarmEndTime"));
        Email email = new Email.Builder(title, to, null).model(map).templateName(templateName).bcc(bcc).build();
        sendEmail(email, config);
    }

    private void sendEmail(Email email, EmailServerConfig config) {
        Long startTime = System.currentTimeMillis();
        // 發件人
        try {
            MimeMessage message = this.getMessage(email, config);
            // 新建一個存放信件內容的BodyPart對象
            Multipart multiPart = new MimeMultipart();
            MimeBodyPart mdp = new MimeBodyPart();
            // 給BodyPart對象設置內容和格式/編碼方式
            setContent(email);
            mdp.setContent(email.getContent(), HTML_CONTENT);
            multiPart.addBodyPart(mdp);
            // 新建一個MimeMultipart對象用來存放BodyPart對象(事實上能夠存放多個)
            if (null != email.getData()) {
                MimeBodyPart attchment = new MimeBodyPart();
                ByteArrayInputStream in = new ByteArrayInputStream(email.getData());
                DataSource fds = new ByteArrayDataSource(in, email.getFileType());
                attchment.setDataHandler(new DataHandler(fds));
                attchment.setFileName(MimeUtility.encodeText(email.getFileName()));
                multiPart.addBodyPart(attchment);
                if (in != null) {
                    in.close();
                }
            }
            message.setContent(multiPart);
            message.saveChanges();
            Transport.send(message);
            Long endTime = System.currentTimeMillis();
            log.info("Email sent successfully, consume time:" + (endTime - startTime) / 1000 + "s");
        } catch (Exception e) {
            log.error("Error while sending mail.", e);
        }
    }


    private Email setContent(Email email) {
        if (StringUtils.isEmpty(email.getContent())) {
            email.setContent("");
        }
        if (!StringUtils.isEmpty(email.getTemplateName()) && null != email.getModel()) {
            String content = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, email.getTemplateName(), "UTF-8", email.getModel());
            email.setContent(content);
        }
        return email;
    }


    private MimeMessage getMessage(Email email, EmailServerConfig config) {
        MimeMessage message = null;
        try {
            if (email.getTo() == null || email.getTo().length == 0 || StringUtils.isEmpty(email.getSubject())) {
                throw new Exception("Recipient or subject is empty.");
            }

            Properties props = new Properties();
            props.setProperty("mail.smtp.host", config.getMailSmtpHost());
            props.setProperty("mail.smtp.socketFactory.class", config.getMailSmtpSocketFatoryClass());
            props.setProperty("mail.smtp.socketFactory.fallback", config.getMailSmtpSocketFatoryFallback());
            props.setProperty("mail.smtp.port", config.getMailSmtpPort());
            props.setProperty("mail.smtp.socketFactory.port", config.getMailSmtpSocketFatoryPort());
            props.setProperty("mail.smtp.auth", config.getMailSmtpAuth());

//解決553的問題,用Session.getInstance取代Session.getDefaultInstance
//            Session mailSession = Session.getDefaultInstance(props, new Authenticator() {
//                protected PasswordAuthentication getPasswordAuthentication() {
//                    return new PasswordAuthentication(config.getMailSmtpFromAddress(), //config.getMailSmtpAuthPass());
//                }
//            });

            Session mailSession = Session.getInstance(props, new Authenticator(){
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(config.getMailSmtpFromAddress(), config.getMailSmtpAuthPass());
                }});
            message = new MimeMessage(mailSession);
            message.setFrom(new InternetAddress(config.getMailSmtpFromAddress()));
            for (String mailTo : email.getTo()) {
                message.addRecipient(Message.RecipientType.TO, new InternetAddress(mailTo));
            }
            List<InternetAddress> ccAddress = new ArrayList<>();
            if (null != email.getBcc()) {
                for (String mailCC : email.getBcc()) {
                    ccAddress.add(new InternetAddress(mailCC));
                }
                message.addRecipients(Message.RecipientType.CC,
                        ccAddress.toArray(new InternetAddress[email.getBcc().length]));
            }
            message.setSentDate(new Date());
            message.setSubject(email.getSubject());
        } catch (Exception e) {
            log.error("Error while sending mail." + e.getMessage(), e);
        }
        return message;
    }
}

Velocity的模板, 提供不一樣語言的模板,模板名稱帶上語言後綴(中文模板:mail_cn.vm)如:spring

<!DOCTYPE html>
<html lang="zh">
<head>
    <META http-equiv=Content-Type content='text/html; charset=UTF-8'>
    <title>Title</title>
    <style type="text/css">
        table.reference, table.tecspec {
            border-collapse: collapse;
            width: 100%;
            margin-bottom: 4px;
            margin-top: 4px;
        }
        table.reference tr:nth-child(even) {
            background-color: #fff;
        }
        table.reference tr:nth-child(odd) {
            background-color: #f6f4f0;
        }
        table.reference th {
            color: #fff;
            background-color: #555;
            border: 1px solid #555;
            font-size: 12px;
            padding: 3px;
            vertical-align: top;
        }
        table.reference td {
            line-height: 2em;
            min-width: 24px;
            border: 1px solid #d4d4d4;
            padding: 5px;
            padding-top: 7px;
            padding-bottom: 7px;
            vertical-align: top;
        }
        .article-body h3 {
            font-size: 1.8em;
            margin: 2px 0;
            line-height: 1.8em;
        }
    </style>
</head>
<body>
<h3 style=";">ESL系統報警信息</h3>
<div>
    <div>時間: $alarmStartTime 至 $alarmEndTime</div>
    <div>商家名稱: $merchantName</div>
    <div>門店名稱: $storeName</div>
    <div>報警內容:</div>

    #if ($priceTagDatas.size() > 0)
        <table class="reference">
            <tbody>
            <tr>價簽報警</tr>
            <tr>
                <th>價籤條碼</th>
                <th>商品條碼</th>
                <th>商品名稱</th>
                <th>報警類型</th>
                <th>報警時間</th>
            </tr>
                #foreach($element in  $priceTagDatas)
                <tr>
                    <td>
                        #if($element.getDeviceMac())
                    $element.getDeviceMac()
                #end
                    </td>
                    <td>
                        #if($element.getItemBarCode())
                    $element.getItemBarCode()
                #end
                    </td>
                    <td>
                        #if($element.getItemName())
                    $element.getItemName()
                #end
                    </td>
                    <td>
                        #if($element.getFaultType())
                    $element.getFaultType()
                #end
                    </td>
                    <td>
                        #if($element.getCreatedTime())
                    $element.getCreatedTime()
                #end
                    </td>
                </tr>
                #end
            </tbody>
        </table>
    #end

    #if ($ApDatas.size() > 0)
        <table class="reference">
            <tbody>
            <tr>基站報警</tr>
            <tr>
                <th>基站名稱</th>
                <th>基站MAC</th>
                <th>報警類型</th>
                <th>報警時間</th>
                <th>狀態</th>
            </tr>
                #foreach($element in $ApDatas)
                <tr>
                    <td>
                        #if($element.getDeviceMac())
                    $element.getDeviceMac()
                #end
                    </td>
                    <td>
                        #if($element.getDeviceMac())
                    $element.getDeviceMac()
                #end
                    </td>
                    <td>
                        #if($element.getFaultType())
                    $element.getFaultType()
                #end
                    </td>
                    <td>
                        #if($element.getCreatedTime())
                    $element.getCreatedTime()
                #end
                    </td>
                    <td>
                        #if($element.getProcessStatus())
                    $element.getProcessStatus()
                #end
                    </td>
                </tr>
                #end
            </tbody>
        </table>
    #end

    <div style="float: left; margin-top: 300px;;">
        <p>系統郵件(請勿回覆) | ESL 報警中心</p>
    </div>
</div>
</body>
</html>

發送郵件是在一個定時任務中,定時任務的代碼如:apache

package com.zk.quartz;

import com.zk.dao.*;
import com.zk.mail.AlarmEmailTitle;
import com.zk.mail.EmailServerConfig;
import com.zk.mail.MailUtils;
import com.zk.model.*;
import com.zk.service.MailSenderService;
import com.zk.util.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.data.jpa.domain.Specification;

import javax.annotation.Resource;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by zk on 2019/3/28.
 */
@Slf4j
public class AlarmJob implements Job {
    @Resource
    private StoreRepository storeRepository;

    @Resource
    private MerchantRepository merchantRepository;

    @Resource
    private AgencyAlarmConfigRepository agencyAlarmConfigRepository;

    @Resource
    private AlarmRepository alarmRepository;

    @Resource
    private MailUtils mailUtils;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        List faultTypeList = (List) jobDataMap.get("faultTypeList");
        String merchantId = (String) jobDataMap.get("merchantId");
        String storeId = (String) jobDataMap.get("storeId");
        String sendTO = (String) jobDataMap.get("sendTo");
        String language = (String) jobDataMap.get("language");
        String templateName = "mail_" + language + ".vm";

        List<Alarm> alarmList = alarmRepository.findAll(getSpecification(merchantId, storeId, faultTypeList));

        if (alarmList.size() == 0) {
            log.info("Alarm job run without alarms for storeId: " + storeId);
            return;
        }

        String merchantName = merchantRepository.findByMerchantIdAndFlag(merchantId, 1).getMerchantName();
        String storeName = storeRepository.findByStoreIdAndFlag(storeId, 1).getStoreName();

        List<Alarm> priceTagAlarmList = alarmList.stream().filter(alarm -> "2".equals(alarm.getAlarmType())).collect(Collectors.toList());
        List<Alarm> apAlarmList = alarmList.stream().filter(alarm -> "1".equals(alarm.getAlarmType())).collect(Collectors.toList());

        Date alarmStartTime = alarmList.stream().map(alarm -> DateUtils.stringToDateTime(alarm.getCreatedTime())).min(Comparator.naturalOrder()).get();
        Date alarmEndTime = alarmList.stream().map(alarm -> DateUtils.stringToDateTime(alarm.getCreatedTime())).max(Comparator.naturalOrder()).get();

        Map<String, String> content = new HashMap<>(4);
        content.put("merchantName", merchantName);
        content.put("storeName", storeName);
        content.put("alarmStartTime", DateUtils.format(alarmStartTime));
        content.put("alarmEndTime", DateUtils.format(alarmEndTime));

        AgencyAlarmConfig agencyAlarmConfig = agencyAlarmConfigRepository.findConfigByAgencyId(merchantId);
        agencyAlarmConfig.setTestMail(sendTO);

        String[] toArr = sendTO.split(",");

        EmailServerConfig config = getEmailServerConfig(agencyAlarmConfig);
        mailUtils.sendEmail(priceTagAlarmList, apAlarmList, AlarmEmailTitle.getTitleFromLanguage(language), toArr, null, templateName, config, content);

        for(Alarm alarm : alarmList) {
            alarm.setHasSent(true);
            alarmRepository.save(alarm);
        }
    }

    private EmailServerConfig getEmailServerConfig(AgencyAlarmConfig agencyAlarmConfig) {
        EmailServerConfig config = new EmailServerConfig();
        config.setMailSmtpHost(agencyAlarmConfig.getSendServer());
        config.setMailSmtpSocketFatoryClass("javax.net.ssl.SSLSocketFactory");
        config.setMailSmtpSocketFatoryFallback("false");
        config.setMailSmtpPort("465");
        config.setMailSmtpSocketFatoryPort("465");
        config.setMailSmtpAuth("true");
        config.setMailSmtpFromAddress(agencyAlarmConfig.getAccount());
        config.setMailSmtpAuthPass(agencyAlarmConfig.getPassword());
        return config;
    }

    private Specification<Alarm> getSpecification(String merchantId, String storeId, List<String> typeList) {
        return new Specification<Alarm>() {
            @Override
            public Predicate toPredicate(Root<Alarm> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                List<Predicate> predicates = new ArrayList<Predicate>();
                Predicate predicate = null;
                if (StringUtils.isNotBlank(merchantId)) {
                    predicate = criteriaBuilder.equal(root.get("merchantId"), merchantId);
                    predicates.add(predicate);
                }
                if (StringUtils.isNotBlank(storeId)) {
                    predicate = criteriaBuilder.equal(root.get("storeId"), storeId);
                    predicates.add(predicate);
                }
                if (typeList != null && typeList.size() > 0) {
                    CriteriaBuilder.In<String> in = criteriaBuilder.in(root.get("faultType"));
                    for (String type : typeList) {
                        in.value(type);
                    }
                    predicates.add(in);
                }

                predicate =  criteriaBuilder.isNull(root.get("hasSent"));
                predicates.add(predicate);

                return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
            }
        };
    }
}

 

後記:部署後遇到了兩個坑服務器

1)Velocity找不到模板文件session

在我本地運行的時候並無這種問題,試了不少種方法,最後只能使用絕對路徑,修改MailUtils中velocityEngine的Velocity.FILE_RESOURCE_LOADER_PATH的值:app

static {
        Properties properties = new Properties();
        // 將basePath修改成服務器上的絕對路徑, 並將模板文件上傳到該路徑下。
        // String basePath = "src/main/resources/mailTemplate/";
        String basePath = "/usr/local/esl/";
        
        properties.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, basePath);
        velocityEngine.init(properties);
    }

問題解決。dom

2)使用了163郵箱做爲測試服務器,遇到了郵件被認爲是垃圾郵件的問題:

解決方法:將郵件抄送一份給發送帳號,在MailUtils的getMessage方法中,添加如下代碼:

List<InternetAddress> ccAddress = new ArrayList<>();

//            if (null != email.getBcc()) {
//                for (String mailCC : email.getBcc()) {
//                    ccAddress.add(new InternetAddress(mailCC));
//                }
//                message.addRecipients(Message.RecipientType.CC,
//                        ccAddress.toArray(new InternetAddress[email.getBcc().length]));
//            }

ccAddress.add(new InternetAddress(config.getMailSmtpFromAddress()));
message.addRecipients(Message.RecipientType.CC, ccAddress.toArray(new InternetAddress[1]));

成功解決554 DT:SPM問題!

後記2:解決郵件發送中出現553問題

在本地用單測進行郵件發送,都沒有問題。可是部署以後,經過前端調用接口的方式,常常會出現553的問題,如:

553意味着mail from和登陸的郵箱帳號存在不一致的狀況,考慮到部署後首次發送是成功的,想到會不會是前一次登陸的帳號信息被保留下來了,觀察代碼,mail from和account的信息分別設置如:

Session mailSession = Session.getDefaultInstance(props, new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(config.getMailSmtpFromAddress(), config.getMailSmtpAuthPass());
                }
            });
     message = new MimeMessage(mailSession);
     message.setFrom(new InternetAddress(config.getMailSmtpFromAddress()));

跟進到Session.getDefaultInstance的代碼發現,defaultSession是一個類靜態變量,首次登陸一個郵箱後這個session就會被保留下來,致使和後續的測試帳戶不匹配從而報錯553。找到緣由以後,使用Session.getInstance()方法取代Session.getDefaultInstance()去從新new一個session,問題獲得解決。

相關文章
相關標籤/搜索