qmail下使用python编写脚本来过滤邮件和发送短信
作者:梅劲松
Email:stephen.cn@gmail.com
时间:2004年9月10日
感谢:HD、刘鑫
短信这么火热,如果自己的邮件服务器也能和短信结合起来,那多么好啊?在qmail下,我用qmail_queue来解决邮件过滤和短信到达通知。
本邮件系统在freebsd 4.10上实施成功。
1、对于邮件系统的安装,没有太多说的。建议大家看HD的http://bsd.huangdong.com/server/mail/qmail.html来安装。如果已经安装了有qmail+vpopmail+qmail_queue补丁的,请直接跳到4继续安装。在新装邮件服务器时有几点需要注意的是,安装qmail的时候应该将步骤改为:
cd /usr/ports/mail/qmail
make WITH_QMAILQUEUE_PATCH=yes WITH_BIG_TODO_PATCH=yes WITH_OUTGOINGIP_PATCH=yes WITH_PRESERVE_CONFIG_FILES=yes extract
cd /usr/ports/mail/qmail/work/qmail-1.03
fetch http://www.nimh.org/dl/qmail-smtpd.c
cd /usr/ports/mail/qmail
make WITH_QMAILQUEUE_PATCH=yes WITH_BIG_TODO_PATCH=yes WITH_OUTGOINGIP_PATCH=yes WITH_PRESERVE_CONFIG_FILES=yes install
make disable-sendmail
make enable-qmail
make clean
在安装vpopmail的时候可能因为port更新的原因让使用vpopmail的时候出现奇怪问题,最好是安装webmin将建立的vpopmail数据库删除后,重新建立,并指定数据库的用户权限。由于webmin的安装不是本文重点,这里将忽略。
2、关于文章里面的杀毒部分因为系统效率问题,我没有安装。各位请自行决定是否安装。
3、反垃圾邮件当然需要,我用的是http://anti-spam.org.cn的cbl+服务。使用的时候只需要将smtpd的脚本改为:
#!/bin/sh
QMAILDUID=`/usr/bin/id -u qmaild`
NOFILESGID=`/usr/bin/id -g qmaild`
exec /usr/local/bin/tcpserver -H -R -l 0 -p -x /
/usr/local/vpopmail/etc/tcp.smtp.cdb -u"$QMAILDUID" /
-g"$NOFILESGID" -v -c100 0 smtp rblsmtpd /
-r cblplus.anti-spam.org.cn /
-r relays.ordb.org /
/var/qmail/bin/qmail-smtpd /usr/local/vpopmail/bin/vchkpw-smtpd /usr/bin/true 2>&1
从http://anti-spam.org.cn/cgi-bin/rblclient/(你的邮件服务器的dns服务器IP地址)看到你的邮件服务器的流量和使用cbl+服务的情况。
4、这里是重点了。过滤和短信到达是需要qmail_queue来完成的,一定要打这个包。
使用python来实现这个功能,当然需要安装python啦。
cd /usr/ports/lang/python
make;make install;make clean
一般来讲这个安装是非常顺利的。
安装结束后。
cd /var/qmail/bin
编辑qmfilt.py,内容如下:
#!/usr/local/bin/python --
import os, sys, string, time, traceback, re, socket
Version = '1.1'
PyVersion = '1.0'
Logging = 1
Debug = 0
QmailD = 82#这里需要和你/etc/password里面qmaild用户的一样
LogFile = None
TestMode = None
Filters = []
MailEnvelope = ''
MailData = ''
ValidActions = { 'trap': '', 'drop' : '', 'block' :'' }
#这里是你通过ucp协议将消息发送到哪个服务器的哪个端口
def mail_sms(msg):
host = "xxx.xxx.114.2"
port = 9999
buf = 500
addr = (host,port)
# Create socket
UDPSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
def_msg = msg;
UDPSock.sendto(def_msg,addr)
# Close socket
UDPSock.close()
def openlog():
global LogFile
if not Logging:
return
try:
LogFile = os.open('/var/log/qmfilt', os.O_APPEND|os.O_WRONLY|os.O_CREAT, 0744)
except:
if TestMode:
print "Can't create /var/log/qmfilt"
else:
# Indicate a write error
sys.exit(53)
def log(message):
message = time.asctime(time.localtime(time.time())) + " - " + message + "/n"
if Logging:
os.write(LogFile, message)
# Indicate a write error
#sys.exit(53)
if TestMode:
print message
def showFilters():
if len(Filters) == 0:
print "No filters"
for f in Filters:
print "Filter - %s - Action: %s Regex: %s" % (f[0], f[1], f[2])
def readFilters():
global Filters
try:
file = open('/var/qmail/control/qmfilt', 'r')
configLines = file.readlines()
file.close()
except:
log("Can't read /var/qmail/control/qmfilt")
if not TestMode:
# Indicate a 'cannot read config file error'
sys.exit(53)
reg = re.compile('(.*)::(.+)::(.+)::')
for line in configLines:
if line[0] == '#':
continue
m = reg.match(line)
if m != None:
action = string.lower(m.group(2))
if not ValidActions.has_key(action):
log("Invalid action in config file [%s] - SKIPPED" %(action))
continue
Filters.append( [m.group(1), string.lower(m.group(2)), m.group(3)] )
def readDescriptor(desc):
result = ''
while 1:
data = os.read(desc, 16384)
if data == '':
break
result = result + data
return result
def writeDescriptor(desc, data):
while data:
num = os.write(desc, data)
data = data[num:]
os.close(desc)
def filterHits():
for regEx in Filters:
reg = re.compile(regEx[2], re.M|re.I)
match = reg.search(MailData)
if match:
log("Matched [%s]" % (regEx[0]))
return regEx[1], regEx[0]
return None,None
def storeInTrap(hit):
try:
pid = os.getpid()
mailFile = os.open('/var/qmail/qmfilt/qmfilt.%s' %(pid), os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0744)
except:
log("Can't create /var/qmail/qmfilt/qmfilt.%s" %(pid))
# Indicate a write error
sys.exit(53)
try:
#如果不屏蔽会出现很多问题,至于为什么我还没弄明白,如果你找到问题所在,请告诉我。
"""(None, inode, None, None, None, None, size, None, None, None) = os.fstat(mailFile)"""
os.rename('/var/qmail/qmfilt/qmfilt.%s' %(pid), '/var/qmail/qmfilt/%s.mail' %(inode))
except:
log("Can't rename /var/qmail/qmfilt/qmfilt.%s -> /var/qmail/qmfilt/%s.mail" %(pid, inode))
# Indicate a write error
sys.exit(53)
try:
envFile = os.open('/var/qmail/qmfilt/%s.envelope' %(inode), os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0744)
mesgFile = os.open('/var/qmail/qmfilt/%s.qmfilt' %(inode), os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0744)
writeDescriptor(mailFile, MailData)
writeDescriptor(envFile, MailEnvelope)
writeDescriptor(mesgFile, "Matched filter [ %s] to file %s" %(hit, inode))
log("Matched filter [ %s] to file %s" %(hit, inode))
except:
log("Can't create/write files into trap")
# Indicate a write error
sys.exit(53)
return 0
def sendToQmailQueue():
# Open a pipe to qmail queue
fd0 = os.pipe()
fd1 = os.pipe()
pid = os.fork()
if pid < 0:
log("Error couldn't fork")
sys.exit(81)
if pid == 0:
# This is the child
os.dup2(fd0[0], 0)
os.dup2(fd1[0], 1)
i = 2
while (i < 64):
try:
os.close(i)
except:
pass
i = i + 1
os.chdir('/var/qmail')
#time.sleep(10)
os.execv("bin/qmail-queue", ('bin/qmail-queue',))
log("Something went wrong")
sys.exit(127) # This is only reached on error
else:
# This is the parent
# close the readable descriptors
os.close(fd0[0])
os.close(fd1[0])
# Send the data
recvHdr = "Received: (QMFILT: %s); " %(Version)
recvHdr = recvHdr + time.strftime("%d %b %Y %H:%M:%S", time.gmtime(time.time()))
recvHdr = recvHdr + " -0000/n"
os.write(fd0[1], recvHdr)
writeDescriptor(fd0[1], MailData)
writeDescriptor(fd1[1], MailEnvelope)
# Catch the exit code to return
result = os.waitpid(pid, 0)[1]
if PyVersion > '1.5.1':
if os.WIFEXITED(result):
return os.WEXITSTATUS(result)
else:
log("Didn't exit normally")
sys.exit(81)
else:
return result
def conver(msg):
msg = msg.replace(chr(00),' ')
msg = msg.replace('F','From:',1)
msg = msg.replace(' T',' To:')
return msg
def main(argv, stdout, environ):
global TestMode
global MailData
global MailEnvelope
global PyVersion
PyVersion = string.split(sys.version)[0]
if len(argv) > 1 and argv[1] == '--test':
TestMode = 1
os.setuid(QmailD)
# Insure Environment is OK
# Try to open log
openlog()
if not os.path.exists('/var/qmail/qmfilt'):
# Indicate a problem with the queue directory
log("Directory /var/qmail/qmfilt doesn't exist")
if not TestMode:
sys.exit(61)
if os.path.exists('/var/qmail/control/qmfilt'):
readFilters()
else:
if TestMode:
print "No filter file /var/qmail/control/qmfilt - no filters applied"
if TestMode:
showFilters()
# Go no further if in test mode
if TestMode:
sys.exit(0)
# Get the data
# Read the data
MailData = readDescriptor(0)
# Read the envelope
MailEnvelope = readDescriptor(1)
if Debug:
log(MailData)
log(MailEnvelope)
action,hit = filterHits()
if action == 'trap':
storeInTrap(hit)
if action == 'block':
log("Matched filter [ %s] and email was BLOCKED/Refused delivery" %(hit))
sys.exit(31)
if action == 'drop':
log("Matched filter [ %s] and email was DROPPED" %(hit))
if action == None:
sendToQmailQueue()
#Log
log(conver(MailEnvelope))
#send sms
mail_sms(conver(MailEnvelope))
if Debug:
log("qmailqueue returned [%d]" %(result))
sys.exit(0)
if __name__ == "__main__":
try:
main(sys.argv, sys.stdout, os.environ)
# Catch the sys.exit() errors
except SystemExit, val:
sys.exit(val)
except:
# return a fatal error for the unknown error
if TestMode:
traceback.print_exc()
sys.exit(81)
然后在cd /var/qmail/control
编辑qmfilt内容如下:
#
# This is the qmfilt control file
# If any email comes in that matches this
# filter, the mail will be redirected
# to the filter directory
#
# A filter regular expression must be on a single
# line and in between a pair of '::'
#
# Valid actions:
# trap - store in the trap directory
# block - tell the smtp sender that we won't take the mail
# drop - accept the mail, but don't queue it
#
# This will match any executable Visual Basic Scripts
VisualBasic::block::^Content-Type: application/octet-stream;/s+name="(.*/.vbs)"::
SHS::block::^Content-Type: application/octet-stream;/s+name="(.*/.shs)"::
SHB::block::^Content-Type: application/octet-stream;/s+name="(.*/.shb)"::
COM::block::^Content-Type: application/octet-stream;/s+name="(.*/.com)"::
EXE::block::^Content-Type: application/octet-stream;/s+name="(.*/.exe)"::
SCR::block::^Content-Type: application/octet-stream;/s+name="(.*/.scr)"::
PIF::block::^Content-Type: application/octet-stream;/s+name="(.*/.pif)"::
BAT::block::^Content-Type: application/octet-stream;/s+name="(.*/.bat)"::
# This is the Outlook date overflow bug
DATE::block::^Date:.{160,}::
在/var/log下建立qmfilt文件,内容为空
chown qmaild qmfilt
在/var/qmail下建立qmfilt目录。
chown -R qmaild qmfilt
好了,来让我们的程序启用吧
先测试一把
/var/qmail/bin/qmfilt.py --test
如果返回了先定义的qmfilt里面的内容,恭喜成功了一半了。内容应该和下面的相似:
Filter - VisualBasic - Action: block Regex: ^Content-Type: application/octet-stream;/s+name="(.*/.vbs)"
Filter - SHS - Action: block Regex: ^Content-Type: application/octet-stream;/s+name="(.*/.shs)"
Filter - SHB - Action: block Regex: ^Content-Type: application/octet-stream;/s+name="(.*/.shb)"
Filter - COM - Action: block Regex: ^Content-Type: application/octet-stream;/s+name="(.*/.com)"
Filter - EXE - Action: block Regex: ^Content-Type: application/octet-stream;/s+name="(.*/.exe)"
Filter - SCR - Action: block Regex: ^Content-Type: application/octet-stream;/s+name="(.*/.scr)"
Filter - PIF - Action: block Regex: ^Content-Type: application/octet-stream;/s+name="(.*/.pif)"
Filter - BAT - Action: block Regex: ^Content-Type: application/octet-stream;/s+name="(.*/.bat)"
Filter - DATE - Action: block Regex: ^Date:.{160,}
好了,让我们的过滤器用起来吧
cd /usr/local/vpopmail/etc
编译tcp.smtp内容如下
127.:allow,RELAYCLIENT="",QMAILQUEUE="bin/qmfilt.py"
:allow,QMAILQUEUE="bin/qmfilt.py"
然后生成tcp.smtp.cdb文件
tcprules tcp.smtp.cdb tcp.smtp.tmp < tcp.smtp
,发个带exe为附件的邮件看看。如果成功过滤,应该在/var/log/qmfilt里面看到如下信息:
Fri Sep 10 14:37:01 2004 - Matched filter [ EXE] and email was BLOCKED/Refused delivery
Fri Sep 10 14:38:09 2004 - Matched [SCR]
如果你是用foxmail等客户端发送带exe为结尾的邮件的话,服务器会提示你拒绝接收的。
到这里,你的邮件过滤已经成功了。
你可以在/var/qmail/control的qmfilt文件里面定义你需要过滤的指定代码,然后测试规则是否生效了。
关于短信到达通知,因为安全和商业上的问题。不能将服务器端的代码贴出来。我将机制给大家讲一下。
qmfilt.py这个程序里面mail_sms用udp将类似From:info@xxx.com.cn To:weilx@xxx.com发送到指定的服务器上。服务器接收到这个数据包后,将这个数据包的内容用短信发送出去。
当然可以将邮件主题等信息一起发送到手机上,大家根据自己的情况更改吧。
这个程序是根据http://sourceforge.net/projects/qmfilt/这个项目更改的,但是直接用他的代码会造成不能收信,大家可以在这个项目的cvs关注他的代码修改情况。
结束,谢谢!