122 lines
3.8 KiB
Python
122 lines
3.8 KiB
Python
import asyncio
|
|
import logging
|
|
from email import message_from_bytes
|
|
|
|
from aiosmtpd.controller import Controller
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
LISTEN_HOST = "0.0.0.0"
|
|
LISTEN_PORT = 25
|
|
FORWARD_HOST = "smtp2http"
|
|
FORWARD_PORT = 25
|
|
|
|
class MTAHandler:
|
|
async def handle_DATA(self, server, session, envelope):
|
|
try:
|
|
peer = session.peer
|
|
client_ip = peer[0] if peer else "unknown"
|
|
|
|
logger.info(f"Received email from {client_ip}: {envelope.mail_from} -> {envelope.rcpt_tos}")
|
|
|
|
message_data = envelope.content
|
|
|
|
msg = message_from_bytes(message_data)
|
|
|
|
# Security: Remove any existing Received headers to prevent spoofing
|
|
# Only the last MTA's Received header should be trusted
|
|
if 'Received' in msg:
|
|
del msg['Received']
|
|
|
|
msg['Received'] = client_ip
|
|
|
|
await self.forward_email(
|
|
msg,
|
|
envelope.mail_from,
|
|
envelope.rcpt_tos
|
|
)
|
|
|
|
return '250 OK: Message accepted for delivery'
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing email: {e}", exc_info=True)
|
|
return '451 Requested action aborted: error in processing'
|
|
|
|
async def forward_email(self, msg, mail_from, rcpt_tos):
|
|
try:
|
|
reader, writer = await asyncio.open_connection(FORWARD_HOST, FORWARD_PORT)
|
|
|
|
# Read banner
|
|
banner = await reader.readline()
|
|
logger.debug(f"smtp2http banner: {banner.decode().strip()}")
|
|
|
|
# Send EHLO
|
|
writer.write(b"EHLO mta\r\n")
|
|
await writer.drain()
|
|
response = await reader.readline()
|
|
logger.debug(f"EHLO response: {response.decode().strip()}")
|
|
|
|
# Send MAIL FROM
|
|
writer.write(f"MAIL FROM:<{mail_from}>\r\n".encode())
|
|
await writer.drain()
|
|
response = await reader.readline()
|
|
logger.debug(f"MAIL FROM response: {response.decode().strip()}")
|
|
|
|
# Send RCPT TO for each recipient
|
|
for recipient in rcpt_tos:
|
|
writer.write(f"RCPT TO:<{recipient}>\r\n".encode())
|
|
await writer.drain()
|
|
response = await reader.readline()
|
|
logger.debug(f"RCPT TO response: {response.decode().strip()}")
|
|
|
|
# Send DATA command
|
|
writer.write(b"DATA\r\n")
|
|
await writer.drain()
|
|
response = await reader.readline()
|
|
logger.debug(f"DATA response: {response.decode().strip()}")
|
|
|
|
email_content = msg.as_string().encode('utf-8')
|
|
writer.write(email_content)
|
|
|
|
if not email_content.endswith(b"\r\n"):
|
|
writer.write(b"\r\n")
|
|
writer.write(b".\r\n")
|
|
await writer.drain()
|
|
response = await reader.readline()
|
|
logger.debug(f"Message response: {response.decode().strip()}")
|
|
|
|
# Send QUIT
|
|
writer.write(b"QUIT\r\n")
|
|
await writer.drain()
|
|
response = await reader.readline()
|
|
logger.debug(f"QUIT response: {response.decode().strip()}")
|
|
|
|
writer.close()
|
|
await writer.wait_closed()
|
|
|
|
logger.info(f"Successfully forwarded email from {mail_from} to {rcpt_tos}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to forward email: {e}", exc_info=True)
|
|
|
|
async def main():
|
|
handler = MTAHandler()
|
|
|
|
controller = Controller(
|
|
handler,
|
|
hostname=LISTEN_HOST,
|
|
port=LISTEN_PORT
|
|
)
|
|
|
|
controller.start()
|
|
logger.info(f"MTA listening on {LISTEN_HOST}:{LISTEN_PORT}")
|
|
|
|
try:
|
|
await asyncio.Event().wait()
|
|
finally:
|
|
controller.stop()
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|