虽然现在邮件用得越来越少,但跟即时通讯相比,邮件还是有它一些独有的特质,因此某些事务中还是会用到它。
SuiteCRM有群发邮件功能,但因为目前国家相关部门的规定限制,通常一个邮箱每天只能发1000个邮件,而且有一定的节奏要求,所以无法使用SuiteCRM来群发邮件,为了完成相应的业务,必须有另外招数。
通常的分享只考虑发或收,在此两个方向都考虑到了,而且结合CRM就可以把相关信息更有针对性地保留下来,并且分配给相关人员。
在此分享一个群发2万份邮件具体思路
Python是眼下最简单有效解决小问题的编程语言,所以选择Python。在网上可以找到很多Python收发邮件的例子,也不需要很长代码。
整体思路:
这是一个从成本角度无法全自动的项目,只能分段进行自动化。
- 从SuiteCRM导出相关的邮箱地址,进行删选,然后生成Python可读地址文件;
- 用Python写一个推送程序,大概200行足矣,账号、密码、发送节奏,循环次数都存放在一个设置文件里,以便随时修改,发送日志;
- 用Python回信处理程序,账号、密码存放在一个设置文件里,以便随时修改,收邮件清单,以便批量处理,特别针对无效邮箱。
具体做法:
在此同各位分享一下具体步骤中的一些细节
- 因为在SuiteCRM的界面上总是有些绊手绊脚的,所以这里笔者直接进入数据库,下面是用SuiteCRM原始潜在客户leads表格写的一句SQL命令,包括的数据项有:邮箱地址、人名加称谓、数据唯一标识码(简称ID)、 数据最后修改日期、所在城市,按修改日期排序;这是一个最基本选择,实际应用场景可以非常多的细分,在此就此略过;在这基础上把数据导入Excel就可以交给任何一个熟练白领根据具体情况进行对数据分组等一系列群发邮件前的准备工作,可以任意发挥,技术上没有什么难度,几乎想怎么样就可以怎么样,效率也是很高的。
SELECT c.email_address, CONCAT(a.last_name, if(a.salutation='Mr.','先生',if(a.salutation='Ms.','女士','先生/女士'))) as name, a.id, a.date_modified, a.primary_address_city FROM leads a INNER JOIN email_addr_bean_rel b ON a.id = b.bean_id INNER JOIN email_addresses c ON b.email_address_id = c.id WHERE a.deleted=0 AND b.deleted=0 AND LENGTH(a.last_name) <10 AND c.invalid_email = 0 ORDER BY a.date_modified
- 在上面的邮箱、人名及分组准备好以后,就可以进行布置群发了;群发需要有几样东西:
a. 要有一个有邮箱,包括邮箱imap地址,用户名,密码
b. 邮件内容包括个性化变量,一般为html格式
c. 收件人邮箱地址和人名清单,在此还有ID,以便把最终结果导入CRM
d. 要有推送程序,在此是在网上参考了许多帖子后自己写的Python 3程序,程序本身包括python源码(mailsender.py)、控制部分(config.txt)、邮件内容(message.txt)、邮箱人名清单(addresses.txt) 四部分,推送程序还会写一个发送成功清单(logs.txt)和一个发送失败清单(failed.txt)用于核对和检查
e. 推送的节奏是有讲究的,太快了会被邮箱服务商挡住,太慢了会来不及,这个需要花时间摸索,一般销售的信息也不是很准,网易企业邮箱每天每个账号只能发一千个邮件(这样相对也出不了大事,避免被封),间隔最好大于10s,其他的说法都不可靠;有的销售会给你一个比较明确的信息,而有的却吾哩嘛里如阿里云的一个销售不给相关信息,来来回回浪费了好几天,具体需要自己花时间挖掘。
# Python 3
import time
import datetime
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
sleeptime = 1
pausetime = 0
numMails = 10
numRounds = 1
SMTPUsername = ''
SMTPPassword = ''
SMTPServer = 'smtp.gmail.com'
SMTPPort = 587
sender = ''
confirmationEmail = ''
j = 0
count = 0
failed = 0
def configuration(resume=""):
global sleeptime
global pausetime
global numMails
global numRounds
global SMTPUsername
global SMTPPassword
global SMTPServer
global SMTPPort
global sender
global j
global confirmationEmail
# resume from last sent mail according to logfile
if resume == "" and os.path.isfile("logs.txt"):
resume = input("Resume from last position?(Y/N)\n")
elif resume == "":
resume = "No"
print("resuming from logs: ", resume)
if resume[0].upper() == "Y":
log = open("logs.txt", encoding='utf-8').read().split("\n")
j = int(log[len(log) - 2].split("\t")[0]) + 1
# load configuration from file
with open("config.txt") as conf:
config = conf.read()
config = config.split("\n")
for line in config:
if len(line) == 0:
continue
if line[0] == "#":
continue
option = line.split(" = ")
if option[0] == "sleeptime":
sleeptime = float(option[1])
elif option[0] == "pausetime":
pausetime = float(option[1]) * 60
elif option[0] == "numMails":
numMails = int(option[1])
elif option[0] == "numRounds":
numRounds = int(option[1])
elif option[0] == "SMTPUsername":
SMTPUsername = option[1]
elif option[0] == "SMTPPassword":
SMTPPassword = option[1]
elif option[0] == "SMTPServer":
SMTPServer = option[1]
elif option[0] == "sender":
sender = option[1]
elif option[0] == "SMTPPort":
SMTPPort = int(option[1])
elif option[0] == "confirmationEmail":
confirmationEmail = option[1]
else:
print("invalid option: ")
print(option)
if sender == "":
sender = SMTPUsername
if confirmationEmail == "":
confirmationEmail = sender
print("configuration finished")
def send():
global sleeptime
global pausetime
global numMails
global numRounds
global SMTPUsername
global SMTPPassword
global SMTPServer
global SMTPPort
global sender
global j
global count
global failed
# connect to mailserver
# server = smtplib.SMTP(SMTPServer, SMTPPort)
# server.starttls()
addresses = (open("addresses.txt", encoding='utf-8').read() + "\n").split("\n")
# if not specified, send all mails
if numMails == 0:
numMails = int((len(addresses) // numRounds) + 1)
for k in range(numRounds):
# connect to mailserver
server = smtplib.SMTP(SMTPServer, SMTPPort)
server.starttls()
# login
try:
server.login(SMTPUsername, SMTPPassword)
except Exception as e:
print("login failed")
print(e)
# assemble individual messages
for i in range(numMails):
sent = False
# find next email reciever
while len(addresses[k * numMails + i + j]) <= 1:
print("skip empty line")
j += 1
if k * numMails + i + j >= len(addresses):
break
index = k * numMails + i + j
if index >= len(addresses):
print("end reached")
break
reciever = addresses[index].split(";")
msg = MIMEMultipart('alternative')
msg['From'] = sender
msg['To'] = reciever[0]
html = ""
try:
with open("message.txt", encoding='utf-8')as f:
subject = f.readline()
html = f.read()
except Exception as e:
print("could not read message")
print(e)
msg['Subject'] = subject
if len(html) < 1:
print("message could be empty")
html = html.replace('placeholder', reciever[1])
part2 = MIMEText(html, 'html')
msg.attach(part2)
try:
server.send_message(msg)
count += 1
sent = True
except Exception as e:
print("message could not be sent")
print(e)
print("messages sent:", count)
# write logs
with open("logs" + ".txt", "a+", encoding='utf-8') as log:
log.write(
str(index) + "\t" + reciever[0] + "\t" + reciever[1] + "\t" + reciever[2] + "\t" + "sent: " + str(
sent) + "\t" + str(
datetime.datetime.now()) + "\n")
if not sent:
failed += 1
with open("failed" + ".txt", "a+", encoding='utf-8') as fail:
fail.write(reciever[0] + ";" + reciever[1] + ";" + reciever[2] + "\n")
print("sleeping", sleeptime, "s")
time.sleep(sleeptime)
if index + 1 >= len(addresses):
print("end reached")
break
print("paused", pausetime, "s")
time.sleep(pausetime)
def confirm():
server = smtplib.SMTP(SMTPServer, SMTPPort)
server.starttls()
server.login(SMTPUsername, SMTPPassword)
msg = MIMEMultipart()
msg['From'] = sender
msg['To'] = confirmationEmail
msg['Subject'] = "Emails sent"
message = "successfully sent " + str(count) + " messages\n" + str(failed) + " messages could not be sent\n" + str(
datetime.datetime.now())
msg.attach(MIMEText(message))
server.send_message(msg)
configuration()
send()
confirm()
这个推送程序的亮点是操作简单,邮箱、节奏、每次群发多少邮件个数,每次群发间的停顿时间,每天群发次数,都在config.txt里定义,可以根据邮件服务器的反馈马上调整,极其方便;可以在一般windows里运行,也可以到服务器上运行;不同账号可以用不同文件夹来同时群发邮件,以便节省时间;有两种日志方便找出问题所在。
上图为在三个文件夹里用网易企业邮箱同时跑三个群发
# filename: config.txt
# SMTP Login Data for Email Service
SMTPServer = smtphm.qiye.163.com (例子网易企业邮箱)
SMTPPort = 587
SMTPUsername = 你的邮箱地址
SMTPPassword = 你的邮箱密码
# optional, accepts SMTPUsername if not specified
sender = 你的邮箱地址
# optional, accepts SMTPUsername if not specified
confirmationEmail = 你的邮箱地址
# Waiting time between emails in seconds
sleeptime = 3
# Waiting time between rounds min
pausetime = 10
# Number of emails to be sent per round, 0 for all mails
numMails = 100
# Number of rounds
numRounds = 9
- 在大量推送以后免不了有很多回复邮件,除了自动回复以外,还有许多退信,日积月累将让人几乎无法招架,面对一大堆没有实际意义的退信谁都会头大,大部分人可能就选择视而不见了,但这会给后人带来更大的负担,在此用Python3写了100行代码,得到下面的结果,这个文件用来批处理就非常有效,自动答复可以忽略,退信只要写个SQL命令交给CRM,下次就不会出现在群发清单里了,剩下的人工处理只是很小很小部分;在导入CRM前,必须要人工检查一下,虽然程序已经让此事已经可行了,程序还是没有那么智慧。
Sat, 13 Jul 2019 03:51:34 +0800 <postmaster@hktdc.org> Undeliverable: 上海浦东第11期“XXXXXXXX创新创业研习班”邀请函 pansy.y@hktdc.org
Sat, 13 Jul 2019 03:41:45 +0800 b'"'光伟b'" <123456@qq.com>' 自动回复:上海浦东第11期“XXXXXXXX创新创业研习班”邀请函
12 Jul 2019 18:08:17 +0800 (CST) MAILER-DAEMON@mx-14-110.mail.sina.com.cn 系统退信 gxqw999@sina.cn
12 Jul 2019 18:34:44 +0800 (CST) MAILER-DAEMON@mx-14-109.mail.sina.com.cn 系统退信 nake98@sina.com
12 Jul 2019 18:39:35 +0800 (CST) MAILER-DAEMON@mx-14-106.mail.sina.com.cn 系统退信 yy7759@sina.com
Thu, 11 Jul 2019 22:39:19 +0000 Anne <anne@hsbc.com.cn> Automatic reply: EXTERNAL: 上海浦东第11期“XXXXXXXX创新创业研习班”邀请函
Thu, 11 Jul 2019 22:29:15 +0800 (CST) <postmaster@usst.edu.cn> 系统退信/Systems bounce ymzh@usst.edu.cn
Thu, 11 Jul 2019 22:12:17 +0000 <postmaster@innovators.net> Undeliverable: geib@ndmneb5.com
Thu, 11 Jul 2019 19:53:25 +0800 (CST) Postmaster@126.com 系统退信 julia_july@126.com
退信处理原则上有两种方法,看上去比较智慧是上直接在服务器上读邮件,但技术难度比较大,因为要读懂邮件的具体中文内容是一件非常棘手的事情;另一种技术难度比较小的就是把服务器上的邮件用客户端全部下载下来再来分析内容,就容易操作了,用thunderbird客户端下载的文件可读性非常好,但如果有很多年积攒起来的邮件,数据量也是很惊人的,所以感觉上这个方法有点low。
最后,因为所有这些都是在SuiteCRM外面操作的,因此为了CRM信息的完整性,还要把相关的信息从数据库层面导入到SuiteCRM。
- 要在SuiteCRM把无效邮箱注释掉,以免下次再发;
- 把这次群发成功的也导入到相关的潜在客户;
- 把有人工回信的也导入到相关的潜在客户。