FakeSMTP-2.1.1使用

下载

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
相关推荐
码匠君1 小时前
首个基于 Spring Boot 4 的正式版发布!Dante Cloud 4.X 新特性全解析
java·spring boot·后端
悟空码字2 小时前
SpringBoot + 百度地图SDK,打造企业级位置服务中台
java·百度·地图·编程技术·后端开发
weixin199701080162 小时前
网易考拉商品详情页前端性能优化实战
java·前端·python·性能优化
Natalia_Portman2 小时前
springboot整合DolphinDB
java·数据库·spring boot·后端·db
hua872222 小时前
【Springboot3+vue3】从零到一搭建Springboot3+vue3前后端分离项目之后端环境搭建
java
不光头强2 小时前
LinkedList知识点
java
跳跳鱼2 小时前
ThreadLocal 核心源码解析:属性、内部类与重要接口深度剖析
java
JamesYoung79712 小时前
第七部分 — 存储 数据建模与迁移提示
java·开发语言·数据结构