譯者/作者:chunzi 出處:中國Perl協(xié)會 FPC(Foundation of Perlchina) 原名:The Evolution of Perl Email Handling 作者:Simon Cozens 原文:http://www./pub/a/2004/06/10/email.html 發(fā)表:June 10, 2004 請保護作者的著作權,維護作者勞動的結晶。
每天我都要花費大量的時間在電子郵件相關的工作上,或者通過郵件來和其他工作伙伴聯(lián)系,或者饒有興致地分析,索引,重新組織以及挖掘郵件內容。很自然的,Perl 協(xié)助我做這些事情。 在
CPAN 上有很多現(xiàn)成的模塊可以用來處理電子郵件,我們將介紹其中幾個主要的。同時我們也將關注由我和 Richard Clamp,Simon
Wistow 以及其他伙伴所致力的 Perl 電子郵件項目(Perl Email
Project),該項目的目標是提供一系列簡單的,有效的,精準的郵件處理模塊。 郵件消息的處理我們從一些比較簡單的,用來描繪一封單獨郵件,提供對郵件頭和郵件體的訪問,甚至修改它們的信息的那些模塊開始介紹。 所有的這些模塊的曾祖父都是 Mail::Internet ,由 Graham Barr 創(chuàng)建,目前 Mark Overmeer 在維護。該模塊提供了通過數(shù)組(元素為字符串行)或者文件句柄來讀取信件內容的構造器,并通過它返回一個描述該信件的 Mail::Internet 對象。在下面的例子中,我們使用變量 $rfc2822 來表示字符串形式的郵件信息內容。 my $obj = Mail::Internet->new( [ split /\n/, $rfc2822 ] );
Mail::Internet 從信件中提取構造出一個郵件頭對象,并連帶郵件體信息。郵件頭對象的類為 Mail::Header 。你可以通過該對象獲取或者設置郵件頭的信息: my $subject = $obj->head->get("Subject"); $obj->head->replace("Subject", "New subject"); 而讀取或者編輯郵件體內容的操作,則可以使用 body 方法: my $old_body = $obj->body; $obj->body("Wasn't worth reading anyway."); 到現(xiàn)在為止我還沒有提到過任何關于 MIME 的東西。對于簡單的任務來說,Mail::Internet 確實非常方便,不過它并不完全支持對 MIME 的處理。謝天謝地,MIME::Entity 作為一個為 MIME 而考慮設計的 Mail::Internet 子類,允許你讀取 MIME 消息的每一個獨立的部分(part): my $num_parts = $obj->parts; for (0..$num_parts) { my $part = $obj->parts($_); ... } 如果 Mail::Internet 和 MIME::Entity 都不適合你,你可以試試 Mark Overmeer 自己的 Mail::Message 模塊,該模塊是令人印象深刻的 Mail::Box 模塊中的一部分。Mail::Message 是個極富特色的、功能全面的模塊,但這些優(yōu)點并不總意味著褒揚。 Mail::Message 對象通常都是在 Mail::Box 讀取一個電子郵件文件夾的時候,在內部構建的。當然它也可以通過 read 方法來讀取一封信件:
$obj = Mail::Message->read($rfc2822); 就像 Mail::Internet 一樣,郵件消息被分割為郵件頭和郵件體,而與 Mail::Internet 不同的是,郵件體也是一個對象。我們如此讀取郵件頭: $obj->head->get("Subject"); 或者,如果是 Subject 頭信息以及其他常見的郵件頭信息,可以如此讀?。? $obj->subject; 我找不到直接設置頭信息的方法,所以最終可能需要這樣做: $obj->head->delete($header); $obj->head->add($header, $_) for @data; 讀取郵件體內容作為字符串形式表達也僅有一點麻煩: $obj->decoded->string 而設置郵件體內容的操作則絕對是惡夢 -- 我們不得不構建一個 Mail::Message::Body 對象來覆蓋現(xiàn)有的。 $obj->body(Mail::Message::Body->new(data => [split /\n/, $body]));
Mail::Message 處理郵件的時候可能有點慢,也著實難用。它的體系也非常復雜,上面我們所看到的這些操作就已經(jīng)用到了 16 種類 (Mail::Address , Mail::Box::Parser , Mail::Box::Parser::Perl , Mail::Message , Mail::Message::Body , Mail::Message::Body::File , Mail::Message::Body::Lines , Mail::Message::Body::Multipart , Mail::Message::Body::Nested , Mail::Message::Construct , Mail::Message::Field , Mail::Message::Field::Fast , Mail::Message::Head , Mail::Message::Head::Complete , Mail::Message::Part , 以及 Mail::Reporter )和 4400 多行的代碼。盡管它確實擁有很多功能,我還是傻傻的覺得郵件的分析處理應該更為簡潔。所以我坐下來決定自己著手編寫盡可能簡潔的郵件處理函數(shù)庫,結果就有了 Email::Simple 模塊,它的交互界面如下所示: my $obj = Email::Simple->new($rfc2822); my $subject = $obj->header("Subject"); $obj->header_set("Subject", "A new subject"); my $old_body = $obj->body; $obj->body_set("A new body\n"); print $obj->as_string; 它做的事情并不多,但卻非常簡單和高效。如果你需要 MIME 處理,可以使用它的子類 Email::MIME , 該類增加了 parts 方法。 實際上,選擇哪一種郵件處理函數(shù)庫完全取決于你,最終用戶,不過并不總是這樣的。有許多輔助性的模塊,幫助你在更高的應用層上處理郵件信息的,可能要求你提供特定的郵件表達對象。比如最近的 Mail::ListDetector 模塊(稍后我們將解析),需要傳給它的郵件為 Mail::Internet 對象,因為該對象的操作界面(API)是已知的。而我不想用 Mail::Internet 對象,但我又需要 Mail::ListDetector 的一些功能,那我可以做些什么呢? 為了讓用戶也能夠有這樣的選擇,我寫了一個用于表達上面各個模塊操作界面的抽象層,叫做 Email::Abstract 。給出上面任何一種類型的對象,我們都可以說: my $subject = Email::Abstract->get_header($obj, "Subject"); Email::Abstract->set_header($obj, "Subject", "My new subject"); my $body = Email::Abstract->get_body($obj); Email::Abstract->set_body($message, "Hello\nTest message\n"); $rfc2822 = Email::Abstract->as_string($obj);
Email::Abstract 知道如何在這些主要的郵件表達對象上作相應的操作。它也抽象了構造郵件消息的過程,并允許你通過類方法 cast 來改變郵件消息對象的操作界面: my $obj = Email::Abstract->cast($rfc2822, "Mail::Internet"); my
$mm = Email::Abstract->cast($obj, "Mail::Message");
這樣使得模塊的作者得以使用“接口預先未知(interface-agnostic)”的方式來撰寫郵件處理函數(shù)庫。我很感謝 Michael
Stevens 立即在 Mail::ListDetector 中使用了 Email::Abstract ?,F(xiàn)在我可以將 Email::Simple 對象傳遞給 Mail::ListDetector 了,而且它工作的非常好。 Email::Abstract 也給了我們對上面所有這些模塊作基準測試(benchmarks)的機會。這里是我使用的測試代碼:
use Email::Abstract; my $message = do { local $/; ; }; my @classes = qw(Email::MIME Email::Simple MIME::Entity Mail::Internet Mail::Message); eval "require $_" or die $@ for @classes; use Benchmark; my %h; for my $class (@classes) { $h{$class} = sub { my $obj = Email::Abstract->cast($message, $class); Email::Abstract->get_header($obj, "Subject"); Email::Abstract->get_body($obj); Email::Abstract->set_header($obj, "Subject", "New Subject"); Email::Abstract->set_body($obj, "A completely new body"); Email::Abstract->as_string($obj); } } timethese(1000, \%h); __DATA__ ... 我把一封短小的郵件放到 DATA 部分中,并運行相同的操作一千次:構造一個新的消息對象,讀取郵件頭,讀取郵件體,并將消息內容作為字符串返回。 Benchmark: timing 1000 iterations of Email::MIME, Email::Simple, MIME::Entity, Mail::Internet, Mail::Message... Email::MIME: 10 wallclock secs ( 7.97 usr + 0.24 sys = 8.21 CPU) @ 121.80/s (n=1000) Email::Simple: 9 wallclock secs ( 7.49 usr + 0.05 sys = 7.54 CPU) @ 132.63/s (n=1000) MIME::Entity: 33 wallclock secs (23.76 usr + 0.35 sys = 24.11 CPU) @ 41.48/s (n=1000) Mail::Internet: 24 wallclock secs (17.34 usr + 0.30 sys = 17.64 CPU) @ 56.69/s (n=1000) Mail::Message: 20 wallclock secs (17.12 usr + 0.27 sys = 17.39 CPU) @ 57.50/s (n=1000) Perl 電子郵件項目確實是成功的:Email::MIME 和 Email::Simple 的運行速度差不多是對手的兩倍。然而,我們要強調一點,這里所做的測試都是非常低級的,如果你要做任何比這里看到的更加復雜的操作,你該考慮哪些老的 Mail:: 模塊。 郵箱的處理對于單獨信件的處理已經(jīng)談了很多了,讓我們來看看對一組郵件或者存放郵件的文件夾該如何處理。我們提到過 Mail::Box ,它絕對是處理郵件文件夾的老大,它支持本地和遠程的文件夾處理,可以編輯文件夾,以及作相應的排序操作等等。要使用它,我們首先需要 Mail::Box::Manager 模塊,它是用來構建 Mail::Box 對象的工廠對象。 use Mail::Box::Manager my $mgr = Mail::Box::Manager->new; 接下來,我們通過管理器來打開文件夾: my $folder = $mgr->open(folder => $folder_file); 而現(xiàn)在,我們可以獲取各個獨立的郵件表達對象(Mail::Message ): for ($folder->messages) { print $_->subject,"\n"; } 與此最為相近的,我喜歡用的郵箱管理器還是 Mail::Util 的 read_mbox 函數(shù)。把 Unix 中 mbox 文件路徑傳遞給它,然后返回一系列的匿名數(shù)組,每個匿名數(shù)組都表示一個郵件消息,其元素為該消息的每一行。如此一來,它非常適合 Mail::Internet->new 或者相近的: for (read_mbox($folder_file)) { my $obj = Mail::Internet->new($_); print $_->head->get("Subject"),"\n"; } 這兩種做法都非常容易,不過似乎在 Mail::Util 的簡潔性和 Mail::Box 的功能上還有些簡化的余地,于是電子郵件項目再次停滯下來,這次的焦點集中在 Email::Folder 和 Email::LocalDelivery 上面。 Email::Folder 可以處理 mbox 和 maildir 格式的郵件文件夾,以及計劃中更多其他格式,并且它有非常簡潔的操作界面: my $folder = Email::Folder->new($folder_file); for ($folder->messages) { print $_->header("Subject"),"\n"; } 默認情況,它返回一系列 Email::Simple 對象用以表達每封郵件,不過這可以通過派生一個子類來改變。例如,如果我們想要原始的 RFC2822 格式的字符串,我們可以這樣做: package Email::Folder::Raw; use base 'Email::Folder'; sub bless_message { my ($self, $rfc2822) = @_; return $rfc2822; } 可能將來我們不用再派生一個子類,然后 bless_message ,而改用 Email::Abstract->cast 來更容易的改變對郵件消息的表達方式。 處理文件夾的另一方面就是如何寫數(shù)據(jù)了?;蛘哒f如何本地投遞。Email::LocalDelivery 模塊的出現(xiàn)是為了輔助 Email::Filter 。問題比聽起來要更難些,因為它必須處理鎖定,跳開郵件體,以及由 mailbox 和 maildir 等不同格式而引發(fā)的問題。而 LocalDelivery 則通過簡單的界面把所有這些都隱藏起來: Email::LocalDelivery->deliver($rfc2822, @mailboxes);
Email::LocalDelivery 和 Email::Folder 都使用了 Email::FolderType 模塊來幫助確定是哪種類型的郵件文件夾(通過文件名來判斷)。 郵件地址的處理我們再次從抽象層面回到低級的處理,有大量的模塊可用于對郵件地址的處理。我很喜歡老的 Mail::Address 模塊。郵件地址可以分割為各種字段,諸如:實際的郵件地址,名稱短語,注釋信息。例如: Example user (Not a real user)
Mail::Address 解析這些郵件地址,并將名稱短語和注釋分離出來,以便獲取各個獨立的部分: for (Mail::Address->parse($from_line)) { print $_->name, "\t", $_->address, "\n"; } 不幸的是,和其他很多郵件模塊一樣,并不真的那么有用。 my ($addr) = Mail::Address->parse('"eBay, Inc." '); print $addr->name # Inc. eBay 得到的結果仍然難以讓人接受,雖然它比之間的版本所返回的 "Inc Ebay" 要好些。于是 Casey West 加入我們并創(chuàng)造了 Email::Address 模塊。它和 Mail::Address 使用一致的交互界面,并且運行地更加快速,差不多兩到三倍。(譯注:上面的例子中,Email::Address 返回 "eBay, Inc." ??磥碓谧髡哐劾?,Mail::Address 的作者畫蛇添足了。) 還有一件我們經(jīng)常需要做的事情就是校驗郵件地址是否合法。比如,某個用戶在站點上注冊,我們就需要對他所提供的郵件地址是否能夠接收郵件作檢查。Email::Valid 模塊是在我們這幫叛逆的人沖進來之前,就已有的 Email:: 名字空間的原住民,這個模塊就是用來做這件事情的。在它最簡約的用法中,我們可以說: if (not Email::Valid->address('test@example.com')) { die "Not a valid address" } 你也可以打開其他檢查的選項,比如確定它的域名擁有一個合法的 MX 記錄,修正常見的 AOL 和 Compuserve 的郵件地址的一些錯誤,如下: if (not Email::Valid->address(-address => 'test@example.com', -mxcheck => 1)) { die "Not a valid address" }
郵件數(shù)據(jù)轉換我們有了自己的信件,接下來會對它們做些什么呢?我發(fā)現(xiàn)大多是對郵件進行文本化分析,這里有三個模塊可以協(xié)助我們: 首先是 Text::Quoted ,它獲取郵件體的文本,實際上可以是任何其他文本,然后嘗試找出某些引用其他郵件的文本部分,然后將之分離并保存到嵌套的數(shù)據(jù)結構中。例如,如果我們有 $message = < foo > # Bar > baz quux EOF 然后運行 extract($message) 就會返回如下的數(shù)據(jù)結構: [ [ { text => 'foo', quoter => '>', raw => '> foo' }, [ { text => 'Bar', quoter => '> #', raw => '> # Bar' } ], { text => 'baz', quoter => '>', raw => '> baz' } ], { empty => 1 }, { text => 'quux', quoter => '', raw => 'quux' } ]; 當你顯示郵件消息的內容時,準備用不同的顏色來區(qū)分不同的引用文本,那么這個模塊就幫到你大忙了。類似概念的還有 Text::Original 模塊,用于搜尋以原始文件內容開頭,沒有被引用的部分。它知道如何識別各種類型的屬性行,所以有: $message = < Why are there so many different mail modules? There's more than one way to do it! Different modules have different focuses, and operate at different levels; some lower, some higher. EOF 那么 first_sentence($message) 將返回 There's more than one way to do it! 。Mariachi 郵件列表存檔程序就使用了這項技術,為一個線索中的郵件給出它的提白。 說到郵件的線索化,Mail::Thread 模塊實現(xiàn)了 Jamie Zawinski 的郵件線索化算法,該算法先是被 Mozilla 所用,繼而許多其他郵件客戶端也開始使用這種技術。當然 Mariachi 也使用了這項技術,最近它還作了更新,使用 Email::Abstract 來處理各種你扔過去的郵件表達對象: my $threader = Mail::Thread->new(@mails); $threader->thread; # 計算線索 for ($threader->rootset) { # 在一個線索內的原始郵件 dump_thread($_); }
郵件過濾經(jīng)典的 Perl 的郵件過濾工具莫不就是 Mail::Audit 了,我還在這里寫過關于如何使用 Mail::Audit 模塊的文章(http://www./pub/a/2001/07/17/mailfiltering.html),以及如何與 Mail::SpamAssassin (http://www./pub/a/2002/03/06/spam.html)模塊相結合使用。 我們已經(jīng)提到過 Mail::ListDetector 模塊好幾次了。我把它和 Mail::Audit 結合在一起使用,幫助自己做了大量的自動郵件過濾工作。Mail::Audit::List 的插件使用 ListDetector 來查找信件中的郵件列表頭信息,諸如 List-Id ,X-Mailman-Version 等等類似的東西,這些頭信息可以幫助判別該郵件是否來自于郵件列表。這意味著我有能力過濾所有來自郵件列表的信件到各自的文件夾中,就像這樣: my $list = Mail::ListDetector->new($obj); if ($list) { my $name = $list->listname; $item->accept("mail/$name.-$date"); } 然而,Mail::Audit 本身還有很長一段路要走,所以如果你新架設的系統(tǒng)的話,我們鼓勵您使用電子郵件項目的 Email::Filter 模塊作為替代,它們的大部分操作界面是一致的,盡管功能并不完全相同。為了追求簡潔和速度,它使用了新式的 Email::Simple 作為郵件表達對象模塊。 郵件信息挖掘最后,我所做的比較高級的事情就是開發(fā)一個自動分類,組織,并索引郵件到數(shù)據(jù)庫的應用框架,并嘗試從中分析并提取有價值的信息。 我的第一個完成這個預期目標的模塊是 Mail::Miner
,它由三個主要部分組成。第一個部分獲取一封郵件后,去除各種附件,并分別存儲到數(shù)據(jù)庫。第二部分縱覽這封郵件并運行一系列的識別
(Recogniser)模塊,如此搜尋郵件地址,電話號碼,一些關鍵字和短語等等,并把它們存儲到另一個獨立的數(shù)據(jù)庫表中。第三部分為命令行工具,用來
查詢數(shù)據(jù)庫中的郵件以及相關的信息。 舉個例子,如果我需要找 Tim O'Reilly 的郵政地址,我就會使用查詢工具 mm ,從他發(fā)來的信中找出該地址: % mm --from "Tim O" --address Address found in message 1835 from "Tim O'Reilly" : Tim O'Reilly @ O'Reilly %26amp; Associates, Inc. 1005 Gravenstein Highway North, Sebastopol, CA 95472 如果要獲取完整的郵件,我可以說 % mm --id 1835 如果它原本包含一個附件,那么我們可能會看到類似下面的部分: [ text/xml attachment something.xml detached - use mm --detach 208 to recover ] 我粘貼中間的那一行 mm --detach 208 到 shell 中,然后很快的,something.xml 寫到了磁盤上。 現(xiàn)在 Mail::Miner 已經(jīng)非常不錯了,不過它把三種思想緊緊地捆綁在一個包中 -- 郵件的歸檔,郵件的數(shù)據(jù)挖掘以及查詢數(shù)據(jù)庫的命令行界面 -- 這使得很難單獨開發(fā)或者擴展每塊的功能。當然,它使用了老式的 Mail:: 名字空間。 這引領我們走到這次郵件模塊旅程的最后一站,最新發(fā)布的:Email::Store 模塊。這是個基于 Class::DBI 的應用框架,用來存儲郵件到數(shù)據(jù)庫并以各種方式索引: use Email::Store 'dbi:SQLite:mail.db'; Email::Store->setup; Email::Store::Mail->store($rfc2822); 緊接著... my ($name) = Email::Store::Name->search( name => "Simon Cozens" ) @mails_from_simon = $name->addressings( role => "From" )->mails; 它可以用來構建類似 Mariachi 的郵件列表歸檔工具,或者類似 Mail::Miner 的數(shù)據(jù)挖掘。它仍然在初步的開發(fā)階段,并在增強模塊的擴展性方面使用了一些新的思想。 在我們使用 Email::Store 寫出第一個郵件歸檔和搜索工具的時候,我會再次給大家作詳細介紹的。這也是為了 perl.org 的新的 Perl 郵件列表處理接口而準備做的工作。 小結我們已經(jīng)看過了 CPAN 上的幾個主要的郵件處理模塊,當然還有更多。很明顯的,我著實偏袒那些自己寫的模塊。特定的 Perl 電子郵件項目的模塊則使用 Email::* 的名字空間。我們特別設計了這些簡潔、高效的模塊,而它們并不總是老式的 Mail::* 模塊的優(yōu)良替換方案,特別像 Mail::Box 之類。到此,我希望各位通過對本文的閱讀,了解和認識更多的郵件處理工具模塊,并在之后使用 Perl 來處理郵件時,胸中有丘壑。
|