Compare commits

...

13 Commits

5 changed files with 169 additions and 123 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
config.json config.json
*.log
.vscode/

View File

@ -38,6 +38,7 @@ pip install pysocks
```json ```json
{ {
"log": "output.log",
"check_interval": 60, "check_interval": 60,
"accounts": [ "accounts": [
{ {
@ -70,6 +71,7 @@ pip install pysocks
### 配置项说明 ### 配置项说明
- `log`: 日志文件路径。
- `check_interval`: 检查邮件的时间间隔,单位为秒。 - `check_interval`: 检查邮件的时间间隔,单位为秒。
- `accounts`: 配置多个邮箱账户。 - `accounts`: 配置多个邮箱账户。
- `email`: 邮箱地址。 - `email`: 邮箱地址。

View File

@ -1,4 +1,5 @@
{ {
"log": "output.log",
"accounts": [ "accounts": [
{ {
"enabled": false, "enabled": false,

105
main.py
View File

@ -1,3 +1,4 @@
# pylint: disable=C0301, C0114
import json import json
import imaplib import imaplib
import smtplib import smtplib
@ -5,86 +6,113 @@ import email
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import parseaddr from email.utils import parseaddr
import time
import logging
import socks
import socket
from email.header import decode_header from email.header import decode_header
import time
import datetime
import logging
import socket
import re
import socks
def provider_in(address, provider_list):
"""Check if the provider is in the provider list"""
for provider in provider_list:
if address.endswith(provider):
return True
return False
def decode_mime_words(s): def decode_mime_words(s):
"""Decodes a string containing multiple MIME words"""
decoded_fragments = decode_header(s) decoded_fragments = decode_header(s)
return ''.join( return ''.join(
str(fragment, encoding or 'utf-8') if isinstance(fragment, bytes) else fragment str(fragment, encoding or 'utf-8') if isinstance(fragment, bytes) else fragment
for fragment, encoding in decoded_fragments for fragment, encoding in decoded_fragments
) )
def add_mask(original_msg, content): def add_mask(original_msg, content, is_html):
"""Adds a mask to the content"""
original_subject = decode_mime_words(original_msg['Subject']) original_subject = decode_mime_words(original_msg['Subject'])
from_name, from_address = parseaddr(original_msg['From']) from_name, from_address = parseaddr(original_msg['From'])
from_name = decode_mime_words(from_name) from_name = decode_mime_words(from_name)
to_name, to_address = parseaddr(original_msg['To']) to_name, to_address = parseaddr(original_msg['To'])
to_name = decode_mime_words(to_name) to_name = decode_mime_words(to_name)
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: {from_name} &lt;{from_address}&gt;<p>To: {to_name} &lt;{to_address}&gt;<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%>""" header = f"""<html><head></head><body><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="font-weight:bolder;color:#fff"><p style="font-size:x-large;margin-block:.2em">Forwarded Email</p>{'' if is_html else '<p style="font-size: medium; margin-block: 0.2em;">This email is plain text, it may have display issues</p>'}</td><td width="50%" align="right" style="color:#fff;text-align:right"><p style="margin-block: 0.1em">From: {from_name} &lt;<a href="mailto:{from_address}">{from_address}</a>&gt;</p><p style="margin-block: 0.1em">To: {to_name} &lt;<a href="mailto:{to_address}">{to_address}</a>&gt;</p><p style="margin-block:.1em">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%">"""
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>""" footer = """</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:#d22;font-size:xx-large;font-weight:bolder">FORWARDED</td><td width="50%" align="right" style="color:#fff;text-align:left"><p style="font-size:x-large;font-weight:700;margin-block:0">Notice:</p><p style="margin-block:.1em">&emsp;This is a automatically forwarded email, which means it may contains something bad.</p><p style="margin-block:.1em">&emsp;You shouldn't reply directly to this email, it will never reach your destination!</p></td></tr></tbody></table></body></html>"""
return header + content + footer return header + content + footer
def load_config(config_file='config.json'): def load_config(config_file='config.json'):
with open(config_file, 'r') as file: """Loads the configuration file"""
with open(config_file, 'r', encoding='utf-8') as file:
config = json.load(file) config = json.load(file)
return config return config
def set_proxy(proxy_config): def set_proxy(proxy_config):
"""Sets the proxy"""
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, proxy_config['server'], proxy_config['port']) socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, proxy_config['server'], proxy_config['port'])
socket.socket = socks.socksocket socket.socket = socks.socksocket
def setup_logging(): def setup_logging(filename):
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') """Sets up the logging"""
logger = logging.getLogger() logger = logging.getLogger()
logger.setLevel('DEBUG')
basic_format = "%(asctime)s >> %(levelname)s - %(message)s"
date_format = '%Y-%m-%d %H:%M:%S'
formatter = logging.Formatter(basic_format, date_format)
chlr = logging.StreamHandler()
chlr.setFormatter(formatter)
chlr.setLevel('INFO')
fhlr = logging.FileHandler(filename, encoding='utf-8')
fhlr.setFormatter(formatter)
logger.addHandler(chlr)
logger.addHandler(fhlr)
return logger return logger
def get_unforwarded_emails(account_config, logger): def get_unforwarded_emails(account_config, logger):
"""Gets the unforwarded emails"""
if account_config['proxy']['enabled']: if account_config['proxy']['enabled']:
set_proxy(account_config['proxy']) set_proxy(account_config['proxy'])
if account_config['imap'].get('use_ssl', True): if account_config['imap'].get('use_ssl', True):
imap = imaplib.IMAP4_SSL(account_config['imap']['server'], account_config['imap']['port']) imap = imaplib.IMAP4_SSL(account_config['imap']['server'], account_config['imap']['port'], timeout=10)
else: else:
imap = imaplib.IMAP4(account_config['imap']['server'], account_config['imap']['port']) imap = imaplib.IMAP4(account_config['imap']['server'], account_config['imap']['port'], timeout=10)
imap.login(account_config['email'], account_config['password']) imap.login(account_config['email'], account_config['password'])
if "163.com" in account_config['email'] or "126.com" in account_config['email']: if provider_in(account_config['email'], ["163.com", "126.com"]):
imaplib.Commands['ID'] = ('AUTH') imaplib.Commands['ID'] = 'AUTH'
args = ("name","XXXX","contact","XXXX@163.com","version","1.0.0","vendor","myclient") args = ("name","XXXX","contact","XXXX@163.com","version","1.0.0","vendor","myclient")
typ, dat = imap._simple_command('ID', '("' + '" "'.join(args) + '")') imap._simple_command('ID', '("' + '" "'.join(args) + '")') # pylint: disable=W0212
imap.select() imap.select()
status, messages = imap.search(None, 'UNSEEN') _, messages = imap.search(None, 'UNSEEN')
email_ids = messages[0].split() email_ids = messages[0].split()
emails = [] emails = []
for email_id in email_ids: for email_id in email_ids:
status, data = imap.fetch(email_id, '(RFC822)') _, data = imap.fetch(email_id, '(RFC822)')
for response_part in data: for response_part in data:
if isinstance(response_part, tuple): if isinstance(response_part, tuple):
msg = email.message_from_bytes(response_part[1]) msg = email.message_from_bytes(response_part[1]) # pylint: disable=E1136
if 'Forwarded' not in msg['Subject']: emails.append((email_id, msg))
emails.append((email_id, msg)) imap.store(email_id, '+FLAGS', '\\Seen')
imap.logout() imap.logout()
logger.info(f"Retrieved {len(emails)} new emails from {account_config['email']}") if len(emails) > 0:
logger.info(f"Retrieved {len(emails)} new emails from {account_config['email']}")
return emails return emails
def forward_emails(account_config, emails, logger): def forward_emails(account_config, emails, logger):
"""Forwards the emails"""
if account_config['proxy']['enabled']: if account_config['proxy']['enabled']:
set_proxy(account_config['proxy']) set_proxy(account_config['proxy'])
smtp = None smtp = None
if account_config['smtp'].get('use_ssl', False): if account_config['smtp'].get('use_ssl', False):
smtp = smtplib.SMTP_SSL(account_config['smtp']['server'], account_config['smtp']['port']) smtp = smtplib.SMTP_SSL(account_config['smtp']['server'], account_config['smtp']['port'], timeout=10)
else: else:
smtp = smtplib.SMTP(account_config['smtp']['server'], account_config['smtp']['port']) smtp = smtplib.SMTP(account_config['smtp']['server'], account_config['smtp']['port'], timeout=10)
if account_config['smtp'].get('use_starttls', False): if account_config['smtp'].get('use_starttls', False):
smtp.starttls() smtp.starttls()
@ -92,9 +120,13 @@ def forward_emails(account_config, emails, logger):
for email_id, original_msg in emails: for email_id, original_msg in emails:
for recipient in account_config['forward']['to']: for recipient in account_config['forward']['to']:
from_name, from_address = parseaddr(original_msg['From'])
from_name = decode_mime_words(from_name)
to_name, to_address = parseaddr(original_msg['To'])
to_name = decode_mime_words(to_name)
msg = MIMEMultipart('mixed') msg = MIMEMultipart('mixed')
msg['From'] = account_config['email'] msg['From'] = f'"{from_name} ({from_address}) via Forwarder" <{account_config["email"]}>'
msg['To'] = recipient msg['To'] = f'"{to_name} ({to_address}) via Forwarder" <{recipient}>'
original_subject = decode_mime_words(original_msg['Subject']) original_subject = decode_mime_words(original_msg['Subject'])
msg['Subject'] = original_subject msg['Subject'] = original_subject
@ -118,7 +150,12 @@ def forward_emails(account_config, emails, logger):
logger.error(f"Failed to extract body from email {email_id}") logger.error(f"Failed to extract body from email {email_id}")
continue continue
html_content = add_mask(original_msg, body) is_html = bool(re.compile(r'<[^>]+>').search(body))
if not is_html:
body = body.replace('\n', '<br>')
html_content = add_mask(original_msg, body, is_html)
msg.attach(MIMEText(html_content, 'html')) msg.attach(MIMEText(html_content, 'html'))
for attachment in attachments: for attachment in attachments:
@ -129,13 +166,14 @@ def forward_emails(account_config, emails, logger):
msg.attach(attachment) msg.attach(attachment)
smtp.sendmail(account_config['email'], recipient, msg.as_string()) smtp.sendmail(account_config['email'], recipient, msg.as_string())
logger.info(f"Forwarded email {original_subject} to {recipient}") logger.info(f"Forwarded email {original_subject} from {account_config['email']} to {recipient}")
smtp.quit() smtp.quit()
def main(): def main():
logger = setup_logging() """Main function"""
config = load_config() config = load_config()
logger = setup_logging(config['log'])
while True: while True:
for account in config['accounts']: for account in config['accounts']:
@ -144,13 +182,10 @@ def main():
emails = get_unforwarded_emails(account, logger) emails = get_unforwarded_emails(account, logger)
if emails: if emails:
forward_emails(account, emails, logger) forward_emails(account, emails, logger)
else: except Exception as e: # pylint: disable=W0718
logger.info(f"No new emails to forward for {account['email']}.") logger.error(f"Error processing account {account['email']}: {str(e)}") # pylint: disable=W1203
except Exception as e:
logger.error(f"Error processing account {account['email']}: {str(e)}")
else:
logger.info(f"Account {account['email']} is disabled.")
logger.info(datetime.datetime.now().strftime("Check finished at %Y-%m-%d %H:%M:%S"))
time.sleep(config.get('check_interval', 60)) time.sleep(config.get('check_interval', 60))
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,72 +1,78 @@
<table <html>
id="relay-email-header" <head></head>
style=" <body>
background: #3d3d3d; <table
padding: 8px 16px; style="
margin-top: 30px; background: #3d3d3d;
margin-bottom: 30px; padding: 8px 16px;
width: 96%; margin-top: 30px;
border-radius: 6px; margin-bottom: 30px;
max-width: 1200px; width: 96%;
" border-radius: 6px;
width="100%" max-width: 1200px;
bgcolor="#3D3D3D" "
align="center"> width="100%"
<tbody> bgcolor="#3D3D3D"
<tr> align="center">
<td <tbody>
width="50%" <tr>
align="left" <td width="50%" align="left" style="font-weight: bolder; color: #ffffff">
style=" <p style="font-size: x-large; margin-block: 0.2em">Forwarded Email</p>
font-size: xx-large; <!-- <p style="font-size: medium; margin-block: 0.2em;">This email is plain text, it may have display issues</p> -->
font-weight: bolder; </td>
color: #ffffff; <td width="50%" align="right" style="color: #ffffff; text-align: right">
"> <p style="margin-block: 0.1em">From: {from_name} &lt;<a href="mailto:{from_address}">{from_address}</a>&gt;</p>
Forwarded Email <p style="margin-block: 0.1em">To: {to_name} &lt;<a href="mailto:{to_address}">{to_address}</a>&gt;</p>
</td> <p style="margin-block: 0.1em">Subject: {original_subject}</p>
<td width="50%" align="right" style="color: #ffffff; text-align: right;"> </td>
<p>From: {from_name} &lt;{from_address}&gt;</p> </tr>
<p>To: {to_name} &lt;{to_address}&gt;</p> </tbody>
<p>Subject: {original_subject}</p> </table>
</td> <table style="padding: 0; max-width: 850px" width="100%" align="center">
</tr> <tbody>
</tbody> <tr>
</table> <td style="padding-left: 15px; padding-right: 15px" width="100%">
<table style="padding: 0; max-width: 850px" width="100%" align="center"> <!-- content -->
<tbody> </td>
<tr> </tr>
<td style="padding-left: 15px; padding-right: 15px" width="100%"> </tbody>
<!-- content --> </table>
</td> <table
</tr> style="
</tbody> background: #3d3d3d;
</table> padding: 8px 16px;
<table margin-top: 30px;
style=" margin-bottom: 30px;
background: #3d3d3d; width: 96%;
padding: 8px 16px; border-radius: 6px;
margin-top: 30px; max-width: 1200px;
margin-bottom: 30px; "
width: 96%; width="100%"
border-radius: 6px; bgcolor="#3D3D3D"
max-width: 1200px; align="center">
" <tbody>
width="100%" <tr>
bgcolor="#3D3D3D" <td
align="center"> width="50%"
<tbody> align="left"
<tr> style="color: #dd2222; font-size: xx-large; font-weight: bolder">
<td FORWARDED
width="50%" </td>
align="left" <td width="50%" align="right" style="color: #ffffff; text-align: left">
style="color: #dd2222; font-size: xx-large; font-weight: bolder"> <p style="font-size: x-large; font-weight: bold; margin-block: 0">
FORWARDED Notice:
</td> </p>
<td width="50%" align="right" style="color: #ffffff; text-align: left;"> <p style="margin-block: 0.1em">
<p style="font-size: x-large; font-weight: bold; margin-block: 0;">Notice:</p> &emsp;This is a automatically forwarded email, which means it may
<p style="margin-block: 0.1em;">&emsp;This is a automatically forwarded email, which means it may contains something bad.</p> contains something bad.
<p style="margin-block: 0.1em;">&emsp;You shouldn't reply directly to this email, it will never reach your destination!</p> </p>
</td> <p style="margin-block: 0.1em">
</tr> &emsp;You shouldn't reply directly to this email, it will never reach
</tbody> your destination!
</table> </p>
</td>
</tr>
</tbody>
</table>
</body>
</html>