
mailcow 邮件解析实战php-mime-mail-parser 安装、API 详解与 Postfix 集成指南【免费下载链接】mailcow-dockerizedmailcow: dockerized - 项目地址: https://gitcode.com/GitHub_Trending/ma/mailcow-dockerized本指南以 mailcow-dockerized 仓库内随附的第三方库 php-mime-mail-parser 官方 README 为主体系统讲解这一 mailparse 扩展封装库的安装配置、四种邮件加载方式、头部/正文/附件解析 API并结合仓库源码剖析其内部实现原理与在 mailcow 隔离区quarantine功能中的真实用法。读完本文你将能够独立使用该库解析 .eml 邮件文件、接入 PHP 脚本处理 Postfix 管道投递的邮件并理解 mailcow 是如何用它实现邮件详情展示的。这个库能做什么php-mime-mail-parser 是一个经过完整测试的 PHP 7.2 邮件解析库本质上是 PHPmailparse扩展的封装器wrapper遵循 Internet Message Format 系列 RFCRFC 822 / 2822 / 5322解析 MIME 邮件。官方 README 明确列出它适用于以下场景解析并读取来自Postfix的邮件通过 content_filter 管道送入 PHP 脚本读取 .eml 格式的邮件文件例如邮件客户端导出的邮件备份构建Webmail网页邮箱将邮件信息主题、HTML 正文、附件等存入数据库为邮件归档、检索、隔离区管理等功能打基础。在 mailcow-dockerized 中该库正是被用于隔离区邮件的详情解析这一点我们会在后文结合 data/web/inc/ajax/qitem_details.php 具体展开。安装与依赖通过 Composer 安装官方推荐使用 Composer 安装最新版本composer require php-mime-mail-parser/php-mime-mail-parser仓库中的 composer.json 给出了该库的精确依赖约束可作为环境判断依据require: { php: ^7.2|^8.0, ext-mailparse: * }, autoload: { psr-4: { PhpMimeMailParser\\: src/ } }两点值得注意一是依赖约束为^7.2|^8.0即 PHP 7.2 及以上含 PHP 8.x均可使用二是必须安装ext-mailparse扩展这是唯一的强依赖库本身无法脱离 mailparse 工作。PHP 版本兼容性README 明确列出的受支持版本为 PHP 7.2、7.3、7.4同时给出了历史版本Previous Versions的兼容对照表PHP 兼容性对应版本HHVMphp-mime-mail-parser 2.11.1PHP 5.4php-mime-mail-parser 2.11.1PHP 5.5php-mime-mail-parser 2.11.1PHP 5.6php-mime-mail-parser 3.0.4PHP 7.0php-mime-mail-parser 3.0.4PHP 7.1php-mime-mail-parser 5.0.5结合仓库内 composer.json 的^7.2|^8.0约束可以推断PHP 8.x 的支持在后续版本中已补齐旧版本仅对老 PHP 环境有意义新项目应直接使用当前版本。安装 mailparse 扩展安装库前必须确认 mailparse 扩展已就绪验证命令php -m | grep mailparse若命令返回mailparse说明扩展可用。Ubuntu、Debian 及衍生发行版sudo apt install php-cli php-mailparse其他平台通过 PECL 编译sudo apt install php-cli php-pear php-dev php-mbstring pecl install mailparse注意php-mbstring也被列为前置依赖——这是因为库的字符集解码Charset 类会优先调用mb_convert_encoding()缺少 mbstring 时才会回退到 iconv详见后文源码分析。从源码编译若发行版仓库与 PECL 均无法满足需求可自行编译示例中的AAAAMMDD应替换为php-config --extension-dir的输出git clone https://github.com/php/pecl-mail-mailparse.git cd pecl-mail-mailparse phpize ./configure sed -i s/#if\s!HAVE_MBSTRING/#ifndef MBFL_MBFILTER_H/ ./mailparse.c make sudo mv modules/mailparse.so /usr/lib/php/AAAAMMDD/ echo extensionmailparse.so | sudo tee /etc/php/7.1/mods-available/mailparse.ini sudo phpenmod mailparse说明其中sed修改mailparse.c是为了适配新版 PHP 头文件中的宏定义变更/etc/php/7.1/mods-available/是原文档示例路径请按实际 PHP 版本目录调整。Windows从 mailparse 的 PECL 页面下载对应 PHP 版本的 DLL并在php.ini中添加一行extensionphp_mailparse.dll另外仓库内还提供了两个辅助文件可供参考compile_mailparse.shmailparse 编译脚本和 mailparse-stubs.phpmailparse 函数的 PHP 桩声明便于 IDE 补全与静态分析。快速上手四种方式加载邮件使用前先引入 Composer 自动加载然后实例化PhpMimeMailParser\Parser即可用以下四种方式之一加载邮件任选其一即可require_once __DIR__./vendor/autoload.php; $path path/to/email.eml; $parser new PhpMimeMailParser\Parser(); // 1. 指定文件路径字符串 $parser-setPath($path); // 2. 指定原始 MIME 邮件文本字符串 $parser-setText(file_get_contents($path)); // 3. 指定 PHP 文件资源流 $parser-setStream(fopen($path, r)); // 4. 指定与邮件服务器协同工作的流流如从标准输入读取 $parser-setStream(fopen(php://stdin, r));第四种方式专门用于 Postfix content_filter 管道投递 场景——邮件服务器把原始邮件写到 PHP 脚本的标准输入脚本再从php://stdin读取解析。从源码看这三种 setter 的实现策略有本质区别理解它们有助于选型setPath()调用mailparse_msg_parse_file()让 mailparse直接从文件增量解析见 Parser.php。注意源码中有一个细节如果文件末尾没有换行符会先补一个PHP_EOL这是为了避免某些邮件最后一行不完整导致解析错位。setStream()先把整个流缓存到临时文件tmpfile()再逐块mailparse_msg_parse()增量解析。源码 Parser.php 还会校验流必须是可读模式r、r、rb、c等$readableModes白名单且未到 EOF否则抛出Exception。setText()直接把内存中的字符串一次性交给mailparse_msg_parse()解析源码注释明确指出这是快速但可能吃内存的方式fast memory hog might explode见 Parser.php。mailcow 隔离区解析用的正是 setText()因为隔离的邮件原文本身就存在数据库字段中。解析完成后parse()内部会用mailparse_msg_get_structure()获取所有 MIME part 的编号如1、1.1、2等逐个包装成MimePart对象存入$parts为后续的头部/正文/附件读取做好准备见 Parser.php。注意除 setText 外其余方法解析的都是文件/流因此解析器在__destruct()中会自动释放文件流与 mailparse 资源无需手动清理见 Parser.php。读取邮件元数据Header发件人与收件人$rawHeaderTo $parser-getHeader(to); // 返回 test testexample.com, test2 test2example.com $arrayHeaderTo $parser-getAddresses(to); // 返回 [[displaytest, addresstestexample.com, is_groupfalse]] $rawHeaderFrom $parser-getHeader(from); // 返回 test testexample.com $arrayHeaderFrom $parser-getAddresses(from); // 返回 [[displaytest, addresstestexample.com, is_groupfalse]]getHeader($name)读取单个头部大小写不敏感返回解码后的字符串头部不存在时返回false。getAddresses($name)调用 mailparse 的mailparse_rfc822_parse_addresses()将地址解析为结构化数组每个元素包含display显示名、address邮箱地址、is_group是否地址组三个键其中display还会经过 RFC 2047 编码解码见 Parser.php。主题与其他头部$subject $parser-getHeader(subject);$stringHeaders $parser-getHeadersRaw(); // 返回所有头部组成的原始字符串不做字符集转换 $arrayHeaders $parser-getHeaders(); // 返回所有头部组成的数组并完成字符集转换需要说明的是getHeaders()返回的数组每个值都会经过单头部解码decodeSingleHeader()而getHeadersRaw()直接按原文返回字符串二者适用场景不同前者适合程序化读取后者适合原样留存如转发、签名校验。头部解码的底层实现从源码 Parser.php 可以看到decodeSingleHeader()的处理流程用正则/(\?([^?])\?(q|b)\?([^?]*)\?)((\s)\?)?/i匹配 RFC 2047 编码词形如?charset?B?base64?或?charset?Q?quoted?编码方式为bbase64时用base64_decode()解码为qquoted-printable时先还原_为空格再逐字节还原XX十六进制转义最后交给 Charset 管理器做字符集转换decodeCharset并支持相邻编码词之间空白折叠$space的处理。getRawHeader()与getHeader()的区别正在于此前者不做任何字符集转换后者会执行完整的编码词解码。读取邮件正文$text $parser-getMessageBody(text); // 返回纯文本版本 $html $parser-getMessageBody(html); // 返回 HTML 版本 $htmlEmbedded $parser-getMessageBody(htmlEmbedded); // 返回 HTML 版本并将内嵌资源如图片一并内联其实现要点见 Parser.php三个取值text、html、htmlEmbedded分别对应 MIME 类型text/plain与text/html正文取自getInlineParts()匹配到的第一个内联 part该 part 必须满足content-type 匹配、content-disposition 不是attachment、且不属于任何附件 part 的子 partpartIdIsChildOfAnAttachment()用于剔除 RFC 822 内嵌邮件中的正文内联 part 的正文会先按Content-Transfer-Encoding解码base64 / quoted-printable再按 part 的 charset 做字符集转换见 Parser.phphtmlEmbedded模式下会扫描所有附件把带有Content-ID的内联图片用cid:xxx引用替换为data:URIgetEmbeddedData()构造data:image/png;base64,...形式见 Parser.php从而让 HTML 在不依赖外部文件的情况下完整渲染。字符集处理Charset 管理器正文与头部的乱码问题由Charset类解决见 Charset.phpgetCharsetAlias()维护了一份包含数百个别名映射的字符集表如latin1→iso-8859-1、x-sjis→shift_jis、ks_c_5601-1987→euc-kr等把邮件里五花八门的字符集写法归一化为标准名未知字符集回退为us-asciidecodeCharset()的优先级为utf-8/us-ascii直接返回无需转换→ 若安装了 mbstring 且字符集受支持则用mb_convert_encoding()转换iso-2022-jp特殊处理为iso-2022-jp-ms→ 兜底使用iconv($charset, utf-8//translit//ignore)转换。该逻辑通过Contracts\CharsetManager接口见 CharsetManager.php抽象意味着你可以注入自定义字符集管理器替换默认实现——Parser构造函数接受一个CharsetManager参数见 Parser.php。附件处理批量保存附件到目录$parser-saveAttachments(/path/to/save/attachments/); // 将所有附件含内联附件保存到该目录 $parser-saveAttachments(/path/to/save/attachments/, false); // 保存所有附件排除内联附件 // 使用 ATTACHMENT_DUPLICATE_SUFFIX 策略默认 $parser-saveAttachments(/path/to/save/attachments/, false, Parser::ATTACHMENT_DUPLICATE_SUFFIX); // 重名文件自动加序号logo.jpg, logo_1.jpg, ..., logo_100.jpg之后切换为随机名 YY34UFHBJ.jpg // 使用 ATTACHMENT_RANDOM_FILENAME 策略 $parser-saveAttachments(/path/to/save/attachments/, false, Parser::ATTACHMENT_RANDOM_FILENAME); // 保存为随机文件名YY34UFHBJ.jpg、F98DBZ9FZF.jpg // 使用 ATTACHMENT_DUPLICATE_THROW 策略 $parser-saveAttachments(/path/to/save/attachments/, false, Parser::ATTACHMENT_DUPLICATE_THROW); // 遇到重名附件时直接抛出异常获取附件列表$attachments $parser-getAttachments(); // 返回所有附件对象数组含内联附件 $attachments $parser-getAttachments(false); // 返回所有附件对象数组排除内联附件遍历附件对象foreach ($attachments as $attachment) { echo Filename : .$attachment-getFilename().br /; // 返回 logo.jpg echo Filesize : .filesize($attach_dir.$attachment-getFilename()).br /; // 返回 1000 echo Filetype : .$attachment-getContentType().br /; // 返回 image/jpeg echo MIME part string : .$attachment-getMimePartStr().br /; // 返回该附件完整的 MIME part 原文 $attachment-save(/path/to/save/myattachment/, Parser::ATTACHMENT_DUPLICATE_SUFFIX); // 返回实际保存的完整路径策略与 saveAttachments 相同 }附件命名的三种策略与实现细节三种策略对应 Parser.php 中定义的三个常量常量行为Parser::ATTACHMENT_DUPLICATE_SUFFIX默认重名时追加数字后缀logo.jpg→logo_1.jpg→ …超过maxDuplicateNumber默认 100后改用uniqid()随机后缀Parser::ATTACHMENT_RANDOM_FILENAME直接使用uniqid()生成随机文件名保留原扩展名天然避免冲突Parser::ATTACHMENT_DUPLICATE_THROW目标文件已存在时抛出异常duplicate filenameAttachment::save()的完整逻辑见 Attachment.php保存前自动mkdir目标目录按策略计算目标路径重名时按策略处理最终逐块写入并返回realpath若目录不可写则抛出异常。数字后缀的递增由suffixFileName()实现循环上限即公开属性$maxDuplicateNumber可自行调整见 Attachment.php。getAttachments()判定附件的规则同样值得注意见 Parser.php附件判定优先级Content-Disposition中的disposition-filename→Content-Type参数中的content-name此时视为 attachment→ 非text/plain、text/html且非multipart/*的 part 兜底视为附件无文件名时统一命名为noname1、noname2…文件名会经过安全清洗剔除路径分隔符、换行符、首尾点号等危险字符preg_replace(((^\.)|\/|[\n|\r|\n\r]|(\.$)), _, $filename)防止路径穿越附件正文在 getAttachmentStream() 中被按Content-Transfer-Encoding解码后写入临时文件流因此大附件解析不会一次性占满内存。Attachment对象还提供getContent()一次性读取全部内容、getStream()流句柄、getContentDisposition()attachment/inline、getContentID()内联资源 ID、getHeaders()附件头部数组等访问器见 Attachment.php。深入解析器内部结构与扩展点除了 README 覆盖的常规用法仓库源码还透露了两个值得了解的架构特性1. 中间件机制MiddlewareParser内部持有一个MiddlewareStack见 MiddlewareStack.php每个 MIME part 在parse()阶段都会流经整条中间件链。你可以用addMiddleware()注册自定义处理逻辑见 Parser.php$Parser-addMiddleware(function(MimePart $part, MiddlewareStack $next) { // 对 $part 做自定义处理如记录、脱敏、改写 return $next($part); });中间件接口定义于 Contracts/Middleware.phpMimePart则是 part 数据的ArrayAccess封装见 MimePart.php可用getPart()/setPart()对深层嵌套数据做安全修改。这套机制让该库在邮件清洗、日志审计、敏感信息过滤等场景中具备扩展能力。2. 传输编码解码decodeContentTransfer()统一处理base64与quoted-printable两种传输编码其他编码原样返回见 Parser.php是正文、附件、头部解码共用的基础能力。在 mailcow 中的实际应用隔离区邮件详情mailcow-dockerized 在隔离区quarantine详情接口中真实使用了该库入口是 data/web/inc/ajax/qitem_details.php。核心流程如下从数据库取出隔离邮件原文$mailc[msg]若超过 10 MiB 则拒绝解析防止内存耗尽实例化解析器并加载原文$mail_parser-setText($mailc[msg])qitem_details.php用getAddresses(to/cc/bcc)收集收件人并经FILTER_VALIDATE_EMAIL校验后与 SMTP 信封收件人合并qitem_details.php用getHeader(from)取发件人、getHeader(subject)取主题后者再经mb_convert_encoding兜底转 UTF-8用getMessageBody(text)取纯文本正文、getMessageBody(html)取 HTML 正文并经Html2Text转为纯文本展示解析失败时回退为截取\r\n\r\n之后的原始内容用saveAttachments($tmpdir, true)将附件含内联附件落盘再用getAttachments(true)逐个读取文件名、MIME 类型、文件大小并计算 SHA256 生成 VirusTotal 检测链接qitem_details.php。这个例子完整串联了本文讲解的setText、getAddresses、getHeader、getMessageBody、saveAttachments、getAttachments六类 API是将邮件信息解析入库/上屏这一 README 核心诉求的典型落地。与 Postfix 集成把邮件管道进 PHP 脚本README 给出了将解析能力接入邮件服务器的标准做法使用 Postfix 的content_filter管道pipe机制把所有入站邮件转发给 PHP 脚本处理。以下配置写入/etc/postfix/master.cf1. 在文件末尾追加 transport 定义将邮件全部送入脚本 test.phpmyhook unix - n n - - pipe flagsF userwww-data argvphp -c /etc/php5/apache2/php.ini -f /var/www/test.php ${sender} ${size} ${recipient}2. 修改 smtp 服务定义以注册 myhook 过滤器smtp inet n - - - - smtpd -o content_filtermyhook:dummy3. PHP 脚本必须使用第四种加载方式——从标准输入读取邮件$parser-setStream(fopen(php://stdin, r));这样 Postfix 投递的每一封邮件都会以原始 MIME 形式进入脚本的标准输入脚本即可执行标题解析、附件归档、内容入库等后续业务。flagsF表示整个邮件作为一条消息整体管道传输${sender}、${size}、${recipient}是 Postfix 提供的信封信息变量。注原文档中的php5/apache2路径为早期示例实际部署请替换为环境对应的 PHP 可执行文件与配置文件路径。参与贡献与测试仓库自带 PHPUnit 测试配置 phpunit.xml.dist本地开发流程为composer install ./vendor/bin/phpunit提交 Issue 时请务必附带触发问题的原始邮件原文这能帮助维护者快速复现并修复。该库以所有已知问题均已复现、修复并测试为质量目标并借助 CIGitHub Actions、覆盖率Codecov与代码质量Codacy工具持续保障可靠性。Licensephp-mime-mail-parser/php-mime-mail-parser 采用 MIT 开源许可证 发布可自由用于商业与个人项目。作为 mailcow-dockerized 的随附依赖它位于仓库的 data/web/inc/lib/vendor/php-mime-mail-parser/ 目录下随 mailcow 一起分发使用。【免费下载链接】mailcow-dockerizedmailcow: dockerized - 项目地址: https://gitcode.com/GitHub_Trending/ma/mailcow-dockerized创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考