雖然現在郵件用得越來越少,但跟即時通訊相比,郵件還是有它一些獨有的特質,因此某些事務中還是會用到它。
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 <[email protected]> Undeliverable: 上海浦東第11期“XXXXXXXX創新創業研習班”邀請函 [email protected]
Sat, 13 Jul 2019 03:41:45 +0800 b'"'光偉b'" <[email protected]>' 自動回覆:上海浦東第11期“XXXXXXXX創新創業研習班”邀請函
12 Jul 2019 18:08:17 +0800 (CST) [email protected] 系統退信 [email protected]
12 Jul 2019 18:34:44 +0800 (CST) [email protected] 系統退信 [email protected]
12 Jul 2019 18:39:35 +0800 (CST) [email protected] 系統退信 [email protected]
Thu, 11 Jul 2019 22:39:19 +0000 Anne <[email protected]> Automatic reply: EXTERNAL: 上海浦東第11期“XXXXXXXX創新創業研習班”邀請函
Thu, 11 Jul 2019 22:29:15 +0800 (CST) <[email protected]> 系統退信/Systems bounce [email protected]
Thu, 11 Jul 2019 22:12:17 +0000 <[email protected]> Undeliverable: [email protected]
Thu, 11 Jul 2019 19:53:25 +0800 (CST) [email protected] 系統退信 [email protected]
退信處理原則上有兩種方法,看上去比較智慧是上直接在服務器上讀郵件,但技術難度比較大,因爲要讀懂郵件的具體中文內容是一件非常棘手的事情;另一種技術難度比較小的就是把服務器上的郵件用客戶端全部下載下來再來分析內容,就容易操作了,用thunderbird客戶端下載的文件可讀性非常好,但如果有很多年積攢起來的郵件,數據量也是很驚人的,所以感覺上這個方法有點low。
最後,因爲所有這些都是在SuiteCRM外面操作的,因此爲了CRM信息的完整性,還要把相關的信息從數據庫層面導入到SuiteCRM。
- 要在SuiteCRM把無效郵箱註釋掉,以免下次再發;
- 把這次羣發成功的也導入到相關的潛在客戶;
- 把有人工回信的也導入到相關的潛在客戶。