使用PHP解析原始电子邮件

35

我正在寻找一段好的、易于使用的PHP代码来解析原始邮件的各个部分。

我写了几个蛮力解决方案,但每次都会出现微小的更改/头文件/空格等问题导致我的整个解析器失效,项目也随之崩溃。

在我被指向PEAR/PECL之前,我需要实际的代码。我的主机配置有些奇怪或其他问题,我似乎无法正确构建.so文件。如果我确实制作了.so文件,路径/环境/php.ini中的某些差异并不总是使它可用(apache vs cron vs CLI)。

哦,还有最后一件事,我正在解析原始电子邮件文本,而不是POP3或IMAP。它通过一个.qmail电子邮件重定向管道传递到PHP脚本中。

我不希望SOF替我编写它,我只是寻找一些关于如何“正确”解决这个问题的提示/起点。这是已经被解决的一个“轮子”问题。

15个回答

24

你希望最终得到什么?正文、主题、发件人、附件?你应该花些时间阅读RFC2822,了解邮件的格式,但这里是编写良好的电子邮件的最简规则:

HEADERS\n
\n
BODY

也就是说,第一个空行(双换行符)是头部和正文之间的分隔符。头部看起来像这样:

HSTRING:HTEXT

HSTRING总是从行首开始,不包含任何空格或冒号。HTEXT可以包含各种文本,包括换行符,只要换行符后面跟着空白字符。

“BODY”实际上只是在第一个双换行符后面的任何数据。(如果您通过SMTP传输邮件,则有不同的规则,但是如果在管道上处理它,则无需担心这一点)。

因此,在非常简单的circa-1982 RFC822术语中,电子邮件看起来像这样:

HEADER: HEADER TEXT
HEADER: MORE HEADER TEXT
  INCLUDING A LINE CONTINUATION
HEADER: LAST HEADER

THIS IS ANY
ARBITRARY DATA
(FOR THE MOST PART)

大多数现代电子邮件比这更复杂。标题可以编码为字符集或RFC2047 mime单词,或者其他我现在没有想到的东西。如果你想让电子邮件正文有意义,那么现在很难自己编写代码。几乎所有由MUA生成的电子邮件都将被MIME编码。它可能是uuencode文本,也可能是html,还可能是一个uuencode Excel电子表格。
我希望这可以提供一个框架来理解一些非常基本的电子邮件内容。如果您提供有关您尝试使用数据的背景,我(或其他人)可能能够提供更好的指导。

20

太棒了,Dan!你怎么去掉=23和=40这种类型的字符? - cwd
@cwd 这是引用文本编码吧。 - Jeyanth Kumar
1
要去除=23等字符,您需要使用quoted_printable_decode函数。 - Alex W
4
可以确认Plancake是垃圾。似乎无法很好地处理从Outlook发送的电子邮件。已经五年没有更新。 - rgbflawed

6

我把这些代码拼凑在一起,但其中有些不是我的,我也不知道它们来自哪里...后来我采用了更强大的"MimeMailParser",但这个也很好用,我使用cPanel将默认电子邮件管道传输到它,效果非常好。

#!/usr/bin/php -q
<?php
// Config
$dbuser = 'emlusr';
$dbpass = 'pass';
$dbname = 'email';
$dbhost = 'localhost';
$notify= 'services@.com'; // an email address required in case of errors
function mailRead($iKlimit = "") 
    { 
        // Purpose: 
        //   Reads piped mail from STDIN 
        // 
        // Arguements: 
        //   $iKlimit (integer, optional): specifies after how many kilobytes reading of mail should stop 
        //   Defaults to 1024k if no value is specified 
        //     A value of -1 will cause reading to continue until the entire message has been read 
        // 
        // Return value: 
        //   A string containing the entire email, headers, body and all. 

        // Variable perparation         
            // Set default limit of 1024k if no limit has been specified 
            if ($iKlimit == "") { 
                $iKlimit = 1024; 
            } 

            // Error strings 
            $sErrorSTDINFail = "Error - failed to read mail from STDIN!"; 

        // Attempt to connect to STDIN 
        $fp = fopen("php://stdin", "r"); 

        // Failed to connect to STDIN? (shouldn't really happen) 
        if (!$fp) { 
            echo $sErrorSTDINFail; 
            exit(); 
        } 

        // Create empty string for storing message 
        $sEmail = ""; 

        // Read message up until limit (if any) 
        if ($iKlimit == -1) { 
            while (!feof($fp)) { 
                $sEmail .= fread($fp, 1024); 
            }                     
        } else { 
            while (!feof($fp) && $i_limit < $iKlimit) { 
                $sEmail .= fread($fp, 1024); 
                $i_limit++; 
            }         
        } 

        // Close connection to STDIN 
        fclose($fp); 

        // Return message 
        return $sEmail; 
    }  
$email = mailRead();

// handle email
$lines = explode("\n", $email);

// empty vars
$from = "";
$subject = "";
$headers = "";
$message = "";
$splittingheaders = true;
for ($i=0; $i < count($lines); $i++) {
    if ($splittingheaders) {
        // this is a header
        $headers .= $lines[$i]."\n";

        // look out for special headers
        if (preg_match("/^Subject: (.*)/", $lines[$i], $matches)) {
            $subject = $matches[1];
        }
        if (preg_match("/^From: (.*)/", $lines[$i], $matches)) {
            $from = $matches[1];
        }
        if (preg_match("/^To: (.*)/", $lines[$i], $matches)) {
            $to = $matches[1];
        }
    } else {
        // not a header, but message
        $message .= $lines[$i]."\n";
    }

    if (trim($lines[$i])=="") {
        // empty line, header section has ended
        $splittingheaders = false;
    }
}

if ($conn = @mysql_connect($dbhost,$dbuser,$dbpass)) {
  if(!@mysql_select_db($dbname,$conn))
    mail($email,'Email Logger Error',"There was an error selecting the email logger database.\n\n".mysql_error());
  $from    = mysql_real_escape_string($from);
  $to    = mysql_real_escape_string($to);
  $subject = mysql_real_escape_string($subject);
  $headers = mysql_real_escape_string($headers);
  $message = mysql_real_escape_string($message);
  $email   = mysql_real_escape_string($email);
  $result = @mysql_query("INSERT INTO email_log (`to`,`from`,`subject`,`headers`,`message`,`source`) VALUES('$to','$from','$subject','$headers','$message','$email')");
  if (mysql_affected_rows() == 0)
    mail($notify,'Email Logger Error',"There was an error inserting into the email logger database.\n\n".mysql_error());
} else {
  mail($notify,'Email Logger Error',"There was an error connecting the email logger database.\n\n".mysql_error());
}
?>

我喜欢这种方法,它在大多数情况下都运行得很好。然而,在故障排除中,我注意到它无法处理标题行的换行,例如如果收件人地址使用超过一行。 - Jason Silver

3

2

这个https://github.com/zbateson/MailMimeParser对我很有用,而且不需要mailparse扩展。

<?php
echo $message->getHeaderValue('from');          // user@example.com
echo $message
    ->getHeader('from')
    ->getPersonName();                          // Person Name
echo $message->getHeaderValue('subject');       // The email's subject

echo $message->getTextContent();                // or getHtmlContent

2


2

有一个用于将原始电子邮件消息解析为php数组的库 - http://flourishlib.com/api/fMailbox#parseMessage

静态方法parseMessage()可用于将完整的MIME电子邮件消息解析为与fetchMessage()返回相同格式的内容,减去uid键。

$parsed_message = fMailbox::parseMessage(file_get_contents('/path/to/email'));

以下是解析后的消息示例:

array(
    'received' => '28 Apr 2010 22:00:38 -0400',
    'headers'  => array(
        'received' => array(
            0 => '(qmail 25838 invoked from network); 28 Apr 2010 22:00:38 -0400',
            1 => 'from example.com (HELO ?192.168.10.2?) (example) by example.com with (DHE-RSA-AES256-SHA encrypted) SMTP; 28 Apr 2010 22:00:38 -0400'
        ),
        'message-id' => '<4BD8E815.1050209@flourishlib.com>',
        'date' => 'Wed, 28 Apr 2010 21:59:49 -0400',
        'from' => array(
            'personal' => 'Will Bond',
            'mailbox'  => 'tests',
            'host'     => 'flourishlib.com'
        ),
        'user-agent'   => 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.9) Gecko/20100317 Thunderbird/3.0.4',
        'mime-version' => '1.0',
        'to' => array(
            0 => array(
                'mailbox' => 'tests',
                'host'    => 'flourishlib.com'
            )
        ),
        'subject' => 'This message is encrypted'
    ),
    'text'      => 'This message is encrypted',
    'decrypted' => TRUE,
    'uid'       => 15
);

1
在PHP中解析电子邮件并不是一项不可能的任务。我的意思是,你不需要一个工程师团队来完成它;作为个人也可以实现。我发现最难的部分是为解析IMAP BODYSTRUCTURE结果创建FSM。在互联网上没有看到过这个,所以我写了自己的。我的例程基本上从命令输出创建嵌套数组的数组,并且数组中的深度大致对应于执行查找所需的部分编号。因此,它可以很好地处理嵌套的MIME结构。
问题在于,PHP的默认imap_*函数提供的细节不多...所以我不得不打开一个套接字到IMAP端口,并编写发送和检索必要信息(例如IMAP FETCH 1 BODY.PEEK[1.2])的函数,这涉及查看RFC文档。
数据的编码(quoted-printable、base64、7bit、8bit等)、消息长度、内容类型等都已经提供给你;对于附件、文本、HTML等,你可能还需要弄清楚你的邮件服务器的细微差别,因为并不是所有字段都总是100%实现。

宝石是FSM...如果你有计算机科学背景,制作这个可以非常有趣(关键在于括号不是常规语法); 否则,使用传统方法将会很困难和/或导致丑陋的代码。此外,你需要一些时间!

希望这可以帮到你!


1

编写自己的 MIME 解析器可能不是什么有趣的事情。你发现存在“过度开发的邮件处理包”的原因是因为 MIME 是一组非常复杂的规则/格式/编码。MIME 部件可以是递归的,这就是其中的乐趣所在。我认为最好的选择是编写最佳的 MIME 处理程序,解析一条消息,丢弃除了 text/plain 或 text/html 外的所有内容,然后强制在传入字符串中加上类似于 COMMAND:之类的前缀命令,以便您可以在混乱中找到它。如果您从这样的规则开始,您就有相当大的机会处理新提供者,但是如果出现新提供者(或者您当前的提供者选择更改其消息架构),您应该准备进行微调。


1

我不确定这是否对您有所帮助 - 希望如此 - 但它肯定会帮助其他对电子邮件更感兴趣的人。Marcus Bointon在今年三月的PHP伦敦大会上做了一次最好的演讲之一,题为“Mail()和Mail()之后的生活”,幻灯片MP3都可以在线查看。他具有深入研究电子邮件和PHP的经验和权威性。

我的看法是,如果您尝试编写一个真正通用的解析器,那么您将面临着巨大的挑战。

编辑 - PHP伦敦网站上的文件似乎已被删除;在Marcus的个人网站上找到了幻灯片:第一部分 第二部分 但是没有找到MP3文件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接