添加邮件过滤器实现邮件的自定义操作

项目背景

跨境店铺做多店铺爬虫时,涉及到店铺登录,每个店铺都需要邮箱验证码登录,如果通过python的zmail模块或者其它模块监控邮件太麻烦,而且目前一些免费的邮箱都有流量限制。
基于这两个前提,我通过宝塔邮局自己搭建了一个邮箱服务,然后将所有店铺的邮件自动转发到我自己的邮箱服务中。这样解决了流量限制和需要监控多邮箱的问题。但随之而来又出现了新问题,当邮件数量过多的时候zmail读取邮件效率变得很慢(也有可能是我服务器配置太低的原因,但是懒得研究了),经常读取一次要3-5分钟,有时候验证码都过期了还没读取出来。

有没有办法能出现邮件之后自动推送到redis中呢?这样我需要验证码的时候直接去redis中获取。
答案就是:Postfix After-Queue过滤器

  • 安装 (ubuntu环境)
sudo apt install postfix-pcre
  • 创建用户和目录
# 创建系统用户
sudo adduser --system --no-create-home --group mail_api

# 创建脚本目录
sudo mkdir -p /opt/mail_processing
sudo cp mail_to_api.py /opt/mail_processing/
sudo chown mail_api:mail_api /opt/mail_processing/mail_to_api.py
sudo chmod 750 /opt/mail_processing/mail_to_api.py

# 创建日志目录
sudo mkdir -p /var/log/mail_processing
sudo chown -R mail_api:mail_api /var/log/mail_processing
sudo chmod -R 770 /var/log/mail_processing
  • 创建过滤脚本 /opt/mail_processing/mail_to_api.py
#!/usr/bin/env python3
import os
import sys
import time
import json
import email
import redis
import logging
import tempfile
import subprocess
from email import policy
from email.parser import BytesParser

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('/var/log/mail_processing/mail_to_api.log'),
        logging.StreamHandler(sys.stderr)
    ]
)
logger = logging.getLogger('mail_to_api')


def parse_email(raw_email):
    """解析邮件内容,返回结构化数据"""
    try:
        # 使用 email 标准库解析邮件
        msg = BytesParser(policy=policy.default).parsebytes(raw_email)
        
        # 提取邮件基本信息
        email_data = {
            'from': msg['from'],
            'to': msg['to'],
            'subject': msg['subject'],
            'date': msg['date'],
            'message_id': msg['message-id'],
            'text_body': '',
            'html_body': '',
            'attachments': []
        }
        
        # 递归解析邮件内容
        for part in msg.walk():
            content_type = part.get_content_type()
            content_disposition = str(part.get("Content-Disposition"))
            
            # 提取纯文本内容
            if content_type == "text/plain" and "attachment" not in content_disposition:
                charset = part.get_content_charset() or 'utf-8'
                email_data['text_body'] = part.get_payload(decode=True).decode(charset, errors='replace')
            
            # 提取HTML内容
            elif content_type == "text/html" and "attachment" not in content_disposition:
                charset = part.get_content_charset() or 'utf-8'
                email_data['html_body'] = part.get_payload(decode=True).decode(charset, errors='replace')
            
            # 处理附件
            elif "attachment" in content_disposition:
                attachment = {
                    'filename': part.get_filename(),
                    'content_type': content_type,
                    'size': len(part.get_payload(decode=True)),
                    # 实际使用中可能需要base64编码或直接存储到对象存储
                    'content': part.get_payload(decode=True).hex()  # 简化为十六进制表示
                }
                email_data['attachments'].append(attachment)
        
        return email_data
    
    except Exception as e:
        logger.error(f"邮件解析失败: {str(e)}")
        # 返回原始邮件作为后备
        return {'raw_email': raw_email.decode('utf-8', errors='replace')}

def reinject_email(raw_email):
    """将原始邮件重新注入Postfix队列"""
    try:
        # 使用Postfix的sendmail命令重新注入邮件
        sendmail = subprocess.Popen(
            ['/usr/sbin/sendmail', '-G', '-i', '-t'],
            stdin=subprocess.PIPE
        )
        sendmail.communicate(input=raw_email)
        
        if sendmail.returncode == 0:
            logger.info("邮件成功重新注入Postfix队列")
            return True
        else:
            logger.error(f"邮件重新注入失败,返回码: {sendmail.returncode}")
            return False
    except Exception as e:
        logger.exception(f"重新注入邮件时发生异常: {str(e)}")
        return False



def send_to_redis(email_data):
    server = {"host": "****", "port": 6379, "pwd": "*****", "db": 2}

    try:
        r = redis.Redis(**{
            "host": server["host"],
            "password": server["pwd"],
            "port": server["port"],
            "db": server["db"],
            "decode_responses": True,
            "socket_timeout": 10,
            "socket_connect_timeout": 10,
            "retry_on_timeout": True,
        })
        r.setex(email_data["to"].lower(), 300, json.dumps(email_data, ensure_ascii=False))
        r.close()
        return True
    except Exception as e:
        logger.exception(f"存入redis异常: {str(e)}")
        return False


def main():

    try:
        # 从命令行参数获取发件人和收件人
        if len(sys.argv) < 3:
            logger.error("缺少发件人或收件人参数")
            sys.exit(1)
        
        sender = sys.argv[1]
        recipient = sys.argv[2]
        
        # 从标准输入读取原始邮件内容
        raw_email = sys.stdin.buffer.read()
        logger.info(f"收到来自 {sender} 到 {recipient} 的邮件, 大小: {len(raw_email)} 字节")
        
        # 解析邮件
        email_data = parse_email(raw_email)
        
        # 添加元数据
        email_data.update({
            'sender': sender,
            'recipient': recipient,
            'raw_size': len(raw_email),
            'get_verify_code_time': time.time() 
        })
        
        # 判断是否为验证码邮件
        subject = email_data.get('subject')
        if 'Verify your login' in subject:
            # 推送到redis
            if send_to_redis(email_data):
                logger.info("邮件处理完成")
            else:
                logger.error("邮件处理失败")
        

        # 重新注入邮件,如果不重新注入邮件将被丢弃,邮箱中将无法接收到邮件
        reinject_result = reinject_email(raw_email)
        if reinject_result:
            logger.info("邮件处理完成并成功重新注入")
            sys.exit(0)
        else:
            logger.error("邮件重新注入失败,退出状态码75(TEMPFail)")
            sys.exit(75)  # Postfix TEMPFail错误代码

    except Exception as e:
        logger.exception(f"处理邮件时出错: {str(e)}")
        sys.exit(75)  # 临时失败,Postfix会重试

if __name__ == "__main__":
    main()
    
  • 配置/etc/postfix/master.cf
# 在文件末尾添加
smtp      inet  n       -       y       -       -       smtpd
  -o content_filter=myfilter:dummy

myfilter    unix  -       n       n       -       -       pipe
  flags=Rq user=mail_api argv=/opt/mail_processing/mail_to_api.py
  -o mynetworks=127.0.0.0/8
  • 重启服务
sudo systemctl restart postfix

posted on 2025-06-14 10:23  信奉上帝的小和尚  阅读(17)  评论(0)    收藏  举报

导航