--- /dev/null
+# Copyright
+
+import asynchat as _asynchat
+import socket as _socket
+
+from pgp_mime import email as _email
+
+from .. import LOG as _LOG
+
+
+class MessageSender (_asynchat.async_chat):
+ """A SMTP message sender using ``asyncore``.
+
+ To test ``PygraderServer``, it's useful to have a message-sender
+ that also uses ``asyncore``. This avoids the need to use
+ multithreaded tests.
+ """
+ def __init__(self, address, messages):
+ super(MessageSender, self).__init__()
+ self.address = address
+ self.messages = messages
+ self.create_socket(_socket.AF_INET, _socket.SOCK_STREAM)
+ self.connect(address)
+ self.intro = None
+ self.ilines = []
+ self.ibuffer = []
+ self.set_terminator(b'\r\n')
+ if self.messages:
+ self.callback = (self.send_message, [], {})
+ else:
+ self.callback = (self.quit_callback, [], {})
+ self.send_command('ehlo [127.0.0.1]')
+
+ def log_info(self, message, type='info'):
+ # TODO: type -> severity
+ _LOG.info(message)
+
+ def send_command(self, command, clear_command_list=True):
+ if clear_command_list:
+ self.commands = [command]
+ self.responses = []
+ _LOG.debug('push: {}'.format(command))
+ self.push(bytes(command + '\r\n', 'ascii'))
+
+ def send_commands(self, commands):
+ self.commands = commands
+ self.responses = []
+ for command in self.commands:
+ self.send_command(command=command, clear_command_list=False)
+
+ def collect_incoming_data(self, data):
+ self.ibuffer.append(data)
+
+ def found_terminator(self):
+ ibuffer = b''.join(self.ibuffer)
+ self.ibuffer = []
+ self.ilines.append(ibuffer)
+ if len(self.ilines[-1]) >= 4 and self.ilines[-1][3] == ord(b' '):
+ response = self.ilines
+ self.ilines = []
+ self.handle_response(response)
+
+ def handle_response(self, response):
+ _LOG.debug('handle response: {}'.format(response))
+ code = int(response[-1][:3])
+ if not self.intro:
+ self.intro = (code, response)
+ else:
+ self.responses.append((code, response))
+ if len(self.responses) == len(self.commands):
+ if self.callback:
+ callback,args,kwargs = self.callback
+ self.callback = None
+ commands = self.commands
+ self.commands = []
+ responses = self.responses
+ self.responses = []
+ _LOG.debug('callback: ({}, {})'.format(callback, list(zip(commands, responses))))
+ callback(commands, responses, *args, **kwargs)
+ else:
+ self.close()
+
+ def close_callback(self, commands, responses):
+ _LOG.debug(commands)
+ _LOG.debug(responses)
+ self.close()
+
+ def quit_callback(self, commands, responses):
+ self.send_command('quit')
+ self.callback = (self.close_callback, [], {})
+
+ def send_message(self, commands, responses):
+ message = self.messages.pop(0)
+ if self.messages:
+ self.callback = (self.send_message, [], {})
+ else:
+ self.callback = (self.quit_callback, [], {})
+ sources = list(_email.email_sources(message))
+ commands = [
+ 'mail FROM:<{}>'.format(sources[0][1])
+ ]
+ for name,address in _email.email_targets(message):
+ commands.append('rcpt TO:<{}>'.format(address))
+ commands.extend([
+ 'DATA',
+ message.as_string() + '\r\n.',
+ ])
+ self.send_commands(commands=commands)
--- /dev/null
+# Copyright
+
+import asyncore as _asyncore
+import email as _email
+import smtpd as _smptd
+import socket as _socket
+
+from .. import LOG as _LOG
+
+
+class SMTPChannel (_smptd.SMTPChannel):
+ def close(self):
+ super(SMTPChannel, self).close()
+ _LOG.debug('close {}'.format(self))
+ self.smtp_server.channel_closed()
+
+
+class SMTPServer (_smptd.SMTPServer):
+ """An SMTP server for testing pygrader.
+
+ >>> from asyncore import loop
+ >>> from smtplib import SMTP
+ >>> from pgp_mime.email import encodedMIMEText
+ >>> from pygrader.test.client import MessageSender
+
+ >>> def process(peer, mailfrom, rcpttos, data):
+ ... print('peer: {}'.format(peer))
+ ... print('mailfrom: {}'.format(mailfrom))
+ ... print('rcpttos: {}'.format(rcpttos))
+ ... print('message:')
+ ... print(data)
+ >>> server = SMTPServer(
+ ... ('localhost', 1025), None, process=process, count=3)
+
+ >>> message = encodedMIMEText('Ping')
+ >>> message['From'] = 'a@example.com'
+ >>> message['To'] = 'b@example.com, c@example.com'
+ >>> message['Cc'] = 'd@example.com'
+ >>> messages = [message, message, message]
+ >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
+ >>> loop() # doctest: +REPORT_UDIFF, +ELLIPSIS
+ peer: ('127.0.0.1', ...)
+ mailfrom: a@example.com
+ rcpttos: ['b@example.com', 'c@example.com', 'd@example.com']
+ message:
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ From: a@example.com
+ To: b@example.com, c@example.com
+ Cc: d@example.com
+ <BLANKLINE>
+ Ping
+ peer: ('127.0.0.1', ...)
+ mailfrom: a@example.com
+ rcpttos: ['b@example.com', 'c@example.com', 'd@example.com']
+ message:
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ From: a@example.com
+ To: b@example.com, c@example.com
+ Cc: d@example.com
+ <BLANKLINE>
+ Ping
+ peer: ('127.0.0.1', ...)
+ mailfrom: a@example.com
+ rcpttos: ['b@example.com', 'c@example.com', 'd@example.com']
+ message:
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ From: a@example.com
+ To: b@example.com, c@example.com
+ Cc: d@example.com
+ <BLANKLINE>
+ Ping
+ """
+ channel_class = SMTPChannel
+
+ def __init__(self, *args, **kwargs):
+ self.count = kwargs.pop('count', None)
+ self.process = kwargs.pop('process', None)
+ self.channels_open = 0
+ super(SMTPServer, self).__init__(*args, **kwargs)
+
+ def log_info(self, message, type='info'):
+ # TODO: type -> severity
+ _LOG.info(message)
+
+ def handle_accepted(self, conn, addr):
+ if self.count <= 0:
+ conn.close()
+ return
+ super(SMTPServer, self).handle_accepted(conn, addr)
+ self.channels_open += 1
+
+ def channel_closed(self):
+ self.channels_open -= 1
+ if self.channels_open == 0 and self.count <= 0:
+ _LOG.debug('close {}'.format(self))
+ self.close()
+
+ def process_message(self, peer, mailfrom, rcpttos, data):
+ if self.count is not None:
+ self.count -= 1
+ _LOG.debug('Count: {}'.format(self.count))
+ _LOG.debug('receiving message from: {}'.format(peer))
+ _LOG.debug('message addressed from: {}'.format(mailfrom))
+ _LOG.debug('message addressed to : {}'.format(rcpttos))
+ _LOG.debug('message length : {}'.format(len(data)))
+ if self.process:
+ self.process(peer, mailfrom, rcpttos, data)
+ return