From b94784dc837d3698f62c2c53ca3a9380009b039d Mon Sep 17 00:00:00 2001 From: Zichao Lin Date: Tue, 23 Jul 2024 23:57:09 +0800 Subject: [PATCH] I think now it works --- .gitignore | 1 + LICENSE | 21 ++++++ README.md | 113 ++++++++++++++++++++++++++++++++ config-example.json | 53 +++++++++++++++ maincopy.py | 152 ++++++++++++++++++++++++++++++++++++++++++++ template.html | 72 +++++++++++++++++++++ 6 files changed, 412 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config-example.json create mode 100644 maincopy.py create mode 100644 template.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cffcb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..757a11c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 earthjasonlin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a2be27 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# MailForwarder + +**此README由 ChatGPT-4o 生成** + +这个项目是一个邮件自动转发器,能够从指定的邮箱中读取未读邮件,并将其自动转发到配置文件中指定的收件人邮箱中。该项目支持使用代理服务器进行连接,并且可以处理包含附件的邮件。 + +## 功能特性 + +- 自动转发未读邮件 +- 支持 HTML 和纯文本格式的邮件内容 +- 支持邮件附件的转发 +- 支持通过 SOCKS5 代理进行连接 +- 支持多账户配置 +- 可配置的检查间隔 + +## 安装与配置 + +### 环境要求 + +- Python 3.6+ +- 需要安装以下 Python 库: + - json + - imaplib + - smtplib + - email + - socks + - logging + +### 安装依赖 + +```sh +pip install pysocks +``` + +### 配置文件 + +请在项目根目录下创建一个名为 `config.json` 的配置文件,并根据以下格式进行配置(见`config-example.json`): + +```json +{ + "check_interval": 60, + "accounts": [ + { + "email": "your_email@example.com", + "password": "your_password", + "enabled": true, + "imap": { + "server": "imap.example.com", + "port": 993, + "use_ssl": true + }, + "smtp": { + "server": "smtp.example.com", + "port": 587, + "use_ssl": false, + "use_starttls": true + }, + "forward": { + "to": ["forward_to@example.com"] + }, + "proxy": { + "enabled": false, + "server": "proxy.example.com", + "port": 1080 + } + } + ] +} +``` + +### 配置项说明 + +- `check_interval`: 检查邮件的时间间隔,单位为秒。 +- `accounts`: 配置多个邮箱账户。 + - `email`: 邮箱地址。 + - `password`: 邮箱密码。 + - `enabled`: 是否启用该账户。 + - `imap`: IMAP 服务器配置。 + - `server`: IMAP 服务器地址。 + - `port`: IMAP 服务器端口。 + - `use_ssl`: 是否使用 SSL 连接。 + - `smtp`: SMTP 服务器配置。 + - `server`: SMTP 服务器地址。 + - `port`: SMTP 服务器端口。 + - `use_ssl`: 是否使用 SSL 连接。 + - `use_starttls`: 是否使用 STARTTLS 加密。 + - `forward`: 转发配置。 + - `to`: 转发邮件的收件人地址列表。 + - `proxy`: 代理服务器配置。 + - `enabled`: 是否启用代理。 + - `server`: 代理服务器地址。 + - `port`: 代理服务器端口。 + +## 使用说明 + +1. 确保已经安装了所有依赖项,并正确配置了 `config.json` 文件。 +2. 运行脚本: + ```sh + python main.py + ``` + +## 日志 + +该脚本会在控制台输出日志信息,记录程序的运行状态、邮件处理情况以及错误信息。 + +## 注意事项 + +- 请确保配置文件中的邮箱账号和密码正确无误,并且邮箱服务支持 IMAP 和 SMTP 协议。 +- 转发的邮件会包含一个自定义的头部和尾部,以提示收件人该邮件是自动转发的邮件。 + +## 许可证 + +本项目遵循 MIT 许可证。详细信息请参见 LICENSE 文件。 \ No newline at end of file diff --git a/config-example.json b/config-example.json new file mode 100644 index 0000000..2f1db09 --- /dev/null +++ b/config-example.json @@ -0,0 +1,53 @@ +{ + "accounts": [ + { + "enabled": false, + "email": "sample@example.com", + "password": "password", + "imap": { + "server": "imap.example.com", + "port": 993, + "use_ssl": true + }, + "smtp": { + "server": "smtp.example.com", + "port": 465, + "use_ssl": true, + "use_starttls": false + }, + "proxy": { + "enabled": true, + "server": "127.0.0.1", + "port": 7897 + }, + "forward": { + "to": ["forward@example.com", "forward2@example.com"] + } + }, + { + "enabled": true, + "email": "active@example.com", + "password": "password", + "imap": { + "server": "imap.active.com", + "port": 993, + "use_ssl": true + }, + "smtp": { + "server": "smtp.active.com", + "port": 465, + "use_ssl": false, + "use_starttls": true + }, + "proxy": { + "enabled": false, + "server": "127.0.0.1", + "port": 7897 + }, + "forward": { + "to": ["forward@example.com"] + } + } + ], + "check_interval": 10 +} \ No newline at end of file diff --git a/maincopy.py b/maincopy.py new file mode 100644 index 0000000..1f5e48f --- /dev/null +++ b/maincopy.py @@ -0,0 +1,152 @@ +import json +import imaplib +import smtplib +import email +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import time +import logging +import socks +import socket + +from email.header import decode_header + +def decode_mime_words(s): + decoded_fragments = decode_header(s) + return ''.join( + str(fragment, encoding or 'utf-8') if isinstance(fragment, bytes) else fragment + for fragment, encoding in decoded_fragments + ) + +def add_mask(original_msg, content): + original_subject = decode_mime_words(original_msg['Subject']) + header = f"""
Forwarded Email

From: {original_msg['From']}

To: {original_msg['To']}

Subject: {original_subject}

""" + footer = f"""
FORWARDED

Notice:

 This is a automatically forwarded email, which means it may contains something bad.

 You shouldn't reply directly to this email, it will never reach your destination!

""" + return header + content + footer + +def load_config(config_file='config.json'): + with open(config_file, 'r') as file: + config = json.load(file) + return config + +def set_proxy(proxy_config): + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, proxy_config['server'], proxy_config['port']) + socket.socket = socks.socksocket + +def setup_logging(): + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + logger = logging.getLogger() + return logger + +def get_unforwarded_emails(account_config, logger): + if account_config['proxy']['enabled']: + set_proxy(account_config['proxy']) + + if account_config['imap'].get('use_ssl', True): + imap = imaplib.IMAP4_SSL(account_config['imap']['server'], account_config['imap']['port']) + else: + imap = imaplib.IMAP4(account_config['imap']['server'], account_config['imap']['port']) + + imap.login(account_config['email'], account_config['password']) + + if "163.com" in account_config['email'] or "126.com" in account_config['email']: + imaplib.Commands['ID'] = ('AUTH') + args = ("name","XXXX","contact","XXXX@163.com","version","1.0.0","vendor","myclient") + typ, dat = imap._simple_command('ID', '("' + '" "'.join(args) + '")') + + imap.select() + + status, messages = imap.search(None, 'UNSEEN') + email_ids = messages[0].split() + + emails = [] + for email_id in email_ids: + status, data = imap.fetch(email_id, '(RFC822)') + for response_part in data: + if isinstance(response_part, tuple): + msg = email.message_from_bytes(response_part[1]) + if 'Forwarded' not in msg['Subject']: + emails.append((email_id, msg)) + imap.logout() + logger.info(f"Retrieved {len(emails)} new emails from {account_config['email']}") + return emails + +def forward_emails(account_config, emails, logger): + if account_config['proxy']['enabled']: + set_proxy(account_config['proxy']) + + smtp = None + if account_config['smtp'].get('use_ssl', False): + smtp = smtplib.SMTP_SSL(account_config['smtp']['server'], account_config['smtp']['port']) + else: + smtp = smtplib.SMTP(account_config['smtp']['server'], account_config['smtp']['port']) + if account_config['smtp'].get('use_starttls', False): + smtp.starttls() + + smtp.login(account_config['email'], account_config['password']) + + for email_id, original_msg in emails: + for recipient in account_config['forward']['to']: + msg = MIMEMultipart('mixed') + msg['From'] = account_config['email'] + msg['To'] = recipient + original_subject = decode_mime_words(original_msg['Subject']) + msg['Subject'] = original_subject + + body = "" + attachments = [] + if original_msg.is_multipart(): + for part in original_msg.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + + if "attachment" in content_disposition or part.get_filename(): + attachments.append(part) + elif content_type == 'text/html': + body = part.get_payload(decode=True).decode() + elif content_type == 'text/plain' and not body: + body = part.get_payload(decode=True).decode() + else: + body = original_msg.get_payload(decode=True).decode() + + if not body: + logger.error(f"Failed to extract body from email {email_id}") + continue + + html_content = add_mask(original_msg, body) + msg.attach(MIMEText(html_content, 'html')) + + for attachment in attachments: + filename = attachment.get_filename() + if decode_header(filename): + filename = decode_mime_words(filename) + attachment.add_header('Content-Disposition', 'attachment', filename=filename) + msg.attach(attachment) + + smtp.sendmail(account_config['email'], recipient, msg.as_string()) + logger.info(f"Forwarded email {original_subject} to {recipient}") + + smtp.quit() + +def main(): + logger = setup_logging() + config = load_config() + + while True: + for account in config['accounts']: + if account['enabled']: + try: + emails = get_unforwarded_emails(account, logger) + if emails: + forward_emails(account, emails, logger) + else: + logger.info(f"No new emails to forward for {account['email']}.") + except Exception as e: + logger.error(f"Error processing account {account['email']}: {str(e)}") + else: + logger.info(f"Account {account['email']} is disabled.") + + time.sleep(config.get('check_interval', 60)) + +if __name__ == "__main__": + main() diff --git a/template.html b/template.html new file mode 100644 index 0000000..9b7b9a7 --- /dev/null +++ b/template.html @@ -0,0 +1,72 @@ + + + + + + + +
+ Forwarded Email + +

From: {original_msg['From']}

+

To: {original_msg['To']}

+

Subject: {original_subject}

+
+ + + + + + +
+ +
+ + + + + + + +
+ FORWARDED + +

Notice:

+

 This is a automatically forwarded email, which means it may contains something bad.

+

 You shouldn't reply directly to this email, it will never reach your destination!

+