##language:zh ''' Qmail with MobilPhone notes 使用 Qmail 与手机短信! ''' -- Zoom.Quiet [<<DateTime(2004-09-11T19:18:27Z)>>] <<TableOfContents>> = Qmail in FreeBSD = == python来过滤邮件和发送短信 == '''实现qmail下邮件过滤和邮件到达短信通知实录''' * 作者:梅劲松 * 时间:2004年9月10日 * 感谢:HD、刘鑫 * 短信这么火热,如果自己的邮件服务器也能和短信结合起来,那多么好啊?在qmail下,我用qmail_queue来解决邮件过滤和短信到达通知。 '''本邮件系统在freebsd 4.10上实施成功。''' === step1 Qmail install === '''对于邮件系统的安装,没有太多说的''' * 建议大家看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的安装不是本文重点,这里将忽略。 ''关于文章里面的杀毒部分因为系统效率问题,我没有安装。各位请自行决定是否安装。'' === step2 SPAM === '''反垃圾邮件当然需要''' * 我用的是[[ 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+服务的情况。 === step3 qmail_queue === '''这里是重点了。过滤和短信到达是需要qmail_queue来完成的,一定要打这个包。''' * 使用python来实现这个功能,当然需要安装python啦。 {{{ cd /usr/ports/lang/python make;make install;make clean }}} * 一般来讲这个安装是非常顺利的。 * 安装结束后。 {{{ cd /var/qmail/bin }}} * 编辑qmfilt.py,内容如下: {{{#!python #!/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关注他的代码修改情况。 * 结束,谢谢! = 讨论 = * stephen 发表,Zoom.Quiet 整理格式