简单的服务器端 2G 移动电话应用程序

用一个很小的服务器端脚本构建适用于全球数十亿台移动电话的应用程序

Comments

目前,科技新闻中充斥着关于最新的 iPhone、Droid 和 Palm Pre™ 应用程序的消息,但是新闻媒介更加关注的是,简单的移动电话如何为世界各地的人们提供新的通信和创业机会,特别是在电力缺乏的地区。这次主要宣传的是,在许多有网络的地方,人们使用移动电话不仅仅是为了通话,SMS 文本消息让他们可以相互交换信息,帮相他们处理细小的事务。

言归正传,您有多少朋友和家人没有一款具有彩色高分辨率触摸屏,能够浏览网站和安装各种专业软件的移动电话?他们使用的是 2G(第二代)移动电话。 当这类移动电话在 20 世纪 90 年代推出时,它们与第一代前辈产品是不同的,因为它们是数字化的,可以发送文本消息。对于比较节俭的人来说,2G 移动电话和按月付费方式还是容易承担的,对于全球大多数人来说,3G 移动电话并不在考虑范围内。去年苹果公司销售了 2500 万台 iPhone,这似乎很多,但 International Telecommunication Union 最近的一项评估表明,到 2010 年底,全球 68 亿人中有 50 亿人将使用移动电话,这说明在今后几年内,世界范围内 3G 移动电话的使用率相对较低。

2G 移动电话可以向电子邮件地址发送文本消息,编写脚本来根据电子邮件内容自动回复也不是什么难事,尤其是在您知道您的脚本将会响应不超过 160 个字符的消息时。将这些结合在一起,您将发现您可以编写对大多数 2G 电话所有者而言类似于可处理其请求的专业信息来源的应用程序。作为开发人员,如果您将这些移动电话看作将参数传递给所编写函数的小型终端,您将会发现,向简单、廉价移动电话的所有者提供信息服务非常容易。

作为例子,看一下这样一个服务,它接收包含一个 3 位数美国区号的文本消息并返回关于该区号的信息。要使用它,假设我移动电话上的 “Missed Calls” 列表显示一个区号为 “407” 的人试图呼叫我。如果我想知道该区号表示哪个地方,我使用 2G 移动电话发送一条 SMS 文本消息 “407” 到我的 Area Code Information 服务,然后会返回以下信息:Florida (Orlando, Florida, St. Cloud and central eastern Florida)。(在本文中,服务的电子邮件地址是 acinfo@snee.com,但在实际应用中(您可以亲自尝试),电子邮件地址为 “aci” 而不是 “acinfo”。)

该应用程序的基本步骤(都使用了简单的脚本)如下:

  1. 检查所有收到的电子邮件,如果来自 acinfo@snee.com,将其发送给 Python 脚本 aci.py,它将执行余下的步骤。
  2. 在一个区号信息列表中搜索收到的电子邮件正文中的文本。
  3. 如果在列表中,将返回消息设置为所存储的相关信息(在上述例子中为 Florida (Orlando, Florida, St. Cloud and central eastern Florida))。
  4. 如果不在列表中,在返回消息中说明没有发现与收到消息相关的信息。
  5. 将返回消息发送回发送原始邮件的地址,并记录下来。

我的应用程序搜索一个简单的文本文件来进行信息查询,但在您的应用程序中,只要您能够想象得到且您的脚本能够访问数据源,您能够实现很多操作。

检查收到的电子邮件,并发送给正确的处理程序:procmail

自动回复所收到消息的关键在于一个称为 procmial 的著名 UNIX® 实用程序。许多扫描垃圾邮件和根据邮件头信息在特定文件夹中排序电子邮件的最早期系统都是在 procmail 基础上建立的,并且现在仍可使用它。如果您带有主机提供程序的帐户使用基于 Linux® 的系统,且提供了 shell 访问, 那么您可以为您的帐户创建一个 procmail 配置文件,扫描所收到邮件的模式并根据发现的结果执行操作。

对于通过此 .procmailrc 配置文件路由的邮件而言,还需要另外一个或两个步骤。在过去,您可以创建 .forward 文件来路由电子邮件,但是现在,您的主机提供程序通常会提供一个 Web 表单供您填写,以告诉它们的系统在邮件到达时检查 .procmailrc 文件。

在主机提供程序中配置帐户来执行此任务时,我通过以下三行向 .procmailrc 文件增加了一条规则:

:0
* ^To: <?acinfo@snee.com>?
| /usr/home/bobd/aci/aci.py

第一行指出这是一个 procmail 规则的开始。第二行以一个星号开始,表示您指定了一个条件,这一行余下的部分是一个正则表达式,指定要在邮件中从一行的开始处开始搜索的内容:“To: acinfo@snee.com”,邮件地址两边的尖括号是可选的。(这些尖括号可有可无,这是您在处理可能来自各种电子邮件客户和电话的电子邮件时必须考虑的不一致性的第一个例子。)我所创建的这个电子邮件地址仅用于区号信息请求,因此这个规则适用于向这个地址发送的所有邮件。

.procmailrc 规则的第三行可以命名应该转发此邮件的邮箱,但这条规则所做的事更加有趣。竖杠符号指定邮件内容应作为输入发送到某个指定的程序:一个名为 aci.py 的 Python 脚本。

分析输入并选择一种脚本语言

查看 aci.py 程序之前,先看一下它必须处理的输入。一条 SMS 文本消息显示为一封带有发送者地址的电子邮件,这个地址包含电话号码和电话公司使用的域名,清单 1 展示了当我通过 Verizon 网络从 LG env2™ 电话以文本消息的形式发送区号 407 时,显示的示例 SMS 电子邮件,在清单中将发出电话号码更改为了 (434) 000-0000。

清单 1. 一条 SMS 文本消息的电子邮件版本
From 4340000000@vtext.com Wed Mar 10 00:50:01 2010
Return-Path: <4340000000@vtext.com>
Delivered-To: bobd-snee:com-acinfo@snee.com
X-Envelope-To: acinfo@snee.com
Received: (qmail 21729 invoked from network); 10 Mar 2010 00:50:00 -0000
Received: from mailwash38.pair.com (66.39.2.38)
         by oomur.pair.com with SMTP; 10 Mar 2010 00:50:00 -0000
Received: from localhost (localhost [127.0.0.1])
         by mailwash38.pair.com (Postfix) with SMTP id 021054142C
         for <acinfo@snee.com>; Tue,  9 Mar 2010 19:50:00 -0500 (EST)
X-Spam-Check-By: mailwash38.pair.com
X-Spam-Status: No, hits=2.9 required=4.0 tests=BAYES_00, FROM_STARTS_WITH_NUMS,
         MISSING_SUBJECT, TVD_SPACE_RATIO autolearn=no version=3.002005
X-Spam-Flag: NO
X-Spam-Level: **
X-Spam-Filtered: a7b240700a36d5e6c2608f9ce43a92c9
Received: from lrx5634xmtasa.alltel.net (lrx5634xmtasa.alltel.net
         [205.142.19.193])
         by mailwash38.pair.com (Postfix) with ESMTP id 3FF774142F
         for <acinfo@snee.com>; Tue,  9 Mar 2010 19:49:59 -0500 (EST)
X-Policy: RELAYLIST-$RELAYED
Received: from unknown (HELO ifs2006qwigfe) ([10.135.9.57])
         by lrx5634xmtasa.alltel.net with ESMTP; 09 Mar 2010 18:49:58 -0600
Message-ID: <26597005.1268182198841.JavaMail.root@ifs2006qwigfe>
From: 4340000000@vtext.com
To: acinfo@snee.com
Subject: 
Mime-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
Date: Tue,  9 Mar 2010 19:49:59 -0500 (EST)

407

这里有很多代码,但脚本仅需要两部分信息:发送消息的设备的电子邮箱地址(4340000000@vtext.com)和它发送的消息(407,在最后一行上)。Perl 常常是首选的简单文本处理脚本编写语言,而且编写 Perl 脚本来从 清单 1 中的电子邮件中提取电子邮件地址和信息,在区号列表中查找该消息,以及将请求的信息发送会回表示发送电话的电子邮件地址,这些都比较容易。

在我的移动电话上,这段 Perl 脚本运行良好,但是,当我在更多的移动电话上进行测试时,发现通过移动电话发送的电子邮件并不像我期望那样一致。我前面已经提到,.procmailrc 文件必须考虑到电子邮件地址在和不在尖括号中两种情况,用 Perl 很容易处理这种情况。事实证明,其余电子邮件结构也有一些可能的差异需要考虑。

清单 2 显示了一封更复杂的电子邮件,它将 “305” 作为一个包括多个部分的 MINE 消息从 iPhone 发出。(当然,iPhone 不是一部 2G 电话, 但我想用它来进行测试是个不错的主意。)不要去找 “305”,它已被编码。寻找正确的消息部分进行解码,这使我的 Perl 脚本越来越长,而且它已经能够用于其他多部电话。

清单 2. 一条 SMS 文本消息的更复杂的电子邮件表示
From 6170000000@mms.att.net Sun Feb 28 21:00:03 2010
Return-Path: <6170000000@mms.att.net>
Delivered-To: bobd-snee:com-acinfo@snee.com
X-Envelope-To: acinfo@snee.com
Received: (qmail 18219 invoked from network); 28 Feb 2010 21:00:03 -0000
Received: from mailwash38.pair.com (66.39.2.38)
         by oomur.pair.com with SMTP; 28 Feb 2010 21:00:03 -0000
Received: from localhost (localhost [127.0.0.1])
         by mailwash38.pair.com (Postfix) with SMTP id B1D8A41430
         for <acinfo@snee.com>; Sun, 28 Feb 2010 16:00:02 -0500 (EST)
X-Spam-Check-By: mailwash38.pair.com
X-Spam-Status: No, hits=3.0 required=4.0 tests=BAYES_20, FROM_STARTS_WITH_NUMS,
         TVD_SPACE_RATIO autolearn=no version=3.002005
X-Spam-Flag: NO
X-Spam-Level: ***
X-Spam-Filtered: a7b240700a36d5e6c2608f9ce43a92c9
Received: from schemailmta08.cingularme.com (schemailmta08.cingularme.com
         [209.183.37.70])
         by mailwash38.pair.com (Postfix) with ESMTP id F35394142C
         for <acinfo@snee.com>; Sun, 28 Feb 2010 16:00:00 -0500 (EST)
X-Mms-MMS-Version: 18
Date: Sun, 28 Feb 2010 15:13:10 -0600
X-Nokia-Ag-Internal: ; smiltype=false; internaldate=1267391590642
Content-Type: multipart/mixed;
         boundary="----=_Part_9705244_14454315.1267391590647"
Received: from schagw01 ([172.16.130.170]) by schemailmta08.cingularme.com
         (InterMail vM.6.01.04.00 201-2131-118-20041027) with ESMTP id
         <20100228210001.QHEZ5910.schemailmta08.cingularme.com@schagw01>
         for <acinfo@snee.com>; Sun, 28 Feb 2010 15:00:01 -0600
X-Mms-Transaction-ID: 1267390700-6
From: <6170000000@mms.att.net>
To: acinfo@snee.com
Mime-Version: 1.0
Message-ID: <33144584.1267391590647.JavaMail.wluser@schagw01>
X-Mms-Message-Type: 0
Subject: Multimedia message
X-Nokia-Ag-Version: 2.0

------=_Part_9705244_14454315.1267391590647
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
Content-Disposition: inline

MzA1
------=_Part_9705244_14454315.1267391590647--

现在我想起了选择编程语言的一条重要准则,那就是可以使用什么样的库来处理应用程序中较为单调的任务,而解析电子邮件头、寻找正确的邮件部分和根据需要解码邮件无疑属于单调的任务。 在 CPAN 上,我找到了一个 Perl 模块来解析各种电子邮件头,这样,无论各种邮件头格式之间有多大差异,我都可以通过函数调用获取所需的信息。然而,这个库依赖于其他 Perl 库,并且我的主机提供程序有其中一个库的过期版本,因此我在这个方面分析了 Python 提供的功能。我找到了一个 Python email 包,编写了一些简单测试,然后决定使用 Python 重新编写我的程序(参见 参考资料,获取电子邮件包的链接)。在网上快速搜索一下,就可以找到一些可用于 Ruby、Java™、PHP 和其他编程语言的类似的库,因此,如果您想编写一个电子邮件自动回复脚本,不再局限于只使用 Perl 或 Python 语言。

自动回复脚本

清单 3 给出了 aci.py 脚本。请注意开始处的 import 语句如何拉入 email.Parser 库以及其他几个流行的 Python 库。底部的 __main__ 部分保存程序的基本逻辑:解析收到的消息,从中拉取发件人地址和消息正文(存储在变量 areaCode 中),使用在脚本中定义的 areaCodeInfo 函数搜索有关特定区号的信息,将此信息作为回复发送,然后记录该消息。

清单 3. aci.py 脚本
#!/usr/local/bin/python

# aci.pl: (area code information) read e-mail message to find area
# code, then send information about that area code.
# Bob DuCharme 2010-01 no warranty expressed or implied

import os
import email.Parser
import sys
import datetime
import re

def multipartBody(msg):
# following code from 
# http://docs.python.org/library/email-examples.html

  partCounter=1

  for part in msg.walk():
    if part.get_content_maintype()=="multipart":
      continue
    name=part.get_param("name")
    if name==None:
      name="part-%i" % partCounter
    partCounter+=1
    msgText = part.get_payload(decode=1)

  msgSender = msgSenderText
  return msgText.strip()      # strip whitespace


def areaCodeInfo(areaCode):

  # Look for data about that area code in areacodes.txt.
  # First initialize values that should get overridden.

  response = "No information available for area code " + areaCode + "."
  foundAreaCode = False
  line = "dummy"
  acFile = open(aciPath + "areacodes.txt")
  while ((not foundAreaCode) and line):
    line = acFile.readline()
    if (line[0:5] == areaCode + ": "):   # e.g. "212: " 
      response = line
      foundAreaCode = True

  return response


def sendReply(msgSender,response):

  f = os.popen("%s -t" % SENDMAIL, "w")
  f.write("To: " + msgSender + "\n")
  f.write("From: area code information <acinfo@snee.com>\n")
  f.write("Return-Path: area code information <acinfo@snee.com>\n")
  f.write("Content-type: text/plain\n\n")
  f.write(response)
  sts = f.close()

def  logIt(msgSenderText,areaCode):

  timestamp = datetime.datetime.today().isoformat()[0:19] 
  log = open(aciPath + "log.txt",'a')
  log.write(timestamp + " " + msgSenderText + " " + areaCode + "\n") 
  log.close()


if __name__ == "__main__":

  SENDMAIL = "/usr/sbin/sendmail" # sendmail location
  aciPath = "/usr/home/bobd/aci/"
  keepFullLog = False # For debugging. More detailed than log.txt.
  response = ""

  # Parse the standard input to find the message and sender value
  mailFile=sys.stdin
  p=email.Parser.Parser()
  msg=p.parse(mailFile)
  mailFile.close()

  msgSenderText = msg['From']  # text showing msg sender's name
  msgSender = msgSenderText    # save msgSenderText for log

  # If msgSender has the form "Some Guy <someguy@example.com>" 
  # then we just want someguy@example.com
  emailAddrRegEx = re.compile(r"\<(?P<emailAddr>.+)\>")
  result = emailAddrRegEx.search(msgSender)
  if result != None:
      msgSender = result.group('emailAddr') 

  if keepFullLog:
    output = open(aciPath + "fulllog.txt",'a')
    output.write(str(msg) + "\n-- end of mail msg --\n\n")
    output.close()

  if msg.has_key("X-Mailer") and \
        msg["X-Mailer"][0:24] == "Microsoft Office Outlook":
    response = "Microsoft Outlook format is not supported."
    areaCode = ""
  else:
    areaCode = multipartBody(msg)
    response = areaCodeInfo(areaCode)

  sendReply(msgSender,response)
  logIt(msgSenderText,areaCode)

几点注意事项:

  • logIt 函数向文本文件 log.txt 中存储一行,这行包含记录的时间、请求信息的移动设备的电子邮件地址、以及这个移动设备希望知道的区号。一个示例行如下:2010-03-19T19:50:02 4340000000@vtext.com 407。如果布尔变量 keepFullLog 设置为 True,程序将收到的整条消息保存在另一个日志文件中。整条消息十分冗长,但对于调试非常有用。
  • Microsoft® Outlook® 发送的电子邮件格式是 email.Parser 库所无法理解的,但是没有 2G 电话会发送这样的电子邮件。当脚本检测到一个 Outlook 邮件时,将会发送适当的回复。
  • 脚本使用另一个著名的 UNIX 实用程序 sendmail 来发送该电子邮件。这并不是某个包含需要调用的函数的特殊库,它的操作与 UNIX 组装应用程序组件的原理更加一致,那就是将来自一个组件的信息传输到另一个组件。Python 脚本就像打开文本文件一样打开 sendmail,向其中写入适当的信息,然后 “关闭” 它。
  • areaCodeInfo 函数在文件 areacodes.txt 中搜索信息。清单 4 给出了这个文件的一部分,我已在一个 Wikipedia 页面上给出了该文件。

清单 4. areacodes.txt 的一部分
210: Texas (San Antonio area)
211: Community Services Hotline (e.g., crisis line, United Way, etc.)
212: New York (Manhattan except for Marble Hill)
213: California (central Los Angeles)
214: Texas (Dallas area)

在文本文件中搜索一个字符串并返回包含该字符串的行,这非常简单。当您开发自己的应用程序来向 2G 移动电话回复消息时,您可以获得更多的创意:您的程序可以对本地和远程存储的任意组合执行数据库查询,交叉引用它发现的信息,执行各种逻辑来向发送查询的电话返回有用的信息,只要它不超过 SMS 消息的 160 字符限制。

可以将移动电话看作运行您应用程序的命令行界面的客户端,记住,您的应用程序可以是一段简单的脚本,可以让其他库来执行困难和复杂的工作。您将看到适用于全球数十亿部电话的服务器端应用程序非常容易。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Open source, Web development
ArticleID=498219
ArticleTitle=简单的服务器端 2G 移动电话应用程序
publish-date=06282010