diff --git a/docs/development.md b/docs/development.md
index e51f7b1..36d2fdf 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -268,8 +268,8 @@ using the aptly-named `createsuperuser` command:
 
 Once this is done, it's beneficial to load some real emails into the system.
 This can be done manually, however it's generally much easier to download
-an archive from a Mailman instance and load these using the `parsearchive.py`
-tool. You can do this like so:
+an archive from a Mailman instance and load these using the `parsearchive`
+command. You can do this like so:
 
     (.venv)$ mm_user=myusername
     (.venv)$ mm_pass=mypassword
@@ -288,8 +288,8 @@ find more informations about this [here][ref-mman-bulk].
 Load these archives into Patchwork. Depending on the size of the downloaded
 archives this may take some time:
 
-    (.venv)$ PYTHONPATH=. ./patchwork/bin/parsearchive.py \
-      --list-id=patchwork.ozlabs.org patchwork.mbox
+    (.venv)$ ./manage.py parsearchive --list-id=patchwork.ozlabs.org \
+      patchwork.mbox
 
 Finally, run the server and browse to the IP address of your board using your
 browser of choice:
diff --git a/patchwork/bin/parsearchive.py b/patchwork/bin/parsearchive.py
deleted file mode 100755
index 8986b22..0000000
--- a/patchwork/bin/parsearchive.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python
-#
-# Patchwork - automated patch tracking system
-# Copyright (C) 2015 Intel Corporation
-#
-# This file is part of the Patchwork package.
-#
-# Patchwork is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# Patchwork is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Patchwork; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-"""Utility to parse an mbox archive file."""
-
-from __future__ import absolute_import
-
-import argparse
-import logging
-import mailbox
-
-import django
-
-from patchwork.parser import parse_mail
-from patchwork import models
-
-LOGGER = logging.getLogger(__name__)
-
-VERBOSITY_LEVELS = {
-    'debug': logging.DEBUG,
-    'info': logging.INFO,
-    'warning': logging.WARNING,
-    'error': logging.ERROR,
-    'critical': logging.CRITICAL
-}
-
-
-def parse_mbox(path, list_id):
-    results = {
-        models.Patch: 0,
-        models.CoverLetter: 0,
-        models.Comment: 0,
-    }
-    duplicates = 0
-    dropped = 0
-
-    mbox = mailbox.mbox(path)
-    for msg in mbox:
-        try:
-            obj = parse_mail(msg, list_id)
-            if obj:
-                results[type(obj)] += 1
-            else:
-                dropped += 1
-        except django.db.utils.IntegrityError:
-            duplicates += 1
-    print('Processed %(total)d messages -->\n'
-          '  %(covers)4d cover letters\n'
-          '  %(patches)4d patches\n'
-          '  %(comments)4d comments\n'
-          '  %(duplicates)4d duplicates\n'
-          '  %(dropped)4d dropped\n'
-          'Total: %(new)s new entries' % {
-              'total': len(mbox),
-              'covers': results[models.CoverLetter],
-              'patches': results[models.Patch],
-              'comments': results[models.Comment],
-              'duplicates': duplicates,
-              'dropped': dropped,
-              'new': len(mbox) - duplicates - dropped,
-          })
-
-
-def main():
-    django.setup()
-    parser = argparse.ArgumentParser(description=__doc__)
-
-    def list_logging_levels():
-        """Give a summary of all available logging levels."""
-        return sorted(VERBOSITY_LEVELS.keys(),
-                      key=lambda x: VERBOSITY_LEVELS[x])
-
-    parser.add_argument('inpath', help='input mbox filename')
-
-    group = parser.add_argument_group('Mail parsing configuration')
-    group.add_argument('--list-id', help='mailing list ID. If not supplied '
-                       'this will be extracted from the mail headers.')
-    group.add_argument('--verbosity', choices=list_logging_levels(),
-                       help='debug level', default='info')
-
-    args = vars(parser.parse_args())
-
-    logging.basicConfig(level=VERBOSITY_LEVELS[args['verbosity']])
-
-    parse_mbox(args['inpath'], args['list_id'])
-
-if __name__ == '__main__':
-    main()
diff --git a/patchwork/management/commands/parsearchive.py b/patchwork/management/commands/parsearchive.py
new file mode 100644
index 0000000..c1c4512
--- /dev/null
+++ b/patchwork/management/commands/parsearchive.py
@@ -0,0 +1,104 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2016 Stephen Finucane <stephenfinucane@hotmail.com>
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import logging
+import mailbox
+from optparse import make_option
+import os
+import sys
+
+import django
+from django.core.management.base import BaseCommand
+
+from patchwork import models
+from patchwork.parser import parse_mail
+
+logger = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+    help = 'Parse an mbox archive file and store any patches/comments found.'
+
+    if django.VERSION < (1, 8):
+        args = '<infile>'
+        option_list = BaseCommand.option_list + (
+            make_option(
+                '--list-id',
+                help='mailing list ID. If not supplied, this will be '
+                'extracted from the mail headers.'),
+        )
+    else:
+        def add_arguments(self, parser):
+            parser.add_argument(
+                'infile',
+                help='input mbox filename')
+            parser.add_argument(
+                '--list-id',
+                help='mailing list ID. If not supplied, this will be '
+                'extracted from the mail headers.')
+
+    def handle(self, *args, **options):
+        results = {
+            models.Patch: 0,
+            models.CoverLetter: 0,
+            models.Comment: 0,
+        }
+        duplicates = 0
+        dropped = 0
+
+        # TODO(stephenfin): Support passing via stdin
+        path = args and args[0] or options['infile']
+        if not os.path.exists(path):
+            self.stdout.write('Invalid path: %s' % path)
+            sys.exit(1)
+
+        mbox = mailbox.mbox(path)
+        count = len(mbox)
+
+        logger.info('Parsing %d mails', count)
+        for i, msg in enumerate(mbox):
+            try:
+                obj = parse_mail(msg, options['list_id'])
+                if obj:
+                    results[type(obj)] += 1
+                else:
+                    dropped += 1
+            except django.db.utils.IntegrityError:
+                duplicates += 1
+
+            if (i % 10) == 0:
+                self.stdout.write('%06d/%06d\r' % (i, count), ending='')
+                self.stdout.flush()
+
+        self.stdout.write(
+            'Processed %(total)d messages -->\n'
+            '  %(covers)4d cover letters\n'
+            '  %(patches)4d patches\n'
+            '  %(comments)4d comments\n'
+            '  %(duplicates)4d duplicates\n'
+            '  %(dropped)4d dropped\n'
+            'Total: %(new)s new entries' % {
+                'total': count,
+                'covers': results[models.CoverLetter],
+                'patches': results[models.Patch],
+                'comments': results[models.Comment],
+                'duplicates': duplicates,
+                'dropped': dropped,
+                'new': count - duplicates - dropped,
+            })
diff --git a/patchwork/tests/test_management.py b/patchwork/tests/test_management.py
index a3f4720..5ef3d29 100644
--- a/patchwork/tests/test_management.py
+++ b/patchwork/tests/test_management.py
@@ -49,3 +49,22 @@ class ParsemailTest(TestCase):
                              stdin=file_)
 
             self.assertEqual(exc.exception.code, 1)
+
+
+class ParsearchiveTest(TestCase):
+    def test_invalid_path(self):
+        out = StringIO()
+        with self.assertRaises(SystemExit) as exc:
+            call_command('parsearchive', 'xyz123random', stdout=out)
+        self.assertEqual(exc.exception.code, 1)
+
+    def test_invalid_mbox(self):
+        out = StringIO()
+        # we haven't created a project yet, so this will fail
+        call_command('parsearchive',
+                     os.path.join(TEST_MAIL_DIR,
+                                  '0001-git-pull-request.mbox'),
+                     stdout=out)
+
+        self.assertIn('Processed 1 messages -->', out.getvalue())
+        self.assertIn('  1 dropped', out.getvalue())
