在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的原始电子邮件源解析诸如:从,到,正文之类的东西[重复]