Skip to content
This repository was archived by the owner on Mar 27, 2025. It is now read-only.

Commit 36bdb1c

Browse files
committed
Implement asyncio receivers
1 parent ba9b37f commit 36bdb1c

File tree

2 files changed

+80
-26
lines changed

2 files changed

+80
-26
lines changed

salmon/server.py

+79-26
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@
1111
import time
1212
import traceback
1313

14+
from aiosmtpd.controller import Controller
15+
from aiosmtpd.lmtp import LMTP
16+
from aiosmtpd.smtp import SMTP
1417
from dns import resolver
1518
import lmtpd
1619

1720
from salmon import __version__, mail, queue, routing
1821
from salmon.bounce import COMBINED_STATUS_CODES, PRIMARY_STATUS_CODES, SECONDARY_STATUS_CODES
1922

20-
lmtpd.__version__ = "Salmon Mail router LMTPD, version %s" % __version__
21-
smtpd.__version__ = "Salmon Mail router SMTPD, version %s" % __version__
23+
ROUTER_VERSION_STRING = "Salmon Mail router, version %s" % __version__
24+
SMTP_MULTIPLE_RCPTS_ERROR = "451 Will not accept multiple recipients in one transaction"
25+
26+
lmtpd.__version__ = ROUTER_VERSION_STRING
27+
smtpd.__version__ = ROUTER_VERSION_STRING
2228

2329

2430
def undeliverable_message(raw_message, failure_type):
@@ -153,6 +159,19 @@ def send(self, To, From, Subject, Body):
153159
self.deliver(msg)
154160

155161

162+
def _deliver(receiver, Peer, From, To, Data, **kwargs):
163+
try:
164+
logging.debug("Message received from Peer: %r, From: %r, to To %r.", Peer, From, To)
165+
routing.Router.deliver(mail.MailRequest(Peer, From, To, Data))
166+
except SMTPError as err:
167+
# looks like they want to return an error, so send it out
168+
return str(err)
169+
except Exception:
170+
logging.exception("Exception while processing message from Peer: %r, From: %r, to To %r.",
171+
Peer, From, To)
172+
undeliverable_message(Data, "Error in message %r:%r:%r, look in logs." % (Peer, From, To))
173+
174+
156175
class SMTPChannel(smtpd.SMTPChannel):
157176
"""Replaces the standard SMTPChannel with one that rejects more than one recipient"""
158177

@@ -175,7 +194,7 @@ def smtp_RCPT(self, arg):
175194
# Of course, if smtpd.SMTPServer or SMTPReceiver implemented a
176195
# queue and bounces like you're meant too...
177196
logging.warning("Client attempted to deliver mail with multiple RCPT TOs. This is not supported.")
178-
self.push("451 Will not accept multiple recipients in one transaction")
197+
self.push(SMTP_MULTIPLE_RCPTS_ERROR)
179198
else:
180199
smtpd.SMTPChannel.smtp_RCPT(self, arg)
181200

@@ -216,17 +235,7 @@ def process_message(self, Peer, From, To, Data, **kwargs):
216235
"""
217236
Called by smtpd.SMTPServer when there's a message received.
218237
"""
219-
220-
try:
221-
logging.debug("Message received from Peer: %r, From: %r, to To %r.", Peer, From, To)
222-
routing.Router.deliver(mail.MailRequest(Peer, From, To, Data))
223-
except SMTPError as err:
224-
# looks like they want to return an error, so send it out
225-
return str(err)
226-
except Exception:
227-
logging.exception("Exception while processing message from Peer: %r, From: %r, to To %r.",
228-
Peer, From, To)
229-
undeliverable_message(Data, "Error in message %r:%r:%r, look in logs." % (Peer, From, To))
238+
return _deliver(self, Peer, From, To, Data, **kwargs)
230239

231240
def close(self):
232241
"""Doesn't do anything except log who called this, since nobody should. Ever."""
@@ -268,25 +277,69 @@ def process_message(self, Peer, From, To, Data, **kwargs):
268277
"""
269278
Called by lmtpd.LMTPServer when there's a message received.
270279
"""
271-
272-
try:
273-
logging.debug("Message received from Peer: %r, From: %r, to To %r.", Peer, From, To)
274-
routing.Router.deliver(mail.MailRequest(Peer, From, To, Data))
275-
except SMTPError as err:
276-
# looks like they want to return an error, so send it out
277-
# and yes, you should still use SMTPError in your handlers
278-
return str(err)
279-
except Exception:
280-
logging.exception("Exception while processing message from Peer: %r, From: %r, to To %r.",
281-
Peer, From, To)
282-
undeliverable_message(Data, "Error in message %r:%r:%r, look in logs." % (Peer, From, To))
280+
return _deliver(self, Peer, From, To, Data, **kwargs)
283281

284282
def close(self):
285283
"""Doesn't do anything except log who called this, since nobody should. Ever."""
286284
trace = traceback.format_exc(chain=False)
287285
logging.error(trace)
288286

289287

288+
class SMTPOnlyOneRcpt(SMTP):
289+
async def smtp_RCPT(self, arg):
290+
if self.envelope.rcpt_tos:
291+
await self.push(SMTP_MULTIPLE_RCPTS_ERROR)
292+
else:
293+
await super().smtp_RCPT(arg)
294+
295+
296+
class SMTPHandler:
297+
async def handle_DATA(self, server, session, envelope):
298+
assert len(envelope.rcpt_tos) == 1, "There should only be one RCPT TO"
299+
return _deliver(self, session.peer, envelope.mail_from, envelope.rcpt_tos[0], envelope.content)
300+
301+
302+
class AsyncSMTPReceiver(Controller):
303+
"""Receives emails and hands it to the Router for further processing."""
304+
def __init__(self, handler=None, **kwargs):
305+
if handler is None:
306+
handler = SMTPHandler()
307+
super().__init__(handler, **kwargs)
308+
309+
def factory(self):
310+
return SMTPOnlyOneRcpt(self.handler, enable_SMTPUTF8=self.enable_SMTPUTF8, ident=ROUTER_VERSION_STRING)
311+
312+
def stop(self):
313+
"""Doesn't do anything except log who called this, since nobody should. Ever."""
314+
trace = traceback.format_exc(chain=False)
315+
logging.error(trace)
316+
317+
318+
class LMTPHandler:
319+
async def handle_DATA(self, server, session, envelope):
320+
statuses = []
321+
for rcpt in envelope.rcpt_tos:
322+
statuses.append(_deliver(self, session.peer, envelope.mail_from, rcpt, envelope.content) or "250 Ok")
323+
return "\r\n".join(statuses)
324+
325+
326+
class AsyncLMTPReceiver(Controller):
327+
"""Receives emails and hands it to the Router for further processing."""
328+
# TODO: override Controller._run and make it choose between create_server and create_unix_server
329+
def __init__(self, handler=None, **kwargs):
330+
if handler is None:
331+
handler = LMTPHandler()
332+
super().__init__(handler, **kwargs)
333+
334+
def factory(self):
335+
return LMTP(self.handler, enable_SMTPUTF8=self.enable_SMTPUTF8, ident=ROUTER_VERSION_STRING)
336+
337+
def stop(self):
338+
"""Doesn't do anything except log who called this, since nobody should. Ever."""
339+
trace = traceback.format_exc(chain=False)
340+
logging.error(trace)
341+
342+
290343
class QueueReceiver:
291344
"""
292345
Rather than listen on a socket this will watch a queue directory and

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import versioneer
44

55
install_requires = [
6+
'aiosmtpd',
67
'chardet',
78
'click',
89
'dnspython',

0 commit comments

Comments
 (0)