app/mailman-web: create

There's a lot of ugly hacks here, but this has been the state of prod
for months now, so we should reflect that.
Also, this bumps a bunch of workspace deps.

Change-Id: I744e0d3aff27036cfed73416cf442c7d62444a8b
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1473
Reviewed-by: q3k <q3k@hackerspace.pl>
diff --git a/WORKSPACE b/WORKSPACE
index f8fbcfb..2b8dc07 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -129,9 +129,9 @@
 
 http_archive(
     name = "io_bazel_rules_docker",
-    sha256 = "4349f2b0b45c860dd2ffe18802e9f79183806af93ce5921fb12cbd6c07ab69a8",
-    strip_prefix = "rules_docker-0.21.0",
-    urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.21.0/rules_docker-v0.21.0.tar.gz"],
+    sha256 = "27d53c1d646fc9537a70427ad7b034734d08a9c38924cc6357cc973fed300820",
+    strip_prefix = "rules_docker-0.24.0",
+    urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.24.0/rules_docker-v0.24.0.tar.gz"],
 )
 
 load("@io_bazel_rules_docker//toolchains/docker:toolchain.bzl", docker_toolchain_configure = "toolchain_configure")
@@ -139,7 +139,6 @@
 # This forces the use of Docker $HOME/.docker configuration.
 docker_toolchain_configure(
     name = "docker_config",
-    client_config = "",
     docker_path = "/usr/bin/docker",
 )
 
@@ -150,11 +149,27 @@
 
 container_repositories()
 
+load(
+    "@io_bazel_rules_docker//python3:image.bzl",
+    _py_image_repos = "repositories",
+)
+
+_py_image_repos()
+
 # Docker base images
 
 load("@io_bazel_rules_docker//container:container.bzl", "container_pull")
 
 container_pull(
+    name = "python-debian",
+    digest = "sha256:cfa3b79333c4e56fc675b6800445b6dcbb3e6cd4d52f2a9ade944ab73dadc6a1",
+    registry = "index.docker.io",
+    repository = "python",
+    tag = "3.10-bullseye",   # use the same version as in python_register_toolchains
+)
+
+
+container_pull(
     name = "prodimage-bionic",
     digest = "sha256:1cd1f84169b8e1414a5d511b42909f2d540831c67b6799ae9af8cd6a80d75b5f",
     registry = "registry.k0.hswaw.net",
diff --git a/app/mailman-web/BUILD b/app/mailman-web/BUILD
new file mode 100644
index 0000000..f79e0e3
--- /dev/null
+++ b/app/mailman-web/BUILD
@@ -0,0 +1,150 @@
+load("@pydeps//:requirements.bzl", "requirement")
+load("@rules_python//python:defs.bzl", "py_binary")
+load("@io_bazel_rules_docker//python:image.bzl", "py_layer")
+load("@io_bazel_rules_docker//python3:image.bzl", "py3_image")
+load("@io_bazel_rules_docker//container:container.bzl", "container_layer", "container_image")
+load("@io_bazel_rules_docker//docker/util:run.bzl",  "container_run_and_extract", "container_run_and_commit_layer")
+load("@io_bazel_rules_docker//docker/package_managers:download_pkgs.bzl", "download_pkgs")
+load("@io_bazel_rules_docker//docker/package_managers:install_pkgs.bzl", "install_pkgs")
+
+# - - base docker stuff - -
+
+download_pkgs(
+    name = "apt_py_is_py3",
+    image_tar = "@python-debian//image",
+    packages = [
+        # rules_docker python wants /usr/bin/python
+        "python-is-python3",
+    ],
+)
+
+install_pkgs(
+    name = "base_image",
+    output_image_name = "base_image",
+    image_tar = "@python-debian//image",
+    installables_tar = ":apt_py_is_py3.tar",
+    installation_cleanup_commands = "rm -rf /var/lib/apt/lists/* /usr/share/doc && apt remove -y libbluetooth3 mariadb-common tk && apt autoremove -y",
+)
+
+BASE_IMAGE = ":base_image"
+
+# overkill rube goldberg setup to build static files begins
+#  - - - -
+
+container_run_and_extract(
+    name = "static_pack",
+    commands = [
+        "tar cpJvf /out.tar.xz -C /opt/mailman/web/static ./",
+    ],
+    extract_file = "/out.tar.xz",
+    image = ":static_build_image.tar",
+)
+
+container_image(
+    name = "static_build_image",
+    layers = [":static_build_layer"],
+    base = BASE_IMAGE,
+)
+
+# this will also contain .pyc files, but the python binary will be the same
+# on prod, so it's fine
+container_run_and_commit_layer(
+    name = "static_build_layer",
+    commands = [
+        "./app/mailman-web/manage collectstatic",
+        "./app/mailman-web/manage compress",
+        # gettext is cursed, TODO make this work
+        #"./app/mailman-web/manage compilemessages",
+    ],
+    image = ":build_container.tar",
+    docker_run_flags = ["--entrypoint="],
+)
+
+py3_image(
+    name = "build_container",
+    srcs = [":manage"],
+    main = "manage.py",
+    base = ":build_tools_container",
+    layers = [":deps_layer"],
+    # this doesn't work for some reason - this is always rebuilt, unless
+    # you pass --nostamp globally
+    stamp = 0,
+)
+
+download_pkgs(
+    name = "build_tools",
+    image_tar = "@python-debian//image",
+    packages = [
+        "sassc",
+        "gettext",
+    ],
+)
+
+install_pkgs(
+    name = "build_tools_container",
+    output_image_name = "build_tools_container",
+    image_tar = BASE_IMAGE + '.tar',
+    installables_tar = ":build_tools.tar",
+    installation_cleanup_commands = "rm -rf /var/lib/apt/lists/* /usr/share/doc",
+)
+
+#  - - - -
+# overkill rube goldberg setup to build static files ends
+
+
+# - - python stuff - -
+
+# this is purely a build optimization - put the pip deps into a separate layer
+py_layer(
+    name = "deps_layer",
+    deps = [
+        requirement("Django"),
+        requirement("postorius"),
+        requirement("hyperkitty"),
+        requirement("gunicorn"),
+        requirement("psycopg2-binary"),
+    ],
+)
+
+py_library(
+    name = "django_base",
+    srcs = ["settings.py", "urls.py"]
+        + glob(["upstream_settings/*.py"]),
+    deps = [
+        requirement("Django"),
+        requirement("postorius"),
+        requirement("hyperkitty"),
+        requirement("gunicorn"),
+        requirement("psycopg2-binary"),
+    ],
+)
+
+py_binary(
+    name = "manage",
+    srcs = ["manage.py"],
+    deps = [":django_base"],
+)
+
+py_binary(
+    name = "serve",
+    srcs = ["serve.py"],
+    deps = [":django_base"],
+)
+
+# prod docker image
+
+py3_image(
+    name = "mailman-web",
+    srcs = ["container_main.py"],
+    deps = [
+        ":django_base",
+        ":manage",
+        ":serve",
+    ],
+    layers = [
+        ":deps_layer",
+    ],
+    main = "container_main.py",
+    #base = ":base_container"
+    base = ":static_build_image",
+)
diff --git a/app/mailman-web/LICENSE b/app/mailman-web/LICENSE
new file mode 100644
index 0000000..4e2fa46
--- /dev/null
+++ b/app/mailman-web/LICENSE
@@ -0,0 +1,3 @@
+Mailman and its components (postorius, hyperkitty) are licensed under GPLv3 and we link/import that code here directly.
+Also, a good portion of this wsgi launcher is copied from https://gitlab.com/mailman/mailman-web, GPLv3 as well.
+Therefore, this entire directory likely falls under GPLv3.
diff --git a/app/mailman-web/README.md b/app/mailman-web/README.md
new file mode 100644
index 0000000..36da2a6
--- /dev/null
+++ b/app/mailman-web/README.md
@@ -0,0 +1,7 @@
+Web parts of mailman3 - postorius and hyperkitty.
+Postgres only, TODO attempt cockroachization.
+
+This currently serves static files via an extremely cursed hack:
+lists.hackerspace.pl points to boston-packets, which serves /static/* from
+a local directory there, made by extracting :static_pack there; and proxy_passes
+every other path to the k8s Service defined here.
diff --git a/app/mailman-web/container_main.py b/app/mailman-web/container_main.py
new file mode 100644
index 0000000..b6797a7
--- /dev/null
+++ b/app/mailman-web/container_main.py
@@ -0,0 +1,13 @@
+from sys import argv, exit
+
+# simple wrapper so we don't need two container entrypoints
+assert len(argv) > 1, "specify a command"
+if argv[1] == "serve":
+    import serve
+    serve.main()
+elif argv[1] == "manage":
+    import manage
+    manage.main(argv[1:])
+else:
+    print("unknown command", argv[1])
+    exit(1)
diff --git a/app/mailman-web/kube/mailman.libsonnet b/app/mailman-web/kube/mailman.libsonnet
new file mode 100644
index 0000000..c71de4e
--- /dev/null
+++ b/app/mailman-web/kube/mailman.libsonnet
@@ -0,0 +1,215 @@
+local kube = import "../../../kube/kube.libsonnet";
+
+{
+    local app = self,
+    local cfg = app.cfg,
+
+    cfg:: {
+        namespace: error "cfg.namespace must be set",
+        webDomain: error "cfg.webDomain must be set",
+        images: {
+            web: "registry.k0.hswaw.net/implr/mailman-web:0.6",
+            # https://github.com/octeep/wireproxy
+            wireproxy: "registry.k0.hswaw.net/implr/wireproxy:1.0.5"
+        },
+        passwords: {
+            postgres: error "cfg.secrets.postgres must be set",
+            mailmanRest: error "cfg.secrets.mailmanRest must be set",
+            mailmanArchiver: error "cfg.secrets.mailmanArchiver must be set",
+        },
+        smtp: {
+            user: "postorius",
+            # from mail server
+            password: error "cfg.smtp.password must be set",
+        },
+        secrets: {
+            djangoSecretKey: error "cfg.secrets.djangoSecretKey must be set",
+        },
+        wg: {
+            peerPubkey: error "cfg.wg.peerPubkey must be set",
+            privkey: error "cfg.wg.privkey must be set",
+            endpoint: error "cfg.wg.endpoint must be set",
+        },
+    },
+
+    env:: {
+        WEB_DOMAIN: cfg.webDomain,
+        BIND_ADDR: "0.0.0.0:8080",
+
+        //DB_HOST: app.postgres.svc.host,
+        DB_HOST: "boston-packets.hackerspace.pl",
+        DB_USER: "mailman",
+        DB_NAME: "mailman-web",
+        DB_PASS: kube.SecretKeyRef(app.config, "postgres-pass"),
+        DB_PORT: "5432",
+
+
+        SMTP_HOST: "mail.hackerspace.pl",
+        SMTP_PORT: "587",
+        SMTP_USER: "postorius",
+        SMTP_PASSWORD: kube.SecretKeyRef(app.config, "smtp-password"),
+
+        SECRET_KEY: kube.SecretKeyRef(app.config, "django-secret-key"),
+        MAILMAN_REST_API_PASS: kube.SecretKeyRef(app.config, 'mailman-api-password'),
+        MAILMAN_ARCHIVER_KEY: kube.SecretKeyRef(app.config, 'mailman-archiver-key'),
+
+    },
+
+    namespace: kube.Namespace(cfg.namespace),
+    local ns = self.namespace,
+
+
+    web: ns.Contain(kube.Deployment("web")) {
+        spec+: {
+            minReadySeconds: 10,
+            replicas: 1,
+            template+: {
+                spec+: {
+                    initContainers_: {
+                        migrate: kube.Container("migrate") {
+                            image: cfg.images.web,
+                            env_: app.env,
+                            args: [
+                                "manage", "migrate",
+                            ],
+                        },
+                    },
+                    volumes_: {
+                        config: kube.SecretVolume(app.wireproxyConfig),
+                    },
+                    containers_: {
+                        default: kube.Container("default") {
+                            image: cfg.images.web,
+                            env_: app.env,
+                            args: ["serve"],
+                            ports_: {
+                                web: { containerPort: 8080 },
+                            },
+                            # readinessProbe: {
+                            #     httpGet: {
+                            #         path: "/",
+                            #         port: "web",
+                            #     },
+                            #     failureThreshold: 10,
+                            #     periodSeconds: 5,
+                            # },
+                            resources: {
+                                requests: {
+                                    cpu: "250m",
+                                    memory: "1024M",
+                                },
+                                limits: {
+                                    cpu: "1",
+                                    memory: "1024M",
+                                },
+                            },
+                        },
+                        wireproxy: kube.Container("wireproxy") {
+                            image: cfg.images.wireproxy,
+                            resources: {
+                                requests: {
+                                    cpu: "100m",
+                                    memory: "64M",
+                                },
+                                limits: {
+                                    cpu: "200m",
+                                    memory: "128M",
+                                },
+                            },
+                            volumeMounts_: {
+                                config: { mountPath: "/etc/wireproxy/config", subPath: "config" }
+                            },
+                        },
+                    },
+                },
+            },
+        },
+    },
+
+    local manifestIniMultisection(sname, values) = std.join('\n',
+        [std.manifestIni({
+                sections: {
+                    [sname]: i,
+            }}) for i in values]),
+    wireproxyConfig: ns.Contain(kube.Secret("wireproxy-config")) {
+        data: {
+            config: std.base64(std.manifestIni({
+                sections: {
+                    Interface: {
+                        Address: cfg.wg.address,
+                        PrivateKey: cfg.wg.privkey,
+                    },
+                    Peer: {
+                        PublicKey: cfg.wg.peerPubkey,
+                        Endpoint: cfg.wg.endpoint,
+                    },
+
+                },
+            }) + manifestIniMultisection("TCPClientTunnel", [
+                # {
+                #     # postgres
+                #     ListenPort: 5432,
+                #     Target: "localhost:5432",
+                # },
+                {
+                    # mailman core api
+                    BindAddress: "127.0.0.1:8001",
+                    Target: "172.17.1.1:8001",
+                },
+            ])),
+        },
+    },
+
+
+    svcWeb: ns.Contain(kube.Service("web")) {
+        target_pod: app.web.spec.template,
+        spec+: {
+            # hax
+            type: "LoadBalancer",
+            externalTrafficPolicy: "Local",
+        },
+    },
+
+
+    #ingress: ns.Contain(kube.Ingress("mailman")) {
+    #    metadata+: {
+    #        annotations+: {
+    #            "kubernetes.io/tls-acme": "true",
+    #            "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
+    #            "nginx.ingress.kubernetes.io/proxy-body-size": "0",
+    #        },
+    #    },
+    #    spec+: {
+    #        tls: [
+    #            {
+    #                hosts: [cfg.webDomain],
+    #                secretName: "mailman-ingress-tls",
+    #            },
+    #        ],
+    #        rules: [
+    #            {
+    #                host: cfg.webDomain,
+    #                http: {
+    #                    paths: [
+    #                        { path: "/", backend: app.svcWeb.name_port },
+    #                        //{ path: "/static/", backend: app.svcStatic.name_port },
+    #                    ],
+    #                },
+    #            },
+    #        ],
+    #    },
+    #},
+
+    config: ns.Contain(kube.Secret("config")) {
+        data_: {
+            "postgres-pass": cfg.passwords.postgres,
+            "django-secret-key": cfg.secrets.djangoSecretKey,
+
+            "smtp-password": cfg.smtp.password,
+
+            "mailman-api-password": cfg.mailmanCore.mailmanApiPass,
+            "mailman-archiver-key": cfg.mailmanCore.mailmanArchiverKey,
+
+        },
+    },
+}
diff --git a/app/mailman-web/kube/prod.jsonnet b/app/mailman-web/kube/prod.jsonnet
new file mode 100644
index 0000000..3fdd75a
--- /dev/null
+++ b/app/mailman-web/kube/prod.jsonnet
@@ -0,0 +1,20 @@
+local mailman = import "mailman.libsonnet";
+local secrets = import "secrets/plain/prod.libsonnet";
+
+mailman {
+    cfg+: secrets {
+        namespace: "mailman-hackerspace-prod",
+        webDomain: "lists2.hackerspace.pl",
+
+        wg+: {
+            address: "172.17.1.2/32",
+            peerPubkey: "sKobxe3U6Gz72MWXEETTr8fSFIPSuX/WOGGFwd3oXy8=",
+            endpoint: "boston-packets.hackerspace.pl:51820"
+        },
+
+        //objectStorage+: {
+            //bucket: "mailman-prod",
+        //},
+
+    },
+}
diff --git a/app/mailman-web/manage.py b/app/mailman-web/manage.py
new file mode 100644
index 0000000..1dc0f86
--- /dev/null
+++ b/app/mailman-web/manage.py
@@ -0,0 +1,13 @@
+import os
+import sys
+
+def main(argv):
+    os.environ['DJANGO_SETTINGS_MODULE'] = "settings"
+
+    os.environ['DJANGO_IS_MANAGEMENT_COMMAND'] = '1'
+    from django.core.management import execute_from_command_line
+    execute_from_command_line(argv)
+
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/app/mailman-web/serve.py b/app/mailman-web/serve.py
new file mode 100644
index 0000000..e56bd04
--- /dev/null
+++ b/app/mailman-web/serve.py
@@ -0,0 +1,38 @@
+import os
+import gunicorn.app.base
+from django.core.wsgi import get_wsgi_application
+
+
+class StandaloneApplication(gunicorn.app.base.BaseApplication):
+
+    def __init__(self, app, options=None):
+        self.options = options or {}
+        self.application = app
+        super().__init__()
+
+    def load_config(self):
+        config = {key: value for key, value in self.options.items()
+                  if key in self.cfg.settings and value is not None}
+        for key, value in config.items():
+            self.cfg.set(key.lower(), value)
+
+    def load(self):
+        return self.application
+
+
+def main():
+    options = {
+        'bind': os.environ.get('BIND_ADDR', '127.0.0.1:8080'),
+        'workers': int(os.environ.get("GUNICORN_WORKERS", "4")),
+        'capture_output': True,
+        'disable_redirect_access_to_syslog': True,
+        'accesslog': '-',
+        'errorlog': '-',
+    }
+    os.environ['DJANGO_SETTINGS_MODULE'] = "settings"
+    application = get_wsgi_application()
+    StandaloneApplication(application, options).run()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/app/mailman-web/settings.py b/app/mailman-web/settings.py
new file mode 100644
index 0000000..8cbfb2b
--- /dev/null
+++ b/app/mailman-web/settings.py
@@ -0,0 +1,102 @@
+import sys
+import os
+
+from upstream_settings.base import *
+from upstream_settings.mailman import *
+
+# we're in a container, stdout only
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'formatters': {
+        'verbose': {
+            'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
+            },
+        },
+    'handlers': {
+        'console': {
+            'level': 'INFO',
+            'class': 'logging.StreamHandler',
+            'stream': sys.stdout,
+            'formatter': 'verbose'
+            },
+        },
+    'loggers': {
+        '': {
+            'handlers': ['console'],
+            'level': 'INFO',
+            'propagate': True,
+            },
+        },
+    }
+
+SECRET_KEY = os.environ.get("SECRET_KEY", "hackme")
+# assert len(SECRET_KEY) > 16
+ROOT_URLCONF = "urls"
+
+ALLOWED_HOSTS = [
+    "localhost",  # Archiving API from Mailman, keep it.
+    os.environ.get('WEB_DOMAIN', "lists.hackerspace.pl"),
+]
+
+ALLOWED_HOSTS = ["*"] # TODO deleteme
+
+MAILMAN_REST_API_URL = 'http://localhost:8001'
+MAILMAN_REST_API_USER = 'restadmin'
+MAILMAN_REST_API_PASS = os.environ.get('MAILMAN_REST_API_PASS')
+MAILMAN_ARCHIVER_KEY = os.environ.get('MAILMAN_ARCHIVER_KEY')
+MAILMAN_ARCHIVER_FROM = ('127.0.0.1', '::1', '185.236.240.38', "2a0d:eb00:2137:2::10")
+
+DATABASES = {
+    'default': {
+        # Use 'sqlite3', 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+        'ENGINE': 'django.db.backends.postgresql_psycopg2',
+        # DB name or path to database file if using sqlite3.
+        'NAME': os.environ.get('DB_NAME', 'mailman-web'),
+        # The following settings are not used with sqlite3:
+        'USER': os.environ.get('DB_USER', 'mailman'),
+        'PASSWORD': os.environ.get('DB_PASS'),
+        # HOST: empty for localhost through domain sockets or '127.0.0.1' for
+        # localhost through TCP.
+        'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
+        # PORT: set to empty string for default.
+        'PORT': os.environ.get('DB_PORT', ''),
+        # OPTIONS: for mysql engine only, do not use with other engines.
+        # 'OPTIONS': {'charset': 'utf8mb4'}  # Enable utf8 4-byte encodings.
+    }
+}
+
+# TODO check this
+USE_X_FORWARDED_HOST = True # behind an Ingress
+
+# And if your proxy does your SSL encoding for you, set SECURE_PROXY_SSL_HEADER
+# https://docs.djangoproject.com/en/1.8/ref/settings/#secure-proxy-ssl-header
+# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_SCHEME', 'https')
+
+DEFAULT_FROM_EMAIL = 'postorius@hackerspace.pl'
+SERVER_EMAIL = 'bofh@hackerspace.pl'
+
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+EMAIL_HOST = os.environ.get('SMTP_HOST', '127.0.0.1')
+EMAIL_PORT = int(os.environ.get('SMTP_PORT', '465'))
+EMAIL_HOST_USER = os.environ.get('SMTP_USER', 'postorius')
+EMAIL_HOST_PASSWORD = os.environ.get('SMTP_PASSWORD')
+EMAIL_TIMEOUT=3
+EMAIL_USE_TLS=True
+
+HAYSTACK_CONNECTIONS = {
+    'default': {
+        'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
+        'PATH': os.environ.get('FULLTEXT_INDEX_PATH', "fulltext_index"),
+        # You can also use the Xapian engine, it's faster and more accurate,
+        # but requires another library.
+        # http://django-haystack.readthedocs.io/en/v2.4.1/installing_search_engines.html#xapian
+        # Example configuration for Xapian:
+        # 'ENGINE': 'xapian_backend.XapianEngine'
+    },
+}
+
+# Only display mailing-lists from the same virtual host as the webserver
+FILTER_VHOST = False
+POSTORIUS_TEMPLATE_BASE_URL = 'https://lists.hackerspace.pl'
diff --git a/app/mailman-web/upstream_settings/README b/app/mailman-web/upstream_settings/README
new file mode 100644
index 0000000..9700643
--- /dev/null
+++ b/app/mailman-web/upstream_settings/README
@@ -0,0 +1 @@
+Unmodified copy of default settings from mailman-web.
diff --git a/app/mailman-web/upstream_settings/__init__.py b/app/mailman-web/upstream_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/mailman-web/upstream_settings/__init__.py
diff --git a/app/mailman-web/upstream_settings/base.py b/app/mailman-web/upstream_settings/base.py
new file mode 100644
index 0000000..7333588
--- /dev/null
+++ b/app/mailman-web/upstream_settings/base.py
@@ -0,0 +1,300 @@
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+from django.contrib.messages import constants as messages
+from pathlib import Path
+
+#: The base directory for logs and database.
+BASE_DIR = Path('/opt/mailman/web')
+
+#: Default list of admins who receive the emails from error logging.
+ADMINS = (
+    ('Mailman Suite Admin', 'root@localhost'),
+)
+
+#: Hosts/domain names that are valid for this site; required if DEBUG is False.
+#: See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = [
+    "localhost",  # Archiving API from Mailman, keep it.
+    # "lists.your-domain.org",
+    # Add here all production URLs you may have.
+]
+
+#: Enable Development Mode.
+DEBUG = False
+
+
+#: URL Configuration for Django
+ROOT_URLCONF = 'mailman_web.urls'
+
+
+#: Default list of django applications.
+#: Each social account provider is an application and by default no social auth
+#: providers are enabled. To enable a social auth provider, you can add them
+#: to list of INSTALLED_APPS. For example::
+#:
+#:     DJANGO_SOCIAL_AUTH_PROVIDERS = [
+#:         'allauth.socialaccount.providers.openid',
+#:         'django_mailman3.lib.auth.fedora',
+#:         'allauth.socialaccount.providers.github',
+#:         'allauth.socialaccount.providers.gitlab',
+#:         'allauth.socialaccount.providers.google',
+#:         'allauth.socialaccount.providers.facebook',
+#:         'allauth.socialaccount.providers.twitter',
+#:         'allauth.socialaccount.providers.stackexchange',
+#:     ]
+#:     INSTALLED_APPS += DJANGO_SOCIAL_AUTH_PROVIDERS
+#:
+#: A full list of providers can be found at
+#: https://django-allauth.readthedocs.io/en/latest/providers.html
+#: Please also note that extra configuration is required after
+#: a provider is enabled. Django-allauth's documentation mentioned
+#: above provides more details about how to configure one.
+INSTALLED_APPS = [
+    'hyperkitty',
+    'postorius',
+    'django_mailman3',
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'rest_framework',
+    'django_gravatar',
+    'compressor',
+    'haystack',
+    'django_extensions',
+    'django_q',
+    'allauth',
+    'allauth.account',
+    'allauth.socialaccount',
+]
+
+
+#: Default Django Middlewares.
+MIDDLEWARE = (
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.middleware.locale.LocaleMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'django.middleware.security.SecurityMiddleware',
+    'django_mailman3.middleware.TimezoneMiddleware',
+    'postorius.middleware.PostoriusMiddleware',
+)
+
+#: Default Template finders.
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.i18n',
+                'django.template.context_processors.media',
+                'django.template.context_processors.static',
+                'django.template.context_processors.tz',
+                'django.template.context_processors.csrf',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+                'django_mailman3.context_processors.common',
+                'hyperkitty.context_processors.common',
+                'postorius.context_processors.postorius',
+            ],
+        },
+    },
+]
+
+#: Wsgi application import path. This will be used by the WSGI server which
+#: will be used to deploy this application.
+WSGI_APPLICATION = 'mailman_web.wsgi.application'
+
+#: Default Database to be used.
+#: Example for PostgreSQL (**recommanded for production**)::
+#:
+#:    'default': {
+#:        'ENGINE': 'django.db.backends.postgresql_psycopg2',
+#:        'NAME': 'database_name',
+#:        'USER': 'database_user',
+#:        'PASSWORD': 'database_password',
+#:        'HOST': 'localhost',
+#:    }
+#:
+#: For MySQL/MariaDB also add the following to the the configuration::
+#:
+#:     'OPTIONS': {'charset': 'utf8mb4'}  # Enable utf8 4-byte encodings.
+#:
+#: Check out
+#: `Django documentation
+#: <https://docs.djangoproject.com/en/3.0/ref/settings/#databases>`_ for
+#: more details.
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'mailman-web.db'),
+        'HOST': '',
+        'PORT': '',
+    }
+}
+
+# Maintain type of autogenerated keys going forward
+# https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+
+
+#: Default password validators.
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME':
+        'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',  # noqa: E501
+    },
+    {
+        'NAME':
+        'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME':
+        'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME':
+        'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+#: Default Language code.
+LANGUAGE_CODE = 'en-us'
+
+#: Default timezone.
+TIME_ZONE = 'UTC'
+
+#: Enable internationalization.
+USE_I18N = True
+
+#: Enable localization.
+USE_L10N = True
+
+#: Use the timezone information.
+USE_TZ = True
+
+
+#: Default path where static files will be placed.
+STATIC_ROOT = os.path.join(BASE_DIR, 'static')
+
+#: URL prefix for static files.
+#: Example: "http://example.com/static/", "http://static.example.com/"
+STATIC_URL = '/static/'
+
+#: Additional locations of static files
+STATICFILES_DIRS = (
+    # Put strings here, like "/home/html/static" or "C:/www/django/static".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+    # BASE_DIR + '/static/',
+)
+
+#: List of finder classes that know how to find static files in
+#: various locations.
+STATICFILES_FINDERS = (
+    'django.contrib.staticfiles.finders.FileSystemFinder',
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+    # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
+    'compressor.finders.CompressorFinder',
+)
+
+
+#: Default Django URL to redirect to for Login.
+LOGIN_URL = 'account_login'
+#: Default Django URL to redirect to after a successful login.
+LOGIN_REDIRECT_URL = 'list_index'
+#: Default Django URL to Logout the user.
+LOGOUT_URL = 'account_logout'
+
+#: If you enable email reporting for error messages, this is where those emails
+#: will appear to be coming from. Make sure you set a valid domain name,
+#: otherwise the emails may get rejected.
+#: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SERVER_EMAIL
+SERVER_EMAIL = 'root@localhost.local'
+
+#: The default implementation to send out emails. This can be customized to
+#: something else for testing purposes.
+#: https://docs.djangoproject.com/en/dev/topics/email/#email-backends
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+
+MESSAGE_TAGS = {
+    messages.ERROR: 'danger'
+}
+
+
+#: Default Logging configuration.
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'filters': {
+        'require_debug_false': {
+            '()': 'django.utils.log.RequireDebugFalse'
+        }
+    },
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'filters': ['require_debug_false'],
+            'class': 'django.utils.log.AdminEmailHandler'
+        },
+        'file': {
+            'level': 'INFO',
+            'class': 'logging.handlers.WatchedFileHandler',
+            'filename': os.path.join(BASE_DIR, 'logs', 'mailmanweb.log'),
+            'formatter': 'verbose',
+        },
+        'console': {
+            'class': 'logging.StreamHandler',
+            'formatter': 'simple',
+        },
+    },
+    'loggers': {
+        'django.request': {
+            'handlers': ['mail_admins', 'file'],
+            'level': 'ERROR',
+            'propagate': True,
+        },
+        'django': {
+            'handlers': ['file'],
+            'level': 'ERROR',
+            'propagate': True,
+        },
+        'hyperkitty': {
+            'handlers': ['file'],
+            'level': 'DEBUG',
+            'propagate': True,
+        },
+        'postorius': {
+            'handlers': ['console', 'file'],
+            'level': 'INFO',
+        },
+        'q': {
+            'level': 'WARNING',
+            'propagate': False,
+            'handlers': ['console', 'file'],
+        },
+    },
+    'formatters': {
+        'verbose': {
+            'format': '%(levelname)s %(asctime)s %(process)d %(name)s %(message)s'  # noqa: E501
+        },
+        'simple': {
+            'format': '%(levelname)s %(message)s'
+        },
+    },
+}
+
+#: Current Django Site being served. This is used to customize the web host
+#: being used to serve the current website. For more details about Django
+#: site, see: https://docs.djangoproject.com/en/dev/ref/contrib/sites/
+SITE_ID = 1
diff --git a/app/mailman-web/upstream_settings/mailman.py b/app/mailman-web/upstream_settings/mailman.py
new file mode 100644
index 0000000..f69a706
--- /dev/null
+++ b/app/mailman-web/upstream_settings/mailman.py
@@ -0,0 +1,124 @@
+#: Mailman Core default API Path
+MAILMAN_REST_API_URL = 'http://localhost:8001'
+#: Mailman Core API user
+MAILMAN_REST_API_USER = 'restadmin'
+#: Mailman Core API user's password.
+MAILMAN_REST_API_PASS = 'restpass'
+#: Mailman Core Shared archiving key. This value is set in the :
+#: mailman-hyperkitty's configuration file.
+MAILMAN_ARCHIVER_KEY = 'SecretArchiverAPIKey'
+#: Host for Mailman Core, from where Hyperkitty will accept connections
+#: for archiving.
+MAILMAN_ARCHIVER_FROM = ('127.0.0.1', '::1')
+
+#: Base URL where Django/Mailman-web would be listening for requests. Used by
+#: Mailman Core for fetching templates.
+POSTORIUS_TEMPLATE_BASE_URL = 'http://localhost:8000'
+
+#: Use gravatar in HyperKitty and Postorius.
+#: If disabled django_gravatar can be removed from INSTALLED_APPS:
+#: INSTALLED_APPS.remove('django_gravatar')
+HYPERKITTY_ENABLE_GRAVATAR = True
+
+#: Filter visible Mailing Lists based on the current host being used to serve.
+FILTER_VHOST = False
+
+#: Sender in Emails sent out by Postorius.
+DEFAULT_FROM_EMAIL = 'postorius@localhost'
+
+
+#: Django Allauth
+ACCOUNT_AUTHENTICATION_METHOD = "username_email"
+ACCOUNT_EMAIL_REQUIRED = True
+ACCOUNT_EMAIL_VERIFICATION = "mandatory"
+ACCOUNT_UNIQUE_EMAIL = True
+
+#: Protocol for URLs generated for authentication, like email
+#: confirmation.
+ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
+
+
+#: Extra configuration for Social auth. For these configuration to be used.
+#: each of the social account providers must be first added in INSTALLED_APPS.
+#: See :py:data:`mailman_web.settings.base.INSTALLED_APPS` for more
+#: configuration.
+SOCIALACCOUNT_PROVIDERS = {
+    'openid': {
+        'SERVERS': [
+            dict(id='yahoo',
+                 name='Yahoo',
+                 openid_url='http://me.yahoo.com'),
+        ],
+    },
+    'google': {
+        'SCOPE': ['profile', 'email'],
+        'AUTH_PARAMS': {'access_type': 'online'},
+    },
+    'facebook': {
+        'METHOD': 'oauth2',
+        'SCOPE': ['email'],
+        'FIELDS': [
+            'email',
+            'name',
+            'first_name',
+            'last_name',
+            'locale',
+            'timezone',
+        ],
+        'VERSION': 'v2.4',
+    },
+}
+
+
+#: django-compressor
+#: https://pypi.python.org/pypi/django_compressor
+COMPRESS_PRECOMPILERS = (
+    ('text/x-scss', 'sassc -t compressed {infile} {outfile}'),
+    ('text/x-sass', 'sassc -t compressed {infile} {outfile}'),
+)
+
+
+# Social auth
+#
+#: Authentication backends for Django to be used.
+AUTHENTICATION_BACKENDS = (
+    'django.contrib.auth.backends.ModelBackend',
+    'allauth.account.auth_backends.AuthenticationBackend',
+)
+
+#
+# Full-text search engine
+#
+#: Django-Haystack connection parameters.
+HAYSTACK_CONNECTIONS = {
+    'default': {
+        'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
+        'PATH': "fulltext_index",
+        # You can also use the Xapian engine, it's faster and more accurate,
+        # but requires another library.
+        # http://django-haystack.readthedocs.io/en/v2.4.1/installing_search_engines.html#xapian
+        # Example configuration for Xapian:
+        # 'ENGINE': 'xapian_backend.XapianEngine'
+    },
+}
+
+
+# Asynchronous tasks
+#
+#: Django Q connection parameters.
+Q_CLUSTER = {
+    'retry': 360,
+    'timeout': 300,
+    'save_limit': 100,
+    'orm': 'default',
+}
+
+#: On a production setup, setting COMPRESS_OFFLINE to True will bring a
+#: significant performance improvement, as CSS files will not need to be
+#: recompiled on each requests. It means running an additional "compress"
+#: management command after each code upgrade.
+#: http://django-compressor.readthedocs.io/en/latest/usage/#offline-compression
+COMPRESS_OFFLINE = True
+
+# Needed for debug mode
+# INTERNAL_IPS = ('127.0.0.1',)
diff --git a/app/mailman-web/urls.py b/app/mailman-web/urls.py
new file mode 100644
index 0000000..e9359c7
--- /dev/null
+++ b/app/mailman-web/urls.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 1998-2016 by the Free Software Foundation, Inc.
+#
+# This file is part of Postorius.
+#
+# Postorius 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 3 of the License, or (at your option)
+# any later version.
+#
+# Postorius 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
+# Postorius.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from django.conf.urls import include
+from django.contrib import admin
+from django.urls import path, reverse_lazy
+from django.views.generic import RedirectView
+
+urlpatterns = [
+    path(
+        '',
+        RedirectView.as_view(url=reverse_lazy('list_index'), permanent=True),
+    ),
+    path('mailman3/', include('postorius.urls')),
+    path('archives/', include('hyperkitty.urls')),
+    path('', include('django_mailman3.urls')),
+    path('accounts/', include('allauth.urls')),
+    path('admin/', admin.site.urls),
+]
diff --git a/third_party/py/requirements.in b/third_party/py/requirements.in
index 93da93e..2b8b40c 100644
--- a/third_party/py/requirements.in
+++ b/third_party/py/requirements.in
@@ -1,12 +1,12 @@
 # grpcio and protobuf are installed directly via WORKSPACE
 # do NOT add them there
 # depending on a py_grpc_library output will pull in the required deps
-arrow==0.14.5
+arrow==1.1.1
 blinker==1.4
 Click==7.0
 cockroachdb==0.3.3
-cryptography==2.9.2
-Django==2.2.28
+cryptography==3.4.8
+Django==3.2.16
 fabric==2.4.0
 Flask==1.1.1
 Flask-Login==0.4.1
@@ -15,12 +15,16 @@
 future==0.17.1
 gevent==22.10.2
 gunicorn==20.1.0
+hyperkitty==1.3.7   # sync with postorius
 itsdangerous==1.1.0
 Jinja2==2.10.1
 MarkupSafe==1.1.1
 oauthlib==3.1.1
 paramiko==2.7.2
+postorius==1.3.7    # sync with hyperkitty
 psycopg2==2.9.4
+# disgusten, but needed for weird container linking problems
+psycopg2-binary==2.9.4
 pyelftools==0.26
 PyNaCl==1.3.0
 python-dateutil==2.8.0
diff --git a/third_party/py/requirements.txt b/third_party/py/requirements.txt
index 939be47..e36bef8 100644
--- a/third_party/py/requirements.txt
+++ b/third_party/py/requirements.txt
@@ -4,10 +4,20 @@
 #
 #    bazel run //third_party/py:requirements.update
 #
-arrow==0.14.5 \
-    --hash=sha256:0186026cfd94ca4fb773f30cc5398289a3027480d335e0e5c0d2772643763137 \
-    --hash=sha256:a12de0124d812d15061ed36c7eb4a421fa1b95026a502a0b2062e9ea00fc4446
-    # via -r third_party/py/requirements.in
+arrow==1.1.1 \
+    --hash=sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510 \
+    --hash=sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a
+    # via
+    #   -r third_party/py/requirements.in
+    #   django-q
+asgiref==3.6.0 \
+    --hash=sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac \
+    --hash=sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506
+    # via django
+atpublic==3.1.1 \
+    --hash=sha256:3098ee12d0107cc5009d61f4e80e5edcfac4cda2bdaa04644af75827cb121b18 \
+    --hash=sha256:37f714748e77b8a7b34d59b7b485fd452a0d5906be52cb1bd28d29a2bd84f295
+    # via flufl-lock
 bcrypt==3.2.2 \
     --hash=sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521 \
     --hash=sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb \
@@ -21,6 +31,14 @@
     --hash=sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40 \
     --hash=sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa
     # via paramiko
+bleach==6.0.0 \
+    --hash=sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414 \
+    --hash=sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4
+    # via readme-renderer
+blessed==1.19.1 \
+    --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \
+    --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc
+    # via django-q
 blinker==1.4 \
     --hash=sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6
     # via -r third_party/py/requirements.in
@@ -81,6 +99,7 @@
     --hash=sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796
     # via
     #   bcrypt
+    #   cmarkgfm
     #   cryptography
     #   pynacl
 chardet==3.0.4 \
@@ -93,37 +112,159 @@
     # via
     #   -r third_party/py/requirements.in
     #   flask
+cmarkgfm==2022.10.27 \
+    --hash=sha256:0023de4b19bb557b143bed274f76cb36551f7f1d1cdffd29b6cde646b85d9ffb \
+    --hash=sha256:0176d51fb57162c642b1d2c70048950a5ae119af81e77565a0383b992b1f86d6 \
+    --hash=sha256:071f5f0dac9475bab6a065878f248a69be52a7736b6c661e06ca7199f25fe097 \
+    --hash=sha256:0756ea0f6b55eff2617ea0518d6730e37d6077c10baaabbe8b46210ff5a250ef \
+    --hash=sha256:07a06d424ccef98528cba1158946f92117e07579f1dc9942ed4fd70f81693b9f \
+    --hash=sha256:1013ce61db1dd3febcaca1ee42cad9eb823852bb76cbae61c1488734ce51f2b7 \
+    --hash=sha256:123ad8d50fbedacd036760ba46e36170bad9dd2c1e83655d8622b7803169bb49 \
+    --hash=sha256:1790164f84e6b037d0b39df11f757e021a9f9c313681297a051d50bc7b5249fc \
+    --hash=sha256:20e897160be161161a565df94ce502714a1aa63af3ad682e6d1f1c7e6656fdbb \
+    --hash=sha256:210c0f0dbc1aadab30bc75c48b14b645414733a668df52b43058028e43a046e8 \
+    --hash=sha256:21557c06a411b1d754eed7f6fc9a8ff41f8a4a004b32c8bd2cec2ab3f3cb4d3c \
+    --hash=sha256:216a540e85258839cffa7274731a87d91b3e17c9079b3b02467c312e784b5281 \
+    --hash=sha256:27149c63b1190ee6e7dd4b32d0a2c313bc1856bcdde7a42a0a5b6ae42d97ed94 \
+    --hash=sha256:325c03644da5ab81a7071aae6fbafa3beb22413f7fd7440baf6d510cfcf7be21 \
+    --hash=sha256:3f510fafa9d904336eecc3aa41536fd287c2d32baa21b14d48950ced802ca531 \
+    --hash=sha256:4325b75a3b5b802d5edcc2378aa6405a1e5df0aeeec583d1b05d73b0562fa7d0 \
+    --hash=sha256:47e267ce890b579585a32f77d347d61de2390b517cfc52bb4ca67c5c4b4c055a \
+    --hash=sha256:483e48613f5c7b3350cdabfd0f69aaa086513542d0de533f39e5669bf4df5de4 \
+    --hash=sha256:5342c6d12e343cc66b4b8dcd09fc0c1977cb32fd1d57c15bd756876606591ee9 \
+    --hash=sha256:5a39333e1fdcd0116c24adc33423999913865bd3cc83fc44b2218aac7fbe5637 \
+    --hash=sha256:5bad39b832f734f588aea00868e53ba1aaf058d569e40e5c9016702edebf88e8 \
+    --hash=sha256:5fc7178a6afd69a5dfc197558791cecedead9fc77e95ec63c201e8219ce33000 \
+    --hash=sha256:6672784820981d315b695bb7ce08d40886502368e133b453d675ff6f2fffae49 \
+    --hash=sha256:670b414274edf3ecc0a950a80580e1de553c599a30658827a5d7f7bccbde5843 \
+    --hash=sha256:69a769feb1b2d16982fe952afd44e124a4d306a44cdfd6857e74b8eb5d47d765 \
+    --hash=sha256:76beb5b50b32d7bafec2154608a037601a2186d15df95cec6ab4cc937afca365 \
+    --hash=sha256:799cf03a82a7849d975a3b955798d5e439a08fb678b657c5078115dc61314674 \
+    --hash=sha256:7a91279ab8e2869c19120595e41ebd81a6f5034c1e6b1cfc5e81cd80d40bf3eb \
+    --hash=sha256:80cf50b52bc0a47c032706de27b9526b6035c73b57ce06662021144cba4b6c5e \
+    --hash=sha256:8744be702511464d04c34000005009607471f1afe65d6037777747d6b4607e5f \
+    --hash=sha256:8830dfb61251f2b677dea7ffc531c3f6037f7e9a66a14ad24bdaf3cefe2dc8c4 \
+    --hash=sha256:89dcd4fea4ae44f1a0697cf805b6931a126b2b3ea23ed1ccdad7e020425224a9 \
+    --hash=sha256:8e9f038a4f0e54c135e468994f1ea97141b086d1f1bd8f498c12f3d559017e8e \
+    --hash=sha256:90ae1b4b2c6b92f8f5b1e5416a2f5b1bba7a5f9aea29b0de79767ed80655527a \
+    --hash=sha256:93d9ac7716ea901ca0bfd18ae3b68f1f6bf51de0830c3f233ef734fcd52a0799 \
+    --hash=sha256:98c0527153daf16589ef095aa72f06a4bdb9213433ff47811fbc4172c91d865b \
+    --hash=sha256:a6a3970cf1c8ba4465d5046dd6a6d7f6024e67d6eec812a4701a21c5161a2fbd \
+    --hash=sha256:b0b13eac6194d59f9d3ab44af7076221510e788572f34e25104ad47b33d960e1 \
+    --hash=sha256:b8daf62cddc81b31a8f3c9093936c4cb75b25a8024c09f276cb027f1647e3326 \
+    --hash=sha256:bd6315e1036d31884bff25719636e3499a7f4593b0f7b47dc742678328f2f26f \
+    --hash=sha256:c04921575e412a6459d645a45ca987061b17d89310c92aedf108f97f2b8b7b91 \
+    --hash=sha256:c3a6e597bdf595f81dc214e821b579b8d665116c55ed5288b599ae941e446098 \
+    --hash=sha256:c66077349e7f7d954aa37d770310de5a8214ac9dca9756440f99e008a0e693de \
+    --hash=sha256:c804446b941dc08dcc3d2def3913cfc4bae954b80babfaa2a502e8ebdea29185 \
+    --hash=sha256:c82af8cdb76a71459662e447f9b1545ae6146cb9287df978705a298f87a76a90 \
+    --hash=sha256:ca0e03a590c6f62738d208f8689da08eae9d3bcc2f4dd97e38df45d8dbc333ab \
+    --hash=sha256:cc70b89309404dd84a524d439aa2b2e54872e0f623f9523bd77e66526251954f \
+    --hash=sha256:ccfc25b5abfe1398426f099d840b5fa7dec118b44f06833e2ba8b67c6ffc12d9 \
+    --hash=sha256:cfe84b8912b355b8036c093ecdd6abbe6df075176879a49867dd72b9e53449f3 \
+    --hash=sha256:d3fd62dd65c3a64ced175a1447ea41b01a7ac1c0df1c8358323267c9326b7745 \
+    --hash=sha256:db3449fdb87752be5ad0698d6f2ca030af320cdf71ebc9a1ebae1b9c1d3661c8 \
+    --hash=sha256:ddc2bbb5572722758787066f5f841745c58452e28c59ce7c13b7228be1cb48f3 \
+    --hash=sha256:e65e492407d7cb3b695f3f715a1cbe6f97db69eb14011b8f156fc10c758b55c7 \
+    --hash=sha256:ea7d6cb95e2d74049cf08fde4ca6cbf030b9bf9ef75009847bbefb35094bb4c2 \
+    --hash=sha256:ea8a84d3702ccc32f8dfd0917dfb95f3d1843a0b6f85131c5cbfd1480d1d31ee \
+    --hash=sha256:f17677e66f95f25999c959c3f5361c05e739ad4f6b70ab9fdd24b1734c3ab029 \
+    --hash=sha256:f2d3bdb7e525abd03366a57eabd03e0c3f3f36bbf8af2267200605b7b712763b \
+    --hash=sha256:f938c503fce528d9cb715314134f8900cf09ddbd7e2bea88cf54a4bad58d0d5b \
+    --hash=sha256:fbec94c3e91b5e03d90a2cc2e865179e5bc58673e92b03ba64b520a97a0e9219
+    # via readme-renderer
 cockroachdb==0.3.3 \
     --hash=sha256:ef7aa1baf47f1ec8b187d7850f7996566a4131457fa69cb9490536e698f4540b
     # via -r third_party/py/requirements.in
-cryptography==2.9.2 \
-    --hash=sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6 \
-    --hash=sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b \
-    --hash=sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5 \
-    --hash=sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf \
-    --hash=sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e \
-    --hash=sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b \
-    --hash=sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae \
-    --hash=sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b \
-    --hash=sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0 \
-    --hash=sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b \
-    --hash=sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d \
-    --hash=sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229 \
-    --hash=sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3 \
-    --hash=sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365 \
-    --hash=sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55 \
-    --hash=sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270 \
-    --hash=sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e \
-    --hash=sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785 \
-    --hash=sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0
+cryptography==3.4.8 \
+    --hash=sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e \
+    --hash=sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b \
+    --hash=sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7 \
+    --hash=sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085 \
+    --hash=sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc \
+    --hash=sha256:3c4129fc3fdc0fa8e40861b5ac0c673315b3c902bbdc05fc176764815b43dd1d \
+    --hash=sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a \
+    --hash=sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498 \
+    --hash=sha256:695104a9223a7239d155d7627ad912953b540929ef97ae0c34c7b8bf30857e89 \
+    --hash=sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9 \
+    --hash=sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c \
+    --hash=sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7 \
+    --hash=sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb \
+    --hash=sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14 \
+    --hash=sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af \
+    --hash=sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e \
+    --hash=sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5 \
+    --hash=sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06 \
+    --hash=sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7
     # via
     #   -r third_party/py/requirements.in
     #   fabric
     #   paramiko
-django==2.2.28 \
-    --hash=sha256:0200b657afbf1bc08003845ddda053c7641b9b24951e52acd51f6abda33a7413 \
-    --hash=sha256:365429d07c1336eb42ba15aa79f45e1c13a0b04d5c21569e7d596696418a6a45
-    # via -r third_party/py/requirements.in
+    #   pyjwt
+defusedxml==0.7.1 \
+    --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
+    --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
+    # via python3-openid
+django==3.2.16 \
+    --hash=sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121 \
+    --hash=sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d
+    # via
+    #   -r third_party/py/requirements.in
+    #   django-allauth
+    #   django-appconf
+    #   django-extensions
+    #   django-haystack
+    #   django-mailman3
+    #   django-picklefield
+    #   django-q
+    #   djangorestframework
+    #   hyperkitty
+    #   postorius
+django-allauth==0.52.0 \
+    --hash=sha256:e380661ceafe55734c40102819ae720403027036f28e9f9827f0faeddc24ed5f
+    # via django-mailman3
+django-appconf==1.0.5 \
+    --hash=sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d \
+    --hash=sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4
+    # via django-compressor
+django-compressor==4.3.1 \
+    --hash=sha256:2c451174acb6f083054af7c8089376599b22d6380bd60311f78ec3fed79acc8e \
+    --hash=sha256:68858c0da6cc099cc29a022d86c3ba8aed114da9d709eeceb0d7b8181b5f8942
+    # via hyperkitty
+django-extensions==3.2.1 \
+    --hash=sha256:2a4f4d757be2563cd1ff7cfdf2e57468f5f931cc88b23cf82ca75717aae504a4 \
+    --hash=sha256:421464be390289513f86cb5e18eb43e5dc1de8b4c27ba9faa3b91261b0d67e09
+    # via hyperkitty
+django-gravatar2==1.4.4 \
+    --hash=sha256:545a6c2c5c624c7635dec29c7bc0be1a2cb89c9b8821af8616ae9838827cc35b \
+    --hash=sha256:c813280967511ced93eea0359f60e5369c35b3311efe565c3e5d4ab35c10c9ee
+    # via
+    #   django-mailman3
+    #   hyperkitty
+django-haystack==3.2.1 \
+    --hash=sha256:97e3197aefc225fe405b6f17600a2534bf827cb4d6743130c20bc1a06f7293a4
+    # via hyperkitty
+django-mailman3==1.3.9 \
+    --hash=sha256:1a92355b43bd689a4b17f99c4b6de4b490d9b0febd4b6cd0cbb76b3a25819d33
+    # via
+    #   hyperkitty
+    #   postorius
+django-picklefield==3.1 \
+    --hash=sha256:c786cbeda78d6def2b43bff4840d19787809c8909f7ad683961703060398d356 \
+    --hash=sha256:d77c504df7311e8ec14e8b779f10ca6fec74de6c7f8e2c136e1ef60cf955125d
+    # via django-q
+django-q==1.3.9 \
+    --hash=sha256:1b74ce3a8931990b136903e3a7bc9b07243282a2b5355117246f05ed5d076e68 \
+    --hash=sha256:5c6b4d530aa3aabf9c6aa57376da1ca2abf89a1562b77038b7a04e52a4a0a91b
+    # via hyperkitty
+djangorestframework==3.14.0 \
+    --hash=sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8 \
+    --hash=sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08
+    # via hyperkitty
+docutils==0.19 \
+    --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \
+    --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc
+    # via readme-renderer
 fabric==2.4.0 \
     --hash=sha256:93684ceaac92e0b78faae551297e29c48370cede12ff0f853cdebf67d4b87068 \
     --hash=sha256:98538f2f3f63cf52497a8d0b24d18424ae83fe67ac7611225c72afb9e67f2cf6
@@ -147,6 +288,10 @@
     --hash=sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36 \
     --hash=sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac
     # via -r third_party/py/requirements.in
+flufl-lock==7.1.1 \
+    --hash=sha256:96d2c0448ba9fd8fc65d5d681ed7217c8e1625149c1c880bba50559bb680a615 \
+    --hash=sha256:af14172b35bbc58687bd06b70d1693fd8d48cbf0ffde7e51a618c148ae24042d
+    # via hyperkitty
 future==0.17.1 \
     --hash=sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8
     # via -r third_party/py/requirements.in
@@ -270,6 +415,9 @@
     --hash=sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e \
     --hash=sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8
     # via -r third_party/py/requirements.in
+hyperkitty==1.3.7 \
+    --hash=sha256:4d74aca3ec30546741ca62339ece72392e298f511d0a89e69bcedbfcdc8102cb
+    # via -r third_party/py/requirements.in
 idna==2.8 \
     --hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \
     --hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c
@@ -290,6 +438,12 @@
     # via
     #   -r third_party/py/requirements.in
     #   flask
+mailmanclient==3.3.5 \
+    --hash=sha256:63581c604ca7eac021489c15aacca06a4958eb76f66574c6fab05eac654dd857
+    # via
+    #   django-mailman3
+    #   hyperkitty
+    #   postorius
 markupsafe==1.1.1 \
     --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \
     --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \
@@ -346,6 +500,14 @@
     # via
     #   -r third_party/py/requirements.in
     #   jinja2
+mistune==2.0.4 \
+    --hash=sha256:182cc5ee6f8ed1b807de6b7bb50155df7b66495412836b9a74c8fbdfc75fe36d \
+    --hash=sha256:9ee0a66053e2267aba772c71e06891fa8f1af6d4b01d5e84e267b4570d4d9808
+    # via hyperkitty
+networkx==3.0 \
+    --hash=sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e \
+    --hash=sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412
+    # via hyperkitty
 oauthlib==3.1.1 \
     --hash=sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc \
     --hash=sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3
@@ -358,6 +520,25 @@
     # via
     #   -r third_party/py/requirements.in
     #   fabric
+postorius==1.3.7 \
+    --hash=sha256:3d40a9d025bfdec0fdeb5d320ed2db3fbbffc77bd0dfeb619f39cf345c03a2f0
+    # via -r third_party/py/requirements.in
+psutil==5.9.4 \
+    --hash=sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff \
+    --hash=sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1 \
+    --hash=sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62 \
+    --hash=sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549 \
+    --hash=sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08 \
+    --hash=sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7 \
+    --hash=sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e \
+    --hash=sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe \
+    --hash=sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24 \
+    --hash=sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad \
+    --hash=sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94 \
+    --hash=sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8 \
+    --hash=sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7 \
+    --hash=sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4
+    # via flufl-lock
 psycopg2==2.9.4 \
     --hash=sha256:07b90a24d5056687781ddaef0ea172fd951f2f7293f6ffdd03d4f5077801f426 \
     --hash=sha256:1da77c061bdaab450581458932ae5e469cc6e36e0d62f988376e9f513f11cb5c \
@@ -371,6 +552,67 @@
     --hash=sha256:c7fa041b4acb913f6968fce10169105af5200f296028251d817ab37847c30184 \
     --hash=sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f
     # via -r third_party/py/requirements.in
+psycopg2-binary==2.9.4 \
+    --hash=sha256:02cde837df012fa5d579b9cf4bc8e1feb460f38d61f7a4ab4a919d55a9f6eeef \
+    --hash=sha256:044b6ab68613de7ea1e63856627deea091bfea09dea5ab4f050b13250fd18cab \
+    --hash=sha256:0a9465f0aa36480c8e7614991cbe8ca8aa16b0517c5398a49648ce345e446c19 \
+    --hash=sha256:0d8e0c9eec79fe1ae66691e06e3cc714da6fbd77981209bf32fa823c03dbaff8 \
+    --hash=sha256:0eae72190be519bf2629062eab7ac8d4ceec5bd132953cefa1596584d86964fe \
+    --hash=sha256:15e0ac0ed8a85f6049e836e95ddee627766561c85be8d23f4b3edb6ddbaa7310 \
+    --hash=sha256:161dc52a617f0bb610a87d391cb2e77fe65b89ebfbd752f4f3217dde701ea196 \
+    --hash=sha256:181ac372a5a5308b4076933601a9b5f0cd139b389b0aa5e164786a2abbcdb978 \
+    --hash=sha256:1c22c59ab7d9dc110d409445f111f58556bf699b0548f3fc5176684a29c629c4 \
+    --hash=sha256:226f11be577b70a57f4910c0ee28591d4d9fcb3d455e966267179156ae2e0c41 \
+    --hash=sha256:24d627ed69e754c48dd142a914124858c600b4108c92546eb0ba822e63c0c6e2 \
+    --hash=sha256:2535f44b00f26f6af0e949c825e6aecb9adcb56c965c17af5b97137fb69f00c0 \
+    --hash=sha256:25e0517ad7ee3c5c3c69dbe3c1d95504c811e42f452b39a3505d0763b1f6caa0 \
+    --hash=sha256:2903bf90b1e6bfc9bbfc94a1db0b50ffa9830a0ca4c042fbc38d93890c02ce08 \
+    --hash=sha256:2f1ded23d17af0d738e7e78087f0b88a53228887845b1989b03af4dfd3fef703 \
+    --hash=sha256:30200b07779446760813eef06098ec6d084131e4365b4e023eb43100de758b11 \
+    --hash=sha256:33ac8b4754e6b6b21f3ee180da169d8526d91aee9408ec1fc573c16ab32b0207 \
+    --hash=sha256:34fd249275faa782c3a2016e86ac2330636ac58d731a1580e7d686e3976b9536 \
+    --hash=sha256:44f5dc9b4384bafca8429759ce76c8960ffc2b583fcad9e5dfb3e5f4894269e4 \
+    --hash=sha256:451550e0bb5889bbabbf92575a6d6eafced941cc28c86be6ae4667f81bf32d67 \
+    --hash=sha256:52383e932e6de5595963f9178cf2af7b9e1f3daacf5135b9c0e21aabbc5bf7c4 \
+    --hash=sha256:55137faec669c4277c5687c6ce7c1fbc4dece0e2f14256ee808f4a652f0a2170 \
+    --hash=sha256:576b9dfbcd154a0e8b5d9dae6316d037450e64a3b31df87dec71d88e2a2d5e5f \
+    --hash=sha256:59a3010d566a48b919490a982f6807f68842686941dc12d568e129d9cd7703d6 \
+    --hash=sha256:61c6a258469c66412ae8358a0501df6ccb3bb48aa9c43b56624571ff9767f91d \
+    --hash=sha256:63edc507f8cbfbb5903adb75bad8a99f9798981c854df9119dbebab2ec3ee0e1 \
+    --hash=sha256:65d5f4e70a2d3fbaa1349236968792611088f3f2dccead36c1626e1d183cc327 \
+    --hash=sha256:6a1618260a112a9c93504511f0b6254b4402a8c41b7130dc6d4c9e39aff3aa0c \
+    --hash=sha256:704f1fcdc5b606b70563ea696c69bda90caee3a2f45ffc9cee60a901b394a79f \
+    --hash=sha256:7751b11cd7f6b952b4b5ec5b93b5be9ce20faba786c18c25c354f5d8717a173c \
+    --hash=sha256:7ad9d032dc1a31a86ca7b059f43554a049a2bfda8fe32d1492ad25f6686aff03 \
+    --hash=sha256:7b01d07006a0ac2216921b69a220b9f0974345d0b1b36efaeabdc7550b1cc4f8 \
+    --hash=sha256:7b47643c45e7619788c081d42e1d9d98c7c8a4933010a9967d097cc3c4c29f41 \
+    --hash=sha256:80ed219ce6cb21a5b53ead0edf5b56b6d23de4cb95389ac606f47670474f4816 \
+    --hash=sha256:82df4a8600999c4c0cb7d6614df1bbdb3c74732f63e79f78487893ffbed3d083 \
+    --hash=sha256:8660112e9127a019969a23c878e1b4a419e8a6427f9a9050c19830f152628c8a \
+    --hash=sha256:89a86c2b35460700d04b4d6461153ab39ee85af5a5385acac9563a8310e6320a \
+    --hash=sha256:8d7bc25729bb6d96b44f49ad78fde0e27a1a867cb205322b7e5f5b49e04d6f1f \
+    --hash=sha256:97e4f3d9b17d12e7c00cb1c29c0040044135cd5146838da4274615dbe0baae78 \
+    --hash=sha256:a431deb6ffdfa551f7400b3a94fa4b964837e67f49e3c37aa26d90dc75970816 \
+    --hash=sha256:a6a2d3d75d8698dee492f4af7ad07606d0734e581edf9e2ce2f74b6fce90f42e \
+    --hash=sha256:ae5b41dbf7731b838021923edfbe3b5ccdec84d92d5795f5229c0d08d32509d9 \
+    --hash=sha256:aff258af03dda9a990960a53759d10c3a9b936837c71fe2f3b581acd356b9121 \
+    --hash=sha256:b216a15e13f6e763db40ac3beb74b588650bc030d10a78fde182b88d273b82b5 \
+    --hash=sha256:b23b25b1243576b952689966205ef7d4285688068b966a1ca0e620bcb390d483 \
+    --hash=sha256:b896637091cde69d170a89253dde9aee814b25ca204b7e213fd0a6462e666638 \
+    --hash=sha256:d5f27b1d1b56470385faa2b2636fcb823e7ac5b5b734e0aa76b14637c66eb3b7 \
+    --hash=sha256:d6ba33f39436191ece7ea2b3d0b4dff00af71acd5c6e6f1d6b7563aa7286e9f2 \
+    --hash=sha256:d6c5e1df6f427d7a82606cf8f07cf3ba9fb3f366804b01e65f1f00f8df6b54f1 \
+    --hash=sha256:e02f77b620ad6b36564fe41980865436912e21a3b1138cdde175cf24afde1bc5 \
+    --hash=sha256:e72491d72870c3cb2f0d6f4174485533caec0e9ed7e717e2859b7cc7ff2ae1c4 \
+    --hash=sha256:ea8d5cd689fa7225d81ae0a049ba03e0165f4ed9ca083b19a405be9ad0b36845 \
+    --hash=sha256:eb5341fc7c53fdd95ac2415be77b1de854ab266488cff71174ebb007baf0e675 \
+    --hash=sha256:edf0a66ce9517365c7dcfed597894d8dd1f27b59e550b77a089054101435213b \
+    --hash=sha256:f225784812b2b57d340f2eb0d2cebef989dcc82c288f5553e28ee9767c7c8344 \
+    --hash=sha256:f5fbb3b325c65010e04af206a9243e2df8606736c510c7f268aca6a93e5294a9 \
+    --hash=sha256:f78cafa25731e0b5aa16fe20bea1abf643d4e853f6bfb8a64421b06b878e2b88 \
+    --hash=sha256:fb639a0e65dce4a9cccbcbdd8ddd0c8c6ab10bca317b827a5c52ac3c3a4ad60a \
+    --hash=sha256:ffb2f288f577a748cc23c65a818290755a4c2da1f87a40d7055b61a096d31e20
+    # via -r third_party/py/requirements.in
 pycparser==2.21 \
     --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
     --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
@@ -379,6 +621,14 @@
     --hash=sha256:86ac6cee19f6c945e8dedf78c6ee74f1112bd14da5a658d8c9d4103aed5756a2 \
     --hash=sha256:cc0ea0de82b240a73ef4056fce44acbb4727dca7d66759371aff2bad457ed711
     # via -r third_party/py/requirements.in
+pygments==2.14.0 \
+    --hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \
+    --hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717
+    # via readme-renderer
+pyjwt[crypto]==2.6.0 \
+    --hash=sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd \
+    --hash=sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14
+    # via django-allauth
 pynacl==1.3.0 \
     --hash=sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255 \
     --hash=sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c \
@@ -410,29 +660,104 @@
     # via
     #   -r third_party/py/requirements.in
     #   arrow
+    #   hyperkitty
+python3-openid==3.2.0 \
+    --hash=sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf \
+    --hash=sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b
+    # via django-allauth
 pytz==2022.7.1 \
     --hash=sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0 \
     --hash=sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a
     # via
     #   -r third_party/py/requirements.in
     #   django
+    #   django-mailman3
+    #   djangorestframework
+    #   hyperkitty
+rcssmin==1.1.1 \
+    --hash=sha256:271e3d2f8614a6d4637ed8fff3d90007f03e2a654cd9444f37d888797662ba72 \
+    --hash=sha256:35da6a6999e9e2c5b0e691b42ed56cc479373e0ecab33ef5277dfecce625e44a \
+    --hash=sha256:42576d95dfad53d77df2e68dfdec95b89b10fad320f241f1af3ca1438578254a \
+    --hash=sha256:4f9400b4366d29f5f5446f58e78549afa8338e6a59740c73115e9f6ac413dc64 \
+    --hash=sha256:705c9112d0ed54ea40aecf97e7fd29bdf0f1c46d278a32d8f957f31dde90778a \
+    --hash=sha256:79421230dd67c37ec61ed9892813d2b839b68f2f48ef55c75f976e81701d60b4 \
+    --hash=sha256:868215e1fd0e92a6122e0ed5973dfc7bb8330fe1e92274d05b2585253b38c0ca \
+    --hash=sha256:8a26fec3c1e6b7a3765ccbaccc20fbb5c0ed3422cc381e01a2607f08d7621c44 \
+    --hash=sha256:8fcfd10ae2a1c4ce231a33013f2539e07c3836bf17cc945cc25cc30bf8e68e45 \
+    --hash=sha256:908fe072efd2432fb0975a61124609a8e05021367f6a3463d45f5e3e74c4fdda \
+    --hash=sha256:914e589f40573035006913861ed2adc28fbe70082a8b6bff5be7ee430b7b5c2e \
+    --hash=sha256:a04d58a2a21e9a089306d3f99c4b12bf5b656a79c198ef2321e80f8fd9afab06 \
+    --hash=sha256:a417735d4023d47d048a6288c88dbceadd20abaaf65a11bb4fda1e8458057019 \
+    --hash=sha256:c30f8bc839747b6da59274e0c6e4361915d66532e26448d589cb2b1846d7bf11 \
+    --hash=sha256:c7278c1c25bb90d8e554df92cfb3b6a1195004ead50f764653d3093933ee0877 \
+    --hash=sha256:c7728e3b546b1b6ea08cab721e8e21409dbcc11b881d0b87d10b0be8930af2a2 \
+    --hash=sha256:cf74d7ea5e191f0f344b354eed8b7c83eeafbd9a97bec3a579c3d26edf11b005 \
+    --hash=sha256:d0afc6e7b64ef30d6dcde88830ec1a237b9f16a39f920a8fd159928684ccf8db \
+    --hash=sha256:d4e263fa9428704fd94c2cb565c7519ca1d225217943f71caffe6741ab5b9df1 \
+    --hash=sha256:e923c105100ab70abde1c01d3196ddd6b07255e32073685542be4e3a60870c8e \
+    --hash=sha256:ee386bec6d62f8c814d65c011d604a7c82d24aa3f718facd66e850eea8d6a5a1 \
+    --hash=sha256:f15673e97f0a68b4c378c4d15b088fe96d60bc106d278c88829923118833c20f \
+    --hash=sha256:f7a1fcdbafaacac0530da04edca4a44303baab430ea42e7d59aece4b3f3e9a51
+    # via django-compressor
+readme-renderer[md]==37.3 \
+    --hash=sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273 \
+    --hash=sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343
+    # via postorius
+redis==3.5.3 \
+    --hash=sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2 \
+    --hash=sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24
+    # via django-q
 requests==2.22.0 \
     --hash=sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4 \
     --hash=sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31
     # via
     #   -r third_party/py/requirements.in
+    #   django-allauth
+    #   mailmanclient
     #   requests-oauthlib
 requests-oauthlib==1.3.0 \
     --hash=sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d \
     --hash=sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a
-    # via -r third_party/py/requirements.in
+    # via
+    #   -r third_party/py/requirements.in
+    #   django-allauth
+rjsmin==1.2.1 \
+    --hash=sha256:113132a40ce7d03b2ced4fac215f0297338ed1c207394b739266efab7831988b \
+    --hash=sha256:122aa52bcf7ad9f12728d309012d1308c6ecfe4d6b09ea867a110dcad7b7728c \
+    --hash=sha256:145c6af8df42d8af102d0d39a6de2e5fa66aef9e38947cfb9d65377d1b9940b2 \
+    --hash=sha256:1f982be8e011438777a94307279b40134a3935fc0f079312ee299725b8af5411 \
+    --hash=sha256:3453ee6d5e7a2723ec45c2909e2382371783400e8d51952b692884c6d850a3d0 \
+    --hash=sha256:35827844d2085bd59d34214dfba6f1fc42a215c455887437b07dbf9c73019cc1 \
+    --hash=sha256:35f21046504544e2941e04190ce24161255479133751550e36ddb3f4af0ecdca \
+    --hash=sha256:5d67ec09da46a492186e35cabca02a0d092eda5ef5b408a419b99ee4acf28d5c \
+    --hash=sha256:747bc9d3bc8a220f40858e6aad50b2ae2eb7f69c924d4fa3803b81be1c1ddd02 \
+    --hash=sha256:7dd58b5ed88233bc61dc80b0ed87b93a1786031d9977c70d335221ef1ac5581a \
+    --hash=sha256:812af25c08d6a5ae98019a2e1b47ebb47f7469abd351670c353d619eaeae4064 \
+    --hash=sha256:8a6710e358c661dcdcfd027e67de3afd72a6af4c88101dcf110de39e9bbded39 \
+    --hash=sha256:8c340e251619c97571a5ade20f147f1f7e8664f66a2d6d7319e05e3ef6a4423c \
+    --hash=sha256:99c074cd6a8302ff47118a9c3d086f89328dc8e5c4b105aa1f348fb85c765a30 \
+    --hash=sha256:b8464629a18fe69f70677854c93a3707976024b226a0ce62707c618f923e1346 \
+    --hash=sha256:bbd7a0abaa394afd951f5d4e05249d306fec1c9674bfee179787674dddd0bdb7 \
+    --hash=sha256:bc5bc2f94e59bc81562c572b7f1bdd6bcec4f61168dc68a2993bad2d355b6e19 \
+    --hash=sha256:bd1faedc425006d9e86b23837d164f01d105b7a8b66b767a9766d0014773db2a \
+    --hash=sha256:ca90630b84fe94bb07739c3e3793e87d30c6ee450dde08653121f0d9153c8d0d \
+    --hash=sha256:d332e44a1b21ad63401cc7eebc81157e3d982d5fb503bb4faaea5028068d71e9 \
+    --hash=sha256:eb770aaf637919b0011c4eb87b9ac6317079fb9800eb17c90dda05fc9de4ebc3 \
+    --hash=sha256:f0895b360dccf7e2d6af8762a52985e3fbaa56778de1bf6b20dbc96134253807 \
+    --hash=sha256:f7cd33602ec0f393a0058e883284496bb4dbbdd34e0bbe23b594c8933ddf9b65
+    # via django-compressor
+robot-detection==0.4 \
+    --hash=sha256:3d8fb72ca47164b8ce55e33bdda93742f62c348def7d3cc3b42b0ceb4795a2f5
+    # via hyperkitty
 six==1.16.0 \
     --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
     --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
     # via
-    #   cryptography
+    #   bleach
+    #   blessed
     #   pynacl
     #   python-dateutil
+    #   robot-detection
 sqlalchemy==1.3.8 \
     --hash=sha256:2f8ff566a4d3a92246d367f2e9cd6ed3edeef670dcd6dda6dfdc9efed88bcd80
     # via
@@ -450,6 +775,14 @@
     # via
     #   -r third_party/py/requirements.in
     #   requests
+wcwidth==0.2.6 \
+    --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \
+    --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0
+    # via blessed
+webencodings==0.5.1 \
+    --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
+    --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
+    # via bleach
 werkzeug==0.15.5 \
     --hash=sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4 \
     --hash=sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6
@@ -521,9 +854,9 @@
     # via gevent
 
 # The following packages are considered to be unsafe in a requirements file:
-setuptools==67.1.0 \
-    --hash=sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378 \
-    --hash=sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300
+setuptools==68.2.0 \
+    --hash=sha256:00478ca80aeebeecb2f288d3206b0de568df5cd2b8fada1209843cc9a8d88a48 \
+    --hash=sha256:af3d5949030c3f493f550876b2fd1dd5ec66689c4ee5d5344f009746f71fd5a8
     # via
     #   gevent
     #   gunicorn