Part of #727. Depends on #728 for the reply format this parses (can be developed in parallel using a hand-crafted test fixture).
Summary
Add IMAP handling for a quiz <token> reply: parse which answers were marked, submit through the existing quiz scoring/grading path, and email the learner their result.
Where this plugs in
ImapInterface.handle_email_message (django_email_learning/services/defaults/imap_interface.py) currently recognizes three subject-line commands: enroll <slug>, verify <code>, drop <slug> (_ACCEPTED_COMMANDS-style dispatch, each backed by a small _enroll/_verify/_drop method). Add quiz as a fourth accepted command, dispatching to a new _quiz(argument, imap_connection, email_message) method.
- This is the first time IMAP handling needs to parse an email body — today only the subject line and
From header are read. email_message (an email.message.EmailMessage, policy=default) needs its text body extracted and scanned for [x]/[ ] marks per question.
- Grading/submission:
QuizSubmissionView.process_quiz_submission(token, answers) (django_email_learning/personalised/api/views.py) already implements JWT decoding, enrollment/attempt validation, scoring (calculate_score_and_passed), and QuizSubmission creation, shared today between the AMP view and the plain click-through view. This logic currently lives as a classmethod on a view class — consider extracting the reusable core (decode token → validate → score → persist) into a plain function/service so the IMAP handler can call it without depending on view-layer code.
Behavior
- Decode
<token> from the subject the same way SendQuizCommand/DeliverySchedule.generate_link() encode it (delivery id/hash, optional question_ids).
- Extract the email body and parse marked answers per question. Needs to tolerate:
- Whitespace variants:
[x], [ x ], [X].
- Quoted-reply artifacts: leading
> on quoted lines, mail clients that reflow or HTML-ify plain text on reply.
- Validate the sender's
From address matches the learner on the delivery's enrollment — same identity check already used for enroll/verify/drop.
- On successful, unambiguous parse: submit via the shared scoring path (same one AMP/web use), then send the learner a new result email (score, pass/fail, any retry/next-steps messaging the existing web flow already surfaces) — needs a new email template and likely a new command model (e.g.
SendQuizReplyResultCommand), following the existing send_*_command.py pattern.
- On failure to parse (ambiguous/missing marks, can't decode token, learner mismatch, etc.): log via the metric recorder (extend or reuse
imap_command_handling_failed) and take no further action — no auto-reply, consistent with how other IMAP command failures are handled today (CheckIMAPJob, django_email_learning/jobs/check_imap_job.py).
Out of scope
Tests
- Extend
tests/services/defaults/test_imap_interface.py with cases for: well-formed single-answer reply, well-formed multi-answer reply, ambiguous/ malformed reply (should silently no-op + log), sender email mismatch, expired/invalid token.
- New test coverage (new file, alongside
tests/services/command_models/test_send_quiz_command.py) for the result-notification command/template.
- Confirm scoring output for a reply-submitted quiz matches what the equivalent AMP/web submission would produce, given the same answers.
Part of #727. Depends on #728 for the reply format this parses (can be developed in parallel using a hand-crafted test fixture).
Summary
Add IMAP handling for a
quiz <token>reply: parse which answers were marked, submit through the existing quiz scoring/grading path, and email the learner their result.Where this plugs in
ImapInterface.handle_email_message(django_email_learning/services/defaults/imap_interface.py) currently recognizes three subject-line commands:enroll <slug>,verify <code>,drop <slug>(_ACCEPTED_COMMANDS-style dispatch, each backed by a small_enroll/_verify/_dropmethod). Addquizas a fourth accepted command, dispatching to a new_quiz(argument, imap_connection, email_message)method.Fromheader are read.email_message(anemail.message.EmailMessage, policy=default) needs its text body extracted and scanned for[x]/[ ]marks per question.QuizSubmissionView.process_quiz_submission(token, answers)(django_email_learning/personalised/api/views.py) already implements JWT decoding, enrollment/attempt validation, scoring (calculate_score_and_passed), andQuizSubmissioncreation, shared today between the AMP view and the plain click-through view. This logic currently lives as a classmethod on a view class — consider extracting the reusable core (decode token → validate → score → persist) into a plain function/service so the IMAP handler can call it without depending on view-layer code.Behavior
<token>from the subject the same waySendQuizCommand/DeliverySchedule.generate_link()encode it (delivery id/hash, optionalquestion_ids).[x],[ x ],[X].>on quoted lines, mail clients that reflow or HTML-ify plain text on reply.Fromaddress matches the learner on the delivery's enrollment — same identity check already used forenroll/verify/drop.SendQuizReplyResultCommand), following the existingsend_*_command.pypattern.imap_command_handling_failed) and take no further action — no auto-reply, consistent with how other IMAP command failures are handled today (CheckIMAPJob,django_email_learning/jobs/check_imap_job.py).Out of scope
Tests
tests/services/defaults/test_imap_interface.pywith cases for: well-formed single-answer reply, well-formed multi-answer reply, ambiguous/ malformed reply (should silently no-op + log), sender email mismatch, expired/invalid token.tests/services/command_models/test_send_quiz_command.py) for the result-notification command/template.