通过网络阅读电子邮件
电子邮件是互联网的无名英雄之一。Web 使互联网变得有趣和有意思,并使我能够舒适地在我在海法的公寓里跟上大多数报纸和杂志的步伐。电子邮件使我能够与朋友、家人和客户保持联系,并以方便的格式接收电子新闻通讯。
我通常带着我可靠的 Linux 笔记本电脑旅行,这意味着在电话线的帮助下,我可以拨号连接到我的互联网提供商并下载最新的邮件。然而,在某些情况下,即使我有完整的互联网访问权限和网络浏览器,我也无法拨号检查我的邮件。我可以注册一个 Hotmail 帐户,但 Hotmail 只允许您阅读发送到其服务器的邮件,而不是发送到互联网上任何邮件服务器的邮件。
本月,我将向您展示如何开发一组 CGI 程序来从任何 POP 服务器读取电子邮件。这些程序不提供功能齐全的电子邮件客户端,但它们确实填补了一个空缺,并且在某些情况下很有用。本月描述的软件应该演示创建此类应用程序是多么相对简单,并且还将额外提供在您离开办公室时的基本功能。
传统上,UNIX 系统上的电子邮件存储在用户的计算机上。如果您在 UNIX 系统上有一个帐户,则发送给您的电子邮件将放在您计算机上的一个文件中。我在我的 Linux 系统上接收邮件,文件位于 /var/spool/mail/reuven。
然而,由于多种原因,这种系统随着时间的推移变得不足。随着用户开始拥有自己的功能齐全的 UNIX 工作站,而不是连接到中央计算机的终端,系统管理员希望将传入邮件集中在一个服务器上。
答案是 POP,“邮局协议”。用户不再从自己系统上的文件中检索邮件,而是从 POP 服务器下载邮件,每个工作组集群只有一个 POP 服务器。POP 服务器通常将传入邮件存储在传统的 UNIX 风格文件中,但允许通过网络检索和删除单个邮件。正如一些城市和城镇要求居民去中央邮局才能领取信件和包裹一样,POP 要求用户从中央服务器检索邮件。
POP 多年来经历了多次更新,最近的更新名为 POP3。随着时间的推移,添加了额外的功能,但基本命令保持不变。POP 允许用户检查他们是否有邮件、检索一条或多条消息以及删除一条或多条消息。
用户通常免受 POP3 的底层机制的影响。大多数现代电子邮件程序都支持 POP3。实际上,非 UNIX 系统上的电子邮件程序依赖于 POP3 服务器的存在,因为它们很少能够运行称为“邮件传输代理”或“MTA”的邮件服务器。Sendmail 和 qmail 是 MTA 的两个示例。
在编写 CGI 程序来读取我们的邮件之前,我们必须了解该程序如何完成这项壮举。我们可以编写我们自己的软件来与 POP3 服务器对话,但正如 Perl 中经常出现的情况一样,已经存在一个模块来为我们处理这个问题。在这种特殊情况下,该模块是 Net::POP3,它是 CPAN 上提供的网络模块“libnet”包的一部分。(有关 CPAN 及其镜像的更多信息,请访问 http://www.cpan.org/。)
Net::POP3 为 POP 提供了面向对象的接口,使得仅需基本了解协议的工作原理即可连接到 POP 服务器。使用以下命令导入模块
use Net::POP3;
然后使用以下命令创建一个新对象
my $pop = new Net::POP3($mailserver);其中 $mailserver 是一个标量,包含我们的 POP3 服务器的名称。如果连接成功,$pop 将是一个对象,其方法允许我们在邮件服务器上读取和删除邮件。如果连接不成功,$pop 将是未定义的。现在 Net::POP3 中的所有方法都以这种方式工作,如果调用不成功,则返回 undef。以下代码检查此条件
die "Error connecting to $mailserver." unless (defined $pop);为了确保电子邮件保持私密性,POP3 服务器要求用户使用用户名和密码登录。login 方法完成了这项工作,返回等待用户的消息数量
my $num_messages = $pop->login($username, $password); die "Error logging in." unless (defined $num_messages);再次注意测试以查看 $num_messages 是否已定义。如果未定义,则可能是用户名或密码中出现了错误。
POP 服务器上的每条消息都用一个索引号标识,范围从 1 到 $num_messages。索引号在单个 POP 会话期间应保持不变,但在将来的会话期间会更改。您可以使用索引号来读取或删除消息
my $message_ref = $pop->read($index);
如果消息编号 $index 存在,则消息头和正文将放入数组引用中。因此,如果 $index 指向我们 POP 服务器上的消息,则 $message_ref 是一个数组引用。数组的每个元素都包含消息中的单行文本。我们可以通过取消引用 $message_ref 来打印消息的内容
print @$message_ref, "\n";
现在我们已经了解了 Net::POP3 如何允许我们从 POP 服务器检索和读取邮件,让我们看看如何将其集成到 CGI 程序中。首先,需要一个 HTML 表单作为输入用户名和密码的方式。这是一个简单的表单
<HTML> <Head> <Title>Read your mail!</Title> </Head> <Body> <H1>Read your mail!</H1> <P>Enter your user name, password, and POP server.</P> <Form method="POST" action="/cgi-bin/print-mail.pl"> <P>POP server: <input type="text" name="mailserver"></P> <P>Username: <input type="text" name="username"></P> <P>Password: <input type="password" name="password"></P> <P><input type="submit" value="Show me my mail!"></P> </Form> </Body> </HTML>
上面的表单向我们的 CGI 程序发送三个参数——从中下载邮件的 POP 服务器的名称、用户名和密码。如果您担心密码以明文形式发送,您可能需要将表单和 CGI 程序放在运行 SSL 的服务器后面,即安全套接字层。您可能还想研究 POP3 的 APOP 登录方法,该方法在某种程度上隐藏了密码。
用于读取邮件的程序相当简单;请参阅存档文件 ftp://ftp.linuxjournal.com/pub/lj/listings/issue61/3359.tgz 中的清单 1。代码首先创建 CGI 的实例,为 CGI 协议提供面向对象的接口。然后,向用户的浏览器发送适当的 MIME 标头,指示响应将是 HTML 格式的文本。接下来,获取检索用户邮件所需的三条信息:POP 服务器的名称、用户名和密码。
检索到该信息后,我们尝试连接到 POP 服务器并登录。通常,在 CGI 程序中调用 die 不是一个好主意,因为它会在用户的屏幕上显示难以理解的消息。但是,由于我们移植了 CGI::Carp 并指定了 fatalsToBrowser,因此任何 die 调用都会将错误消息的描述发送到浏览器以及 Web 服务器的错误日志。即使您的最终生产代码要求您隐藏潜在的错误消息,这也可以成为调试的宝贵工具。
一旦知道 POP 服务器上等待的消息数量,我们就可以使用一个简单的循环来检索它们
foreach my $index (1 .. $num_messages) { print "<H2>Message $index</H2>\n"; my $message_ref = $pop->get($index); print "<pre>\n", @$message_ref, "</pre><HR>\n"; }
我们将邮件括在 <pre> 和 </pre> 标签内,因为大多数电子邮件都依赖于固定宽度的字体和格式。
您可能会惊讶于如此简单的程序可以用来读取您的邮件,但它确实可以,并且应该可以在任何具有任何 Web 浏览器的系统上工作。它可以用于快速检查是否有新邮件到达,而不会影响您使用常用电子邮件程序下载和阅读邮件的能力。
与新程序的情况一样,我们的第一个尝试是功能性的,但缺少一些有用的功能。例如,大多数用户不需要查看消息附带的所有标头。通常,他们只想看到“发件人”、“收件人”、“主题”、“抄送”和“日期”标头。
Perl 使使用正则表达式删除不需要的标头变得轻而易举。标头可以被认为是名称、值对,用冒号分隔。冒号的左侧是标头名称,它可以包含任何字母数字字符或连字符。冒号的右侧是标头的值,它可以包含几乎任何字符。
一个需要考虑的因素是标头可能会跨多行展开。也就是说,这两行
Subject: This is a subject header that continues onto a second line
都应被视为“主题”标头的一部分,因为第二行以一个或多个空格字符开头。
这个问题通过创建一个哈希 %KEEP 来解决,其中键命名要保留的标头。例如
my %KEEP = ("To" => 1, "From" => 1, "Subject" => 1, "Date" => 1);
然后,代码通过检查 $KEEP{$header_name} 的值来检查是否要保留标头,其中 $header_name 包含要检查的标头的值。
在对标头进行任何操作之前,必须将它们放入与消息正文分开的标量中。使用 split 执行此操作
my ($headers, $body) = split "\n \n", $contents, 2;
请注意,split 有三个参数,告诉 Perl 将 $contents 分割为最多两个元素。如果省略了 2,则 $body 将仅包含消息的第一段,而不是整个文本。
一旦消息标头存储在 $headers 中,就可以将其拆分回数组,然后代码可以迭代遍历数组元素。@headers 的每个元素都是单个标头行,它可能标记新标头的开始或现有标头的延续。如果这是一个新标头,并且其名称在 %KEEP 中,则该标头将写入用户的浏览器。如果标头的名称不在 %KEEP 中,则会被忽略,程序将继续下一行。
这不能解决多行标头的问题。这可以通过假设 @headers 中的每一行都将以标头(例如,Received: 或 X-Mailer:)或空格开头来处理。如果行开头的模式与标头值匹配,则程序检查 %KEEP,如果找到,则打印该行。如果模式与标头值不匹配,则假定它是空格,并且仅当上一行已打印时才打印该行。
以下是一些用于打印标头的基本代码
my @headers = split "\n", $headers; my $previous = ""; foreach my $line (@headers) { if ($line =~ m/^([\w-]+):/i) { $previous = $1; } print $line, "\n" if $KEEP{$previous}; }
此代码包含在存档文件中的清单 2 better-print-mail.pl 中。这是我们原始的简陋程序的改进版本,结合了此更改和其他更改。
在 Web 浏览器中显示电子邮件消息既有优点也有缺点。一方面,我们必须小心地将特殊字符(例如 < 和 >)转换为它们的字面等价物。同时,我们可以利用 Web 浏览器使电子邮件地址和 URL 可点击。
由于我们要确保字符出现在标头和消息正文中,因此我们在分离标头和正文之前修改了 $contents,即包含整个消息内容的变量。我们将 < 和 > 分别转换为 < 和 >,确保文字文本不会被解释为包含在 HTML 标签中
$contents =~ s/</</g; $contents =~ s/>/>/g;
使电子邮件地址可点击需要使用正则表达式来匹配电子邮件地址。我决定使用以下代码
$contents =~ s|([\w-.]+@[\w-.]+\.[a-z]{2,3})| <a href="mailto:$1">$1</a>|gi;它查找字母数字字符、连字符和句点的任意组合,后跟一个 @,后跟相同的字符组合,后跟一个两位或三位字母的顶级域名。这确保我们不会意外地将类似
three pickles @ 20 cents/pickle的内容变成电子邮件地址。通过将实际的电子邮件地址变成“mailto”链接,用户可以单击该链接以向该地址发送邮件。
使 URL 可点击有些困难,因为我们必须处理更多组合。以下代码似乎可以匹配大量 URL
s|(\w+tps?://[^\s&\"\']+[\w/])| <a href="$1">$1</a>|gi;
在这里,我们查找以“tp”结尾的任何字母,末尾带有可选的“s”。这使我们能够匹配“ftp”、“http”和“https”,所有这些都是有效的协议。然后,我们允许两个斜杠后面的任何字符组合,排除空格和几个无法在 URL 中传输的字符。
如果首先对引号和空格进行 URL 编码,则可以发送它们。当字符的 ASCII 代码的十六进制值前面有一个百分号时,字符会被 URL 编码。例如,空格字符是 ASCII 32 或 0x20;因此,它可以作为 %20 在 URL 中发送。CGI.pm 会自动解码此类字符,因此在大多数情况下您无需担心。
我们的正则表达式的最后一部分规定 URL 的最后一个字符必须是字母数字字符或斜杠。这确保了奇怪的尾随字符(例如句点和逗号)不会被意外地拖入 URL 并突出显示。
如果您想查看邮箱中的所有消息,上面的程序可以正常工作。如果您收到很多电子邮件,在一个长 Web 文档中查看所有邮件可能会令人沮丧。
程序 better-print-mail.pl 考虑了我们可能只想查看选定消息列表的事实。例如
if ($query->param("to_view")) { @message_indices = $query->param("to_view"); } else { @message_indices = (1 .. $num_messages); }
可以多次设置 HTML 表单元素,这意味着元素 "to_view" 可能包含零个、一个或多个元素。除非未设置 to_view,否则所有这些元素都放在 @message_indices 中,在这种情况下,默认情况下会显示所有消息。
我们如何获取当前消息列表?一个名为 mail-index.pl 的程序(请参阅存档文件中的清单 3)应该可以解决问题。可以从我们已经看到的相同类型的表单中调用此程序;只需修改“action”以指向 mail-index.pl,而不是 better-print-mail.pl。与 print-mail.pl 和 better-print-mail.pl 一样,mail-index.pl 必须接收用户名、密码和邮件服务器名称才能运行。有了这些信息,它会登录到 POP 服务器并显示等待读取的邮件的消息头。
每条消息都附带一个复选框。通过选中消息旁边的框,用户表示他想阅读该特定消息。当用户单击“提交”按钮时,better-print-mail.pl 不仅会收到用户名、密码和邮件服务器,还会收到选中的消息列表。正如我们所见,better-print-mail.pl 已经知道如何处理此列表,并且仅打印请求的邮件消息。
