From 1d39e8a3b67de7062443d962215e306664b8f54f Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 12 Jul 2024 10:14:39 +0900 Subject: [PATCH] SMTP fix #35 --- base.py | 12 +++++-- requirements.txt | 1 + smtp.py | 87 ++++++++++++++++++++++++++++-------------------- 3 files changed, 60 insertions(+), 40 deletions(-) diff --git a/base.py b/base.py index a205e6b..e7f4a34 100644 --- a/base.py +++ b/base.py @@ -8,7 +8,7 @@ # Euiseo Cha (Wonkwang University) # https://github.com/gnh1201/caterpillar # Created at: 2024-05-20 -# Updated at: 2024-07-09 +# Updated at: 2024-07-12 # import logging @@ -48,9 +48,16 @@ def jsonrpc2_encode(method, params=None): "params": params } id = jsonrpc2_create_id(data) - data['id'] = id + id = data.get('id') return (id, json.dumps(data)) +def jsonrpc2_decode(text): + data = json.loads(text) + type = 'error' if 'error' in data else 'result' if 'result' in data else None + id = data.get('id') + rpcdata = data.get(type) if type else None + return type, id, rpcdata + def jsonrpc2_result_encode(result, id=''): data = { @@ -69,7 +76,6 @@ def jsonrpc2_error_encode(error, id=''): } return json.dumps(data) - class Extension(): extensions = [] protocols = [] diff --git a/requirements.txt b/requirements.txt index c93c6d8..7c4d4a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ python-decouple requests +aiosmtpd \ No newline at end of file diff --git a/smtp.py b/smtp.py index bc1c054..1c12b2b 100644 --- a/smtp.py +++ b/smtp.py @@ -1,26 +1,26 @@ #!/usr/bin/python3 # # smtp.py -# SMTP over HTTP gateway +# SMTP mail sender over HTTP/S # # Caterpillar Proxy - The simple web debugging proxy (formerly, php-httpproxy) # Namyheon Go (Catswords Research) # https://github.com/gnh1201/caterpillar # Created at: 2024-03-01 -# Updated at: 2024-05-20 +# Updated at: 2024-07-12 # - -import asyncore -from smtpd import SMTPServer - +import asyncio +from aiosmtpd.controller import Controller +from aiosmtpd.handlers import Message +from email.message import EmailMessage import re import sys import json import requests - +from platform import python_version from decouple import config from requests.auth import HTTPBasicAuth -from base import extract_credentials, jsonrpc2_create_id, jsonrpc2_encode, jsonrpc2_result_encode, Logger +from base import extract_credentials, jsonrpc2_create_id, jsonrpc2_encode, jsonrpc2_decode, jsonrpc2_result_encode, Logger logger = Logger(name="smtp") @@ -37,30 +37,25 @@ auth = None if _username: auth = HTTPBasicAuth(_username, _password) -class CaterpillarSMTPServer(SMTPServer): - def __init__(self, localaddr, remoteaddr): - self.__class__.smtpd_hostname = "CaterpillarSMTPServer" - self.__class__.smtp_version = "0.1.6" - super().__init__(localaddr, remoteaddr) +class CaterpillarSMTPHandler: + def __init__(self): + self.smtpd_hostname = "CaterpillarSMTPServer" + self.smtp_version = "0.1.6" - def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): - message_lines = data.decode('utf-8').split('\n') - subject = '' - to = '' - for line in message_lines: - pos = line.find(':') - if pos > -1: - k = line[0:pos] - v = line[pos+1:] - if k == 'Subject': - subject = v - elif k == 'To': - to = v + async def handle_DATA(self, server, session, envelope): + mailfrom = envelope.mail_from + rcpttos = envelope.rcpt_tos + data = envelope.content + + message = EmailMessage() + message.set_content(data) + + subject = message.get('Subject', '') + to = message.get('To', '') - # build a data proxy_data = { 'headers': { - "User-Agent": "php-httpproxy/0.1.6 (Client; Python " + python_version() + "; Caterpillar; abuse@catswords.net)", + "User-Agent": f"php-httpproxy/0.1.6 (Client; Python {python_version()}; Caterpillar; abuse@catswords.net)", }, 'data': { "to": to, @@ -71,22 +66,40 @@ class CaterpillarSMTPServer(SMTPServer): } _, raw_data = jsonrpc2_encode('relay_sendmail', proxy_data['data']) - # send HTTP POST request try: - response = requests.post(server_url, headers=proxy_data['headers'], data=raw_data, auth=auth) + response = await asyncio.to_thread( + requests.post, + server_url, + headers=proxy_data['headers'], + data=raw_data, + auth=auth + ) if response.status_code == 200: - type, id, method, rpcdata = jsonrpc2_decode(response.text) + type, id, rpcdata = jsonrpc2_decode(response.text) if rpcdata['success']: logger.info("[*] Email sent successfully.") else: - raise Exception("(%s) %s" % (str(rpcdata['code']), rpcdata['message'])) + raise Exception(f"({rpcdata['code']}) {rpcdata['message']}") else: - raise Exception("Status %s" % (str(response.status_code))) + raise Exception(f"Status {response.status_code}") except Exception as e: logger.error("[*] Failed to send email", exc_info=e) + return '500 Could not process your message. ' + str(e) -# Start SMTP server -smtp_server = CaterpillarSMTPServer((smtp_host, smtp_port), None) + return '250 OK' -# Start asynchronous event loop -asyncore.loop() +# https://aiosmtpd-pepoluan.readthedocs.io/en/latest/migrating.html +def main(): + handler = CaterpillarSMTPHandler() + controller = Controller(handler, hostname=smtp_host, port=smtp_port) + # Run the event loop in a separate thread. + controller.start() + # Wait for the user to press Return. + input('SMTP server running. Press Return to stop server and exit.') + controller.stop() + logger.warning("[*] User has requested an interrupt") + logger.warning("[*] Application Exiting.....") + sys.exit() + +if __name__ == "__main__": + main()