添加邮件过滤器实现邮件的自定义操作
项目背景
跨境店铺做多店铺爬虫时,涉及到店铺登录,每个店铺都需要邮箱验证码登录,如果通过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
浙公网安备 33010602011771号