I think now it works
This commit is contained in:
commit
b94784dc83
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
config.json
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
113
README.md
Normal file
113
README.md
Normal file
@ -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 文件。
|
53
config-example.json
Normal file
53
config-example.json
Normal file
@ -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
|
||||
}
|
152
maincopy.py
Normal file
152
maincopy.py
Normal file
@ -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"""<table align=center style="background:#3d3d3d;padding:8px 16px;margin-top:30px;margin-bottom:30px;width:96%;border-radius:6px;max-width:1200px"width=100% bgcolor=#3D3D3D id=relay-email-header><tr><td style=font-size:xx-large;font-weight:bolder;color:#fff width=50% align=left>Forwarded Email<td style=color:#fff;text-align:right width=50% align=right><p>From: {original_msg['From']}<p>To: {original_msg['To']}<p>Subject: {original_subject}</table><table align=center style=padding:0;max-width:850px width=100%><tr><td style=padding-left:15px;padding-right:15px width=100%>"""
|
||||
footer = f"""<table align=center bgcolor=#3D3D3D style="background:#3d3d3d;padding:8px 16px;margin-top:30px;margin-bottom:30px;width:96%;border-radius:6px;max-width:1200px"width=100%><tr><td align=left style=color:#d22;font-size:xx-large;font-weight:bolder width=50%>FORWARDED<td align=right style=color:#fff;text-align:left width=50%><p style=font-size:x-large;font-weight:700;margin-block:0>Notice:<p style=margin-block:.1em> This is a automatically forwarded email, which means it may contains something bad.<p style=margin-block:.1em> You shouldn't reply directly to this email, it will never reach your destination!</table>"""
|
||||
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()
|
72
template.html
Normal file
72
template.html
Normal file
@ -0,0 +1,72 @@
|
||||
<table
|
||||
id="relay-email-header"
|
||||
style="
|
||||
background: #3d3d3d;
|
||||
padding: 8px 16px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
width: 96%;
|
||||
border-radius: 6px;
|
||||
max-width: 1200px;
|
||||
"
|
||||
width="100%"
|
||||
bgcolor="#3D3D3D"
|
||||
align="center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
width="50%"
|
||||
align="left"
|
||||
style="
|
||||
font-size: xx-large;
|
||||
font-weight: bolder;
|
||||
color: #ffffff;
|
||||
">
|
||||
Forwarded Email
|
||||
</td>
|
||||
<td width="50%" align="right" style="color: #ffffff; text-align: right;">
|
||||
<p>From: {original_msg['From']}</p>
|
||||
<p>To: {original_msg['To']}</p>
|
||||
<p>Subject: {original_subject}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table style="padding: 0; max-width: 850px" width="100%" align="center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding-left: 15px; padding-right: 15px" width="100%">
|
||||
<!-- content -->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
style="
|
||||
background: #3d3d3d;
|
||||
padding: 8px 16px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
width: 96%;
|
||||
border-radius: 6px;
|
||||
max-width: 1200px;
|
||||
"
|
||||
width="100%"
|
||||
bgcolor="#3D3D3D"
|
||||
align="center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
width="50%"
|
||||
align="left"
|
||||
style="color: #dd2222; font-size: xx-large; font-weight: bolder">
|
||||
FORWARDED
|
||||
</td>
|
||||
<td width="50%" align="right" style="color: #ffffff; text-align: left;">
|
||||
<p style="font-size: x-large; font-weight: bold; margin-block: 0;">Notice:</p>
|
||||
<p style="margin-block: 0.1em;"> This is a automatically forwarded email, which means it may contains something bad.</p>
|
||||
<p style="margin-block: 0.1em;"> You shouldn't reply directly to this email, it will never reach your destination!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
Loading…
x
Reference in New Issue
Block a user