diff --git a/mailman/patchset.py b/mailman/patchset.py
new file mode 100644
index 00000000..078452c4
--- /dev/null
+++ b/mailman/patchset.py
@@ -0,0 +1,89 @@
+import git
+import pathlib
+import sys
+import tempfile
+
+REMOTE_URL = "http://host.containers.internal:3366/cgi-bin/git-receive-pack/grading.git"  # NOQA: E501
+
+MAIL_DIR_ABSPATH = "/var/lib/email/mail"
+
+
+def try_or_false(do, exc):
+    try:
+        do()
+        return True
+    except exc as e:
+        print(e, file=sys.stderr)
+        return False
+
+
+def tag_and_push(repo_path, tag_name):
+    try:
+        repo = git.Repo(repo_path)
+        repo.create_tag(tag_name)
+        repo.create_remote("origin", REMOTE_URL)
+        repo.git.push("origin", tags=True)
+        return True
+    except git.GitCommandError as e:
+        print(e, file=sys.stderr)
+        return False
+
+
+def do_check(repo_path, cover_letter, patches):
+    repo = git.Repo.init(repo_path)
+    maildir = pathlib.Path(MAIL_DIR_ABSPATH)
+    author_args = ["-c", "user.name=Denis", "-c",
+                   "user.email=daemon@mailman.d"]
+    git_am_args = ["git", *author_args, "am", "--keep"]
+    whitespace_errors = []
+
+    def am_cover_letter(keep_empty=True):
+        args = git_am_args
+        if keep_empty:
+            args.append("--empty=keep")
+        repo.git.execute([*args, str(maildir/cover_letter.msg_id)])
+
+    if try_or_false(lambda: am_cover_letter(keep_empty=False),
+                    git.GitCommandError):
+        return "missing cover letter"
+
+    repo.git.execute(["git", *author_args, "am", "--abort"])
+    if not try_or_false(lambda: am_cover_letter(keep_empty=True),
+                        git.GitCommandError):
+        return ("missing cover letter and "
+                "first patch failed to apply")
+
+    for i, patch in enumerate(patches):
+        patch_abspath = str(maildir / patch.msg_id)
+
+        # Try and apply and fail if there are whitespace errors
+        def do_git_am(extra_args=[]):
+            repo.git.execute([*git_am_args, *extra_args, patch_abspath]),
+
+        # If this fails, the patch may apply with whitespace errors
+        if try_or_false(lambda: do_git_am(['--whitespace=error-all']),
+                        git.GitCommandError):
+            continue
+
+        repo.git.execute(["git", *author_args, "am", "--abort"])
+
+        # Try again, if we succeed, count this patch as a whitespace error
+        if try_or_false(lambda: do_git_am(), git.GitCommandError):
+            whitespace_errors.append(str(i+1))
+            continue
+
+        # If we still fail, the patch does not apply
+        return f'patch {i+1} failed to apply'
+
+    if whitespace_errors:
+        return ('whitespace error patch(es) '
+                f'{",".join(whitespace_errors)}')
+    else:
+        return 'pathchset applies'
+
+
+def check(cover_letter, patches, submission_id):
+    with tempfile.TemporaryDirectory() as repo_path:
+        status = do_check(repo_path, cover_letter, patches)
+        tag_and_push(repo_path, submission_id)
+    return status
diff --git a/mailman/submit.py b/mailman/submit.py
index a5db5c69..9f3fffb6 100755
--- a/mailman/submit.py
+++ b/mailman/submit.py
@@ -3,9 +3,8 @@
 import collections
 from pathlib import Path
 import sys
-import git
-import tempfile
 
+import patchset
 import db
 
 ASSIGNMENT_LIST = ["introductions",
@@ -13,19 +12,6 @@
                    "programming0", "programming1", "programming2",
                    "final0", "final1"]
 
-MAIL_DIR_ABSPATH = "/var/lib/email/mail"
-
-REMOTE_URL = "http://host.containers.internal:3366/cgi-bin/git-receive-pack/grading.git"  # NOQA: E501
-
-
-def try_or_false(do, exc):
-    try:
-        do()
-        return True
-    except exc as e:
-        print(e, file=sys.stderr)
-        return False
-
 
 Email = collections.namedtuple('Email', ['rcpt', 'msg_id'])
 
@@ -78,67 +64,8 @@ def main(argv):
         sub.save()
         return 0
 
-    with tempfile.TemporaryDirectory() as repo_path:
-        repo = git.Repo.init(repo_path)
-        maildir = Path(MAIL_DIR_ABSPATH)
-        author_args = ["-c", "user.name=Denis", "-c",
-                       "user.email=daemon@mailman.d"]
-        git_am_args = ["git", *author_args, "am", "--keep"]
-        whitespace_errors = []
-
-        def am_cover_letter(keep_empty=True):
-            args = git_am_args
-            if keep_empty:
-                args.append("--empty=keep")
-            repo.git.execute([*args, str(maildir/cover_letter.msg_id)])
-
-        if try_or_false(lambda: am_cover_letter(keep_empty=False),
-                        git.GitCommandError):
-            sub.status = "missing cover letter"
-            sub.save()
-            return 0
-        repo.git.execute(["git", *author_args, "am", "--abort"])
-        if not try_or_false(lambda: am_cover_letter(keep_empty=True),
-                            git.GitCommandError):
-            sub.status = ("missing cover letter and "
-                          "first patch failed to apply")
-            sub.save()
-            return 0
-
-        for i, patch in enumerate(patches):
-            patch_abspath = str(maildir / patch.msg_id)
-
-            # Try and apply and fail if there are whitespace errors
-            def do_git_am(extra_args=[]):
-                repo.git.execute([*git_am_args, *extra_args, patch_abspath]),
-
-            # If this fails, the patch may apply with whitespace errors
-            if try_or_false(lambda: do_git_am(['--whitespace=error-all']),
-                            git.GitCommandError):
-                continue
-
-            repo.git.execute(["git", *author_args, "am", "--abort"])
-
-            # Try again, if we succeed, count this patch as a whitespace error
-            if try_or_false(lambda: do_git_am(), git.GitCommandError):
-                whitespace_errors.append(str(i+1))
-                continue
-
-            # If we still fail, the patch does not apply
-            sub.status = f'patch {i+1} failed to apply'
-            sub.save()
-            return 0
-
-        if whitespace_errors:
-            sub.status = ('whitespace error patch(es) '
-                          f'{",".join(whitespace_errors)}')
-        else:
-            sub.status = 'patchset applies'
-        sub.save()
-
-        repo.create_tag(logfile)
-        repo.create_remote("origin", REMOTE_URL)
-        repo.git.push("origin", tags=True)
+    sub.status = patchset.check(cover_letter, patches, submission_id=logfile)
+    sub.save()
 
 
 if __name__ == "__main__":
