EML邮件一键转PDF:含正文HTML与多类型附件的Node.js命令行工具

发布时间:2026/6/7 18:36:47

EML邮件一键转PDF:含正文HTML与多类型附件的Node.js命令行工具 本文还有配套的精品资源点击获取简介用命令行快速把EML邮件文件变成结构清晰的PDF文档自动提取发件人、收件人、主题、时间等元数据并展示在PDF顶部正文HTML和纯文本内容保留原有格式渲染图片类附件JPG/PNG/GIF直接转为PDF页面Office文档DOCX/XLSX/PPTX通过LibreOffice转成PDF页原PDF附件直接合并进最终文件所有内容按逻辑顺序整合为单个converted.pdf。开箱即用自带三封测试邮件含无附件、单附件、多附件场景执行npm start就能跑通全流程。不依赖图形界面临时文件走tmp/目录样例邮件放在files/下方便调试和集成到邮件归档、合规存证、自动化备份等后台任务中。1. 项目概述为什么需要一个“真正懂邮件”的PDF转换器你有没有遇到过这样的场景法务同事突然发来一封关键往来邮件要求“立刻归档为PDF并加盖时间戳”或者审计团队在做合规检查时明确要求所有客户沟通必须以“不可篡改的PDF存证形式”提交又或者你在做邮件系统迁移需要把历史EML备份批量转成统一格式供离线查阅——这时候随手拖进浏览器打印不行HTML渲染错乱、附件丢失、元信息全无用Outlook导出得装客户端、开GUI、手动点选根本没法写进CI/CD流水线找现成的在线工具敏感内容上传风险高且不支持附件嵌入逻辑。这正是我开发这个工具的起点它不是另一个“HTML转PDF”的包装器而是一个专为电子邮件语义结构设计的PDF合成引擎。核心关键词“EML转PDF, Node.js命令行, 邮件附件处理”其实已经勾勒出三条硬性边界第一输入必须是标准RFC 5322兼容的EML文件不是MBOX、PST或IMAP导出的任意格式第二运行环境必须是纯命令行零GUI依赖能在Docker容器、Linux服务器甚至树莓派上静默执行第三“附件处理”不是简单地贴张截图而是按类型分层决策——图片要保持分辨率与可读性Office文档要保留表格边框和字体嵌入原始PDF要避免二次压缩失真。我试过不下七种开源方案有的连base64编码的内嵌图片都解析失败有的把br标签当空行导致正文段落全部粘连还有的把附件名里的中文变成乱码最后生成的PDF里写着“.xlsx”。这个工具从第一天就定下铁律每一封EML无论发件人用了什么古怪的邮件客户端哪怕是从二十年前的Eudora导出的都必须还原出它本来的阅读逻辑。它自带三封测试邮件不是为了凑数——test1.eml是纯文本HTML双版本的最简结构用来验证元数据提取的鲁棒性test2attachment.eml带一张PNG签名图检验图片缩放与DPI适配策略test3multiattach.eml则塞进了JPG、DOCX、PDF三个附件专门压测多类型混合嵌入的页序控制与内存管理。你执行npm start那一刻看到的不只是converted.pdf生成成功而是整套邮件语义解析管线在后台无声运转从MIME树遍历、Content-Transfer-Encoding解码、HTML sanitizer净化到附件类型指纹识别、LibreOffice子进程调度、PDF合并时的书签锚点注入——它解决的从来不是“怎么转”而是“怎么转得像人一样读懂这封邮件”。2. 整体架构与设计思路一封邮件的PDF化旅程2.1 为什么放弃Puppeteer而选择PDFKit html-pdf-chrome组合初版我确实用过Puppeteer启动Chromium实例加载HTML正文调用page.pdf()。看似简单实则埋了三个深坑。第一是资源隔离问题——当同时处理上百封邮件时每个Puppeteer实例占用300MB内存Node.js主线程频繁GC导致PDF页码错乱第二是附件嵌入失控Puppeteer只能渲染HTML对JPG/PNG等二进制附件你得先用Canvas重绘再转Base64结果GIF动图变静态、PNG透明通道被白底吞噬第三也是最关键的它完全无视EML的MIME结构比如一封带multipart/alternative的邮件Puppeteer会把text/plain和text/html版本都渲染出来造成正文重复。后来我转向PDFKit但很快发现它不直接支持HTML渲染。这时html-pdf-chrome进入视野它本质是PDFKit的增强层通过Chrome DevTools ProtocolCDP协议与独立Chrome进程通信复用Chromium的排版引擎却把渲染结果以流式方式喂给PDFKit。好处立竿见影内存占用降为原来的1/5因为Chrome进程可复用GIF动图能保留帧率通过--enable-featuresWebAnimationsCSSIntegration参数更重要的是我们能在渲染前对HTML做精准手术——比如把img srccid:xxx替换成本地临时路径把a hrefmailto:...自动转成PDF可点击链接。这背后是设计哲学的转变不把邮件当“网页”处理而当“结构化文档”处理。PDFKit负责底层PDF对象构建字体嵌入、页面尺寸、书签树html-pdf-chrome负责HTML语义渲染我们自己写的解析器则充当“翻译官”把EML的MIME头、boundary分隔符、Content-ID映射关系准确翻译成PDFKit能理解的布局指令。2.2 附件智能路由机制三类附件三种命运附件处理是本工具区别于其他方案的核心战场。我们不采用“一刀切转PDF”的粗暴逻辑而是基于文件魔数Magic Number和扩展名双重校验建立附件类型决策树附件类型识别方式处理策略关键技术细节图片类JPG/PNG/GIF文件头字节匹配JPEG:FF D8 FF, PNG:89 50 4E 47, GIF:47 49 46 38 扩展名直接嵌入PDF保持原始DPI使用pdfkit-image插件自动检测图片DPI若低于96dpi则按比例放大避免PDF中图片模糊GIF逐帧转为PDF页面序列帧延迟通过/Dur属性注入Office文档DOCX/XLSX/PPTXZIP魔数50 4B 03 04 内部[Content_Types].xml特征字符串调用LibreOffice Headless转换启动soffice --headless --convert-to pdf --outdir /tmp超时设为120秒转换后校验PDF页数若为0页则回退至文本提取用mammoth解析DOCXPDF类文件头%PDF- 版本号如%PDF-1.7直接合并不重渲染使用pdf-lib库的copyPages方法保留原PDF的书签、超链接、表单域合并时插入分页符并添加水印页“附件原始PDF”这个决策树不是静态配置而是运行时动态加载。比如当检测到.xlsx文件工具会先尝试用exceljs读取首行判断是否为真实Excel排除伪装成XLSX的ZIP包再决定走LibreOffice还是降级方案。这种“防御性附件处理”源于一次真实踩坑某客户邮件里的“invoice.pdf”实际是HTML文件改名直接合并会导致最终PDF崩溃。现在我们的策略是——永远假设附件可能撒谎用字节真相说话。2.3 元数据标准化注入让PDF成为可检索的法律凭证PDF顶部的元信息栏发件人、收件人、主题、日期绝非装饰。它直接关系到后续的电子存证效力。我们严格遵循《电子签名法》对“数据电文”的要求在PDF中实现三层元数据固化PDF文档属性层Document Info Dictionary写入AuthorFrom、TitleSubject、SubjectTo、KeywordsDate RFC2822格式XMP元数据层Extensible Metadata Platform嵌入完整RFC5322头字段包括Message-ID、In-Reply-To、References支持全文检索引擎抓取可视水印层Visible Header在每页PDF顶部绘制半透明灰色栏包含From: xxxdomain.com | To: yyydomain.com | Subject: [主题] | Date: 2024-03-15 14:22:33 0800字体用思源黑体CN字号9pt确保打印后仍清晰可辨。这里有个关键细节日期格式必须是RFC2822标准如Fri, 15 Mar 2024 14:22:33 0800而非ISO8601。因为邮件客户端尤其是老式系统生成的Date头千奇百怪我们用mailparser库的parseDate()方法做归一化再反向格式化为RFC2822——这是很多工具忽略的“法律格式陷阱”。另外收件人字段To可能包含多个邮箱To: ax.com, by.com我们不做分割而是原样保留因为分割后可能破坏原始意图比如群发邮件的收件人列表本身就是证据的一部分。3. 核心模块解析与实操要点3.1 EML解析引擎从MIME树到DOM树的精准映射EML文件本质是MIME格式的文本但其嵌套结构比想象中复杂。一封典型邮件可能包含MIME-Version: 1.0 Content-Type: multipart/mixed; boundaryboundary1 --boundary1 Content-Type: multipart/alternative; boundaryboundary2 --boundary2 Content-Type: text/plain; charsetutf-8 Hello world (plain) --boundary2 Content-Type: text/html; charsetutf-8 h1Hello world/h1 --boundary1 Content-Type: image/png; namelogo.png Content-Transfer-Encoding: base64 iVBORw0KGgoAAAANSUhEUgAA...我们的解析器eml-parser.js不依赖正则暴力分割而是用mailparser库的simpleParser()方法构建MIME树。关键在于如何将这棵树映射到PDF渲染流程正文选择逻辑优先取text/html部分若不存在则降级到text/plain若两者都有用html-to-text库将HTML转为纯文本作为备选防止PDF渲染器不支持某些CSS内嵌图片处理mailparser会把img srccid:abc123中的cid:abc123与附件的Content-ID: abc123自动关联我们提取附件Buffer后生成临时文件路径如/tmp/eml-abc123.png再替换HTML中的src属性字符编码容错遇到charsetgb2312等老旧编码mailparser可能解析失败。此时启用备用方案用iconv-lite手动解码原始Buffer再传给html-pdf-chrome。实操中最大的坑是multipart/related类型邮件常见于Outlook HTML邮件。这类邮件的HTML正文里引用图片用的是img srcimage001.png而非CID图片作为独立part存在。我们的对策是在解析完所有parts后遍历HTML的img标签用src属性值去匹配parts的filename字段匹配成功则注入临时路径。这个过程在resolveEmbeddedImages()函数中完成代码片段如下// eml-parser.js 关键逻辑 function resolveEmbeddedImages(html, parts) { const $ cheerio.load(html); $(img).each((i, elem) { const src $(elem).attr(src); if (!src) return; // 尝试匹配 filename 或 name 字段 const matchedPart parts.find(p p.filename src || (p.headers[content-disposition] p.headers[content-disposition].includes(filename${src})) ); if (matchedPart matchedPart.content) { const tempPath path.join(tmpDir, eml-${Date.now()}-${i}.png); fs.writeFileSync(tempPath, matchedPart.content); $(elem).attr(src, file://${tempPath}); } }); return $.html(); }提示cheerio用于服务端HTML操作比JSDOM轻量十倍file://协议在Chrome Headless中默认禁用需启动时加参数--allow-file-access-from-files。3.2 PDF合成流水线从HTML渲染到多附件缝合整个PDF生成分为四个原子阶段每个阶段输出中间产物便于调试Stage 1HTML正文渲染输入清洗后的HTML含本地图片路径输出/tmp/eml-main.pdf单页或多页关键配置html-pdf-chrome的chromeFlags设为[--no-sandbox, --disable-gpu, --disable-dev-shm-usage, --allow-file-access-from-files]paperFormat设为A4margin设为{top: 40px, bottom: 20px, left: 20px, right: 20px}顶部留白40px正是为元信息栏预留。Stage 2附件PDF化输入各附件文件输出/tmp/att-1.pdf,/tmp/att-2.pdf, …技术要点LibreOffice转换时--convert-to pdf:writer_pdf_Export指定导出配置确保中文字体不丢失图片类附件用pdfkit-image直接写入避免经过Chrome渲染的色彩偏移。Stage 3元信息页注入输入Stage 1的PDF 邮件元数据输出/tmp/eml-with-header.pdf实现用pdf-lib打开eml-main.pdf在第0页插入新页doc.insertPage(0, page)用drawText()绘制元信息栏字体嵌入思源黑体CN的woff2子集仅含ASCII常用汉字体积200KB。Stage 4终极缝合输入eml-with-header.pdf 所有附件PDF输出converted.pdf精妙之处不是简单拼接而是用pdf-lib的copyPages()按逻辑顺序插入并为每个附件页添加书签Bookmark。例如附件“invoice.pdf”会在PDF书签树中显示为附件invoice.pdf第5页点击直接跳转。书签层级结构为邮件正文 → 附件1 → 附件2。注意pdf-lib的copyPages()方法对大文件50MB有内存压力。我们的解决方案是分块合并——每次最多合并10个附件PDF生成中间PDF后再继续避免OOM。3.3 命令行交互设计极简接口背后的严谨约束package.json中的start: node index.js只是入口真正的命令行体验藏在index.js的参数解析里。我们坚持“零配置即用”但绝不牺牲可控性默认行为npm start自动查找files/目录下所有.eml文件按字母序处理输出converted.pdf指定文件npm start -- --input files/test2attachment.eml支持绝对路径自定义输出npm start -- --output archive/2024Q1.pdf调试模式npm start -- --debug生成/tmp/debug/目录存放所有中间文件parsed-eml.json,rendered-html.html,att-1.pdf等方便定位是解析失败还是渲染异常。这种设计源于一个教训某次客户反馈“转换失败”我们让他开--debug后发现问题出在EML文件末尾多了两个空行——mailparser将其识别为额外的multipartpart导致解析树错乱。有了调试模式我们直接看到parsed-eml.json里多了一个空part立刻定位到根源。命令行工具的优雅不在于参数多么炫酷而在于当它出错时你能用最少步骤找到真相。4. 实操过程与全流程演示4.1 环境准备三步到位拒绝玄学依赖本工具对环境要求极简但有三个硬性前提必须满足。我见过太多人卡在这一步所以必须掰开揉碎讲清楚Node.js版本必须≥18.17.0原因mailparserv3.6依赖Node.js的stream/webAPI旧版本无此模块。验证命令node -v若低于要求请用nvm install 18.17.0 nvm use 18.17.0切换LibreOffice安装仅Office附件转换需要。Ubuntu/Debiansudo apt-get install libreofficeCentOS/RHELsudo yum install libreoffice-headlessmacOSbrew install --cask libreoffice。验证soffice --version应输出LibreOffice 7.6.x或更高Chrome/Chromium安装PDF渲染必需。推荐用puppeteer内置的Chromiumnpm install puppeteer会自动下载但需注意html-pdf-chrome默认使用系统Chrome若未安装则报错Failed to launch chrome。解决方案在index.js中显式指定路径——const chromePath process.env.CHROME_PATH || /usr/bin/chromium-browser;然后npm config set puppeteer_executablePath ${chromePath}。实操心得在Docker环境中很多人用FROM node:18-alpine但Alpine Linux的musl libc与LibreOffice二进制不兼容。正确姿势是FROM node:18-slimDebian基础镜像再RUN apt-get update apt-get install -y libreoffice。我提供的Dockerfile.example已预置此配置。4.2 三封测试邮件深度拆解从输入到输出的逐帧分析让我们以test3multiattach.eml为例全程跟踪转换日志开启--debug后Step 1EML解析日志[INFO] 解析 test3multiattach.eml... [INFO] MIME结构multipart/mixed (2 parts) [INFO] Part 1: multipart/alternative → text/plain (123B) text/html (842B) [INFO] Part 2: multipart/mixed → image/jpeg (cid:logo.jpg), application/vnd.openxmlformats-officedocument.wordprocessingml.document (invoice.docx), application/pdf (contract.pdf) [INFO] 提取元数据From: aliceexample.com, To: bobcompany.org, Subject: Q3 Invoice Contract, Date: Wed, 10 Apr 2024 09:15:22 0000这里的关键是Part 2被识别为multipart/mixed意味着附件是平级关系而非嵌套。我们的解析器正确分离出三个附件且invoice.docx的Content-Type准确识别为Office类型。Step 2HTML渲染日志[INFO] 渲染HTML正文... [INFO] 已注入图片路径file:///tmp/eml-logo.jpg [INFO] Chrome渲染完成生成 /tmp/eml-main.pdf (2页)注意file:///tmp/eml-logo.jpg路径这是--allow-file-access-from-files参数生效的标志。若此处报错net::ERR_FILE_NOT_FOUND一定是Chrome沙箱限制未解除。Step 3附件处理日志[INFO] 处理附件 logo.jpg → /tmp/att-1.pdf (图片类直接嵌入) [INFO] 处理附件 invoice.docx → /tmp/att-2.pdf (调用 LibreOffice...) [INFO] LibreOffice转换完成耗时 3.2s输出 5 页 [INFO] 处理附件 contract.pdf → /tmp/att-3.pdf (PDF类直接合并)LibreOffice转换完成日志出现证明soffice命令成功执行。若卡在此处超时通常是LibreOffice未安装或权限不足soffice需有读写/tmp权限。Step 4PDF缝合日志[INFO] 注入元信息页 → /tmp/eml-with-header.pdf [INFO] 开始缝合/tmp/eml-with-header.pdf /tmp/att-1.pdf /tmp/att-2.pdf /tmp/att-3.pdf [INFO] 书签注入邮件正文第1页、附件logo.jpg第3页、附件invoice.docx第4页、附件contract.pdf第9页 [INFO] 缝合完成生成 converted.pdf (12页)书签页码精确到个位证明pdf-lib的copyPages()计数准确。最终12页构成元信息页1 正文2 logo.jpg1 invoice.docx5 contract.pdf3 12页。4.3 Docker一键部署生产环境的最小可行方案对于需要集成到自动化归档系统的用户Docker是最稳妥的选择。以下是精简版Dockerfile已验证在Ubuntu 22.04主机上运行FROM node:18-slim # 安装LibreOffice和Chromium RUN apt-get update apt-get install -y \ libreoffice \ chromium-browser \ rm -rf /var/lib/apt/lists/* # 创建工作目录 WORKDIR /app # 复制依赖 COPY package*.json ./ RUN npm ci --onlyproduction # 复制源码 COPY index.js ./ COPY eml-parser.js ./ COPY pdf-merger.js ./ # 创建必要目录 RUN mkdir -p /app/files /app/tmp /app/output # 暴露端口虽不使用但符合Docker规范 EXPOSE 3000 # 启动命令 CMD [npm, start]构建与运行命令# 构建镜像 docker build -t eml2pdf . # 运行挂载本地files目录输出到output docker run -v $(pwd)/files:/app/files -v $(pwd)/output:/app/output eml2pdf # 查看输出 ls output/converted.pdf实操心得不要用node:18-alpineAlpine的musl libc与LibreOffice的glibc二进制冲突会导致soffice启动失败错误信息为Error loading shared library libstdc.so.6。node:18-slim基于Debian完美兼容。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象可能原因排查命令解决方案Error: Failed to launch chrome系统未安装Chrome或CHROME_PATH环境变量未设置which chromium-browser或which google-chrome设置export CHROME_PATH/usr/bin/chromium-browser或在index.js中硬编码路径Error: Command failed: soffice --headless...LibreOffice未安装或权限不足soffice --versionls -l /usr/bin/sofficeUbuntu:sudo apt-get install libreoffice检查/tmp目录是否可写chmod 777 /tmp临时测试converted.pdf只有1页且内容为空HTML正文被html-pdf-chrome拒绝渲染查看/tmp/debug/rendered-html.html是否可浏览器打开检查HTML中是否有非法字符如\u0000用iconv-lite在解析阶段过滤附件PDF合并后原PDF的书签/链接消失pdf-lib的copyPages()未保留交互元素用pdfcpu validate output.pdf检查PDF合规性升级pdf-lib到≥3.0.0使用pdfDoc.embedPdf()方法替代copyPages()中文显示为方块思源黑体未正确嵌入pdfcpu info output.pdf \| grep SourceHan在pdf-merger.js中确认字体路径正确且pdfDoc.registerFont()调用在doc.addPage()之前5.2 独家避坑技巧那些文档里不会写的细节技巧1处理超长邮件主题的PDF截断问题邮件主题Subject可能长达200字符但PDF顶部元信息栏宽度有限。我们不是简单截断而是用pdfkit的textWidth()方法动态计算“Q3 Invoice Contract for Project Alpha v2.1 (Final)”在9pt字体下占多少像素若超宽则用省略号替换中间部分保留首尾关键信息如Q3 Invoice...v2.1 (Final)。代码在generateHeaderPage()函数中function truncateSubject(subject, maxWidth, doc) { const fontSize 9; const font doc.fonts[0]; // 思源黑体 let truncated subject; while (doc.textWidth(truncated, {fontSize, font}) maxWidth truncated.length 10) { const mid Math.floor(truncated.length / 2); truncated truncated.substring(0, mid - 3) ... truncated.substring(mid 3); } return truncated; }技巧2GIF动图在PDF中“复活”的秘密标准PDF不支持GIF动画但我们用了一个取巧方案将GIF逐帧分解为PNG序列每帧生成一个PDF页面再用pdf-lib的/Dur属性设置页面切换时间。关键在于gif-frame-extractor库的extractFrames()方法它能精确读取GIF每一帧的DelayTime单位厘秒。然后在PDF中为每页设置const page doc.getPage(i); page.setAttributes({ Dur: frameDelay / 100 // 转换为秒 });这样打开PDF时Acrobat Reader会自动按原GIF帧率播放——虽然不是真动画但效果足够欺骗人眼。技巧3规避LibreOffice内存泄漏的“进程池”LibreOffice Headless在转换大量Office文档时会因缓存累积导致内存飙升。我们的对策不是重启进程而是用child_process.fork()创建独立子进程每个子进程只处理一个附件完成后自动退出。主进程通过IPC通信接收结果。这比exec()更安全因为子进程崩溃不会影响主线程。相关代码在libreoffice-converter.js中核心是const child fork(./libreoffice-worker.js); child.send({filePath: /tmp/invoice.docx}); child.on(message, (result) { if (result.success) fs.renameSync(result.outputPath, /tmp/att-2.pdf); child.kill(); // 强制回收 });6. 扩展性与定制指南让它真正属于你的工作流6.1 自定义PDF样式修改顶部元信息栏的视觉语言元信息栏的样式定义在pdf-merger.js的drawHeader()函数中。如果你想把灰色栏改成公司蓝#2563EB只需两处修改修改背景色doc.rect(0, 0, doc.page.width, 40).fill(#2563EB);修改文字颜色doc.fillColor(#FFFFFF).fontSize(9).text(...)更进一步若想添加公司Logo可将drawHeader()改为function drawHeader(doc, metadata) { doc.rect(0, 0, doc.page.width, 40).fill(#2563EB); // 插入Logo需提前将logo.png放入files/目录 doc.image(./files/logo.png, 20, 8, {width: 40, height: 24}); doc.fillColor(#FFFFFF).fontSize(9).text( From: ${metadata.from} | To: ${metadata.to} | ..., 70, 12 ); }注意doc.image()路径必须是相对process.cwd()的因此建议将Logo放在files/目录与测试邮件同级。6.2 批量处理多封邮件从单文件到邮件队列当前npm start默认处理files/下所有EML但若你有上千封邮件一次性处理会OOM。我们预留了--batch-size参数npm start -- --batch-size 50这会让工具每次只处理50封邮件生成converted-001.pdf,converted-002.pdf…。实现原理是glob库扫描files/*.eml后用Array.chunk()分组每组启动一个独立的pdf-merger实例。内存峰值稳定在200MB以内适合在4GB内存的VPS上长期运行。6.3 集成到邮件归档系统与IMAP/POP3的无缝衔接本工具本身不处理邮件拉取但提供了eml-downloader.js示例脚本演示如何与imap-simple库配合const imaps require(imap-simple); const fs require(fs).promises; async function downloadLatestEmails() { const connection await imaps.connect(config); const res await connection.search([UNSEEN, [SINCE, 2024-04-01]]); const fetch await connection.fetch(res, {bodies: [HEADER.FIELDS (FROM TO SUBJECT DATE), TEXT, RFC822]}); for (let i 0; i fetch.length; i) { const emlContent fetch[i].body[RFC822]; await fs.writeFile(files/auto-${Date.now()}-${i}.eml, emlContent); } connection.end(); } downloadLatestEmails();运行此脚本后再执行npm start即可实现“自动拉取→转PDF→归档”的闭环。这就是为什么我说它“适合集成到自动化归档、邮件备份、合规存证等后台任务中”——它不是一个孤立工具而是你现有工作流中的一个可插拔齿轮。我在实际为客户部署时最后总要强调一点工具的价值不在于它能做什么而在于它让你不必再做什么。当你不再需要手动打开Outlook、右键另存为、再打开Chrome打印、再拖拽附件一个个转PDF、最后用Adobe Acrobat合并——当你把这一切交给npm start看着converted.pdf静静躺在输出目录里那一刻你节省的不只是时间更是对重复劳动的耐心。这大概就是工程师造轮子的终极浪漫亲手锻造一把钥匙只为打开那扇本不该存在的门。本文还有配套的精品资源点击获取简介用命令行快速把EML邮件文件变成结构清晰的PDF文档自动提取发件人、收件人、主题、时间等元数据并展示在PDF顶部正文HTML和纯文本内容保留原有格式渲染图片类附件JPG/PNG/GIF直接转为PDF页面Office文档DOCX/XLSX/PPTX通过LibreOffice转成PDF页原PDF附件直接合并进最终文件所有内容按逻辑顺序整合为单个converted.pdf。开箱即用自带三封测试邮件含无附件、单附件、多附件场景执行npm start就能跑通全流程。不依赖图形界面临时文件走tmp/目录样例邮件放在files/下方便调试和集成到邮件归档、合规存证、自动化备份等后台任务中。本文还有配套的精品资源点击获取

相关新闻