下载
https://tntim96.github.io/FakeSMTP/download.html

运行
运行需要 java11
https://github.com/Nilhcem/FakeSMTP/blob/master/README.md
运行命令:
bash
java -jar ${FAKESMTP_JAR} -p 25 -o ./emails -s -b
- -p:运行端口
- -o:接收到的邮件的保存位置
- -s:启动服务
- -b:后台运行服务
在 docker 里运行:以官方的镜像maven:3.6.3-openjdk-11为例
bash
#!/bin/bash
# 脚本名称:run_fakesmtp.sh
# 功能:基于maven:3.6.3-openjdk11镜像启动FakeSMTP容器
# 适用系统:Ubuntu/Debian
# ===================== 配置项(可根据需要修改) =====================
HOST_PORT=25
CONTAINER_NAME="fakesmtp-server"
FAKESMTP_JAR="fakeSMTP-2.1.1.jar"
LOCAL_EMAIL_DIR="$(pwd)/emails"
# ==================================================================
# 1. 检查Docker是否运行
if ! docker info >/dev/null 2>&1; then
echo "错误:Docker未运行,请先启动Docker服务!"
echo "Ubuntu启动Docker命令:sudo systemctl start docker"
exit 1
fi
# 2. 检查FakeSMTP jar包是否存在
if [ ! -f "${FAKESMTP_JAR}" ]; then
echo "错误:未找到${FAKESMTP_JAR}文件!"
echo "请下载:https://github.com/Nilhcem/FakeSMTP/releases/download/2.1.1/fakeSMTP-2.1.1.jar"
exit 1
fi
# 3. 创建本地邮件目录并放开权限
mkdir -p "${LOCAL_EMAIL_DIR}"
chmod 777 "${LOCAL_EMAIL_DIR}"
# 4. 清理旧容器
if docker ps -a --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
echo "停止并删除已有容器 ${CONTAINER_NAME}..."
docker stop ${CONTAINER_NAME} >/dev/null 2>&1
docker rm ${CONTAINER_NAME} >/dev/null 2>&1
fi
# 5. 启动容器(核心:--user root 解决权限)
echo "启动FakeSMTP服务器(端口:${HOST_PORT})..."
docker run -d \
--name ${CONTAINER_NAME} \
--user root \
-p ${HOST_PORT}:25 \
--restart unless-stopped \
-v "$(pwd)/${FAKESMTP_JAR}:/fakeSMTP/${FAKESMTP_JAR}" \
-v "${LOCAL_EMAIL_DIR}:/fakeSMTP/emails" \
-w /fakeSMTP \
maven:3.6.3-openjdk-11 \
java -jar ${FAKESMTP_JAR} -p 25 -o /fakeSMTP/emails -s -b
# 6. 检查启动状态
sleep 3
if docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
echo "✅ FakeSMTP启动成功!"
echo "📌 容器名称:${CONTAINER_NAME}"
echo "📌 监听端口:${HOST_PORT}"
echo "📌 查看收到的邮件(本地):ls ${LOCAL_EMAIL_DIR}"
echo "📌 查看容器日志:docker logs ${CONTAINER_NAME}"
echo "📌 停止容器:docker stop ${CONTAINER_NAME}"
else
echo "❌ FakeSMTP启动失败!"
echo "查看日志:docker logs ${CONTAINER_NAME}"
exit 1
fi
查看邮件
fakeSMTP 是只收不发的服务器,所以你只能在它这里./emails里看邮件:

邮件内容是 Base64 编码的:

写了一个 python 脚本来解码:
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
解析指定目录下所有.eml邮件文件,按时间排序后保存到Markdown文件
报告默认保存到eml目录的上一级,开头包含带接收时间的可跳转目录,正文直接显示
使用方式:
python3 parse_emails_to_md.py <eml文件目录> [输出md文件路径(可选)]
示例:
python3 parse_emails_to_md.py ./emails
python3 parse_emails_to_md.py ./emails ./custom_report.md
"""
import base64
import email
import email.header
import sys
import re
from pathlib import Path
from datetime import datetime
def decode_email_header(header_value):
"""Decode email header (subject, from, to etc.)"""
if not header_value:
return ""
try:
decoded_parts = email.header.decode_header(header_value)
result = []
for part, encoding in decoded_parts:
if isinstance(part, bytes):
result.append(part.decode(encoding or 'utf-8'))
else:
result.append(part)
return ''.join(result)
except Exception as e:
return f"Decode failed: {e}"
def sanitize_anchor(text):
"""Sanitize text for markdown anchor link (remove special chars, replace space with -)"""
# Remove special characters except letters, numbers, space
text = re.sub(r'[^\w\s-]', '', text)
# Replace spaces with hyphens
text = text.replace(' ', '-')
# Convert to lowercase
text = text.lower()
return text
def parse_eml_file(eml_path):
"""Parse single .eml file and return result dict"""
eml_path = Path(eml_path)
parse_result = {
"file_path": eml_path.absolute(),
"file_name": eml_path.name,
"date": None,
"date_str_formatted": "", # 新增:格式化的接收时间字符串
"from": "",
"to": "",
"subject": "",
"body": "",
"error": ""
}
# Check if file exists
if not eml_path.exists():
parse_result["error"] = f"File does not exist"
return parse_result
# Check if it's eml file
if eml_path.suffix.lower() != '.eml':
parse_result["error"] = f"Not an .eml file"
return parse_result
try:
# Read eml file
with open(eml_path, 'rb') as f:
msg = email.message_from_bytes(f.read())
# Extract basic email info
parse_result["from"] = decode_email_header(msg.get('From', 'Unknown'))
parse_result["to"] = decode_email_header(msg.get('To', 'Unknown'))
parse_result["subject"] = decode_email_header(msg.get('Subject', 'Unknown'))
# Parse send time (for sorting and display)
date_str = msg.get('Date', '')
if date_str:
try:
# Parse email time string to datetime object (support multiple formats)
date_obj = email.utils.parsedate_to_datetime(date_str)
parse_result["date"] = date_obj
# 格式化接收时间为易读格式
parse_result["date_str_formatted"] = date_obj.strftime('%Y-%m-%d %H:%M:%S')
except:
# Use file modify time if parse failed
parse_result["date"] = datetime.fromtimestamp(eml_path.stat().st_mtime)
parse_result["date_str_formatted"] = parse_result["date"].strftime('%Y-%m-%d %H:%M:%S')
else:
# Use file modify time if no email time
parse_result["date"] = datetime.fromtimestamp(eml_path.stat().st_mtime)
parse_result["date_str_formatted"] = parse_result["date"].strftime('%Y-%m-%d %H:%M:%S')
# Parse email body (handle Base64 encoding)
body = ""
if msg.is_multipart():
# Handle multipart email
for part in msg.walk():
content_type = part.get_content_type()
content_encoding = part.get('Content-Transfer-Encoding', '').lower()
# Only handle HTML or plain text content
if content_type in ['text/html', 'text/plain']:
payload = part.get_payload(decode=False)
# Decode Base64 if needed
if content_encoding == 'base64':
try:
body = base64.b64decode(payload).decode('utf-8')
except Exception as e:
body = f"Base64 decode failed: {e}"
else:
body = payload
break
else:
# Handle single part email
content_encoding = msg.get('Content-Transfer-Encoding', '').lower()
payload = msg.get_payload(decode=False)
if content_encoding == 'base64':
try:
body = base64.b64decode(payload).decode('utf-8')
except Exception as e:
body = f"Base64 decode failed: {e}"
else:
body = payload
parse_result["body"] = body
except Exception as e:
parse_result["error"] = f"Parse failed: {str(e)}"
# 解析失败时也设置默认时间格式
parse_result["date_str_formatted"] = "Unknown time"
return parse_result
def generate_md_content(parse_results, target_dir):
"""Generate Markdown content with table of contents (include receive time)"""
md_lines = []
# MD file header
md_lines.append(f"# Email Parse Result")
md_lines.append(f"Parse Directory: {target_dir.absolute()}")
md_lines.append(f"Parse Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
md_lines.append(f"Total Parsed Files: {len(parse_results)}")
md_lines.append("\n")
# Add Table of Contents (include receive time)
md_lines.append("## Table of Contents")
if parse_results:
for idx, result in enumerate(parse_results, 1):
# Create anchor link (compatible with markdown)
anchor = sanitize_anchor(f"{idx}. {result['file_name']}")
# 目录条目:序号 + [文件名 (接收时间)] (跳转链接)
toc_text = f"{idx}. [{result['file_name']} ({result['date_str_formatted']})](#{anchor})"
md_lines.append(toc_text)
else:
md_lines.append("No email files to display")
md_lines.append("\n---\n")
# Generate content for each email sorted by time
for idx, result in enumerate(parse_results, 1):
file_name = result['file_name']
# Create heading with anchor (sanitized)
anchor = sanitize_anchor(f"{idx}. {file_name}")
md_lines.append(f"## {idx}. {file_name} {{#{anchor}}}")
md_lines.append(f"### File Info")
md_lines.append(f"- File Path: `{result['file_path']}`")
if result['error']:
md_lines.append(f"- Parse Status: ❌ {result['error']}")
md_lines.append(f"- Receive Time: {result['date_str_formatted']}")
else:
md_lines.append(f"- Parse Status: ✅ Success")
md_lines.append(f"- Receive Time: {result['date_str_formatted']} (Original: {result.get('date_str', 'Unknown')})")
md_lines.append(f"- Sender: {result['from']}")
md_lines.append(f"- Recipient: {result['to']}")
md_lines.append(f"- Subject: {result['subject']}")
md_lines.append(f"### Email Body")
if result['error']:
md_lines.append(f"> {result['error']}")
else:
# Directly display body without code block
md_lines.append(result['body'])
md_lines.append("\n---\n")
return '\n'.join(md_lines)
def main():
"""Main function: parse all eml files in specified directory and generate MD report"""
# Check command line arguments
if len(sys.argv) < 2:
print("📚 Usage:")
print(f" python3 {sys.argv[0]} <eml_directory> [output_md_path (optional)]")
print("Examples:")
print(f" python3 {sys.argv[0]} ./emails")
print(f" python3 {sys.argv[0]} ./emails ./custom_report.md")
sys.exit(1)
# Get target directory
target_dir = Path(sys.argv[1]).absolute()
if not target_dir.is_dir():
print(f"❌ Error: {target_dir} is not a valid directory!")
sys.exit(1)
# Get output MD file path (save to parent directory by default)
if len(sys.argv) >= 3:
md_file = Path(sys.argv[2]).absolute()
else:
# Generate timestamp filename, save to parent directory of eml directory
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
md_file_name = f"email_parse_result_{timestamp}.md"
# Parent directory = target_dir.parent
md_file = target_dir.parent / md_file_name
# Get all .eml files in directory
eml_files = list(target_dir.glob("*.eml"))
if not eml_files:
print(f"📭 No .eml files found in directory {target_dir}!")
sys.exit(0)
print(f"🔍 Found {len(eml_files)} .eml files, start parsing...")
# Parse all eml files
parse_results = []
for eml_file in eml_files:
result = parse_eml_file(eml_file)
parse_results.append(result)
# Fix: use single quote to avoid quote conflict
status = 'Success' if not result['error'] else f'Failed({result["error"]})'
print(f" - Parsing {eml_file.name}: {status}")
# Sort by time (ascending: earliest first)
parse_results.sort(key=lambda x: x['date'])
# Generate MD content and save
print(f"\n📝 Generating Markdown report: {md_file}")
md_content = generate_md_content(parse_results, target_dir)
with open(md_file, 'w', encoding='utf-8') as f:
f.write(md_content)
print("✅ Parse completed! Markdown file saved to parent directory of eml directory with table of contents (include receive time).")
if __name__ == "__main__":
main()

SpringBoot 配置
yaml
spring:
mail:
host: localhost # 若容器在远程服务器,填服务器IP
port: 25 # 和脚本中HOST_PORT一致
username: test@test.com # 随便填
password: 123456 # 随便填
properties:
mail:
smtp:
auth: false # 无需认证
starttls:
enable: false # 关闭TLS
socketFactory:
class: javax.net.SocketFactory # 禁用SSL
default-encoding: UTF-8