在php中解析原始电子邮件

Posted

技术标签:

【中文标题】在php中解析原始电子邮件【英文标题】:parsing raw email in php 【发布时间】:2010-09-05 23:42:57 【问题描述】:

我正在寻找使用 php 代码将原始电子邮件解析成部分的好/工作/简单的代码。

我已经编写了几个蛮力解决方案,但每次都会出现一个小的更改/标题/空格/某些东西,我的整个解析器都失败了,项目也崩溃了。

在我提到 PEAR/PECL 之前,我需要实际的代码。我的主机有一些古怪的配置或其他东西,我似乎永远无法正确构建.so。如果我确实得到了 .so,路径/环境/php.ini 中的一些差异并不总是使其可用(apache vs cron vs CLI)。

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

我不希望 SOF 为我编写它,我正在寻找一些“正确”的技巧/起点。这是我知道已经解决的“***”问题之一。

【问题讨论】:

【参考方案1】:

最后你希望得到什么?正文、主题、发件人、附件?您应该花一些时间与RFC2822 一起了解邮件的格式,但以下是格式正确的电子邮件的最简单规则:

HEADERS\n
\n
BODY

也就是说,第一个空行(双换行符)是 HEADERS 和 BODY 之间的分隔符。 HEADER 如下所示:

HSTRING:HTEXT

HSTRING 始终从行首开始,并且不包含任何空格或冒号。 HTEXT 可以包含多种文本,包括换行符,只要换行符后跟空格即可。

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

所以,在非常简单的情况下,大约在 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 编码。可能是 uuencoded 文本,也可能是 html,也可能是 uuencoded excel 电子表格。

我希望这有助于为理解一些非常基本的电子邮件桶提供一个框架。如果您提供更多关于您尝试如何处理数据的背景信息,我(或其他人)也许能够提供更好的指导。

【讨论】:

【参考方案2】:

是的,我已经能够根据 rfc 和其他一些基本教程编写一个基本的解析器。但它的多部分 mime 嵌套边界一直让我感到困惑。

我发现从我的手机发送的彩信(不是短信)只是标准电子邮件,所以我有一个系统可以读取收到的电子邮件,检查发件人(只允许来自我的手机),并使用正文部分在我的服务器上运行不同的命令。它有点像通过电子邮件进行远程控制。

因为系统是为发送图片而设计的,所以它有一堆不同编码的部分。一个 mms.smil.txt 部分,一个 text/plain(没用,只是说“这是一条 html 消息”),一个 application/smil 部分(手机会在上面拍照的部分),一个 text/html 部分有我的运营商的广告,然后是我的消息,但全部用 html 包装,最后是带有我的纯消息的文本文件附件(这是我使用的部分)(如果我将图像作为附件推到消息中,则放在附件1,base64编码,然后我的文本部分作为附件2)

我让它与我的运营商的确切邮件格式一起工作,但是当我通过它从别人的电话中发送一条消息时,它以一大堆悲惨的方式失败了。

我有其他项目我想将此电话->邮件->解析->命令系统扩展到,但我需要有一个稳定/可靠/通用的解析器来从邮件中获取不同的部分以供使用它。

我的最终目标是拥有一个函数,我可以将原始管道邮件输入其中,并返回一个包含标题 var:val 对的关联子数组的大数组,以及一个用于将正文文本作为整个字符串的数组

我搜索的越多,我就越发现相同的东西:巨大的过度开发的邮件处理包,可以在阳光下完成与邮件相关的所有事情,或者无用的(对我来说,在这个项目中)教程。

我想我得硬着头皮自己写点东西了。

【讨论】:

【参考方案3】:

编写自己的 MIME 解析器可能不会有太多乐趣。您发现“过度开发的邮件处理包”的原因是因为 MIME 是一组非常复杂的规则/格式/编码。 MIME 部分可以递归,这是乐趣的一部分。我认为你最好的选择是编写最好的 MIME 处理程序,解析消息,丢弃所有不是 text/plain 或 text/html 的内容,然后强制传入字符串中的命令以 COMMAND: 或类似内容作为前缀这样你就可以在泥土中找到它。如果您从这样的规则开始,您就有很大的机会处理新的提供商,但您应该准备好在新的提供商出现时进行调整(或者,如果您当前的提供商选择更改他们的消息传递架构)。

【讨论】:

【参考方案4】:

我不确定这是否会对您有所帮助 - 希望如此 - 但它肯定会帮助其他有兴趣了解有关电子邮件的更多信息的人。 Marcus Bointon 在今年 3 月的 PHP 伦敦会议上做了题为“Mail() 和 Mail() 之后的生活”的最佳演讲之一,slides 和 MP3 在线。他说话有一定权威性,在电子邮件和 PHP 方面进行了广泛深入的工作。

我的看法是,您在尝试编写一个真正通用的解析器时遇到了一个痛苦的世界。

编辑 - PHP London 站点上的文件似乎已被删除;在 Marcus 的 own site: Part 1 Part 2 上找到了幻灯片,但在任何地方都看不到 MP3

【讨论】:

【参考方案5】:

您可以尝试一些 Mailparse 函数:http://php.net/manual/en/book.mailparse.php,但是不在默认的 php conf 中。

【讨论】:

【参考方案6】:

在 PHP 中解析电子邮件并非不可能完成的任务。我的意思是,你不需要一个工程师团队来做这件事;作为个人是可以实现的。我发现真正最难的部分是创建 FSM 来解析 IMAP BODYSTRUCTURE 结果。我在 Internet 上的任何地方都没有看到这个,所以我自己写了。我的例程基本上从命令输出创建了一个嵌套数组的数组,数组中的深度大致对应于执行查找所需的部件号。因此它可以非常优雅地处理嵌套的 MIME 结构。

问题是 PHP 的默认 imap_* 函数没有提供太多的粒度......所以我必须打开一个到 IMAP 端口的套接字并编写函数来发送和检索必要的信息(IMAP FETCH 1 BODY.PEEK [1.2] 例如),这涉及查看 RFC 文档。

数据的编码(quoted-printable、base64、7bit、8bit等)、消息长度、content-type等都提供给您;对于附件、文本、html 等。您可能还必须弄清楚邮件服务器的细微差别,因为并非所有字段都始终 100% 实现。

宝石是 FSM...如果你有 Comp Sci 的背景,那么做这个真的很有趣(关键是括号不是常规语法;));否则,使用传统方法将是一场斗争和/或导致丑陋的代码。你也需要一些时间!

希望这会有所帮助!

【讨论】:

【参考方案7】:

我拼凑起来,有些代码不是我的,但我不知道它来自哪里......后来我采用了更强大的“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());

?>

【讨论】:

我喜欢这种方法,而且大部分情况下效果都很好。但是我在故障排除中注意到它不会处理包装标题行,例如,如果 to: 地址使用多行。【参考方案8】:

试试 Plancake PHP 电子邮件解析器: https://github.com/plancake/official-library-php-email-parser

我已将它用于我的项目。它工作得很好,它只是一个类,而且是开源的。

【讨论】:

伟大的图书馆丹!你如何摆脱 =23 和 =40 类型的字符? @cwd 我猜这是引用的文本编码。 要去掉=23等,需要使用quoted_printable_decode函数。 可以确认 Plancake 是垃圾。似乎在处理从 Outlook 发送的电子邮件方面做得不好。五年未更新。【参考方案9】:

Pear 库 Mail_mimeDecode 是用纯 PHP 编写的,您可以在此处查看:Mail_mimeDecode source

【讨论】:

链接应该是svn.php.net/viewvc/pear/packages/Mail_mimeDecode/trunk/Mail/…【参考方案10】:

这个库工作得很好:

http://www.phpclasses.org/package/3169-PHP-Decode-MIME-e-mail-messages.html

【讨论】:

-1 你需要注册那个站点才能得到它,然后文档不清楚。你能举一个至少如何使用它的例子吗? plancake 答案看起来比这个更好。【参考方案11】:

有一个用于将原始电子邮件消息解析为 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
);

【讨论】:

【参考方案12】:

我遇到了同样的问题,所以我编写了以下类:Email_Parser。它接收原始电子邮件并将其变成一个不错的对象。

它需要 PEAR Mail_mimeDecode,但这应该很容易通过 WHM 安装或直接从命令行安装。

在这里获取:https://github.com/optimumweb/php-email-reader-parser

【讨论】:

【参考方案13】:

这个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

【讨论】:

【参考方案14】:

简单的 PhpMimeParser https://github.com/breakermind/PhpMimeParser Yuo 可以从文件、字符串中剪切 mime 消息。获取文件、html 和内联图像。

$str = file_get_contents('mime-mixed-related-alternative.eml');

// MimeParser
$m = new PhpMimeParser($str);

// Emails
print_r($m->mTo);
print_r($m->mFrom);

// Message
echo $m->mSubject;
echo $m->mHtml;
echo $m->mText;

// Attachments and inline images
print_r($m->mFiles);
print_r($m->mInlineList);

【讨论】:

使用您的课程时出现错误:Warning: Invalid argument supplied for foreach() 在解析器第 315 行,变量 $AllPartsUnique 为空。可能您的代码在特定的 eml 文件上中断。【参考方案15】:

如果您尝试从 Docker 容器执行此操作,请在构建时使用 PEAR 安装 Mail 和 Mail_mimeDecode。

FROM php:7.4-apache
WORKDIR /var/www/html
EXPOSE 80
WORKDIR /var/www
RUN chown -R www-data html
RUN docker-php-ext-install mysqli
RUN pear install --alldeps mail
RUN pear install Mail_mimeDecode

然后在您的 PHP 代码中,如下所示:

<?php

require_once "/usr/local/lib/php/Mail.php";
require_once "/usr/local/lib/php/Mail/mimeDecode.php";

$mailfiles = ['/var/www/mail/mailFile1','/var/www/mail/mailFile2'];

foreach($mailfiles as $filename)
    $theFile = fopen($filename, "r") or die("Unable to open file!");
    $rawEmail = fread($theFile, filesize($filename));
    fclose($theFile);

    $args = [];
    $args['include_bodies'] = true;
    $args['decode_bodies'] = FALSE;
    $args['decode_headers'] = FALSE;
    $objMail = new Mail_mimeDecode($rawEmail);
    $return = $objMail->decode($args);

    if (PEAR::isError($return)) 
        echo("<p>" . $return->getMessage() . "</p>");
        var_dump($return);
     else 
        //echo("No error in PEAR::isError(return)");
    

    if($return->body)
        $decoded = base64_decode($return->body, true);
        var_dump($decoded);
    //end if(body)

//end foreach(mailfiles as file)


?>

【讨论】:

以上是关于在php中解析原始电子邮件的主要内容,如果未能解决你的问题,请参考以下文章

Python:鉴于原始电子邮件没有“正文”标签或任何东西,如何从原始电子邮件中解析正文

Python:如何从带有Python的原始电子邮件源解析诸如:从,到,正文之类的东西[重复]

使用 MYSQL db 中的正文通过 PHP 发送电子邮件并解析 \n

PHP 将电子邮件消息解析为变量

电子邮件中的 UTF-8 编码,解析正文

使用 7BIT 内容传输编码解析电子邮件正文 - PHP