Qmail with MobilPhone notes

使用 Qmail 与手机短信!

-- Zoom.Quiet [2004-09-11 19:18:27]

1. Qmail in FreeBSD

1.1. python来过滤邮件和发送短信

实现qmail下邮件过滤和邮件到达短信通知实录

  • 作者:梅劲松
  • 时间:2004年9月10日
  • 感谢:HD、刘鑫
    • 短信这么火热,如果自己的邮件服务器也能和短信结合起来,那多么好啊?在qmail下,我用qmail_queue来解决邮件过滤和短信到达通知。

本邮件系统在freebsd 4.10上实施成功。

1.1.1. 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的安装不是本文重点,这里将忽略。

关于文章里面的杀毒部分因为系统效率问题,我没有安装。各位请自行决定是否安装。

1.1.2. step2 SPAM

反垃圾邮件当然需要

#!/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

1.1.3. step3 qmail_queue

这里是重点了。过滤和短信到达是需要qmail_queue来完成的,一定要打这个包。

  • 使用python来实现这个功能,当然需要安装python啦。

cd /usr/ports/lang/python
make;make install;make clean
  • 一般来讲这个安装是非常顺利的。
  • 安装结束后。

cd /var/qmail/bin
  • 编辑qmfilt.py,内容如下:

   1 #!/usr/local/bin/python --
   2 
   3 import os, sys, string, time, traceback, re, socket
   4 
   5 Version  = '1.1'
   6 PyVersion  = '1.0'
   7 Logging  = 1
   8 Debug    = 0
   9 QmailD   = 82#这里需要和你/etc/password里面qmaild用户的一样
  10 LogFile  = None
  11 TestMode = None
  12 Filters  = []
  13 MailEnvelope = ''
  14 MailData     = ''
  15 ValidActions = { 'trap': '', 'drop' : '', 'block' :'' }
  16 
  17 #这里是你通过ucp协议将消息发送到哪个服务器的哪个端口
  18 def mail_sms(msg):
  19     host = "xxx.xxx.114.2"
  20     port = 9999
  21     buf = 500
  22     addr = (host,port)
  23 
  24     # Create socket
  25     UDPSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
  26 
  27     def_msg = msg;
  28     UDPSock.sendto(def_msg,addr)
  29 
  30     # Close socket
  31     UDPSock.close()
  32 
  33 def openlog():
  34   global LogFile
  35 
  36   if not Logging:
  37      return
  38   try:
  39     LogFile = os.open('/var/log/qmfilt', os.O_APPEND|os.O_WRONLY|os.O_CREAT, 0744)
  40   except:
  41     if TestMode:
  42       print "Can't create /var/log/qmfilt"
  43     else:
  44       # Indicate a write error
  45       sys.exit(53)
  46 
  47 def log(message):
  48   message = time.asctime(time.localtime(time.time())) + " - " + message + "\n"
  49   if Logging:
  50     os.write(LogFile, message)
  51     # Indicate a write error
  52     #sys.exit(53)
  53   if TestMode:
  54     print message
  55 
  56 
  57 def showFilters():
  58 
  59   if len(Filters) == 0:
  60     print "No filters"
  61   for f in Filters:
  62     print "Filter -  %s - Action: %s  Regex: %s" % (f[0], f[1], f[2])
  63 
  64 def readFilters():
  65   global Filters
  66 
  67   try:
  68     file = open('/var/qmail/control/qmfilt', 'r')
  69     configLines = file.readlines()
  70     file.close()
  71   except:
  72     log("Can't read /var/qmail/control/qmfilt")
  73     if not TestMode:
  74       # Indicate a 'cannot read config file error'
  75       sys.exit(53)
  76 
  77   reg = re.compile('(.*)::(.+)::(.+)::')
  78   for line in configLines:
  79     if line[0] == '#':
  80       continue
  81     m = reg.match(line)
  82     if m != None:
  83       action = string.lower(m.group(2))
  84       if not ValidActions.has_key(action):
  85          log("Invalid action in config file [%s] - SKIPPED" %(action))
  86          continue
  87       Filters.append( [m.group(1), string.lower(m.group(2)), m.group(3)] )
  88 
  89 def readDescriptor(desc):
  90   result = ''
  91   while 1:
  92     data = os.read(desc, 16384)
  93     if data == '':
  94       break
  95     result = result + data
  96 
  97   return result
  98 
  99 def writeDescriptor(desc, data):
 100   while data:
 101     num  = os.write(desc, data)
 102     data = data[num:]
 103 
 104   os.close(desc)
 105 
 106 
 107 def filterHits():
 108 
 109   for regEx in Filters:
 110     reg = re.compile(regEx[2], re.M|re.I)
 111     match = reg.search(MailData)
 112     if match:
 113       log("Matched [%s]" % (regEx[0]))
 114       return regEx[1], regEx[0]
 115 
 116   return None,None
 117 
 118 
 119 def storeInTrap(hit):
 120   try:
 121     pid = os.getpid()
 122     mailFile = os.open('/var/qmail/qmfilt/qmfilt.%s' %(pid), os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0744)
 123   except:
 124     log("Can't create /var/qmail/qmfilt/qmfilt.%s" %(pid))
 125     # Indicate a write error
 126     sys.exit(53)
 127 
 128   try:
 129     #如果不屏蔽会出现很多问题,至于为什么我还没弄明白,如果你找到问题所在,请告诉我。
 130     """(None, inode, None, None, None, None, size, None, None, None) = os.fstat(mailFile)"""
 131     os.rename('/var/qmail/qmfilt/qmfilt.%s' %(pid), '/var/qmail/qmfilt/%s.mail' %(inode))
 132   except:
 133     log("Can't rename /var/qmail/qmfilt/qmfilt.%s -> /var/qmail/qmfilt/%s.mail" %(pid, inode))
 134     # Indicate a write error
 135     sys.exit(53)
 136 
 137   try:
 138     envFile = os.open('/var/qmail/qmfilt/%s.envelope' %(inode), os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0744)
 139     mesgFile = os.open('/var/qmail/qmfilt/%s.qmfilt' %(inode), os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0744)
 140     writeDescriptor(mailFile, MailData)
 141     writeDescriptor(envFile,  MailEnvelope)
 142     writeDescriptor(mesgFile, "Matched filter [ %s] to file %s" %(hit, inode))
 143     log("Matched filter [ %s] to file %s" %(hit, inode))
 144   except:
 145     log("Can't create/write files into trap")
 146     # Indicate a write error
 147     sys.exit(53)
 148 
 149   return 0
 150 
 151 
 152 def sendToQmailQueue():
 153 
 154   # Open a pipe to qmail queue
 155   fd0 = os.pipe()
 156   fd1 = os.pipe()
 157   pid = os.fork()
 158   if pid < 0:
 159     log("Error couldn't fork")
 160     sys.exit(81)
 161   if pid == 0:
 162     # This is the child
 163     os.dup2(fd0[0], 0)
 164     os.dup2(fd1[0], 1)
 165     i = 2
 166     while (i < 64):
 167       try:
 168         os.close(i)
 169       except:
 170         pass
 171       i = i + 1
 172     os.chdir('/var/qmail')
 173     #time.sleep(10)
 174     os.execv("bin/qmail-queue", ('bin/qmail-queue',))
 175     log("Something went wrong")
 176     sys.exit(127)  # This is only reached on error
 177   else:
 178     # This is the parent
 179     # close the readable descriptors
 180     os.close(fd0[0])
 181     os.close(fd1[0])
 182 
 183 
 184   # Send the data
 185   recvHdr = "Received: (QMFILT: %s); " %(Version)
 186   recvHdr = recvHdr + time.strftime("%d %b %Y %H:%M:%S", time.gmtime(time.time()))
 187   recvHdr = recvHdr + " -0000\n"
 188   os.write(fd0[1], recvHdr)
 189   writeDescriptor(fd0[1], MailData)
 190   writeDescriptor(fd1[1], MailEnvelope)
 191 
 192   # Catch the exit code to return
 193   result = os.waitpid(pid, 0)[1]
 194   if PyVersion > '1.5.1':
 195     if os.WIFEXITED(result):
 196       return os.WEXITSTATUS(result)
 197     else:
 198       log("Didn't exit normally")
 199       sys.exit(81)
 200   else:
 201     return result
 202 
 203 def conver(msg):
 204   msg = msg.replace(chr(00),' ')
 205   msg = msg.replace('F','From:',1)
 206   msg = msg.replace(' T',' To:')
 207   return msg
 208 
 209 
 210 def main(argv, stdout, environ):
 211 
 212   global TestMode
 213   global MailData
 214   global MailEnvelope
 215   global PyVersion
 216 
 217   PyVersion = string.split(sys.version)[0]
 218 
 219   if len(argv) > 1 and argv[1] == '--test':
 220     TestMode = 1
 221 
 222   os.setuid(QmailD)
 223 
 224   # Insure Environment is OK
 225 
 226   # Try to open log
 227   openlog()
 228 
 229   if not os.path.exists('/var/qmail/qmfilt'):
 230     # Indicate a problem with the queue directory
 231     log("Directory /var/qmail/qmfilt doesn't exist")
 232     if not TestMode:
 233       sys.exit(61)
 234 
 235   if os.path.exists('/var/qmail/control/qmfilt'):
 236     readFilters()
 237   else:
 238     if TestMode:
 239       print "No filter file /var/qmail/control/qmfilt - no filters applied"
 240 
 241   if TestMode:
 242     showFilters()
 243 
 244   # Go no further if in test mode
 245   if TestMode:
 246     sys.exit(0)
 247 
 248 
 249 
 250   # Get the data
 251 
 252   # Read the data
 253   MailData     = readDescriptor(0)
 254 
 255   # Read the envelope
 256   MailEnvelope = readDescriptor(1)
 257   if Debug:
 258     log(MailData)
 259     log(MailEnvelope)
 260 
 261   action,hit = filterHits()
 262 
 263   if action == 'trap':
 264     storeInTrap(hit)
 265   if action == 'block':
 266     log("Matched filter [ %s] and email was BLOCKED/Refused delivery" %(hit))
 267     sys.exit(31)
 268   if action == 'drop':
 269     log("Matched filter [ %s] and email was DROPPED" %(hit))
 270   if action == None:
 271     sendToQmailQueue()
 272   #Log
 273   log(conver(MailEnvelope))
 274   #send sms
 275   mail_sms(conver(MailEnvelope))
 276 
 277   if Debug:
 278     log("qmailqueue returned [%d]" %(result))
 279   sys.exit(0)
 280 
 281 if __name__ == "__main__":
 282   try:
 283     main(sys.argv, sys.stdout, os.environ)
 284 
 285   # Catch the sys.exit() errors
 286   except SystemExit, val:
 287     sys.exit(val)
 288   except:
 289     # return a fatal error for the unknown error
 290     if TestMode:
 291       traceback.print_exc()
 292     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:[email protected] To:[email protected] 发送到指定的服务器上。服务器接收到这个数据包后,将这个数据包的内容用短信发送出去。

  • 当然可以将邮件主题等信息一起发送到手机上,大家根据自己的情况更改吧。
  • 这个程序是根据 http://sourceforge.net/projects/qmfilt/ 这个项目更改的,但是直接用他的代码会造成不能收信,大家可以在这个项目的cvs关注他的代码修改情况。

  • 结束,谢谢!

2. 讨论

  • stephen 发表,Zoom.Quiet 整理格式