from email import message_from_file as _message_from_file
from email.header import decode_header as _decode_header
+from email.utils import formatdate as _formatdate
import hashlib as _hashlib
import locale as _locale
import mailbox as _mailbox
from . import LOG as _LOG
from .color import standard_colors as _standard_colors
from .color import color_string as _color_string
+from .email import construct_response as _construct_response
from .extract_mime import extract_mime as _extract_mime
from .extract_mime import message_time as _message_time
+from .model.person import Person as _Person
from .storage import assignment_path as _assignment_path
from .storage import set_late as _set_late
def mailpipe(basedir, course, stream=None, mailbox=None, input_=None,
- output=None, max_late=0, use_color=None, dry_run=False, **kwargs):
+ output=None, max_late=0, respond=None, use_color=None,
+ dry_run=False, **kwargs):
"""Run from procmail to sort incomming submissions
For example, you can setup your ``.procmailrc`` like this::
>>> message['Received'] = (
... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
- ... 'for <wking@tremily.us>; Mon, 09 Oct 2011 11:50:46 -0400 (EDT)')
+ ... 'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
>>> message['From'] = 'Billy B <bb@greyhavens.net>'
- >>> message['To'] = 'S <eye@tower.edu>'
+ >>> message['To'] = 'phys101 <phys101@tower.edu>'
>>> message['Subject'] = 'assignment 1 submission'
>>> messages = [message]
>>> ms = MessageSender(address=('localhost', 1025), messages=messages)
mail/new/...
mail/tmp
+ You can send receipts to the acknowledge incoming messages, which
+ includes warnings about dropped messages (except for messages
+ without ``Return-Path`` and messages where the ``Return-Path``
+ email belongs to multiple ``People``. Both of these cases should
+ only come from problems with pygrader configuration).
+
+ Response to a successful submission:
+
+ >>> def respond(message):
+ ... print('respond with:\\n{}'.format(message.as_string()))
+ >>> def process(peer, mailfrom, rcpttos, data):
+ ... mailpipe(
+ ... basedir=course.basedir, course=course.course,
+ ... stream=StringIO(data), output=course.mailbox,
+ ... respond=respond)
+ >>> server = SMTPServer(
+ ... ('localhost', 1025), None, process=process, count=1)
+ >>> del message['Message-ID']
+ >>> message['Message-ID'] = '<hgi.jlk@home.net>'
+ >>> messages = [message]
+ >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
+ >>> loop() # doctest: +REPORT_UDIFF, +ELLIPSIS
+ respond with:
+ Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ Date: ...
+ From: Robot101 <phys101@tower.edu>
+ Reply-to: Robot101 <phys101@tower.edu>
+ To: Bilbo Baggins <bb@shire.org>
+ Subject: received Assignment 1 submission
+ <BLANKLINE>
+ --===============...==
+ Content-Type: multipart/mixed; boundary="===============...=="
+ MIME-Version: 1.0
+ <BLANKLINE>
+ --===============...==
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ Billy,
+ <BLANKLINE>
+ We received your submission for Assignment 1 on Mon, 10 Oct 2011 16:50:46 -0000.
+ <BLANKLINE>
+ Yours,
+ phys-101 robot
+ --===============...==
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ From: Billy B <bb@greyhavens.net>
+ To: phys101 <phys101@tower.edu>
+ Subject: assignment 1 submission
+ Return-Path: <bb@greyhavens.net>
+ Received: from smtp.mail.uu.edu (localhost.localdomain [127.0.0.1]) by smtp.mail.uu.edu (Postfix) with SMTP id 68CB45C8453 for <wking@tremily.us>; Mon, 10 Oct 2011 12:50:46 -0400 (EDT)
+ Received: from smtp.home.net (smtp.home.net [123.456.123.456]) by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF for <wking@tremily.us>; Mon, 09 Oct 2011 11:50:46 -0400 (EDT)
+ Message-ID: <hgi.jlk@home.net>
+ <BLANKLINE>
+ The answer is 42.
+ --===============...==--
+ --===============...==
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Description: OpenPGP digital signature
+ Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
+ <BLANKLINE>
+ -----BEGIN PGP SIGNATURE-----
+ Version: GnuPG v2.0.17 (GNU/Linux)
+ <BLANKLINE>
+ ...
+ -----END PGP SIGNATURE-----
+ <BLANKLINE>
+ --===============...==--
+
+ Response to a bad subject:
+
+ >>> server = SMTPServer(
+ ... ('localhost', 1025), None, process=process, count=1)
+ >>> del message['Subject']
+ >>> message['Subject'] = 'need help for the first homework'
+ >>> messages = [message]
+ >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
+ >>> loop() # doctest: +REPORT_UDIFF, +ELLIPSIS
+ respond with:
+ Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ Date: ...
+ From: Robot101 <phys101@tower.edu>
+ Reply-to: Robot101 <phys101@tower.edu>
+ To: Bilbo Baggins <bb@shire.org>
+ Subject: received 'need help for the first homework'
+ <BLANKLINE>
+ --===============...==
+ Content-Type: multipart/mixed; boundary="===============...=="
+ MIME-Version: 1.0
+ <BLANKLINE>
+ --===============...==
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ Billy,
+ <BLANKLINE>
+ We got an email from you with the following subject:
+ 'need help for the first homework'
+ which does not match any submittable assignment name for
+ phys101.
+ Remember to use the full name for the assignment in the
+ subject. For example:
+ Assignment 1 submission
+ <BLANKLINE>
+ Yours,
+ phys-101 robot
+ --===============...==
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ From: Billy B <bb@greyhavens.net>
+ To: phys101 <phys101@tower.edu>
+ Return-Path: <bb@greyhavens.net>
+ Received: from smtp.mail.uu.edu (localhost.localdomain [127.0.0.1]) by smtp.mail.uu.edu (Postfix) with SMTP id 68CB45C8453 for <wking@tremily.us>; Mon, 10 Oct 2011 12:50:46 -0400 (EDT)
+ Received: from smtp.home.net (smtp.home.net [123.456.123.456]) by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF for <wking@tremily.us>; Mon, 09 Oct 2011 11:50:46 -0400 (EDT)
+ Message-ID: <hgi.jlk@home.net>
+ Subject: need help for the first homework
+ <BLANKLINE>
+ The answer is 42.
+ --===============...==--
+ --===============...==
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Description: OpenPGP digital signature
+ Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
+ <BLANKLINE>
+ -----BEGIN PGP SIGNATURE-----
+ Version: GnuPG v2.0.17 (GNU/Linux)
+ <BLANKLINE>
+ ...
+ -----END PGP SIGNATURE-----
+ <BLANKLINE>
+ --===============...==--
+
+ >>> del message['Return-Path']
+ >>> message['Return-Path'] = '<bb@greyhavens.net>'
+
>>> course.cleanup()
"""
if stream is None:
stream = _sys.stdin
for msg,person,assignment,time in _load_messages(
course=course, stream=stream, mailbox=mailbox, input_=input_,
- output=output, use_color=use_color, dry_run=dry_run):
+ output=output, respond=respond, use_color=use_color, dry_run=dry_run):
assignment_path = _assignment_path(basedir, assignment, person)
_save_local_message_copy(
msg=msg, person=person, assignment_path=assignment_path,
max_late=max_late, use_color=use_color, dry_run=dry_run)
def _load_messages(course, stream, mailbox=None, input_=None, output=None,
- use_color=None, dry_run=False):
+ respond=None, use_color=None, dry_run=False):
if mailbox is None:
mbox = None
messages = [(None,_message_from_file(stream))]
raise ValueError(mailbox)
for key,msg in messages:
ret = _parse_message(
- course=course, msg=msg, use_color=use_color)
+ course=course, msg=msg, respond=respond, use_color=use_color)
if ret:
if output is not None and dry_run is False:
# move message from input mailbox to output mailbox
del mbox[key]
yield ret
-def _parse_message(course, msg, use_color=None):
+def _parse_message(course, msg, respond=None, use_color=None):
highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
mid = msg['Message-ID']
sender = msg['Return-Path'] # RFC 822
string='no Return-Path in {}'.format(mid), color=lowlight))
return None
sender = sender[1:-1] # strip wrapping '<' and '>'
+ time = _message_time(message=msg, use_color=use_color)
+
+ if respond:
+ if time:
+ time_str = _formatdate(time)
+ else:
+ time_str = 'unknown time'
+ response_subject = 'received {} at {}'.format(mid, time_str)
people = list(course.find_people(email=sender))
if len(people) == 0:
_LOG.warn(_color_string(
string='no person found to match {}'.format(sender),
color=bad))
+ if respond:
+ person = _Person(name=None, emails=[sender])
+ response_text = (
+ '{},\n\n'
+ 'Your email address is not registered with pygrader for\n'
+ '{}. If you feel it should be, contact your professor\n'
+ 'or TA.\n\n'
+ 'Yours,\n{}').format(
+ sender, course.robot.alias())
+ response_text = 'Address {} is not registered for {}.'.format(
+ sender, course.name)
+ response = _construct_response(
+ author=course.robot, targets=[person],
+ subject=response_subject, text=response_text, original=msg)
+ respond(response)
return None
if len(people) > 1:
_LOG.warn(_color_string(
if person.pgp_key:
msg = _get_verified_message(msg, person.pgp_key, use_color=use_color)
if msg is None:
+ if respond:
+ response_text = (
+ '{},\n\n'
+ 'We received an email message from you without a valid\n'
+ 'PGP signature.\n\n'
+ 'Yours,\n{}').format(
+ person.alias(), course.robot.alias())
+ response_text = 'Message not signed by {}.'.format(
+ person.pgp_key)
+ response = _construct_response(
+ author=course.robot, targets=[person],
+ subject=response_subject, text=response_text, original=msg)
+ respond(response)
return None
if msg['Subject'] is None:
_LOG.warn(_color_string(
string='no subject in {}'.format(mid), color=bad))
+ if respond:
+ response_text = (
+ '{},\n\n'
+ 'We received an email message from you without a subject.\n\n'
+ 'Yours,\n{}').format(
+ person.alias(), course.robot.alias())
+ response = _construct_response(
+ author=course.robot, targets=[person],
+ subject=response_subject, text=response_text, original=msg)
+ respond(response)
return None
parts = _decode_header(msg['Subject'])
if len(parts) != 1:
if encoding is None:
encoding = 'ascii'
_LOG.debug('decoded header {} -> {}'.format(parts[0], subject))
+
subject = subject.lower().replace('#', '')
for assignment in course.assignments:
if _match_assignment(assignment, subject):
_LOG.warn(_color_string(
string='no assignment found in {}'.format(repr(subject)),
color=bad))
+ if respond:
+ response_subject = "received '{}'".format(subject)
+ submittable_assignments = [
+ a for a in course.assignments if a.submittable]
+ if not submittable_assignments:
+ hint = (
+ 'In fact, there are no submittable assignments for\n'
+ 'this course!\n')
+ else:
+ hint = (
+ 'Remember to use the full name for the assignment in the\n'
+ 'subject. For example:\n'
+ ' {} submission\n\n').format(
+ submittable_assignments[0].name)
+ response_text = (
+ '{},\n\n'
+ 'We got an email from you with the following subject:\n'
+ ' {}\n'
+ 'which does not match any submittable assignment name for\n'
+ '{}.\n'
+ '{}'
+ 'Yours,\n{}').format(
+ person.alias(), repr(subject), course.name, hint,
+ course.robot.alias())
+ response = _construct_response(
+ author=course.robot, targets=[person],
+ subject=response_subject, text=response_text, original=msg)
+ respond(response)
return None
- time = _message_time(message=msg, use_color=use_color)
+ if respond:
+ response_subject = 'received {} submission'.format(assignment.name)
+ response_text = (
+ '{},\n\n'
+ 'We received your submission for {} on {}.\n\n'
+ 'Yours,\n{}').format(
+ person.alias(), assignment.name, time_str, course.robot.alias())
+ response = _construct_response(
+ author=course.robot, targets=[person],
+ subject=response_subject, text=response_text, original=msg)
+ respond(response)
return (msg, person, assignment, time)
def _match_assignment(assignment, subject):