前言
一些企业常常要要群发邮件给客户来做推广营销,大量重复内容往往很容易被邮箱运营商判别为垃圾邮件,直接放到用户垃圾邮箱里,甚至是直接屏蔽发邮件的ip,这样就会造成群发邮件的低到达率。
如何识别垃圾邮件
那么,邮箱服务商,比如网易163,腾讯qq邮箱,怎么识别垃圾邮件呢?
一般是以下几种方法:
1、关键词识别
它首先将垃圾邮件中一些特征性的字眼收集起来(比如打折、免费、促销等),形成一个大的数据库,当一封邮件发出来的时候就会自动匹配邮件头、邮件标题、邮件内容中与这些库里的关键词特征,如果有相类似的字眼,就会判定为垃圾邮件。这种方法简单粗暴,误判的概率稍高。
2、IP黑白名单
和第一种一样,它首先会把经常发送垃圾邮件的IP收集起来,形成一个黑IP库,俗称黑名单,只要是这个黑名单中发出来的邮件,自然会被判定为垃圾邮件,相反,如果IP被划定到白名单中,那你的邮件就会畅通无阻,但前提是制作规范的邮件。
3、蜜罐技术
这是目前QQ邮箱等主流邮局采用最多的方法,它会在网络上分布很多的邮箱地址让你采集到,也会在QQ群等人流密集的地方设置一些邮箱,当你把这些邮箱采集 到自己的数据库,并且发送了邮件之后,邮局马上会发现你在发未经许可的垃圾邮件,并且将这些垃圾邮件放到数据库中,之后发送的邮件都会在这个库里进行匹 配,从而有效判定于垃圾邮件以及垃圾网址。这个判定标准也告诉我们,做邮件营销尽量不要随便从网络上采集邮址,很容易掉入蜜罐陷阱。
4、贝叶斯算法
贝叶斯算法是目前世界上用的比较多的一种算法,它首先会收集大量的垃圾邮件和非垃圾邮件,建立垃圾邮件库和非垃圾邮件库,然后提取其中的特征字符串,对大量的网络发出的邮件进行匹配和甄别,垃圾邮件的识别率非常高。
5、评分算法
这种方法是建立在关键字技术基础之上的,单一的关键字会出现大量的误判情况,为了解决这个问题,出现了多关键字检测的方式—评分。为每个可能在垃圾邮件中 出现的关键字赋予分数,分数的多少要根据关键字在垃圾邮件中出现的可能性和严重性来决定。对一封邮件进行扫描,其中有一个关键字就加一定的分数,最后将所 有的得分同设置好的阀值进行比较,从而有效判定出垃圾邮件。市场上大部分ESP都运用了此项方法。
6、DNS反向查找
在发邮件的时候,随意编造一个域名是非常容易的,如果采用阻断非法域名的方式来防止垃圾邮件的话。那么,用户可以说是被动到极点了,而且根本没有办法防 止,因为那些域名都是根本不存在的。DNS反向查找技术就是在收到邮件时对发件人的地址的真实性进行核查,防止DNS欺骗。这也是为什么正规的ESP都要 求域名进行spf和dkim设置的原因。
7、 意图分析技术
垃圾邮件技术如今变得愈加复杂,许多垃圾邮件变得与正常的邮件几乎一样,在这 些邮件中含有URL链接,这个链接往往指向一些不健康的网站,或某个商品促销的网站。ESP为此创建了意图分析技术,构建了垃圾邮件URLS地址数据库。 它检查邮件中的URL链接,确定邮件是否为垃圾邮件。
如何应对
知道了以上这些垃圾邮件识别方法,我们反其道而行之,是不是可以降低被判为垃圾邮件的概率呢?我们可以做以下尝试:
- 降低同一个邮箱地址发送邮件的频率,使用多个邮箱账号发送邮件;
- 降低同一个ip地址发送邮件的频率,使用多个ip发送邮件;
- 邮件中减少易被判为垃圾邮件的相关词汇;
- 邮件内容页面美观,尽量避免用户反感,减少用户投诉;
- 增加邮件退订功能,一旦收件人点了退订,就不要再次给人家发送,不要像那些垃圾短信一样搞个假退订,糊弄谁呢,只能让人更反感;
下面,使用Java代码来实现邮件群发,我的想法是:
- 大量的邮件不要在很短时间内全部发送完,应该有一定的时间间隔;
- 通过设置阈值(邮件运营商能接受的频率,比如同一个发件人一天只能给136邮箱发送一百封邮件)来控制邮件发送的频率,一个邮箱账号每天发送到同一个ESP的邮件数不超过阈值;
- 当天发送不完的邮件,往后延续一天发送,如何延迟发送呢?把邮件放到ActiveMQ里,利用mq的延时投递功能;
- 邮件发送时间尽量在白天,比如当天早上八点,到晚上八点,不要在八点以后骚扰人家;
Java实现
MQ的配置如下,把邮件消息放到一个叫BATCH_EMAIL的消息队列里,然后监听这个消息队列,收到消息就发送邮件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="queueConnectionFactory" class="org.apache.activemq.spring.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://127.0.0.1:61616" />
<property name="userName" value="${mqUserName}" />
<property name="password" value="${mqPassword}" />
<property name="useAsyncSend" value="true" />
</bean>
<bean id="emailDestination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="BATCH_EMAIL" />
</bean>
<bean id="simpleMessageConverter" class="org.springframework.jms.support.converter.SimpleMessageConverter" />
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="queueConnectionFactory" />
<property name="defaultDestination" ref="emailDestination" />
<property name="messageConverter" ref="simpleMessageConverter"/>
<property name="pubSubDomain" value="false" />
</bean>
<bean id="emailMessageReceiver" class="cn.mns.mq.EmailMessageReceiver" />
<bean id="listenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="queueConnectionFactory" />
<property name="destination" ref="emailDestination" />
<property name="messageListener" ref="emailMessageReceiver" />
</bean>
</beans>
制定邮件发送计划,将群发的邮件信息(邮件内容,地址,主题等)放到消息队列里:
package cn.mns.tools.email;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import cn.mns.common.SpringContextInit;
import cn.mns.mq.EmailProductor;
import cn.mns.tools.DateUtil;
import cn.mns.tools.StringUtil;
/**
* 邮件群发计划
* @author bukale
* @date 2019-12-18
*/
public class BatchEmailPlan {
//每个账号单个邮件服务商每天发送邮件数阈值
private static int LIMIT = 100;
//账号数,自己用来发送邮件的账号数
private static int ACCOUNT_COUNT = 80;
//第几天计数器,用于计算发送邮件时间
private static int DAY_COUNTER = 0;
private static List<String> accountList;
static{
//初始化账号list,我的账号是有规律的,可以改成你自己的实现,放到accountList即可
accountList = new ArrayList<String>();
for(int i = 1;i <= ACCOUNT_COUNT;i ++){
accountList.add("post" + i + "@test.com");
}
}
/**
* 制定群发计划
* 将邮件分配给不同的账号并发送至mq
* @param emailList 邮件列表
* @param subject 邮件主题
* @param templateFile 模板文件路径
* @param siteType 网站类型
*/
public static void makeBatchPlan(List<String> emailList,String subject,String templateFile,String siteType){
System.out.println("=======================" + emailList.size() + "======================");
EmailProductor emailProductor = getEmailProductor();
// 一个账号可以发的邮件数=域名数*阈值*天数 5*100*1=500
Map<String,Integer> counter = new HashMap<String, Integer>();//域名和对应邮件数,用于统计
Map<String,List<String>> domainEmails = new HashMap<String, List<String>>();//域名和对应邮件地址list
List<String> nextList = new ArrayList<String>();//每天超过上限的邮件,存放起来,隔天发送
String email = "";
String domain = "";
Integer count = 0;
//遍历,对所有邮件地址进行按域名统计和分类
for(int i = 0;i < emailList.size();i ++){
email = emailList.get(i);
if(!StringUtil.isEmail(email)){
continue;
}
domain = email.replaceAll("\\S+@","");
count = counter.get(domain);
counter.put(domain,count == null ? 1 : count + 1);
if(domainEmails.containsKey(domain)){
domainEmails.get(domain).add(email);
}else {
List<String> list = new ArrayList<String>();
list.add(email);
domainEmails.put(domain,list);
}
}
int maxCount = 0;
for(String key : counter.keySet()){
int value = counter.get(key);
System.out.println("域名:" + key + "\t邮件数:" + value);
if(maxCount < value){
maxCount = value;
}
}
System.out.println("最大邮件数:" + maxCount + "\t阈值:" + LIMIT);
System.out.println("预计天数:" + (int)Math.ceil(maxCount*1.0 / (LIMIT * ACCOUNT_COUNT)));
long time1 = getStartTime();
long time2 = getEndTime();
Iterator<Entry<String, List<String>>> ite = domainEmails.entrySet().iterator();
Entry<String, List<String>> entry = null;
String key = "";
List<String> eList = null;
while(ite.hasNext()){
entry = ite.next();
key = entry.getKey();
eList = domainEmails.get(key);
int size = eList.size();
//所有账号可以发送的邮件总数
int limit = LIMIT * ACCOUNT_COUNT;
//找出超出发送总量的邮件,用于下次执行
if(size > limit){
List<String> subList = eList.subList(limit,size);
nextList.addAll(subList);
}
String userEmail = "";
//每次分配的起点终点表示区间为
//[total*taskNumber/cores, total*(taskNumber+1)/cores)
int total = size > limit ? limit : size;//总邮件数
for(int taskNumber = 0; taskNumber < ACCOUNT_COUNT; taskNumber++){
int max = total * (taskNumber + 1) / ACCOUNT_COUNT;
int j = total * taskNumber / ACCOUNT_COUNT;
// System.out.println();
// System.out.print(taskNumber + "-" + accountList.get(taskNumber) + ":");
for (int i = j; i < max; i++) {
userEmail = eList.get(i);
// System.out.print(userEmail + ",");
Map<String,String> map = new HashMap<String, String>();
map.put("email",userEmail);//收件人邮箱地址
map.put("subject", subject);//邮件主题
map.put("account",accountList.get(taskNumber));
map.put("templateFile",templateFile);//邮件内容模板,即邮件内容
//邮件信息发送到mq
emailProductor.sendEmailMessage(map,getDelay(time1,time2,Math.abs(i-max)));
//emailProductor.sendEmailMessage(map,300000);
}
}
}
System.out.println();
System.out.println("nextList:" + nextList.size());
//没法送完的,继续下一次发送
if(nextList.size() > 0){
DAY_COUNTER ++;
makeBatchPlan(nextList,subject,templateFile,siteType);
}else {
DAY_COUNTER = 0;
}
}
public static EmailProductor getEmailProductor(){
return SpringContextInit.getApplicationContext().getBean(EmailProductor.class);
}
/**
* 获取起始时间,当天早上八点,或者第二天早上八点
* @return
* @date 2019-12-23
*/
public static long getStartTime(){
DateUtil du = new DateUtil();
Date startDate = du.DATEADD("d",DAY_COUNTER,new Date());
String timeStr = du.FormatDate(startDate,"yyyy-MM-dd HH");
int hour = Integer.valueOf(timeStr.substring(11));
String dateStr = timeStr.substring(0,10);
String str = "";
if(hour < 8 || DAY_COUNTER > 0){
str = dateStr + " 08:00:00";
}else if(hour >= 20){
str = du.DATEADD("d",1,dateStr) + " 08:00:00";
}else {
str = du.FormatDate(startDate,"yyyy-MM-dd HH:mm:ss");
}
startDate = DateUtil.getFormatDate(str,"yyyy-MM-dd HH:mm:ss");
System.out.println("start:" + du.FormatDate(startDate,"yyyy-MM-dd HH:mm:ss"));
return startDate.getTime();
}
/**
* 获取截止时间,当天晚上八点,或者第二天晚上八点
* @return
* @date 2019-12-23
*/
public static long getEndTime(){
DateUtil du = new DateUtil();
Date endDate = du.DATEADD("d",DAY_COUNTER,new Date());
String dateStr = du.FormatDate(endDate,"yyyy-MM-dd") + " 20:00:00";
endDate = DateUtil.getFormatDate(dateStr,"yyyy-MM-dd HH:mm:ss");
System.out.println("end:" + du.FormatDate(endDate,"yyyy-MM-dd HH:mm:ss"));
return endDate.getTime();
}
/**
* 获取延时时间
* @param delay
* @param i
* @return
* @date 2019-12-23
*/
public static long getDelay(long startTime,long endTime,int i){
long delay = (endTime - startTime) / LIMIT;//每封邮件时间间隔
long basic = Math.abs(startTime - System.currentTimeMillis());
return basic + delay * i + new Random().nextInt(600000);//标准时间+10分钟的随机
}
public static void main(String[] args) {
List<String> emailList = new ArrayList<String>();
for(int i = 0;i < 10000;i ++){
emailList.add("e" + new Random().nextInt(100) + "@qq.com");
}
for(int i = 0;i < 3000;i ++){
emailList.add("e" + new Random().nextInt(100) + "@136.cn");
}
for(int i = 0;i < 2000;i ++){
emailList.add("e" + new Random().nextInt(100) + "@gmail.com");
}
for(int i = 0;i < 5000;i ++){
emailList.add("e" + new Random().nextInt(100) + "@163.com");
}
String template = "";
String subject = "";
BatchEmailPlan.makeBatchPlan(emailList,subject,template,"");
}
}
先注释掉邮件发送到mq的方法调用,运行main方法,可以看到,邮件的发送计划已经制定好了:
package cn.mns.mq;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
/**
* 邮件消息发送
* @author bukale
* @date 2019-12-20
*/
@Component
public class EmailProductor {
@Autowired
private JmsTemplate jmsTemplate;
public void sendEmailMessage(Map<String,String> map,long delay){
jmsTemplate.convertAndSend(map,new DelayMessagePostProcessor(delay));
}
}
接收队列里的消息并发送邮件:
package cn.mns.mq;
import java.util.HashMap;
import java.util.Map;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageListener;
import cn.mns.constant.SiteEnum;
import cn.mns.tools.email.EmailHelper;
import cn.mns.tools.email.EmailHelperFactory;
import cn.mns.tools.email.EmailSendException;
/**
* 接收MQ消息队列中的邮件消息并发送邮件
*
*/
public class EmailMessageReceiver implements MessageListener {
public void onMessage(Message msg) {
MapMessage mapMsg = (MapMessage)msg;
try {
String email = mapMsg.getString("email");//要发送的邮箱地址
String account = mapMsg.getString("account");//发送该邮件的邮箱账号
String subject = mapMsg.getString("subject");//邮件主题
String templateFile = mapMsg.getString("templateFile");//邮件内容
// 这个类是一个自己项目的类,就是实现了发送邮件的功能
EmailHelper emailHelper = EmailHelperFactory.getEmailHelper();
emailHelper.setSubject(subject);
emailHelper.setTemplateFile(templateFile);
Map<String,Object> params = new HashMap<String,Object>();
params.put("subject",subject);
params.put("email",email);
try {
// 发送邮件
emailHelper.sendEmail(email,account,params);
} catch (EmailSendException e) {
e.printStackTrace();
}
} catch (JMSException e) {
e.printStackTrace();
}
}
}
日期工具类:
package cn.mns.tools;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class DateUtil {
public static String getFormatDate(Date date,String format) {
if(date == null){
return null;
}
return new SimpleDateFormat(format).format(date);
}
public static Date getFormatDate (String strDate, String format) {
Date date = null;
if (strDate != null && !"".equals(strDate)) {
if (format == null || "".equals(format)) {
format = "yyyy-MM-dd";
}
SimpleDateFormat sdf = new SimpleDateFormat(format);
try {
date = sdf.parse(strDate);
} catch (ParseException e) {
e.printStackTrace();
date = new Date();
}
}
return date;
}
//输入Date得到所需格式的日期字符
public String FormatDate(Date date, String formatType) {
java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat(
formatType);
String strDate = formatter.format(date);
return strDate;
}
//得到特定日期加上给定时间后的日期
public GregorianCalendar DATEADD(String datepart, int number,
GregorianCalendar inputdate) {
int normalDatepart = 0;
datepart = datepart.toLowerCase();
if (datepart.equals("y") || datepart.equals("yy")
|| datepart.equals("yyy") || datepart.equals("yyyy"))
normalDatepart = inputdate.YEAR;
if (datepart.equals("m") || datepart.equals("mm"))
normalDatepart = inputdate.MONTH;
if (datepart.equals("d") || datepart.equals("dd"))
normalDatepart = inputdate.DATE;
if (datepart.equals("wk") || datepart.equals("ww")) {
normalDatepart = inputdate.DATE;
number = number * 7;
}
if (datepart.equals("hh") || datepart.equals("h"))
normalDatepart = inputdate.HOUR;
if (datepart.equals("mi"))
normalDatepart = inputdate.MINUTE;
if (datepart.equals("s") || datepart.equals("ss"))
normalDatepart = inputdate.SECOND;
if (datepart.equals("ms"))
normalDatepart = inputdate.MILLISECOND;
GregorianCalendar tempDate = new GregorianCalendar();
tempDate.setTime(inputdate.getTime());
tempDate.add(normalDatepart, number);
return tempDate;
}
//输入字符串
public String DATEADD(String datepart, int number, String strInputDate) {
java.util.GregorianCalendar gcResult = new GregorianCalendar();
String outDate = "";
String formatType = "yyyy-MM-dd";
String tempInputDate = "";
if (strInputDate.indexOf(".") >= 0)
formatType = "yyyy.MM.dd";
tempInputDate = strInputDate;
if (strInputDate.indexOf(".") >= 0)
formatType = "yyyy.MM.dd";
if (tempInputDate.indexOf(":") >= 0) //判断是哪种格式.
formatType = formatType + " hh:mm";
tempInputDate = tempInputDate.substring(tempInputDate.indexOf(":") + 1);
if (tempInputDate.indexOf(":") >= 0)
formatType = formatType + ":ss";
java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat(
formatType);
java.text.ParsePosition pos = new java.text.ParsePosition(0);
java.util.Date sourceDate = formatter.parse(strInputDate, pos);
GregorianCalendar gc = new GregorianCalendar();
gc.setTime(sourceDate);
gcResult = DATEADD(datepart, number, gc);
String strDate = formatter.format(gcResult.getTime());
outDate = strDate;
if (formatType.equals("yyyy-MM-dd hh:mm")
|| formatType.equals("yyyy-MM-dd hh:mm:ss")
|| formatType.equals("yyyy.MM.dd hh:mm")
|| formatType.equals("yyyy.MM.dd hh:mm:ss")) //如果是下午,则要加12个小时。
{
if (gcResult.get(Calendar.AM_PM) == 1) //AM_PM的值 1代表下午 0 代表上午
{
String strPMHour = Integer.toString(Integer.parseInt(strDate
.substring(11, 13)) + 12);
outDate = strDate.substring(0, 11) + strPMHour
+ strDate.substring(13);
}
}
return outDate;
}
//输入Date
public Date DATEADD(String datepart, int number, Date dateInputDate) {
java.util.GregorianCalendar gc = new GregorianCalendar();
java.util.GregorianCalendar gcResult = new GregorianCalendar();
gc.setTime(dateInputDate);
gcResult = DATEADD(datepart, number, gc);
return gcResult.getTime();
}
public static void main(String[] args){
System.out.println(getFormatDate("2015-08", "yyyy-MM"));
}
}