diff --git a/aleksis/core/__init__.py b/aleksis/core/__init__.py
index 2484be76410f3d59a7bed7dda78c5f4e0f2a1c63..21a49f0785f3977d9a265059a32ec957c9e1eb3c 100644
--- a/aleksis/core/__init__.py
+++ b/aleksis/core/__init__.py
@@ -1,5 +1,11 @@
 import pkg_resources
 
+try:
+    from .celery import app as celery_app
+except ModuleNotFoundError:
+    # Celery is not available
+    celery_app = None
+
 try:
     __version__ = pkg_resources.get_distribution("AlekSIS").version
 except Exception:
diff --git a/aleksis/core/celery.py b/aleksis/core/celery.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f4dce954576a5d688311c466bc2b9fb4ad4e151
--- /dev/null
+++ b/aleksis/core/celery.py
@@ -0,0 +1,8 @@
+import os
+from celery import Celery
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings")
+
+app = Celery("aleksis")  # noqa
+app.config_from_object('django.conf:settings', namespace='CELERY')
+app.autodiscover_tasks()
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index a4ea184b7523af9b0fd034fe3efcf21716847c5f..199ba298fbbaa201bd5090325d4e786ec4e54e9a 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -368,7 +368,16 @@ if _settings.get("2fa.twilio.sid", None):
     TWILIO_TOKEN = _settings.get("2fa.twilio.token")
     TWILIO_CALLER_ID = _settings.get("2fa.twilio.callerid")
 
-_settings.populate_obj(sys.modules[__name__])
+if _settings.get("celery.enabled", False):
+    INSTALLED_APPS += ("django_celery_beat", "django_celery_results")
+    CELERY_BROKER_URL = "redis://localhost"
+    CELERY_RESULT_BACKEND = "django-db"
+    CELERY_CACHE_BACKEND = "django-cache"
+    CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
+
+    if _settings.get("celery.email", False):
+        INSTALLED_APPS += ("djcelery_email",)
+        EMAIL_BACKEND = "djcelery_email.backends.CeleryEmailBackend"
 
 PWA_APP_NAME = "AlekSIS"  # dbsettings
 PWA_APP_DESCRIPTION = "AlekSIS – The free school information system"  # dbsettings
diff --git a/aleksis/core/templatetags/templatetags/msg_box.py b/aleksis/core/templatetags/msg_box.py
similarity index 100%
rename from aleksis/core/templatetags/templatetags/msg_box.py
rename to aleksis/core/templatetags/msg_box.py
diff --git a/aleksis/core/templatetags/templatetags/__init__.py b/aleksis/core/templatetags/templatetags/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/aleksis/core/templatetags/templatetags/copy_filter.py b/aleksis/core/templatetags/templatetags/copy_filter.py
deleted file mode 100644
index 9ee6181b430174b1f8dec5eff3276531f7e7e44b..0000000000000000000000000000000000000000
--- a/aleksis/core/templatetags/templatetags/copy_filter.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import copy as copylib
-
-from django import template
-
-register = template.Library()
-
-register.filter("copy", copylib.copy)
-register.filter("deepcopy", copylib.deepcopy)
diff --git a/aleksis/core/templatetags/templatetags/tex.py b/aleksis/core/templatetags/templatetags/tex.py
deleted file mode 100644
index 333c3d31d819eb4c3b41ef6513d3b2b9df3ff30e..0000000000000000000000000000000000000000
--- a/aleksis/core/templatetags/templatetags/tex.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Django filters, needed when creating LaTeX files with the django template language
-
-Written by Rocco Schulz (http://is-gr8.com/), modified by SchoolApps-Team
-"""
-from django.template.defaultfilters import stringfilter, register
-from django.template.loader import render_to_string
-
-
-@register.filter
-@stringfilter
-def brackets(value):
-    """
-    surrounds the value with { }
-    You have to use this filter whenever you would need something like
-    {{{ field }}} in a template.
-    """
-    return "{%s}" % value
-
-
-REPLACEMENTS = {
-    "§": "\\textsection{}",
-    "$": "\\textdollar{}",
-    "LaTeX": "\\LaTeX \\ ",
-    " TeX": " \\TeX \\ ",
-    "€": "\\euro",
-    ">": "$>$",
-    "<": "$<$"
-}
-
-ESCAPES = ("&", "{", "}", "%")
-
-
-@register.filter
-@stringfilter
-def texify(value):
-    """
-    escapes/replaces special character with appropriate latex commands
-    """
-    tex_value = []
-
-    # escape special symbols
-    for char in value:
-        tex_value.append("%s" % ("\\%s" % char if char in ESCAPES else char))
-    tex_value = "".join(tex_value)
-
-    # replace symbols / words with latex commands
-    for key, value in REPLACEMENTS.items():
-        tex_value = tex_value.replace(key, value)
-
-    return "%s" % tex_value
diff --git a/aleksis/core/templatetags/templatetags/url_name.py b/aleksis/core/templatetags/templatetags/url_name.py
deleted file mode 100644
index 20f63c673b00f0d736652db62937f9dc68947db2..0000000000000000000000000000000000000000
--- a/aleksis/core/templatetags/templatetags/url_name.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from django.urls import resolve
-from django import template
-
-register = template.Library()
-
-
-def get_url_name(request):  # Only one argument.
-    """Gets url_name"""
-    try:
-        return resolve(request.path_info).url_name
-    except Exception as e:
-        return e
-
-
-register.filter("url_name", get_url_name)
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 0a5aab7cb8765fd6e9a70a020770a083e01c13a1..5669323611a2a09a6b82a9d0a8523336381bfa7e 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -102,3 +102,22 @@ def has_person(obj: Union[HttpRequest, Model]) -> bool:
             return False
 
     return getattr(obj, "person", None) is not None
+
+
+def celery_optional(orig: Callable) -> Callable:
+    """ Decorator that makes Celery optional for a function.
+    
+    If Celery is configured and available, it wraps the function in a Task
+    and calls its delay method when invoked; if not, it leaves it untouched
+    and it is executed synchronously.
+    """
+
+    if hasattr(settings, "CELERY_RESULT_BACKEND"):
+        from ..celery import app  # noqa
+        task = app.task(orig)
+
+        def wrapped(*args, **kwargs):
+            task.delay(*args, **kwargs)
+        return wrapped
+    else:
+        return orig
diff --git a/poetry.lock b/poetry.lock
index 73e774ec7523336f1d76a01ea821b22aeaebfa23..f26d4ed1eb2979d0ede5264676e9236de394f58a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -6,6 +6,17 @@ optional = false
 python-versions = "*"
 version = "0.7.12"
 
+[[package]]
+category = "main"
+description = "Low-level AMQP client for Python (fork of amqplib)."
+name = "amqp"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "2.5.2"
+
+[package.dependencies]
+vine = ">=1.1.3,<5.0.0a1"
+
 [[package]]
 category = "dev"
 description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
@@ -89,6 +100,14 @@ soupsieve = ">=1.2"
 html5lib = ["html5lib"]
 lxml = ["lxml"]
 
+[[package]]
+category = "main"
+description = "Python multiprocessing fork with improvements and bugfixes"
+name = "billiard"
+optional = true
+python-versions = "*"
+version = "3.6.1.0"
+
 [[package]]
 category = "dev"
 description = "The uncompromising code formatter."
@@ -109,6 +128,62 @@ typed-ast = ">=1.4.0"
 [package.extras]
 d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
 
+[[package]]
+category = "main"
+description = "Distributed Task Queue."
+name = "celery"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*,"
+version = "4.4.0"
+
+[package.dependencies]
+billiard = ">=3.6.1,<4.0"
+kombu = ">=4.6.7,<4.7"
+pytz = ">0.0-dev"
+vine = "1.3.0"
+
+[package.dependencies.Django]
+optional = true
+version = ">=1.11"
+
+[package.dependencies.redis]
+optional = true
+version = ">=3.2.0"
+
+[package.extras]
+arangodb = ["pyArango (>=1.3.2)"]
+auth = ["cryptography"]
+azureblockblob = ["azure-storage (0.36.0)", "azure-common (1.1.5)", "azure-storage-common (1.1.0)"]
+brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"]
+cassandra = ["cassandra-driver"]
+consul = ["python-consul"]
+cosmosdbsql = ["pydocumentdb (2.3.2)"]
+couchbase = ["couchbase", "couchbase-cffi"]
+couchdb = ["pycouchdb"]
+django = ["Django (>=1.11)"]
+dynamodb = ["boto3 (>=1.9.178)"]
+elasticsearch = ["elasticsearch"]
+eventlet = ["eventlet (>=0.24.1)"]
+gevent = ["gevent"]
+librabbitmq = ["librabbitmq (>=1.5.0)"]
+lzma = ["backports.lzma"]
+memcache = ["pylibmc"]
+mongodb = ["pymongo (>=3.3.0)"]
+msgpack = ["msgpack"]
+pymemcache = ["python-memcached"]
+pyro = ["pyro4"]
+redis = ["redis (>=3.2.0)"]
+riak = ["riak (>=2.0)"]
+s3 = ["boto3 (>=1.9.125)"]
+slmq = ["softlayer-messaging (>=1.0.3)"]
+solar = ["ephem"]
+sqlalchemy = ["sqlalchemy"]
+sqs = ["boto3 (>=1.9.125)", "pycurl"]
+tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"]
+yaml = ["PyYAML (>=3.10)"]
+zookeeper = ["kazoo (>=1.3.1)"]
+zstd = ["zstandard"]
+
 [[package]]
 category = "main"
 description = "Python package for providing Mozilla's CA Bundle."
@@ -161,6 +236,9 @@ optional = false
 python-versions = "*"
 version = "5.0.6"
 
+[package.dependencies]
+six = "*"
+
 [[package]]
 category = "dev"
 description = "Code coverage measurement for Python"
@@ -254,6 +332,42 @@ version = "2.2.0"
 [package.dependencies]
 Django = ">=1.8"
 
+[[package]]
+category = "main"
+description = "Database-backed Periodic Tasks."
+name = "django-celery-beat"
+optional = true
+python-versions = "*"
+version = "1.5.0"
+
+[package.dependencies]
+django-timezone-field = ">=2.0"
+python-crontab = ">=2.3.4"
+
+[[package]]
+category = "main"
+description = "An async Django email backend using celery"
+name = "django-celery-email"
+optional = true
+python-versions = "*"
+version = "3.0.0"
+
+[package.dependencies]
+celery = ">=4.0"
+django = ">=2.2"
+django-appconf = "*"
+
+[[package]]
+category = "main"
+description = "Celery result backends for Django."
+name = "django-celery-results"
+optional = true
+python-versions = "*"
+version = "1.1.2"
+
+[package.dependencies]
+celery = ">=4.3,<5.0"
+
 [[package]]
 category = "main"
 description = "Django admin CKEditor integration."
@@ -273,6 +387,11 @@ optional = false
 python-versions = "*"
 version = "2.5.0"
 
+[package.dependencies]
+[package.dependencies.django-picklefield]
+optional = true
+version = "*"
+
 [package.extras]
 database = ["django-picklefield"]
 redis = ["redis"]
@@ -281,7 +400,6 @@ redis = ["redis"]
 reference = "590fa02eb30e377da0eda5cc3a84254b839176a7"
 type = "git"
 url = "https://github.com/jazzband/django-constance"
-
 [[package]]
 category = "main"
 description = "A configurable set of panels that display various debug information about the current request/response."
@@ -581,6 +699,18 @@ version = "2.3.0"
 django-render-block = ">=0.5"
 six = ">=1"
 
+[[package]]
+category = "main"
+description = "A Django app providing database and form fields for pytz timezone objects."
+name = "django-timezone-field"
+optional = true
+python-versions = ">=3.5"
+version = "4.0"
+
+[package.dependencies]
+django = ">=2.2"
+pytz = "*"
+
 [[package]]
 category = "main"
 description = "Complete Two-Factor Authentication for Django"
@@ -721,12 +851,11 @@ category = "main"
 description = "Faker is a Python package that generates fake data for you."
 name = "faker"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "3.0.1"
+python-versions = ">=3.4"
+version = "4.0.0"
 
 [package.dependencies]
 python-dateutil = ">=2.4"
-six = ">=1.10"
 text-unidecode = "1.3"
 
 [[package]]
@@ -916,7 +1045,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 version = "1.2.0"
 
 [[package]]
-category = "dev"
+category = "main"
 description = "Read metadata from Python packages"
 marker = "python_version < \"3.8\""
 name = "importlib-metadata"
@@ -959,6 +1088,37 @@ MarkupSafe = ">=0.23"
 [package.extras]
 i18n = ["Babel (>=0.8)"]
 
+[[package]]
+category = "main"
+description = "Messaging library for Python."
+name = "kombu"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "4.6.7"
+
+[package.dependencies]
+amqp = ">=2.5.2,<2.6"
+
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = ">=0.18"
+
+[package.extras]
+azureservicebus = ["azure-servicebus (>=0.21.1)"]
+azurestoragequeues = ["azure-storage-queue"]
+consul = ["python-consul (>=0.6.0)"]
+librabbitmq = ["librabbitmq (>=1.5.2)"]
+mongodb = ["pymongo (>=3.3.0)"]
+msgpack = ["msgpack"]
+pyro = ["pyro4"]
+qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"]
+redis = ["redis (>=3.3.11)"]
+slmq = ["softlayer-messaging (>=1.0.3)"]
+sqlalchemy = ["sqlalchemy"]
+sqs = ["boto3 (>=1.4.4)", "pycurl (7.43.0.2)"]
+yaml = ["PyYAML (>=3.10)"]
+zookeeper = ["kazoo (>=1.3.1)"]
+
 [[package]]
 category = "main"
 description = "Sass for Python: A straightforward binding of libsass for Python."
@@ -987,7 +1147,7 @@ python-versions = "*"
 version = "0.6.1"
 
 [[package]]
-category = "dev"
+category = "main"
 description = "More routines for operating on iterables, beyond itertools"
 name = "more-itertools"
 optional = false
@@ -1279,6 +1439,21 @@ version = "3.4.6"
 [package.extras]
 testing = ["pytest", "coverage (>=3.6)", "pytest-cov"]
 
+[[package]]
+category = "main"
+description = "Python Crontab API"
+name = "python-crontab"
+optional = true
+python-versions = "*"
+version = "2.4.0"
+
+[package.dependencies]
+python-dateutil = "*"
+
+[package.extras]
+cron-description = ["cron-descriptor"]
+cron-schedule = ["croniter"]
+
 [[package]]
 category = "main"
 description = "Extensions to the standard Python datetime module"
@@ -1337,7 +1512,7 @@ category = "main"
 description = "YAML parser and emitter for Python"
 name = "pyyaml"
 optional = false
-python-versions = "*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 version = "5.3"
 
 [[package]]
@@ -1358,6 +1533,17 @@ maintainer = ["zest.releaser"]
 pil = ["pillow"]
 test = ["pytest", "pytest-cov", "mock"]
 
+[[package]]
+category = "main"
+description = "Python client for Redis key-value store"
+name = "redis"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "3.3.11"
+
+[package.extras]
+hiredis = ["hiredis (>=0.1.3)"]
+
 [[package]]
 category = "dev"
 description = "Alternative regular expression module, to replace re."
@@ -1434,8 +1620,8 @@ category = "main"
 description = "Python 2 and 3 compatibility utilities"
 name = "six"
 optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*"
-version = "1.13.0"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "1.14.0"
 
 [[package]]
 category = "dev"
@@ -1721,6 +1907,14 @@ brotli = ["brotlipy (>=0.6.0)"]
 secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
 socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
 
+[[package]]
+category = "main"
+description = "Promises, promises, promises."
+name = "vine"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "1.3.0"
+
 [[package]]
 category = "dev"
 description = "Measures number of Terminal column cells of wide-character codes"
@@ -1742,7 +1936,7 @@ pycryptodome = "*"
 six = "*"
 
 [[package]]
-category = "dev"
+category = "main"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 marker = "python_version < \"3.8\""
 name = "zipp"
@@ -1758,10 +1952,11 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
 testing = ["pathlib2", "contextlib2", "unittest2"]
 
 [extras]
+celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celery-email"]
 ldap = ["django-auth-ldap"]
 
 [metadata]
-content-hash = "e312e9803fee3289e4d403de297d432c3747e437c1b3fedbd2b8baeca3bfa9b0"
+content-hash = "b2a7c1d9e1318e93322b69bf5b9eaeaee6b6fc4b3f983f1a0c88d2c934f8d848"
 python-versions = "^3.7"
 
 [metadata.files]
@@ -1769,6 +1964,10 @@ alabaster = [
     {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
     {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
 ]
+amqp = [
+    {file = "amqp-2.5.2-py2.py3-none-any.whl", hash = "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8"},
+    {file = "amqp-2.5.2.tar.gz", hash = "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"},
+]
 appdirs = [
     {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
     {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"},
@@ -1798,10 +1997,18 @@ beautifulsoup4 = [
     {file = "beautifulsoup4-4.8.2-py3-none-any.whl", hash = "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887"},
     {file = "beautifulsoup4-4.8.2.tar.gz", hash = "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a"},
 ]
+billiard = [
+    {file = "billiard-3.6.1.0-py3-none-any.whl", hash = "sha256:01afcb4e7c4fd6480940cfbd4d9edc19d7a7509d6ada533984d0d0f49901ec82"},
+    {file = "billiard-3.6.1.0.tar.gz", hash = "sha256:b8809c74f648dfe69b973c8e660bcec00603758c9db8ba89d7719f88d5f01f26"},
+]
 black = [
     {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
     {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
 ]
+celery = [
+    {file = "celery-4.4.0-py2.py3-none-any.whl", hash = "sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f"},
+    {file = "celery-4.4.0.tar.gz", hash = "sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"},
+]
 certifi = [
     {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
     {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"},
@@ -1885,6 +2092,18 @@ django-bulk-update = [
     {file = "django-bulk-update-2.2.0.tar.gz", hash = "sha256:5ab7ce8a65eac26d19143cc189c0f041d5c03b9d1b290ca240dc4f3d6aaeb337"},
     {file = "django_bulk_update-2.2.0-py2.py3-none-any.whl", hash = "sha256:49a403392ae05ea872494d74fb3dfa3515f8df5c07cc277c3dc94724c0ee6985"},
 ]
+django-celery-beat = [
+    {file = "django-celery-beat-1.5.0.tar.gz", hash = "sha256:659b39232c454ac27022bf679939bce0471fd482f3ee9276f5199716cb4afad9"},
+    {file = "django_celery_beat-1.5.0-py2.py3-none-any.whl", hash = "sha256:61c92d4b600a9f24406ee0b8d01a9b192253e15d047e3325e1d81e2cacf7aba6"},
+]
+django-celery-email = [
+    {file = "django-celery-email-3.0.0.tar.gz", hash = "sha256:5546cbba80952cc3b8a0ffa4206ce90a4a996a7ffd1c385a2bdb65903ca18ece"},
+    {file = "django_celery_email-3.0.0-py2.py3-none-any.whl", hash = "sha256:0f72da39cb2ea83c69440566e87f27cd72f68f247f98ce99fb29889fcf329406"},
+]
+django-celery-results = [
+    {file = "django_celery_results-1.1.2-py2.py3-none-any.whl", hash = "sha256:932277e9382528f74778b30cf90e17941cba577b7d73cee09ed55e4972972c32"},
+    {file = "django_celery_results-1.1.2.tar.gz", hash = "sha256:e735dc3e705a0e21afc3b6fa2918ec388258145fcbaad3727c493c5707d25034"},
+]
 django-ckeditor = [
     {file = "django-ckeditor-5.8.0.tar.gz", hash = "sha256:46fc9c7346ea36183dc0cea350f98704f8b04c4722b7fe4fb18baf8ae20423fb"},
     {file = "django_ckeditor-5.8.0-py2.py3-none-any.whl", hash = "sha256:a59bab13f4481318f8a048b1b0aef5c7da768a6352dcfb9ba0e77d91fbb9462a"},
@@ -1985,6 +2204,10 @@ django-tables2 = [
 django-templated-email = [
     {file = "django-templated-email-2.3.0.tar.gz", hash = "sha256:536c4e5ae099eabfb9aab36087d4d7799948c654e73da55a744213d086d5bb33"},
 ]
+django-timezone-field = [
+    {file = "django-timezone-field-4.0.tar.gz", hash = "sha256:7e3620fe2211c2d372fad54db8f86ff884098d018d56fda4dca5e64929e05ffc"},
+    {file = "django_timezone_field-4.0-py3-none-any.whl", hash = "sha256:758b7d41084e9ea2e89e59eb616e9b6326e6fbbf9d14b6ef062d624fe8cc6246"},
+]
 django-two-factor-auth = [
     {file = "django-two-factor-auth-1.10.0.tar.gz", hash = "sha256:3c3af3cd747462be18e7494c4068a2bdc606d7a2d2b2914f8d4590fc80995a71"},
     {file = "django_two_factor_auth-1.10.0-py2.py3-none-any.whl", hash = "sha256:0945260fa84e4522d8fa951c35e401616579fd8564938441614399dc588a1c1f"},
@@ -2016,8 +2239,8 @@ entrypoints = [
     {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
 ]
 faker = [
-    {file = "Faker-3.0.1-py2.py3-none-any.whl", hash = "sha256:6eb3581e990e36ef6f1cf37f70f9a799e119e1a7b94a6062a14f1b8d781c67e4"},
-    {file = "Faker-3.0.1.tar.gz", hash = "sha256:c7f7466cb9ba58d582f713494acdb5ebcc462336c5e38c5230b0cdab37069985"},
+    {file = "Faker-4.0.0-py3-none-any.whl", hash = "sha256:047d4d1791bfb3756264da670d99df13d799bb36e7d88774b1585a82d05dbaec"},
+    {file = "Faker-4.0.0.tar.gz", hash = "sha256:1b1a58961683b30c574520d0c739c4443e0ef6a185c04382e8cc888273dbebed"},
 ]
 flake8 = [
     {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"},
@@ -2091,6 +2314,10 @@ jinja2 = [
     {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"},
     {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"},
 ]
+kombu = [
+    {file = "kombu-4.6.7-py2.py3-none-any.whl", hash = "sha256:2a9e7adff14d046c9996752b2c48b6d9185d0b992106d5160e1a179907a5d4ac"},
+    {file = "kombu-4.6.7.tar.gz", hash = "sha256:67b32ccb6fea030f8799f8fd50dd08e03a4b99464ebc4952d71d8747b1a52ad1"},
+]
 libsass = [
     {file = "libsass-0.19.4-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:74acd9adf506142699dfa292f0e569fdccbd9e7cf619e8226f7117de73566e32"},
     {file = "libsass-0.19.4-cp27-cp27m-win32.whl", hash = "sha256:50778d4be269a021ba2bf42b5b8f6ff3704ab96a82175a052680bddf3ba7cc9f"},
@@ -2346,6 +2573,9 @@ python-box = [
     {file = "python-box-3.4.6.tar.gz", hash = "sha256:694a7555e3ff9fbbce734bbaef3aad92b8e4ed0659d3ed04d56b6a0a0eff26a9"},
     {file = "python_box-3.4.6-py2.py3-none-any.whl", hash = "sha256:a71d3dc9dbaa34c8597d3517c89a8041bd62fa875f23c0f3dad55e1958e3ce10"},
 ]
+python-crontab = [
+    {file = "python-crontab-2.4.0.tar.gz", hash = "sha256:3ac1608ff76032e6fc6e16b5fbf83b51557e0e066bf78e9f88571571e7bd7ae6"},
+]
 python-dateutil = [
     {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
     {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
@@ -2382,6 +2612,10 @@ qrcode = [
     {file = "qrcode-6.1-py2.py3-none-any.whl", hash = "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5"},
     {file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"},
 ]
+redis = [
+    {file = "redis-3.3.11-py2.py3-none-any.whl", hash = "sha256:3613daad9ce5951e426f460deddd5caf469e08a3af633e9578fc77d362becf62"},
+    {file = "redis-3.3.11.tar.gz", hash = "sha256:8d0fc278d3f5e1249967cba2eb4a5632d19e45ce5c09442b8422d15ee2c22cc2"},
+]
 regex = [
     {file = "regex-2020.1.8-cp27-cp27m-win32.whl", hash = "sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161"},
     {file = "regex-2020.1.8-cp27-cp27m-win_amd64.whl", hash = "sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242"},
@@ -2425,8 +2659,8 @@ selenium = [
     {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"},
 ]
 six = [
-    {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"},
-    {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"},
+    {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
+    {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
 ]
 smmap2 = [
     {file = "smmap2-2.0.5-py2.py3-none-any.whl", hash = "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde"},
@@ -2547,8 +2781,13 @@ urllib3 = [
     {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"},
     {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"},
 ]
+vine = [
+    {file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"},
+    {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"},
+]
 wcwidth = [
     {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"},
+    {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"},
 ]
 yubiotp = [
     {file = "YubiOTP-0.2.2.post1-py2.py3-none-any.whl", hash = "sha256:7e281801b24678f4bda855ce8ab975a7688a912f5a6cb22b6c2b16263a93cbd2"},
diff --git a/pyproject.toml b/pyproject.toml
index 35d340e82953303a1ec553bed59ddce25057f715..77cdc3486b024ae6ff0b0d4ded2175a651146705 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,13 +54,18 @@ django-constance = {git = "https://github.com/jazzband/django-constance", rev =
 django_widget_tweaks = "^1.4.5"
 django-filter = "^2.2.0"
 django-templated-email = "^2.3.0"
-html2text = "^2019.9.26"
+html2text = "^2020.0.0"
 django-ckeditor = "^5.8.0"
 django-js-reverse = "^0.9.1"
 calendarweek = "^0.4.3"
+Celery = {version="^4.4.0", optional=true, extras=["django", "redis"]}
+django-celery-results = {version="^1.1.2", optional=true}
+django-celery-beat = {version="^1.5.0", optional=true}
+django-celery-email = {version="^3.0.0", optional=true}
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]
+celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celery-email"]
 
 [tool.poetry.dev-dependencies]
 sphinx = "^2.1"