1 import os
2 import asyncio
3 import logging
4 import base64
5 from email import message_from_bytes
6 from email.message import Message
7 from datetime import datetime
8
9 import aiosmtpd
10 from aiosmtpd.controller import Controller
11 from aiosmtpd.smtp import SMTP as Server, syntax
12 from jinja2 import Template
13
14 mail_path = "mails"
15 hostname = "0.0.0.0"
16 port = 8025
17
18 html = """\
19 <!DOCTYPE html>
20 <html lang="en">
21 <head>
22 <meta charset="UTF-8">
23 <title>email</title>
24 </head>
25 <body>
26 <div><span>发件人: </span><span>{{ from_addr|e }}</span></div>
27 <div><span>收件人: </span><span>{{ to_addr|e }}</span></div>
28 <div><span>主题: </span><span>{{ subject }}</span></div>
29 <div>
30 {{ payload }}
31 </div>
32 </body>
33 </html>
34 """
35
36
37 class ExampleHandler:
38 async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
39 envelope.rcpt_tos.append(address)
40 return "250 OK"
41
42 async def handle_DATA(self, server, session, envelope: aiosmtpd.smtp.Envelope):
43 message: Message = message_from_bytes(envelope.content)
44 message_info = await self.parse_message(message)
45 template = Template(html)
46 if not os.path.exists(mail_path):
47 os.makedirs(mail_path)
48 with open(os.path.join(mail_path, f"mail_{datetime.now().strftime('%Y-%m-%d-%H_%M_%S_%f')[:-3]}.html"), "w") as f:
49 f.write(template.render(message_info))
50 return "250 Message accepted for delivery"
51
52 def get(self, message, item):
53 value = message.get(item)
54 try:
55 value = self.to_true_str(value)
56 except Exception:
57 pass
58 return value
59
60 async def parse_message(self, message: Message):
61 self.charset = message.get_content_charset() or "utf-8"
62 payload = message.get_payload()
63 subject = self.get(message, "Subject")
64 from_addr = self.get(message, "From")
65 to_addr = self.get(message, "To")
66 try:
67 if isinstance(payload, (list, tuple)):
68 payload = self.parse_payload(payload)
69 except Exception:
70 pass
71 return {"subject": subject, "payload": payload, "from_addr": from_addr, "to_addr": to_addr}
72
73 def parse_payload(self, payload):
74 # todo 暂时不处理附件的问题,目前仅处理 text/html 与 text/plain 共存的情况
75 data = None
76 for item in payload:
77 if isinstance(item, Message):
78 data = item.get_payload()
79 if item.get_content_type == "text/html":
80 break
81
82 try:
83 # 测试发现 html 有概率是转 base64
84 data = self.to_true_str(data)
85 except Exception:
86 pass
87
88 return data
89
90 def to_true_str(self, raw: str, charset=None):
91 if raw.startswith("=?"):
92 tmp_list = raw.split("?")
93 if len(tmp_list) > 2:
94 raw = tmp_list[-2]
95 charset = tmp_list[1]
96 else:
97 charset = self.charset
98 return base64.b64decode(raw).decode(charset)
99
100 async def handle_EHLO(self, *args, **kwargs):
101 return """\
102 250-mail
103 250-PIPELINING
104 250-AUTH LOGIN PLAIN
105 250-AUTH=LOGIN PLAIN
106 250-coremail
107 250-STARTTLS
108 250-SMTPUTF8
109 250 8BITMIME"""
110
111
112 class MyServer(Server):
113
114 @syntax("AUTH PLAIN")
115 @asyncio.coroutine
116 def smtp_AUTH(self, PLAIN, *args, **kwargs):
117 yield from self.push("235 auth successfully")
118
119 @syntax("EHLO hostname")
120 async def smtp_EHLO(self, hostname):
121 status = await self._call_handler_hook("EHLO", hostname)
122 self.session.host_name = hostname
123 await self.push(status)
124
125
126 class MyController(Controller):
127 def factory(self):
128 return MyServer(self.handler)
129
130
131 async def amain(loop):
132 controller = MyController(ExampleHandler(), hostname=hostname, port=port)
133 controller.start()
134
135
136 if __name__ == "__main__":
137 logging.basicConfig(level=logging.ERROR)
138 loop = asyncio.get_event_loop()
139 loop.create_task(amain(loop=loop))
140 try:
141 loop.run_forever()
142 except KeyboardInterrupt:
143 pass