mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-07 04:52:50 +08:00
155 lines
5.4 KiB
Python
155 lines
5.4 KiB
Python
import asyncio
|
|
import logging
|
|
import email
|
|
import httpx
|
|
|
|
from aiosmtpd.controller import Controller
|
|
from aiosmtpd.smtp import SMTP, Session, Envelope, AuthResult, LoginPassword
|
|
|
|
from config import settings
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
_logger.setLevel(logging.INFO)
|
|
|
|
|
|
class CustomSMTPHandler:
|
|
|
|
def authenticator(self, server, session, envelope, mechanism, auth_data):
|
|
fail_nothandled = AuthResult(success=False, handled=False)
|
|
if mechanism not in ("LOGIN", "PLAIN"):
|
|
_logger.warning(f"Unsupported mechanism {mechanism}")
|
|
return fail_nothandled
|
|
if not isinstance(auth_data, LoginPassword):
|
|
_logger.warning(f"Invalid auth data {auth_data}")
|
|
return fail_nothandled
|
|
return AuthResult(success=True, auth_data=auth_data)
|
|
|
|
async def handle_DATA(self, server: SMTP, session: Session, envelope: Envelope) -> str:
|
|
_logger.info(
|
|
f"handle_DATA from {envelope.mail_from} to {envelope.rcpt_tos}"
|
|
)
|
|
if not isinstance(session.auth_data, LoginPassword):
|
|
return '530 Authentication required'
|
|
if len(envelope.rcpt_tos) != 1:
|
|
return '500 Only one recipient allowed'
|
|
# Only one recipient allowed
|
|
to_mail = envelope.rcpt_tos[0]
|
|
# Parse email
|
|
msg = email.message_from_string(envelope.content)
|
|
content_list = []
|
|
if msg.is_multipart():
|
|
for part in msg.walk():
|
|
content_type = part.get_content_type()
|
|
charset = part.get_content_charset()
|
|
cte = str(part.get('content-transfer-encoding', '')).lower()
|
|
if content_type not in ["text/plain", "text/html"]:
|
|
_logger.warning(f"Skipping {content_type}")
|
|
continue
|
|
if cte == "8bit":
|
|
value = part.get_payload(decode=False)
|
|
else:
|
|
payload = part.get_payload(decode=True)
|
|
value = payload.decode(charset) if charset else payload
|
|
if not value:
|
|
continue
|
|
content_list.append({
|
|
"type": content_type,
|
|
"value": value
|
|
})
|
|
elif msg.get_content_type() in ["text/plain", "text/html"] and msg.get_payload(decode=True):
|
|
cte = str(msg.get('content-transfer-encoding', '')).lower()
|
|
charset = msg.get_content_charset()
|
|
if cte == "8bit":
|
|
value = msg.get_payload(decode=False)
|
|
else:
|
|
payload = msg.get_payload(decode=True)
|
|
value = payload.decode(charset) if charset else payload
|
|
_logger.info(f"Payload {msg._payload} charset {charset}")
|
|
content_list.append({
|
|
"type": msg.get_content_type(),
|
|
"value": value
|
|
})
|
|
|
|
if not content_list:
|
|
return '500 Invalid content'
|
|
body = max(
|
|
content_list,
|
|
key=lambda x: (x["type"] == "text/html", len(x["value"]))
|
|
)
|
|
from_name, _ = email.utils.parseaddr(
|
|
str(email.header.make_header(
|
|
email.header.decode_header(msg['From'])
|
|
))
|
|
)
|
|
to_mail_map = {}
|
|
for to in str(email.header.make_header(
|
|
email.header.decode_header(msg['To'])
|
|
)).split(","):
|
|
tmp_to_name, tmp_to_mail = email.utils.parseaddr(to)
|
|
to_mail_map[tmp_to_mail] = tmp_to_name
|
|
_logger.info(f"Parsed mail from {from_name} to {to_mail_map}")
|
|
# Send mail
|
|
send_body = {
|
|
"token": session.auth_data.password.decode(),
|
|
"from_name": from_name,
|
|
"to_name": to_mail_map.get(to_mail),
|
|
"to_mail": to_mail,
|
|
"subject": str(email.header.make_header(
|
|
email.header.decode_header(msg['Subject'])
|
|
)),
|
|
"is_html": body["type"] == "text/html",
|
|
"content": body["value"],
|
|
}
|
|
_logger.info(f"Send mail {dict(send_body, token='***')}")
|
|
try:
|
|
res = httpx.post(
|
|
f"{settings.proxy_url}/external/api/send_mail",
|
|
json=send_body, headers={
|
|
"Content-Type": "application/json"
|
|
}
|
|
)
|
|
if res.status_code != 200:
|
|
_logger.error(
|
|
"Failed to send mail "
|
|
f"code=[{res.status_code}] text=[{res.text}]"
|
|
)
|
|
return f'500 Internal server error code=[{res.status_code}] text=[{res.text}]'
|
|
except Exception as e:
|
|
_logger.error(e)
|
|
return '500 Internal server error'
|
|
|
|
return '250 OK'
|
|
|
|
|
|
handler = CustomSMTPHandler()
|
|
server = Controller(
|
|
handler,
|
|
hostname="",
|
|
port=settings.port,
|
|
auth_require_tls=False,
|
|
decode_data=True,
|
|
authenticator=handler.authenticator,
|
|
auth_exclude_mechanism=["DONT"]
|
|
)
|
|
|
|
|
|
async def start():
|
|
_logger.info(f"Starting server on port {settings.port}")
|
|
server.start()
|
|
|
|
|
|
def start_smtp_server():
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
task = loop.create_task(start())
|
|
try:
|
|
loop.run_forever()
|
|
except KeyboardInterrupt:
|
|
_logger.info("Got KeyboardInterrupt, stopping")
|
|
server.stop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_logger.info(f"Starting server settings[{settings}]")
|
|
start_smtp_server()
|