通过 Web 发送邮件
上个月,我们研究了一个简单的 CGI 程序 (read-mail.pl),它允许我们从 Web 浏览器中查看电子邮件。该程序利用了当今大多数电子邮件都发送到“邮局”服务器的事实,用户的邮件阅读程序从该服务器下载邮件。邮件客户端使用 POP3 协议从邮局服务器检索邮件,这意味着我们的程序可以通过连接到服务器并检索一条或多条消息来检索用户的邮件。
read-mail.pl 对于基本用途来说已经足够好了,因为它使得从世界上任何 Web 浏览器检索邮件成为可能。但是,虽然它可以读取电子邮件,但它不提供任何邮件发送功能。诚然,许多 Web 浏览器都包含这种功能——但许多浏览器,例如 Netscape Navigator,则不包含。
那么,本月,我们将看看如何通过 Web 发送邮件。在 read-mail.pl(上个月的程序)和 send-mail.pl(本文中的程序)之间,我们将拥有一个简单的集成邮件系统,允许用户从任何 Web 浏览器执行所有基本任务。
基于 HTML 表单内容发送邮件是 CGI 程序最早的用途之一,早在 1993 年 CGI 和 HTML 表单首次出现时就已出现。正如我们将看到的,从 CGI 程序中发送电子邮件并不特别困难。但是,我们将研究与安全性相关的问题,以及我们需要做些什么才能将这些简单的程序变成成熟的 Hotmail 竞争对手。
从程序中发送电子邮件通常非常简单,特别是如果您使用的是 Perl。您打开一个管道连接到邮件发送程序,并将您要发送的消息的标头和数据发送给它。例如,这是一个简单的程序,它向我的地址 reuven@lerner.co.il 发送一条简短的“hello, world”消息
#!/usr/bin/perl -w use strict; use diagnostics; my $mailprog = '/usr/lib/sendmail'; my $recipient = 'reuven@lerner.co.il'; open (MAIL, "|$mailprog $recipient") die "Cannot open $mailprog: $! "; print MAIL "From: nobody\n"; print MAIL "To: $recipient\n"; print "\n"; print MAIL "Hello there!\n"; close MAIL;
关于这个程序,有几件事需要注意。首先,我们将 $mailprog 设置为“/usr/lib/sendmail”,这是 Linux 系统上邮件传输代理 (MTA) 的默认名称和位置。如果您的 sendmail 副本位于其他位置,或者您使用的是 sendmail 以外的程序,则需要更改 $mailprog 的值。
同样,邮件发送到单个地址,即 $recipient 中定义的地址。稍后,当我们讨论程序安全性问题时,我们将讨论收件人的问题。请记住,限制程序将发送电子邮件的收件人数量会降低您的程序被垃圾邮件发送者或其他对发送匿名邮件感兴趣的人变成邮件网关的可能性。
我们使用 Perl 的 open 函数打开与 $mailprog 的连接,这使我们能够通过将程序名称视为文件名并在程序名称前加上 | 字符来写入程序的标准输入 (STDIN)。我们打印或写入该文件句柄的任何内容都将被视为发送到程序的 STDIN。来自程序的任何输出都将被忽略。
最后,请注意我们如何在最终标头和消息正文之间插入换行符。与 HTTP 一样,SMTP(用于 Internet 上大多数邮件传输的“简单邮件传输协议”)需要在标头和任何数据之间留一个空行。这允许接收程序识别哪些行是标头,哪些行是正文。
我们这些一直在 Perl 程序中发送邮件的人,当 Milivoj Ivkovic 编写的 Mail::Sendmail 模块发布到 CPAN(Comprehensive Perl Archive Network,位于 http://www.cpan.org/)时,感到非常高兴。该模块提供了一种从 Perl 程序中发送邮件的可移植方法,但也提供了程序和底层邮件系统之间的抽象层。
了解邮件发送机制在您的计算机上的工作方式非常重要,尤其是在调试发送或接收电子邮件的问题时。但是,能够使用三四行 Perl 代码从一个独立于您的程序维护和更新的包中发送邮件,使得编写更短、更可靠的程序成为可能。我已经开始在所有发送电子邮件的程序中使用 Mail::Sendmail,我建议您也这样做,除非您有充分的理由坚持使用上面描述的旧系统。不使用 Mail::Sendmail 的一个可能原因是您的程序将安装在没有此包的系统上,并且您不能期望在该系统上安装它。然而,考虑到可以轻松地从 CPAN 下载和安装软件包,这不应该严重阻止您。
Mail::Sendmail 模块,像所有其他模块一样,必须在使用它的任何程序的顶部使用 use 语句导入
use Mail::Sendmail;
如果您没有安装该模块,或者该模块未安装在 @INC(Perl 在导入模块时搜索的路径)中命名的目录之一中,则 Perl 将因致命错误而失败。
导入 Mail::Sendmail 后,发送消息将成为一个两步过程。在第一阶段,定义一个哈希,其中键是消息的标头和内容。使用 Message(或 Body 或 Text)键指定消息正文。例如
my %mail = (To => $recipient, From => $sender, Message => "Hello, there!");
然后,您可以使用以下语句发送邮件
sendmail %mail;sendmail 函数与 use Mail::Sendmail 指令自动导入到当前命名空间中。Mail::Sendmail 也定义了许多其他函数,但除非您明确请求,否则这些函数都不会导入到默认命名空间中。
如您所见,上面的代码比我们最初所做的代码要短得多,也更容易理解。它更具可移植性且更易于维护,这是一个不错的额外好处。
现在我们已经了解了如何从程序中发送邮件,我们可以专注于如何从 CGI 程序中创建一个简单的邮件发送工具。列表 1 显示了 send-mail.pl 的初步尝试,它是上述功能周围的 CGI 包装器。
从程序顶部可以看出,send-mail.pl 在开始业务之前导入了大量模块。它使用 strict 和 diagnostics 来确保我们的变量是词法变量(即,使用 my 定义的临时变量),仅使用硬引用,并且裸字不被视为子例程调用。(裸字是 Perl 术语,指程序中用途不明确的单词。最初,任何此类单词都被禁止。现在子例程可以在没有前导 & 的情况下调用,裸字被解释为子例程。这可能会使程序员感到困惑并导致程序出现错误,因此通常最好避免使用它们。)
然后,因为这是一个 CGI 程序,我们导入了 CGI.pm,这是一个模块,它为我们提供了我们可以想象到的所有 CGI 功能,可用于接收用户输入并将输出发送到 Web 浏览器。我们还导入了 CGI::Carp,它为我们提供了 Web 服务器错误日志中改进的消息。通过从 CGI::Carp 导入 fatalsToBrowser 符号,我们还确保将致命错误消息发送到用户的浏览器以及错误日志。通常,CGI 程序中的致命错误会在用户的浏览器上产生无法理解的数字错误消息。虽然来自 fatalsToBrowser 的输出对于非程序员来说可能看起来并没有更有用或更容易理解,但它不像一组数字代码那样可怕。此外,它使程序比其他情况更容易调试。
最后,我们如前所述导入 Mail::Sendmail。
除了检索三个 HTML 表单参数(sender、recipient 和 message)并在 Mail::Sendmail::sendmail 的调用中使用它们之外,此程序几乎没有您以前没见过的东西。我们确实希望在报告邮件已发送之前确保邮件已发送,因此我们使用 die 以致命错误退出;它将在向用户的浏览器和错误日志打印错误消息后结束程序。
我们可以通过检查“sendmail”子例程的返回值来确定邮件是否已发送。如果它返回 true,我们就知道邮件已发送。如果它返回 false,则程序在发送邮件之前停止。这是一种实现此目的的简单方法
if (sendmail %mail) { # Print a message for success } else { die "Error sending mail: $Mail::Sendmail::error \n"; }
变量 $Mail::Sendmail::error(即包 Mail::Sendmail 内的变量 $error)包含邮件未发送的详细描述。由于 sendmail 子例程在成功时返回 true,在失败时返回 false,因此上面的构造告诉 Perl,“尝试发送包含在 %mail 中的邮件——如果不能,则退出并打印一条消息描述它为什么失败。”
如果邮件发送成功,用户将收到一条消息,指示程序已执行其任务。它还会打印邮件的内容。向用户提供这种详细的反馈始终比打印简单的“成功”消息要好,因为用户可能不确定正在引用哪个电子邮件。
现在我们有了一个能够发送邮件的 CGI 程序,我们需要某种方法来调用它。我们可以将参数作为名称-值对在 URL 中传递,但这很困难且不太用户友好。因此,我们将使用 POST 发送名称-值对,POST 将它们发送到程序的标准输入 (STDIN)。程序的 POST 输入通常来自 HTML 表单。这是一个调用 send-mail.pl 的示例表单
<HTML> <Head> <Title>Send e-mail!</Title> </Head> <Body> <H1>Send e-mail!</H1> <Form method="POST" action="/cgi-bin/send-mail.pl"> <P>Sender: <input type="text" name="sender"></P> <P>Recipient: <input type="text" name="recipient"></P> <P>Message:</P> <textarea cols="60" rows="20" name="message"></textarea> <P><input type="submit"></P> </Form> </Body> </HTML>
此表单有三个元素,分别命名为 sender、recipient 和 message。这些是我们在 send-mail.pl 中使用 param 方法检索的相同元素。如果您修改 HTML 表单中参数的名称,请确保也修改程序,否则表单元素将不会被拾取。
所有 HTML 表单元素都作为名称-值对发送,其中值是文本字符串。接收和解释数据的 CGI 程序不知道,而且也不必知道,输入字段是文本字段、文本区域、复选框、单选按钮还是下拉菜单。
实际上,我们甚至可以用隐藏字段(它不会出现在 Web 浏览器上,并且用户无法更改)替换文本字段,如果我们想硬编码一个值(例如收件人的值),这会很方便。只需将 recipient 行替换为
<input type="hidden" name="recipient" value="reuven@lerner.co.il">
所有邮件都将发送到我的地址。
同样,如果您想允许人们向多个地址发送邮件,但仍然对他们进行一些限制,您可以使用选择列表
<select name="recipient"> <option value="reuven@lerner.co.il">Reuven <option value="eviltwin@lerner.co.il">Reuven's evil twin <option value="ljeditor@linuxjournal.com.com">LJ editor </select>
以任何这些方式更改我们的 HTML 表单都不需要更改我们的 CGI 程序。再一次,send-mail.pl 期望接收一个名称-值对,其中名称是 recipient,值是有效的电子邮件地址。
有了上面的表单和 CGI 程序,我们应该能够向 Internet 上的任何电子邮件地址发送邮件。
上述表单的问题在于它确实允许任何人向 Internet 上的任何地址发送邮件。此外,它允许发送者冒充 Internet 上的任何地址。这正是垃圾邮件发送者喜欢利用的那种工具。如果您将原始版本的 send-mail.pl 放在您的网站上,您最终会发现有人正在使用您的服务器和带宽来发送他们的垃圾邮件。
可以使用几种可能的方法来防止这种情况。一种方法是消除向选定列表之外的用户或域发送邮件的可能性。例如,我们可以定义一个哈希,其中键是批准的电子邮件地址
my %approved_recipient = ('reuven@lerner.co.il' => 1, 'ljeditor@linuxjournal.com.com' => 1);
使用哈希允许我们在恒定的时间间隔内检查任何电子邮件地址的状态,而不管地址的数量如何。例如,如果我们使用数组,我们可能必须搜索整个数组才能确定地址的状态,这意味着执行此类测试所需的时间将与数组中元素的数量成正比增长。
因此,我们可以通过插入以下代码来检查地址是否已批准
if (!$approved_recipient{$recipient}) { die "Unapproved address \"$recipient\": Mail" . " was not sent.\n"; }
带有上述代码的 send-mail.pl 版本可以在存档文件(ftp://ftp.linuxjournal.com/pub/lj/listings/issue62/3449.tgz)中的列表 2 中找到。
类似地,我们可以通过将所有批准的域放入数组中来允许邮件发送到特定域内的任何地址
my @approved_domains = ('lerner.co.il' 'linuxjournal.com');
然后我们创建一个变量 $match_found,其默认值为 0
my $match_found = 0;只有当批准的域之一与 $recipient 中的域匹配时,$match_found 才会设置为 1。我们使用一个简短的循环来检查这一点
foreach my $domain (@approved_domains) { if ($recipient =~ m/$domain$/) { $match_found = 1; last; } }当我们找到匹配项时,我们使用 last 跳出循环,以节省一些时间。如果您知道某些域将比其他域更频繁地接收邮件,您应该将它们放在 @approved_domains 的开头,因为项目在该数组中出现得越早,就越早找到匹配项。
然后,我们仅在 $match_found 为 true(即非零)时发送邮件。如果 $match_found 为 0,我们打印一条错误消息
# If the domain was not approved else { die "Mail was not sent: The recipient's domain " . "is not approved.\n"; }
存档中列表 3 中的 send-mail.pl 版本具有这些添加内容。
如果我们希望我们的程序是健壮的,我们必须做的不仅仅是检查安全违规。我们必须检查来自用户的输入,这些输入可能不会影响安全性,但可能会导致错误或其他不愉快的意外。
例如,如果我们直接从 URL 调用 send-mail.pl,例如
http://www.lerner.co.il/cgi-bin/send-mail.pl
该程序将报告邮件已发送,但发送者、收件人和消息均为空白。这很糟糕,原因有两个。首先,没有发送邮件,因为必要的标头没有分配任何值,因此程序向我们提供了不正确的信息。其次,我们永远不应该达到接受来自用户的空白数据作为邮件输入的地步。
我们可以通过确保始终使用 POST 调用 send-mail,并且 $sender、$recipient 和 $message 为非空白来防止这种情况。如果这些中的任何一个等同于空字符串,我们将提前从程序退出,告诉用户每个都必须具有非空白值。再一次,在调试环境中,使用 die 比在生产代码中更好,仅仅是因为它产生的错误消息的样式。没有理由您不能将用户转发到错误消息页面,或者打印一个设计精美的页面来描述缺少的内容,而不是简单地 die。
在 send-mail.pl 和 read-mail.pl 之间,我们创建了一个小型系统来发送和接收电子邮件。这是否足以与 Hotmail 竞争,创建我们自己的小型基于 Web 的邮件服务?简短的答案是“否”,尽管较长的答案是,它可能足以满足大多数用途。
部分问题在于这两个程序是使用 CGI 运行的。虽然 CGI 跨平台和语言可移植,但它本质上很慢,每次调用程序时都需要 Web 服务器创建一个新进程。虽然这对于负载较轻的机器来说已经足够了,但随着点击次数的增加,它很快就会成为性能瓶颈。
每个 HTTP 服务器都开发了自己的本机接口,允许您将程序附加到服务器进程。由于 Apache 是免费软件,因此已为其开发了多个此类接口,包括 mod_perl 和 mod_php。前者允许您在 Perl 中编写类似 CGI 的程序,并将它们附加到服务器进程。这意味着您的功能成为服务器程序中的子例程,而不是必须单独调用的外部程序。在 mod_perl 下运行的程序和 CGI 程序中相同的功能之间的速度差异是惊人的,应该说服几乎任何顽固的 CGI 用户切换到 mod_perl。
希望与 Hotmail 竞争的站点可能希望使用 mod_perl 或类似的服务器特定 API,以便从其硬件中获得最大性能。
除了性能之外,另一个问题是邮件的存储位置。我们讨论过的程序 read-mail.pl 和 send-mail.pl 期望用户的邮件存储在 Internet 上其他位置的 POP 服务器上。Hotmail 和类似的服务有自己的 POP 服务器用于接收邮件,以及在他们的系统上运行的自己的 MTA(通常是 sendmail,尽管其他 MTA 显然更适合高容量邮件服务器)。
但是,Hotmail 只允许您从他们自己的 POP 服务器检索邮件,而 read-mail.pl 允许您从任何 POP 服务器检索邮件,包括通常没有 Web 界面的服务器。您是否将用户对邮件的检查限制在您自己的系统、组织内的多个服务器或任何其他位置,这取决于您自己。
最后,诸如 Hotmail 之类的服务之所以能够生存下来,是因为广告,而最流行的广告方式之一是在每条消息的底部添加一条简短的注释,指示使用了哪个基于 Web 的邮件服务来发送它。我们可以通过将我们自己的页脚连接到用户使用这些说明发送的消息来轻松做到这一点
my $footer = "-\nBrought to you by ReuvenMail\n"; my $message = $query->param("message"); $message .= $footer; my %mail = (To => $recipient, From => $sender, Message => $message);
现在每个人都会知道当您从基于 Web 的系统发送邮件时,您使用的是哪个邮件服务。此功能包含在程序的最终版本中,即存档文件中的列表 3。
最后,Hotmail 拥有数百万会员,这意味着它依赖的不仅仅是一台运行 Linux 的计算机来进行邮件传递。运行单个系统来发送和接收邮件远不如创建一个大型、可扩展的系统那么困难。如果您真的有兴趣与 Hotmail 竞争,除了 Linux、Apache 和上述程序之外,您还需要资本投资和对网络协议的良好知识。
从 HTML 表单发送邮件是 CGI 标准最古老的用途之一。已经创建了许多这样的程序,尽管它们并非总是谨慎地限制垃圾邮件或其他滥用行为。正如我们所看到的,有可能绕过大多数这些问题,但在将您的系统上线之前考虑这些问题非常重要。
通过将简单的邮件阅读程序与简单的邮件发送程序相结合,我们可以创建一个基本的基于 Web 的邮件服务,该服务可以根据我们的需要开放或封闭。也许这些程序的可扩展性不如 Hotmail,但它们确实让我们深入了解了该服务(和类似服务)的工作方式,以及为了使我们自己的程序尽可能有用,我们可能需要做些什么。
