From a0ca4d6daae93fb688d88853ffbd53c518e2b7ef Mon Sep 17 00:00:00 2001
From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de>
Date: Sun, 8 Apr 2018 17:42:13 +0200
Subject: [PATCH] Parse, check and generate the configuration

The configuration is defined in configproxy.py. Add new keys there.
Use ./configproxy.py check to check the config validity.
    ./configproxy.py create to create an example config.
---
 calendarpush.py    |   2 +-
 common             |   2 +-
 config.py.example  | 480 ++++++++++++----------
 configproxy.py     | 997 +++++++++++++++++++++++++++++++++++++++++++++
 legacy.py          |   2 +-
 models/database.py |   3 +-
 protoparser.py     |   2 +-
 requirements.txt   |   2 +-
 server.py          |  16 +-
 shared.py          |   7 +-
 tasks.py           |   4 +-
 utils.py           |  40 +-
 views/forms.py     |   2 +-
 views/tables.py    |   2 +-
 wiki.py            |   2 +-
 15 files changed, 1320 insertions(+), 243 deletions(-)
 create mode 100755 configproxy.py

diff --git a/calendarpush.py b/calendarpush.py
index 5f0564a..f69e7d0 100644
--- a/calendarpush.py
+++ b/calendarpush.py
@@ -5,7 +5,7 @@ import quopri
 from caldav import DAVClient
 from vobject.base import ContentLine
 
-import config
+from shared import config
 
 
 class CalendarException(Exception):
diff --git a/common b/common
index 4c0c16f..2a0f881 160000
--- a/common
+++ b/common
@@ -1 +1 @@
-Subproject commit 4c0c16f069bb39d593b53eae0c2b92b1f05eba90
+Subproject commit 2a0f881728f1000f27fb00321bc37fb869007fa9
diff --git a/config.py.example b/config.py.example
index 19e0343..b08b389 100644
--- a/config.py.example
+++ b/config.py.example
@@ -1,217 +1,263 @@
-# (local) database
-SQLALCHEMY_DATABASE_URI = "postgresql://user:password@host/database" # change this
-#SQLALCHEMY_DATABASE_URI = "mysql://user:password@host/database"
-#SQLALCHEMY_DATABASE_URI = "sqlite:///path/to/database.db"
-SQLALCHEMY_TRACK_MODIFICATIONS = False # do not change
-
-SECRET_KEY = "something random" # change this
-
-SERVER_NAME = "protokolle.example.com"
-PREFERRED_URL_SCHEME = "https" # change to http for development
-
-DEBUG = False # do not change
-
-# mailserver (optional)
-MAIL_ACTIVE = True
-MAIL_FROM = "protokolle@example.com"
-MAIL_HOST = "mail.example.com:465"
-MAIL_USER = "user" # set to "" for unauthenticated sending
-MAIL_PASSWORD = "password" # set to "" for unauthenticated sending
-MAIL_USE_TLS = True # should match the port in MAIL_HOST (if present there)
-MAIL_USE_STARTTLS = False # Usually, it's either this or SMTPS, not both
-
-# (local) message queue (necessary)
-CELERY_BROKER_URL = "redis://localhost:6379/0" # change this if you do not use redis or it is running somewhere else
-CELERY_TASK_SERIALIZER = "pickle" # do not change
-CELERY_ACCEPT_CONTENT = ["pickle"] # do not change
-
-# Send exceptions to sentry (optional)
-# SENTRY_DSN = "https://********:********@sentry.example.com//1"
-
-# CUPS printserver (optional)
-PRINTING_ACTIVE = True
-PRINTING_SERVER = "printsrv.example.com:631"
-PRINTING_USER = "protocols"
-PRINTING_PRINTERS = {
-    "example_printer": ["Duplex=DuplexNoTumble", "option2=value"],
-    "other_printer": ["list", "of", "options"]
-}
-
-# etherpad (optional)
-ETHERPAD_ACTIVE = True
-ETHERPAD_URL = "https://example.com/etherpad" # without /p/…
-EMPTY_ETHERPAD = """Welcome to Etherpad!
-
-This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!
-
-Get involved with Etherpad at http://etherpad.org
-
-""" # do not change
-
-# wiki (optional)
-WIKI_ACTIVE = True
-WIKI_TYPE = "MEDIAWIKI"
-WIKI_API_URL = "https://wiki.example.com/wiki/api.php"
-WIKI_ANONYMOUS = False
-WIKI_USER = "user"
-WIKI_PASSWORD = "password"
-WIKI_DOMAIN = "domain" # set to None if not necessary
-
-# CalDAV calendar (optional)
-CALENDAR_ACTIVE = True
-CALENDAR_URL = "https://user:password@calendar.example.com/dav/"
-CALENDAR_DEFAULT_DURATION = 3 # default meeting length in hours
-CALENDAR_MAX_REQUESTS = 10 # number of retries before giving up (some caldav servers like to randomly reply with errors)
-
-CALENDAR_TIMEZONE_MAP = {
-    "CET": "Europe/Berlin",
-    "CEST": "Europe/Berlin",
-}
-
-SESSION_PROTECTION = "strong" # do not change
-
-# authentication
-SECURITY_KEY = "some other random string" # change this
-AUTH_MAX_DURATION = 300
-from common.auth import LdapManager, ADManager, StaticUserManager
-AUTH_BACKENDS = [
-    LdapManager(
-        host="ldap.example.com",
-        user_dn="uid={},ou=users,dc=example,dc=com",
-        group_dn="dc=example,dc=com"),
-    ADManager(
-        host="ad.example.com", # pass multiple hosts for replication
-        domain="EXAMPLE",
-        user_dn="cn=users,dc=example,dc=com",
-        group_dn="dc=example,dc=com",
-        ca_cert="/etc/ssl/certs/example-ca.pem"),
-    StaticUserManager(
-        users=(
-            ("username", "password", ("group1", "group2")),
-            ("testuser", "abc123", ("group1",)),
-        )
-    ),
-    PAMManager(),
-]
-
-OBSOLETION_WARNING = """Please migrate your account!""" # not important
-
-# lines of error description
-ERROR_CONTEXT_LINES = 3
-
-# pagination
-PAGE_LENGTH = 20
-PAGE_DIFF = 3
-
-# upcoming meetings within this number of days from today are shown on the index page
-MAX_INDEX_DAYS = 14
-MAX_PAST_INDEX_DAYS = 2
-MAX_PAST_INDEX_DAYS_BEFORE_REMINDER = 14
-
-# mail to contact in case of complex errors
-ADMIN_MAIL = "admin@example.com"
-# users with this group may see and do everything
-ADMIN_GROUP = "admin"
-
-# accept protocols even with some errors
-# useful for importing old protocols
-# not recommended for regular operation
-PARSER_LAZY = False
-
-# minimum similarity (0-100) todos need to have to be considered equal while importing
-FUZZY_MIN_SCORE = 90
-
-# choose something nice from fc-list
-# Nimbus Sans looks very much like Computer Modern
-FONTS = {
-    "main": {
-        "extension": ".otf",
-        "path": "/usr/share/fonts/OTF/",
-        "regular": "NimbusSans-Regular",
-        "bold": "NimbusSans-Bold",
-        "italic": "NimbusSans-Oblique",
-        "bolditalic": "NimbusSans-BoldOblique"
-    },
-    "roman": {
-        "extension": ".otf",
-        "path": "/usr/share/fonts/OTF/",
-        "regular": "NimbusRoman-Regular",
-        "bold": "NimbusRoman-Bold",
-        "italic": "NimbusRoman-Italic",
-        "bolditalic": "NimbusRoman-BoldItalic"
-    },
-    "sans": {
-        "extension": ".otf",
-        "path": "/usr/share/fonts/OTF/",
-        "regular": "NimbusSans-Regular",
-        "bold": "NimbusSans-Bold",
-        "italic": "NimbusSans-Oblique",
-        "bolditalic": "NimbusSans-BoldOblique"
-    },
-    "mono": {
-        "extension": ".otf",
-        "path": "/usr/share/fonts/OTF/",
-        "regular": "NimbusMonoPS-Regular",
-        "bold": "NimbusMonoPS-Bold",
-        "italic": "NimbusMonoPS-Italic",
-        "bolditalic": "NimbusMonoPS-BoldItalic"
-    }
-}
-
-# local filesystem path to save compiled and uploaded protocols (and attachments)
-# create this path!
-DOCUMENTS_PATH = "documents"
-
-# keywords indicating private protocol parts
-PRIVATE_KEYWORDS = ["private", "internal", "privat", "intern"]
-
-# list of bulletpoints to use in latex
-# these are latex-defaults, add more if you like more
-# they are cycled as often as necessary to allow (theoretically) infinite nesting depth
-LATEX_BULLETPOINTS = [
-    r"\textbullet",
-    r"\normalfont \bfseries \textendash",
-    r"\textasteriskcentered",
-    r"\textperiodcentered"
-]
-
-# optional: path to additional jinja-templates, will be need in combination with LATEX_TEMPLATES
-#LATEX_LOCAL_TEMPLATES = "local-templates"
-# optional: the template to include at the top of protocol.tex
-#LATEX_LOGO_TEMPLATE = "asta-logo.tex"
-# optional: custom protocol page geometry
-#LATEX_GEOMETRY = "bottom=1.6cm,top=1.6cm,inner=2.5cm,outer=1.0cm,footskip=1.0cm,headsep=0.6cm"
-# optional: custom protocol pagestyle
-#LATEX_PAGESTYLE = "fancy"
-# optional: additional latex packages
-#LATEX_ADDITIONAL_PACKAGES = ["[absolute]{textpos}", "{fancyheadings}"]
-# optional: include header and footer in asta-style, not just a page number on top
-#LATEX_HEADER_FOOTER = True
-# optional: define multiple LaTeX-templates to use with a each protocol type individually overiding the general LATEX options
-# the LATEX_LOCAL_TEMPLATES parameter is need to provide the path for the templates
-# each template must be placed in an individual folder named by its ID in LATEX_TEMPLATES and must contain the provided template files: e.g.
-# - the files for the template "yourtemplate" need to be in the folder named "yourtemplate"
-# - the templates provides the files: "protokoll2.cls" (class), "protocol.tex" (protocol), "decision.tex" (decision) and "asta-logo.tex"
-#LATEX_TEMPLATES = {
-#    "yourtemplate": {
-#        "name": "Dein Template",
-#        "provides": ["class", "protocol", "decision"], # optional: if this option is set the corresponding files must be provided
-#        "logo": "asta-logo.tex", # optional: replaces the general template to include at the top of protocol.tex set by LATEX_LOGO_TEMPLATE
-#        "geometry": "bottom=1.6cm,top=1.6cm,inner=2.5cm,outer=1.0cm,footskip=1.0cm,headsep=0.6cm", # optional: replaces the general protocol page geometry set by LATEX_GEOMETRY
-#        "pagestyle": "fancy", # optional: replaces the general protocol pagestyle set by LATEX_PAGESTYLE
-#        "additional_packages": ["[absolute]{textpos}", "{fancyheadings}"], # optional: replaces the general latex packages set by LATEX_ADDITIONAL_PACKAGES
-#        "headerfooter": True # optional: replaces the general LATEX_HEADER_FOOTER option
-#    }
-#}
-
-HTML_LEVEL_OFFSET = 3
-
-def dummy_todomail_provider():
-    return {"example": ("Name", "mail@example.com")}
-
-# if you want to generate this mapping automatically
-# manually creating todomails through the web interface will still be possible for every authenticated user
-# list of functions that return dicts mapping todomail-keys to a tuple containing name and mail address
-ADDITIONAL_TODOMAIL_PROVIDERS = [
-    dummy_todomail_provider
-]
+# [Database]
+#  Settings for SQLAlchemy
+
+#  Database connection string. See
+#  http://docs.sqlalchemy.org/en/latest/core/engines.html for details. SQLite
+#  does not work with Alembic migrations.
+SQLALCHEMY_DATABASE_URI = 'engine://user:password@host/database'
+
+
+
+# [Security]
+#  Secret keys and random strings
+
+#  Secret random key used for session security.
+SECRET_KEY = b'\x86\xb1;\xfd\x8a\x05f\xf5/\xb22"\x07_\xc3\x021\x0f\xa2\xf6\xd56<i\xc8\xd4\xe0\xa4R\xf9D.\xa3B`\x8b#\xddYk\xfc\xcc\xe3\xe4\xce\xa1 \x81([\xc5e\xe7\xb0#\\x\x87\xa6-~\x15\x8c>\x19\xcc\xf1\x8f\xb0d\xc7\xdc\x0f\xcf\xe9\xc6\x03D\xfc~!7\xa8\xa9\x15\xe7\xa2\xde9\x18\x93N`\x0f\xdb\xb1\x0b\x08\xd6\xaa\xbb\x7f\x8f\xc5\x91\xf77\x07\x01\xce,\xcd\xfb\xfd\x1c\xd7N\xd3\x13\xbdR\xb8\x05\xd3\x98\xe8\xdcQ'
+
+#  Secret random key used for user sessions.
+SECURITY_KEY = b"\xad\xf5\xfav\xbc\xb3u\xfc\xc0\xd3\xf4\xa1\x8bu\x81\x85\t%P\xba\xe5\xb8\xacv\x92\xa8\x16U]\xc5d\xdb\xf9\t\x95\xcc\x91\x88T\xedK]%\xd1\xe6gis\xf3N\xc1\xac\x98\x10q\xd1*\xcd\xce.\xec\xe4\x0f\x9b\xdd\xd1c\xa1\x01\xba\xdf\x0f%\x88\xdd\x03H\xd8\xc84F\xe6\xeb\xb1\x8a\x08\xd3<S?\xff\x9c\x19\xf4\xa5J\x84\xf4jy\xf1[\xf9\xe5\xce:>\xa9DS\xe9\xe3\x8f\x8d!\x8f\xa3\xf6p+\\\x86\xc9,:~E'"
+
+
+
+# [URL Root]
+#  Where is the website hosted
+
+#  Domain on which this website is hosted
+SERVER_NAME = 'protokolle.example.com'
+
+#  Protocol used by this website. Either 'http' or 'https'.
+# PREFERRED_URL_SCHEME = 'https'
+
+
+# [Debug]
+#  Debug mode. Do not set in production.
+
+#  Activate debug mode
+# DEBUG = False
+
+
+# [Celery]
+#  Settings for the task scheduler.
+
+#  Connection to the celery broker. See
+#  http://docs.celeryproject.org/en/latest/userguide/configuration.html
+CELERY_BROKER_URL = 'redis://localhost:6379/0'
+
+
+
+
+# [Sentry]
+#  Connection information for sentry exception reporting.
+
+#  Connection string for sentry. See
+#  https://docs.sentry.io/quickstart/#configure-the-dsn
+# SENTRY_DSN = None
+
+
+from common import auth
+# [Authentication]
+#  User Authentication backend settings.
+
+#  Time in seconds for which non-permanent sessions remain valid
+# AUTH_MAX_DURATION = 300
+
+#  Active Authentication backends
+AUTH_BACKENDS = [StaticUserManager([('user', 'password', ('group',))])]
+
+
+# [Error Report Context]
+#  Compiling error context settings
+
+#  Number of lines before and after an error to include in the error
+#  description
+# ERROR_CONTEXT_LINES = 3
+
+
+# [Pagination]
+#  Pagination settings, used for list pages
+
+#  Default number of entries per page
+# PAGE_LENGTH = 20
+
+#  Number of pages before and after the current one with a direct link.
+# PAGE_DIFF = 3
+
+
+# [Index Page]
+#  Settings for what to show on the index page
+
+#  Next days to list upcoming meetings on
+# MAX_INDEX_DAYS = 14
+
+#  Past days to list unfinished meetings on
+# MAX_PAST_INDEX_DAYS = 2
+
+#  If a meeting is unfinished this many days after its date, a reminder is
+#  sent by mail
+# MAX_PAST_INDEX_DAYS_BEFORE_REMINDER = 14
+
+
+# [Admin data]
+#  Settings for who is an admin and how to contact them
+
+#  Mail address to tell users to contact in case of errors
+ADMIN_MAIL = 'admin@example.com'
+
+#  Users with this group are admins and are allowed to do and see everything.
+ADMIN_GROUP = 'admin'
+
+
+# [Parser]
+#  Settings for the protocol syntax parser
+
+#  Do not enforce some parser policies.
+# PARSER_LAZY = False
+
+#  Todos with at least this equality score are considered equal whe importing
+#  old protocols.
+# FUZZY_MIN_SCORE = 90
+
+#  Keywords indicating private protocol parts
+# PRIVATE_KEYWORDS = ['private', 'internal', 'privat', 'intern']
+
+
+# [Rendering]
+#  Settings for rendering protocols to pdf, html, etc.
+
+#  fonts for xelatex
+FONTS = {'main': {'extension': '.otf', 'path': '/usr/share/fonts/gsfonts/', 'regular': 'NimbusSans-Regular', 'bold': 'NimbusSans-Bold', 'italic': 'NimbusSans-Italic', 'bolditalic': 'NimbusSans-BoldItalic'}, 'roman': {'extension': '.otf', 'path': '/usr/share/fonts/gsfonts/', 'regular': 'NimbusRoman-Regular', 'bold': 'NimbusRoman-Bold', 'italic': 'NimbusRoman-Italic', 'bolditalic': 'NimbusRoman-BoldItalic'}, 'sans': {'extension': '.otf', 'path': '/usr/share/fonts/gsfonts/', 'regular': 'NimbusSans-Regular', 'bold': 'NimbusSans-Bold', 'italic': 'NimbusSans-Italic', 'bolditalic': 'NimbusSans-BoldItalic'}, 'mono': {'extension': '.otf', 'path': '/usr/share/fonts/gsfonts/', 'regular': 'NimbusMonoPS-Regular', 'bold': 'NimbusMonoPS-Bold', 'italic': 'NimbusMonoPS-Italic', 'bolditalic': 'NimbusMonoPS-BoldItalic'}}
+
+#  Path to the directory to save protocols in. Write access is necessary.
+# DOCUMENTS_PATH = 'documents'
+
+#  list of bulletpoints to use in latex
+# LATEX_BULLETPOINTS = ['\\textbullet', '\\normalfont \\bfseries \\textendash', '\\textasteriskcentered', '\\textperiodcentered']
+
+#  Header level at which to start with HTML headlines
+# HTML_LEVEL_OFFSET = 3
+
+#  path to additional jinja2 templates
+# LATEX_LOCAL_TEMPLATES = None
+
+#  template to include at the top of protocols
+# LATEX_LOGO_TEMPLATE = None
+
+#  custom latex page geometry
+# LATEX_GEOMETRY = None
+
+#  custom latex pagestyle, e.g. 'fancy'
+# LATEX_PAGESTYLE = None
+
+#  Include a header and footer in protocols
+# LATEX_HEADER_FOOTER = False
+
+#  define multiple LaTeX-templates to use with a each protocol type
+#  individually overiding the general LATEX options the LATEX_LOCAL_TEMPLATES
+#  parameter is need to provide the path for the templates each template must
+#  be placed in an individual folder named by its ID in LATEX_TEMPLATES and
+#  must contain the provided template files: e.g. the files for the template
+#  'yourtemplate' need to be in the folder named 'yourtemplate', and the
+#  templates provides the files: 'protokoll2.cls' (class), 'protocol.tex'
+#  (protocol), 'decision.tex' (decision) and 'asta-logo.tex'
+# LATEX_TEMPLATES = None
+
+
+# [MAIL] (optional)
+#  Mail server connection
+
+#  deactivate with
+# MAIL_ACTIVE = False
+
+#  Mail sender address
+MAIL_FROM = 'protokolle@example.com'
+
+#  SMTP Mail server address with port
+MAIL_HOST = 'mail.example.com:465'
+
+#  Mail server login user. Empty for no authentication.
+# MAIL_USER = ''
+
+#  Mail server login password. Empty for no authentication.
+# MAIL_PASSWORD = ''
+
+#  Use SMPTS (not STARTTLS). Should match port.
+# MAIL_USE_TLS = True
+
+#  Use SMTP with STARTTLS. Should match port.
+# MAIL_USE_STARTTLS = False
+
+
+# [PRINTING] (optional)
+#  CUPS printing settings
+
+#  deactivate with
+# PRINTING_ACTIVE = False
+
+#  CUPS printserver connection
+PRINTING_SERVER = 'printsrv.example.com:631'
+
+#  CUPS user for print jobs
+# PRINTING_USER = 'protocols'
+
+#  printers with corresponding options
+PRINTING_PRINTERS = {'example_printer': ['Duplex=DuplexNoTumble', 'option2=value'], 'other_printer': ['list', 'of', 'options']}
+
+
+# [ETHERPAD] (optional)
+#  Etherpad settings
+
+#  deactivate with
+# ETHERPAD_ACTIVE = False
+
+#  URL of the etherpad installation. Do not include the '/p'!
+ETHERPAD_URL = 'https://example.com/etherpad'
+
+#  The content a new etherpad contains.
+# EMPTY_ETHERPAD = 'Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at http://etherpad.org\n\n'
+
+
+# [WIKI] (optional)
+#  Mediawiki or Dokuwiki settings
+
+#  deactivate with
+# WIKI_ACTIVE = False
+
+#  'MEDIAWIKI' or 'DOKUWIKI'
+WIKI_TYPE = 'MEDIAWIKI'
+
+#  API Endpoint for Mediawiki,
+#  'https://user:password@wiki.example.com/lib/exe/xmlrpc.php' for Dokuwiki
+WIKI_API_URL = 'https://wiki.example.com/wiki/api.php'
+
+#  Skip login (only for Mediawiki)
+# WIKI_ANONYMOUS = False
+
+#  Login user (only for Mediawiki)
+# WIKI_USER = None
+
+#  Login password (only for Mediawiki)
+# WIKI_PASSWORD = None
+
+#  Login domain (only for Mediawiki)
+# WIKI_DOMAIN = None
+
+
+# [CALENDAR] (optional)
+#  CalDAV settings
+
+#  deactivate with
+# CALENDAR_ACTIVE = False
+
+#  CalDAV server URL
+CALENDAR_URL = 'https://user:password@calendar.example.com/dav/'
+
+#  Default meeting length in hours
+# CALENDAR_DEFAULT_DURATION = 3
+
+#  Number of retries before giving a connection attempt up. Some CalDAV
+#  servers like to randomly reply with errors.
+# CALENDAR_MAX_REQUESTS = 10
+
+#  Timezone abbreviation map. Add as needed.
+# CALENDAR_TIMEZONE_MAP = {'CET': 'Europe/Berlin', 'CEST': 'Europe/Berline'}
+
diff --git a/configproxy.py b/configproxy.py
new file mode 100755
index 0000000..09b1c9c
--- /dev/null
+++ b/configproxy.py
@@ -0,0 +1,997 @@
+#!/usr/bin/env python3
+import os
+from common import auth
+
+import logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+def split_line(line, max_length=76):
+    words = list(filter(None, map(str.strip, line.split(" "))))
+    lines = []
+    current_line = []
+    current_line_length = 0
+    for word in words:
+        if (current_line and current_line_length + len(word) + 1 > max_length):
+            lines.append(" ".join(current_line))
+            current_line = []
+            current_line_length = 0
+        current_line.append(word)
+        current_line_length += len(word) + 1
+    if current_line:
+        lines.append(" ".join(current_line))
+    return lines
+
+
+def prefix_lines(lines, prefix="#  "):
+    return map(lambda line: "".join((prefix, line)), lines)
+
+
+class ConfigEntry:
+    def __init__(self, name, default, required=True, internal=True,
+                 immutable=False, no_sane_default=False,
+                 description=None, value=None):
+        self.name = name
+        self.default = default
+        self.required = required
+        self.internal = internal
+        self.immutable = immutable
+        self.no_sane_default = no_sane_default
+        self.description = description
+        self.value = value
+
+    def extract(self, config):
+        if self.immutable:
+            self.value = self.default
+            return
+        if not hasattr(config, self.name) and self.required:
+            raise ValueError(
+                "Missing required config entry {}!".format(self.name))
+        self.value = getattr(config, self.name, self.default)
+        if self.no_sane_default and self.value == self.default:
+            raise ValueError(
+                "You have not configured {}, which is required!".format(
+                    self.name))
+
+    def get_example(self):
+        if self.immutable:
+            return []
+        lines = []
+        if self.description is not None:
+            lines.extend(prefix_lines(split_line(self.description)))
+        entry_line = "{} = {}".format(self.name, repr(self.default))
+        if not self.required:
+            entry_line = "# {}".format(entry_line)
+        lines.append(entry_line)
+        return lines
+
+
+class ConfigSection:
+    def __init__(self, name, entries, check, description=None,
+                 deactivatable=False, active=None, imports=None):
+        self.name = name
+        self.entries = entries
+        self.check = check
+        self.description = description
+        self.deactivatable = deactivatable
+        self.active = active
+        if isinstance(imports, str):
+            imports = [imports]
+        self.imports = imports
+
+    def extract(self, config):
+        if self.deactivatable:
+            self.active = getattr(config, "{}_ACTIVE".format(self.name), False)
+            if not self.active:
+                return
+        for entry in self.entries:
+            entry.extract(config)
+
+    def get_example(self):
+        lines = []
+        if self.imports:
+            lines.extend(self.imports)
+        header_line = (
+            "# [{}]".format(self.name) +
+            (" (optional)" if self.deactivatable else ""))
+        lines.append(header_line)
+        if self.description is not None:
+            lines.extend(prefix_lines(split_line(self.description)))
+            lines.append("")
+        if self.deactivatable:
+            lines.extend(prefix_lines(split_line("deactivate with")))
+            lines.append("# {}_ACTIVE = False".format(self.name))
+            lines.append("")
+        for entry in self.entries:
+            lines.extend(entry.get_example())
+            lines.append("")
+        lines.append("")
+        return "\n".join(lines)
+
+    def fill_modules(self, config, public_config):
+        if self.deactivatable:
+            setattr(config, "{}_ACTIVE".format(self.name), self.active)
+            setattr(public_config, "{}_ACTIVE".format(self.name), self.active)
+            if not self.active:
+                return
+        for entry in self.entries:
+            setattr(config, entry.name, entry.value)
+            if not entry.internal:
+                setattr(public_config, entry.name, entry.value)
+
+
+def check_choice(option, value, choices):
+    if value not in choices:
+        raise ValueError(
+            "{} is not allowed choices! Should be one of {}, is {}".format(
+                option, choices, value))
+
+
+def check_database(
+        SQLALCHEMY_DATABASE_URI,
+        SQLALCHEMY_TRACK_MODIFICATIONS):
+    from sqlalchemy_utils import database_exists
+    from sqlalchemy import exc
+    try:
+        if not database_exists(SQLALCHEMY_DATABASE_URI):
+            raise ValueError(
+                "The database '{}' does not exist! "
+                "Please configure it correctly or create it!".format(
+                    SQLALCHEMY_DATABASE_URI))
+    except (exc.NoSuchModuleError, exc.OperationalError) as error:
+        raise ValueError(
+            "The database uri '{}' does not state a valid "
+            "database: {}".format(SQLALCHEMY_DATABASE_URI, error))
+
+
+def check_security(SECRET_KEY, SECURITY_KEY, SESSION_PROTECTION):
+    MIN_KEY_LENGTH = 20
+    if len(SECRET_KEY) < MIN_KEY_LENGTH:
+        raise ValueError(
+            "Insufficient length of SECRET_KEY, should be at least {}!".format(
+                MIN_KEY_LENGTH))
+    if len(SECURITY_KEY) < MIN_KEY_LENGTH:
+        raise ValueError(
+            "Insufficient length of SECURITY_KEY, should be at "
+            "least {}!".format(
+                MIN_KEY_LENGTH))
+    check_choice("SESSION_PROTECTION", SESSION_PROTECTION, ["strong"])
+
+
+def check_server_name(SERVER_NAME, PREFERRED_URL_SCHEME):
+    # todo: check ip address and server name
+    check_choice(
+        "PREFERRED_URL_SCHEME", PREFERRED_URL_SCHEME,
+        ["http", "https"])
+
+
+def check_debug(DEBUG):
+    if DEBUG:
+        logger.warning("DEBUG mode is activated!")
+
+
+def check_celery(
+        CELERY_BROKER_URL, CELERY_TASK_SERIALIZER, CELERY_ACCEPT_CONTENT):
+    pass
+    # todo: check broker url
+    check_choice(
+        "CELERY_TASK_SERIALIZER", CELERY_TASK_SERIALIZER,
+        ["pickle"])
+    check_choice(
+        "CELERY_ACCEPT_CONTENT", CELERY_ACCEPT_CONTENT,
+        [["pickle"]])
+
+
+def check_sentry(SENTRY_DSN):
+    pass
+
+
+def check_authentication(AUTH_MAX_DURATION, AUTH_BACKENDS):
+    if AUTH_MAX_DURATION <= 0:
+        raise ValueError(
+            "AUTH_MAX_DURATION should be positive, is {}!".format(
+                AUTH_MAX_DURATION))
+    if not AUTH_BACKENDS:
+        raise ValueError("No authentication backends have been configured!")
+
+
+def check_error_report_context(ERROR_CONTEXT_LINES):
+    if ERROR_CONTEXT_LINES < 0:
+        raise ValueError(
+            "ERROR_CONTEXT_LINES should be positive, is {}!".format(
+                ERROR_CONTEXT_LINES))
+
+
+def check_pagination(PAGE_LENGTH, PAGE_DIFF):
+    if PAGE_LENGTH <= 0:
+        raise ValueError(
+            "PAGE_LENGTH should be positive, is {}!".format(
+                PAGE_LENGTH))
+    if PAGE_DIFF < 0:
+        raise ValueError(
+            "PAGE_DIFF should be positive, is {}!".format(
+                PAGE_DIFF))
+
+
+def check_index_page(
+        MAX_INDEX_DAYS, MAX_PAST_INDEX_DAYS,
+        MAX_PAST_INDEX_DAYS_BEFORE_REMINDER):
+    if MAX_INDEX_DAYS < 0:
+        raise ValueError(
+            "MAX_INDEX_DAYS should be positive, is {}!".format(
+                MAX_INDEX_DAYS))
+    if MAX_PAST_INDEX_DAYS < 0:
+        raise ValueError(
+            "MAX_PAST_INDEX_DAYS should be positive, is {}!".format(
+                MAX_PAST_INDEX_DAYS))
+    if MAX_PAST_INDEX_DAYS_BEFORE_REMINDER < 0:
+        raise ValueError(
+            "MAX_PAST_INDEX_DAYS_BEFORE_REMINDER should be "
+            "positive, is {}!".format(
+                MAX_PAST_INDEX_DAYS_BEFORE_REMINDER))
+
+
+def check_admin_data(ADMIN_MAIL, ADMIN_GROUP):
+    if not ADMIN_MAIL:
+        raise ValueError("No admin mail address given!")
+    if not ADMIN_GROUP:
+        raise ValueError("No admin group given!")
+
+
+def check_parser(PARSER_LAZY, FUZZY_MIN_SCORE, PRIVATE_KEYWORDS):
+    if PARSER_LAZY:
+        logger.warning(
+            "Parser lazy mode is activated, this is not meant or useful "
+            "for production operation!")
+    if not 0 <= FUZZY_MIN_SCORE <= 100:
+        raise ValueError(
+            "FUZZY_MIN_SCORE should be a percentage from 0 to 100")
+    for keyword in PRIVATE_KEYWORDS:
+        if not keyword:
+            raise ValueError("Invalid private keyword given: {}".format(
+                keyword))
+
+
+def check_rendering(
+        FONTS, DOCUMENTS_PATH, LATEX_BULLETPOINTS, HTML_LEVEL_OFFSET,
+        LATEX_LOCAL_TEMPLATES, LATEX_LOGO_TEMPLATE, LATEX_GEOMETRY,
+        LATEX_PAGESTYLE, LATEX_HEADER_FOOTER, LATEX_TEMPLATES):
+    for key in ("main", "roman", "sans", "mono"):
+        if key not in FONTS:
+            raise ValueError("No font for type {} given!".format(key))
+        try:
+            path = FONTS[key]["path"]
+            extension = FONTS[key]["extension"]
+            for face in ("regular", "bold", "italic", "bolditalic"):
+                if face not in FONTS[key]:
+                    raise ValueError("Missing fontface {} for {} font!".format(
+                        face, key))
+                filepath = os.path.join(
+                    path, "".join((FONTS[key][face], extension)))
+                if not os.path.exists(filepath):
+                    raise ValueError(
+                        "Font {} does not exist in the filesystem! "
+                        "Expected it at {}".format(
+                            FONTS[key][face], filepath))
+        except KeyError:
+            raise ValueError(
+                "Missing path information in font specification "
+                "{key}: ".format(key=key))
+    if not os.path.isdir(DOCUMENTS_PATH):
+        raise ValueError(
+            "Document savedir does not exist! Should be at {}.".format(
+                DOCUMENTS_PATH))
+    if not LATEX_BULLETPOINTS:
+        raise ValueError("LATEX_BULLETPOINTS is empty.")
+    if not 1 <= HTML_LEVEL_OFFSET <= 4:
+        raise ValueError(
+            "HTML_LEVEL_OFFSET should be from 1 to 4, but is {}".format(
+                HTML_LEVEL_OFFSET))
+    # todo: check templates stuff
+
+
+def check_mail(MAIL_FROM, MAIL_HOST, MAIL_USER, MAIL_PASSWORD,
+               MAIL_USE_TLS, MAIL_USE_STARTTLS):
+    if MAIL_USE_TLS and MAIL_USE_STARTTLS:
+        raise ValueError(
+            "Both TLS and STARTTLS are set for Mail! Please set only one.")
+    from utils import MailManager
+    import socket
+    import smtplib
+    import ssl
+    mail_config = Config()
+    mail_config.MAIL_ACTIVE = True
+    mail_config.MAIL_FROM = MAIL_FROM
+    mail_config.MAIL_HOST = MAIL_HOST
+    mail_config.MAIL_USER = MAIL_USER
+    mail_config.MAIL_PASSWORD = MAIL_PASSWORD
+    mail_config.MAIL_USE_TLS = MAIL_USE_TLS
+    mail_config.MAIL_USE_STARTTLS = MAIL_USE_STARTTLS
+    mail_manager = MailManager(mail_config)
+    try:
+        if not mail_manager.check():
+            raise ValueError("The mail connection is improperly configured.")
+    except (ConnectionRefusedError, socket.gaierror) as error:
+        raise ValueError("Mail Cannot connect to the server: {}".format(error))
+    except smtplib.SMTPAuthenticationError as error:
+        raise ValueError("Mail Cannot authenticate: {}".format(error))
+    except (ssl.SSLError, smtplib.SMTPNotSupportedError) as error:
+        raise ValueError("TLS for Mail does not work: {}".format(error))
+    except Exception as error:
+        raise ValueError(
+            "Testing the mail connection failed: {}".format(error))
+
+
+def check_printing(PRINTING_SERVER, PRINTING_USER, PRINTING_PRINTERS):
+    LPR_PATH = "/usr/bin/lpr"
+    LPSTAT_PATH = "/usr/bin/lpstat"
+    LPOPTIONS_PATH = "/usr/bin/lpoptions"
+    for path in [LPR_PATH, LPSTAT_PATH, LPOPTIONS_PATH]:
+        if not os.path.exists(path):
+            raise ValueError(
+                "{} is not installed! It is required for printing".format(
+                    path))
+    import subprocess as sp
+    list_printers_command = [LPSTAT_PATH, "-h", PRINTING_SERVER, "-p"]
+    raw_printers = sp.check_output(list_printers_command).decode("utf-8")
+    printers = [
+        line.split(" ")[1].lower()
+        for line in raw_printers.splitlines()
+        if not line.startswith(" ")
+    ]
+    configured_printers = [key.lower() for key in PRINTING_PRINTERS]
+    missing_printers = set(configured_printers) - set(printers)
+    if missing_printers:
+        raise ValueError(
+            "Some configured printers are not available: {}, "
+            "available are: {}".format(
+                missing_printers, printers))
+
+    def _strip_star(value):
+        if value.startswith("*"):
+            return value[1:]
+        return value
+    for printer in configured_printers:
+        options_command = [
+            LPOPTIONS_PATH, "-h", PRINTING_SERVER, "-p", printer, "-l"]
+        raw_options = sp.check_output(options_command).decode("utf-8")
+        available_options = {}
+        for line in raw_options.splitlines():
+            name_part, value_part = line.split(":")
+            values = list(map(_strip_star, value_part.strip().split(" ")))
+            for name in name_part.split("/"):
+                available_options[name] = values
+        for option in PRINTING_PRINTERS[printer]:
+            name, value = option.split("=")
+            if name not in available_options:
+                logger.warning(
+                    "Printer {} has unknown option {} set!".format(
+                        printer, name))
+            elif value not in available_options[name]:
+                logger.warning(
+                    "Printer {} has the invalid value {} specified "
+                    "for option {}! Valid values are {}.".format(
+                        printer, value, name, available_options[name]))
+
+
+def check_etherpad(ETHERPAD_URL, EMPTY_ETHERPAD):
+    import requests
+    key = "Server"
+    try:
+        answer = requests.get(ETHERPAD_URL)
+        if answer.status_code != 200:
+            raise ValueError(
+                "The etherpad does not return 200 OK at {}".format(
+                    ETHERPAD_URL))
+        headers = answer.headers
+        if key not in headers or "Etherpad" not in headers[key]:
+            raise ValueError("{} does not look like an etherpad!".format(
+                ETHERPAD_URL))
+    except requests.exceptions.ConnectionError as error:
+        raise ValueError("Cannot connect to the etherpad at {}: {}".format(
+            ETHERPAD_URL, error))
+
+
+def check_wiki(
+        WIKI_TYPE, WIKI_API_URL, WIKI_ANONYMOUS,
+        WIKI_USER, WIKI_PASSWORD, WIKI_DOMAIN):
+    check_choice("WIKI_TYPE", WIKI_TYPE, ["MEDIAWIKI", "DOKUWIKI"])
+    # todo: check the connection
+
+
+def check_calendar(
+        CALENDAR_URL, CALENDAR_DEFAULT_DURATION, CALENDAR_MAX_REQUESTS,
+        CALENDAR_TIMEZONE_MAP):
+    from calendarpush import Client, CalendarException
+    try:
+        client = Client(url=CALENDAR_URL)
+        client.get_calendars()
+    except (KeyError, CalendarException) as error:
+        raise ValueError("Cannot connect to the calendar at {}!".format(
+            CALENDAR_URL))
+
+
+CONFIG_SECTIONS = [
+    ConfigSection(
+        name="Database",
+        entries=[
+            ConfigEntry(
+                name="SQLALCHEMY_DATABASE_URI",
+                default="engine://user:password@host/database",
+                required=True, internal=True, no_sane_default=True,
+                description=(
+                    "Database connection string. See "
+                    "http://docs.sqlalchemy.org/en/latest/core/engines.html "
+                    "for details. SQLite does not work with Alembic "
+                    "migrations.")),
+            ConfigEntry(
+                name="SQLALCHEMY_TRACK_MODIFICATIONS",
+                default=False,
+                required=False, internal=True, immutable=True,
+                description="Necessary option. Do not change."),
+        ],
+        check=check_database,
+        description="Settings for SQLAlchemy"),
+    ConfigSection(
+        name="Security",
+        entries=[
+            ConfigEntry(
+                name="SECRET_KEY",
+                default=os.urandom(128),
+                required=True, internal=True,
+                description="Secret random key used for session security."),
+            ConfigEntry(
+                name="SECURITY_KEY",
+                default=os.urandom(128),
+                required=True, internal=True,
+                description="Secret random key used for user sessions."),
+            ConfigEntry(
+                name="SESSION_PROTECTION",
+                default="strong",
+                required=False, internal=True, immutable=True,
+                description="Flask setting for sessions. Do not change."),
+        ],
+        check=check_security,
+        description="Secret keys and random strings"),
+    ConfigSection(
+        name="URL Root",
+        entries=[
+            ConfigEntry(
+                name="SERVER_NAME",
+                default="protokolle.example.com",
+                required=True, internal=False, no_sane_default=True,
+                description="Domain on which this website is hosted"),
+            ConfigEntry(
+                name="PREFERRED_URL_SCHEME",
+                default="https",
+                required=False, internal=False,
+                description="Protocol used by this website. "
+                            "Either 'http' or 'https'."),
+        ],
+        check=check_server_name,
+        description="Where is the website hosted"),
+    ConfigSection(
+        name="Debug",
+        entries=[
+            ConfigEntry(
+                name="DEBUG",
+                default=False,
+                required=False, internal=True,
+                description="Activate debug mode"),
+        ],
+        check=check_debug,
+        description="Debug mode. Do not set in production."),
+    ConfigSection(
+        name="Celery",
+        entries=[
+            ConfigEntry(
+                name="CELERY_BROKER_URL",
+                default="redis://localhost:6379/0",
+                required=True, internal=True,
+                description=(
+                    "Connection to the celery broker. See http://docs.celery"
+                    "project.org/en/latest/userguide/configuration.html")),
+            ConfigEntry(
+                name="CELERY_TASK_SERIALIZER",
+                default="pickle",
+                required=False, internal=True, immutable=True,
+                description="How celery serializes tasks. Do not change."),
+            ConfigEntry(
+                name="CELERY_ACCEPT_CONTENT",
+                default=['pickle'],
+                required=False, internal=True, immutable=True,
+                description="How celery deserializes tasks. Do not change."),
+        ],
+        check=check_celery,
+        description="Settings for the task scheduler."),
+    ConfigSection(
+        name="Sentry",
+        entries=[
+            ConfigEntry(
+                name="SENTRY_DSN",
+                default=None,
+                required=False, internal=True,
+                description=(
+                    "Connection string for sentry. See "
+                    "https://docs.sentry.io/quickstart/#configure-the-dsn")),
+        ],
+        check=check_sentry,
+        description="Connection information for sentry exception reporting."),
+    ConfigSection(
+        name="Authentication",
+        entries=[
+            ConfigEntry(
+                name="AUTH_MAX_DURATION",
+                default=300,
+                required=False, internal=False,
+                description="Time in seconds for which non-permanent "
+                            "sessions remain valid"),
+            ConfigEntry(
+                name="AUTH_BACKENDS",
+                default=[
+                    auth.StaticUserManager((("user", "password", ["group"]),)),
+                ],
+                required=True, internal=True, no_sane_default=True,
+                description="Active Authentication backends"),
+        ],
+        check=check_authentication,
+        description="User Authentication backend settings.",
+        imports="from common import auth"),
+    ConfigSection(
+        name="Error Report Context",
+        entries=[
+            ConfigEntry(
+                name="ERROR_CONTEXT_LINES",
+                default=3,
+                required=False, internal=False,
+                description=(
+                    "Number of lines before and after an error to include "
+                    "in the error description")),
+        ],
+        check=check_error_report_context,
+        description="Compiling error context settings"),
+    ConfigSection(
+        name="Pagination",
+        entries=[
+            ConfigEntry(
+                name="PAGE_LENGTH",
+                default=20,
+                required=False, internal=False,
+                description="Default number of entries per page"),
+            ConfigEntry(
+                name="PAGE_DIFF",
+                default=3,
+                required=False, internal=False,
+                description=(
+                    "Number of pages before and after the current one "
+                    "with a direct link.")),
+        ],
+        check=check_pagination,
+        description="Pagination settings, used for list pages"),
+    ConfigSection(
+        name="Index Page",
+        entries=[
+            ConfigEntry(
+                name="MAX_INDEX_DAYS",
+                default=14,
+                required=False, internal=False,
+                description="Next days to list upcoming meetings on"),
+            ConfigEntry(
+                name="MAX_PAST_INDEX_DAYS",
+                default=2,
+                required=False, internal=False,
+                description="Past days to list unfinished meetings on"),
+            ConfigEntry(
+                name="MAX_PAST_INDEX_DAYS_BEFORE_REMINDER",
+                default=14,
+                required=False, internal=True,
+                description=(
+                    "If a meeting is unfinished this many days after its "
+                    "date, a reminder is sent by mail")),
+        ],
+        check=check_index_page,
+        description="Settings for what to show on the index page"),
+    ConfigSection(
+        name="Admin data",
+        entries=[
+            ConfigEntry(
+                name="ADMIN_MAIL",
+                default="admin@example.com",
+                required=True, internal=False, no_sane_default=True,
+                description="Mail address to tell users to "
+                            "contact in case of errors"),
+            ConfigEntry(
+                name="ADMIN_GROUP",
+                default="admin",
+                required=True, internal=False,
+                description="Users with this group are admins and are "
+                            "allowed to do and see everything."),
+        ],
+        check=check_admin_data,
+        description="Settings for who is an admin and how to contact them"),
+    ConfigSection(
+        name="Parser",
+        entries=[
+            ConfigEntry(
+                name="PARSER_LAZY",
+                default=False,
+                required=False, internal=False,
+                description="Do not enforce some parser policies."),
+            ConfigEntry(
+                name="FUZZY_MIN_SCORE",
+                default=90,
+                required=False, internal=False,
+                description="Todos with at least this equality score are "
+                            "considered equal whe importing old protocols."),
+            ConfigEntry(
+                name="PRIVATE_KEYWORDS",
+                default=["private", "internal", "privat", "intern"],
+                required=False, internal=False,
+                description="Keywords indicating private protocol parts"),
+        ],
+        check=check_parser,
+        description="Settings for the protocol syntax parser"),
+    ConfigSection(
+        name="Rendering",
+        entries=[
+            ConfigEntry(
+                name="FONTS",
+                default={
+                    "main": {
+                        "extension": ".otf",
+                        "path": "/usr/share/fonts/gsfonts/",
+                        "regular": "NimbusSans-Regular",
+                        "bold": "NimbusSans-Bold",
+                        "italic": "NimbusSans-Italic",
+                        "bolditalic": "NimbusSans-BoldItalic"
+                    },
+                    "roman": {
+                        "extension": ".otf",
+                        "path": "/usr/share/fonts/gsfonts/",
+                        "regular": "NimbusRoman-Regular",
+                        "bold": "NimbusRoman-Bold",
+                        "italic": "NimbusRoman-Italic",
+                        "bolditalic": "NimbusRoman-BoldItalic"
+                    },
+                    "sans": {
+                        "extension": ".otf",
+                        "path": "/usr/share/fonts/gsfonts/",
+                        "regular": "NimbusSans-Regular",
+                        "bold": "NimbusSans-Bold",
+                        "italic": "NimbusSans-Italic",
+                        "bolditalic": "NimbusSans-BoldItalic"
+                    },
+                    "mono": {
+                        "extension": ".otf",
+                        "path": "/usr/share/fonts/gsfonts/",
+                        "regular": "NimbusMonoPS-Regular",
+                        "bold": "NimbusMonoPS-Bold",
+                        "italic": "NimbusMonoPS-Italic",
+                        "bolditalic": "NimbusMonoPS-BoldItalic"
+                    }
+                },
+                required=True, internal=False,
+                description="fonts for xelatex"),
+            ConfigEntry(
+                name="DOCUMENTS_PATH",
+                default="documents",
+                required=False, internal=True,
+                description="Path to the directory to save protocols in. "
+                            "Write access is necessary."),
+            ConfigEntry(
+                name="LATEX_BULLETPOINTS",
+                default=[
+                    r"\textbullet", r"\normalfont \bfseries \textendash",
+                    r"\textasteriskcentered", r"\textperiodcentered"],
+                required=False, internal=False,
+                description="list of bulletpoints to use in latex"),
+            ConfigEntry(
+                name="HTML_LEVEL_OFFSET",
+                default=3,
+                required=False, internal=False,
+                description="Header level at which to start with "
+                            "HTML headlines"),
+            ConfigEntry(
+                name="LATEX_LOCAL_TEMPLATES",
+                default=None,
+                required=False, internal=False,
+                description="path to additional jinja2 templates"),
+            ConfigEntry(
+                name="LATEX_LOGO_TEMPLATE",
+                default=None,
+                required=False, internal=False,
+                description="template to include at the top of protocols"),
+            ConfigEntry(
+                name="LATEX_GEOMETRY",
+                default=None,
+                required=False, internal=False,
+                description="custom latex page geometry"),
+            ConfigEntry(
+                name="LATEX_PAGESTYLE",
+                default=None,
+                required=False, internal=False,
+                description="custom latex pagestyle, e.g. 'fancy'"),
+            ConfigEntry(
+                name="LATEX_HEADER_FOOTER",
+                default=False,
+                required=False, internal=False,
+                description="Include a header and footer in protocols"),
+            ConfigEntry(
+                name="LATEX_TEMPLATES",
+                default=None,
+                required=False, internal=False,
+                description=(
+                    "define multiple LaTeX-templates to use with a each "
+                    "protocol type individually overiding the general LATEX "
+                    "options the LATEX_LOCAL_TEMPLATES parameter is need to "
+                    "provide the path for the templates each template must "
+                    "be placed in an individual folder named by its ID in "
+                    "LATEX_TEMPLATES and must contain the provided template "
+                    "files: e.g. the files for the template 'yourtemplate' "
+                    "need to be in the folder named 'yourtemplate', and the "
+                    "templates provides the files: 'protokoll2.cls' (class), "
+                    "'protocol.tex' (protocol), 'decision.tex' (decision) and "
+                    "'asta-logo.tex'")),
+        ],
+        check=check_rendering,
+        description="Settings for rendering protocols to pdf, html, etc."),
+    ConfigSection(
+        name="MAIL",
+        entries=[
+            ConfigEntry(
+                name="MAIL_FROM",
+                default="protokolle@example.com",
+                required=True, internal=False, no_sane_default=True,
+                description="Mail sender address"),
+            ConfigEntry(
+                name="MAIL_HOST",
+                default="mail.example.com:465",
+                required=True, internal=True, no_sane_default=True,
+                description="SMTP Mail server address with port"),
+            ConfigEntry(
+                name="MAIL_USER",
+                default="",
+                required=False, internal=True,
+                description="Mail server login user. "
+                            "Empty for no authentication."),
+            ConfigEntry(
+                name="MAIL_PASSWORD",
+                default="",
+                required=False, internal=True,
+                description="Mail server login password. "
+                            "Empty for no authentication."),
+            ConfigEntry(
+                name="MAIL_USE_TLS",
+                default=True,
+                required=False, internal=True,
+                description="Use SMPTS (not STARTTLS). Should match port."),
+            ConfigEntry(
+                name="MAIL_USE_STARTTLS",
+                default=False,
+                required=False, internal=True,
+                description="Use SMTP with STARTTLS. Should match port."),
+
+        ],
+        check=check_mail,
+        deactivatable=True,
+        description="Mail server connection"),
+    ConfigSection(
+        name="PRINTING",
+        entries=[
+            ConfigEntry(
+                name="PRINTING_SERVER",
+                default="printsrv.example.com:631",
+                required=True, internal=True, no_sane_default=True,
+                description="CUPS printserver connection"),
+            ConfigEntry(
+                name="PRINTING_USER",
+                default="protocols",
+                required=False, internal=True,
+                description="CUPS user for print jobs"),
+            ConfigEntry(
+                name="PRINTING_PRINTERS",
+                default={
+                    "example_printer":
+                        ["Duplex=DuplexNoTumble", "option2=value"],
+                    "other_printer": ["list", "of", "options"],
+                },
+                required=True, internal=False, no_sane_default=True,
+                description="printers with corresponding options"),
+        ],
+        check=check_printing,
+        deactivatable=True,
+        description="CUPS printing settings"),
+    ConfigSection(
+        name="ETHERPAD",
+        entries=[
+            ConfigEntry(
+                name="ETHERPAD_URL",
+                default="https://example.com/etherpad",
+                required=True, internal=False, no_sane_default=True,
+                description=(
+                    "URL of the etherpad installation. "
+                    "Do not include the '/p'!")),
+            ConfigEntry(
+                name="EMPTY_ETHERPAD",
+                default="\n".join([
+                    "Welcome to Etherpad!",
+                    "",
+                    "This pad text is synchronized as you type, so that "
+                    "everyone viewing this page sees the same text. This "
+                    "allows you to collaborate seamlessly on documents!",
+                    "",
+                    "Get involved with Etherpad at http://etherpad.org",
+                    ""
+                ]),
+                required=False, internal=False,
+                description="The content a new etherpad contains."),
+        ],
+        check=check_etherpad,
+        deactivatable=True,
+        description="Etherpad settings"),
+    ConfigSection(
+        name="WIKI",
+        entries=[
+            ConfigEntry(
+                name="WIKI_TYPE",
+                default="MEDIAWIKI",
+                required=True, internal=False,
+                description="'MEDIAWIKI' or 'DOKUWIKI'"),
+            ConfigEntry(
+                name="WIKI_API_URL",
+                default="https://wiki.example.com/wiki/api.php",
+                required=True, internal=True, no_sane_default=True,
+                description=(
+                    "API Endpoint for Mediawiki, "
+                    "'https://user:password@wiki.example.com/lib/exe/"
+                    "xmlrpc.php' for Dokuwiki")),
+            ConfigEntry(
+                name="WIKI_ANONYMOUS",
+                default=False,
+                required=False, internal=True,
+                description="Skip login (only for Mediawiki)"),
+            ConfigEntry(
+                name="WIKI_USER",
+                default=None,
+                required=False, internal=True,
+                description="Login user (only for Mediawiki)"),
+            ConfigEntry(
+                name="WIKI_PASSWORD",
+                default=None,
+                required=False, internal=True,
+                description="Login password (only for Mediawiki)"),
+            ConfigEntry(
+                name="WIKI_DOMAIN",
+                default=None,
+                required=False, internal=True,
+                description="Login domain (only for Mediawiki)"),
+        ],
+        check=check_wiki,
+        deactivatable=True,
+        description="Mediawiki or Dokuwiki settings"),
+    ConfigSection(
+        name="CALENDAR",
+        entries=[
+            ConfigEntry(
+                name="CALENDAR_URL",
+                default="https://user:password@calendar.example.com/dav/",
+                required=True, internal=True, no_sane_default=True,
+                description="CalDAV server URL"),
+            ConfigEntry(
+                name="CALENDAR_DEFAULT_DURATION",
+                default=3,
+                required=False, internal=False,
+                description="Default meeting length in hours"),
+            ConfigEntry(
+                name="CALENDAR_MAX_REQUESTS",
+                default=10,
+                required=False, internal=False,
+                description=(
+                    "Number of retries before giving a connection attempt up. "
+                    "Some CalDAV servers reply randomly with errors.")),
+            ConfigEntry(
+                name="CALENDAR_TIMEZONE_MAP",
+                default={
+                    "CET": "Europe/Berlin",
+                    "CEST": "Europe/Berline",
+                },
+                required=False, internal=False,
+                description="Timezone abbreviation map. Add as needed."),
+        ],
+        check=check_calendar,
+        deactivatable=True,
+        description="CalDAV settings"),
+]
+
+
+class Config:
+    def __init__(self, data=None):
+        if data is None:
+            data = {}
+        object.__setattr__(self, "data", data)
+
+    def __setattr__(self, key, value):
+        self.data[key] = value
+
+    def __getattr__(self, key):
+        try:
+            return self.data[key]
+        except KeyError:
+            raise AttributeError
+
+    def __dir__(self):
+        return self.data.keys()
+
+
+def import_config(sections=CONFIG_SECTIONS):
+    import config
+    for section in sections:
+        section.extract(config)
+    internal_config, public_config = Config(), Config()
+    for section in sections:
+        section.fill_modules(internal_config, public_config)
+    return internal_config, public_config
+
+
+def check_config(sections=CONFIG_SECTIONS):
+    import config
+    successful = True
+    for section in sections:
+        section.extract(config)
+        if not section.deactivatable or section.active:
+            logger.info("Checking {}".format(section.name))
+            try:
+                section.check(
+                    **{entry.name: entry.value for entry in section.entries})
+            except ValueError as error:
+                logger.error(error.args[0])
+                successful = False
+    return successful
+
+
+def write_example_config(
+        sections=CONFIG_SECTIONS, filename="config.py.example"):
+    example_config = "\n".join([
+        section.get_example()
+        for section in sections
+    ])
+    with open(filename, "w") as example_config_file:
+        example_config_file.write(example_config)
+
+
+if __name__ == "__main__":
+    import argparse
+    import sys
+    parser = argparse.ArgumentParser(
+        description="Create and check the config")
+    subparsers = parser.add_subparsers(dest="command")
+    subparsers.required = True
+
+    check_parser = subparsers.add_parser("check")
+    check_parser.add_argument(
+        "--log-level", choices=["error", "warning", "info", "debug"])
+
+    create_parser = subparsers.add_parser("create")
+    create_parser.add_argument("--filename", default="config.py.example")
+
+    show_parser = subparsers.add_parser("show")
+
+    arguments = parser.parse_args()
+
+    if arguments.command == "check":
+        if arguments.log_level:
+            logger.setLevel(arguments.log_level.upper())
+        sys.exit(not check_config())
+    elif arguments.command == "create":
+        filename = arguments.filename
+        write_example_config(filename=filename)
+        print("An example config has been generated at {}. "
+              "Please change the requried entries.".format(filename))
+    elif arguments.command == "show":
+        import pprint
+        internal_config, public_config = import_config()
+        print("The complete config is:")
+        pprint.pprint(internal_config.data)
+        print("The public part is:")
+        pprint.pprint(public_config.data)
diff --git a/legacy.py b/legacy.py
index 507634a..2570a26 100644
--- a/legacy.py
+++ b/legacy.py
@@ -4,7 +4,7 @@ from fuzzywuzzy import process
 from models.database import OldTodo, Protocol, ProtocolType, TodoMail
 from shared import db
 
-import config
+from shared import config
 
 
 def lookup_todo_id(old_candidates, new_who, new_description):
diff --git a/models/database.py b/models/database.py
index 11c68bf..9987f52 100644
--- a/models/database.py
+++ b/models/database.py
@@ -7,7 +7,7 @@ from uuid import uuid4
 
 from shared import (
     db, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY,
-    current_user)
+    current_user, config)
 from utils import get_etherpad_url, split_terms, check_ip_in_networks
 from models.errors import DateNotMatchingException
 from dateutil import tz
@@ -17,7 +17,6 @@ import os
 from sqlalchemy import event
 from sqlalchemy.orm import relationship, backref
 
-import config
 from todostates import make_states
 
 
diff --git a/protoparser.py b/protoparser.py
index 5bcba3d..9410453 100644
--- a/protoparser.py
+++ b/protoparser.py
@@ -6,7 +6,7 @@ from enum import Enum
 from shared import escape_tex
 from utils import footnote_hash
 
-import config
+from shared import config
 
 INDENT_LETTER = "-"
 
diff --git a/requirements.txt b/requirements.txt
index 8991986..4fcba0c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -21,7 +21,6 @@ flake8==3.5.0
 Flask==0.12.2
 Flask-Migrate==2.1.1
 Flask-Script==2.0.6
-Flask-SocketIO==2.9.3
 Flask-SQLAlchemy==2.3.2
 Flask-WTF==0.14.2
 fuzzywuzzy==0.16.0
@@ -60,6 +59,7 @@ regex==2018.2.8
 requests==2.18.4
 six==1.11.0
 SQLAlchemy==1.2.3
+SQLAlchemy-Utils==0.33.2
 typing==3.6.4
 tzlocal==1.5.1
 urllib3==1.22
diff --git a/server.py b/server.py
index 10eeacd..3fc7b04 100755
--- a/server.py
+++ b/server.py
@@ -21,9 +21,8 @@ from datetime import datetime, timedelta
 import math
 import mimetypes
 
-import config
 from shared import (
-    db, date_filter, datetime_filter, date_filter_long,
+    config, db, date_filter, datetime_filter, date_filter_long,
     date_filter_short, time_filter, time_filter_short, user_manager,
     security_manager, current_user, check_login, login_required,
     class_filter, needs_date_test, todostate_name_filter,
@@ -180,6 +179,19 @@ def merge_duplicate_todos():
             todo_by_id[todo_id] = todo
 
 
+@manager.command
+def check_config():
+    #  TODO: check how to set return codes
+    import configproxy
+    return configproxy.check_config()
+
+
+@manager.command
+def create_example_config(filename):
+    import configproxy
+    return configproxy.write_example_config(filename=filename)
+
+
 @manager.command
 def runserver():
     app.run()
diff --git a/shared.py b/shared.py
index e3fbccb..585bc2f 100644
--- a/shared.py
+++ b/shared.py
@@ -7,7 +7,12 @@ from enum import Enum
 
 from common import back
 
-import config
+try:
+    import configproxy
+    config, public_config = configproxy.import_config()
+except ImportError as error:
+    print(error)
+    raise
 
 db = SQLAlchemy()
 
diff --git a/tasks.py b/tasks.py
index b94e8be..6693d2b 100644
--- a/tasks.py
+++ b/tasks.py
@@ -16,7 +16,7 @@ from models.errors import DateNotMatchingException
 from server import celery, app
 from shared import (
     db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long,
-    date_filter_short, time_filter, class_filter, KNOWN_KEYS, WikiType)
+    date_filter_short, time_filter, class_filter, KNOWN_KEYS, WikiType, config)
 from utils import (
     mail_manager, add_line_numbers,
     set_etherpad_text, parse_datetime_from_string)
@@ -25,8 +25,6 @@ from wiki import WikiClient, WikiException
 from calendarpush import Client as CalendarClient, CalendarException
 from legacy import lookup_todo_id
 
-import config
-
 texenv = app.create_jinja_environment()
 texenv.block_start_string = r"\ENV{"
 texenv.block_end_string = r"}"
diff --git a/utils.py b/utils.py
index 9c75998..e60ddb9 100644
--- a/utils.py
+++ b/utils.py
@@ -14,8 +14,9 @@ import ipaddress
 from socket import getfqdn
 from uuid import uuid4
 import subprocess
+import contextlib
 
-import config
+from shared import config
 
 
 def random_string(length):
@@ -64,6 +65,20 @@ class MailManager:
             return smtplib.SMTP_SSL
         return smtplib.SMTP
 
+    def connect(self):
+        server = self._get_smtp()(self.hostname)
+        if self.use_starttls:
+            server.starttls()
+        if self.username not in [None, ""] and self.password not in [None, ""]:
+            server.login(self.username, self.password)
+        return server
+
+    @contextlib.contextmanager
+    def session(self):
+        server = self.connect()
+        yield server
+        server.quit()
+
     def send(self, to_addr, subject, content, appendix=None, reply_to=None):
         if (not self.active
                 or not self.hostname
@@ -83,13 +98,20 @@ class MailManager:
                 part["Content-Disposition"] = (
                     'attachment; filename="{}"'.format(name))
                 msg.attach(part)
-        server = self._get_smtp()(self.hostname)
-        if self.use_starttls:
-            server.starttls()
-        if self.username not in [None, ""] and self.password not in [None, ""]:
-            server.login(self.username, self.password)
-        server.sendmail(self.from_addr, to_addr.split(","), msg.as_string())
-        server.quit()
+        with self.session() as server:
+            server.sendmail(
+                self.from_addr,
+                to_addr.split(","),
+                msg.as_string())
+
+    def check(self):
+        if not self.active:
+            return True
+        if not self.hostname or not self.from_addr:
+            return False
+        with self.session():
+            pass
+        return True
 
 
 mail_manager = MailManager(config)
@@ -128,7 +150,6 @@ def get_etherpad_text(pad):
 
 
 def set_etherpad_text(pad, text, only_if_default=True):
-    print(pad)
     if only_if_default:
         current_text = get_etherpad_text(pad)
         if (current_text != config.EMPTY_ETHERPAD
@@ -137,7 +158,6 @@ def set_etherpad_text(pad, text, only_if_default=True):
     file_like = BytesIO(text.encode("utf-8"))
     files = {"file": file_like}
     url = get_etherpad_import_url(pad)
-    print(url)
     req = requests.post(url, files=files)
     return req.status_code == 200
 
diff --git a/views/forms.py b/views/forms.py
index 5e94ec7..3b7e64e 100644
--- a/views/forms.py
+++ b/views/forms.py
@@ -11,7 +11,7 @@ from models.database import TodoState
 from validators import CheckTodoDateByState
 from shared import current_user
 
-import config
+from shared import config
 
 
 def get_protocoltype_choices(protocoltypes, add_all=True):
diff --git a/views/tables.py b/views/tables.py
index c006122..6298835 100644
--- a/views/tables.py
+++ b/views/tables.py
@@ -2,7 +2,7 @@ from flask import Markup, url_for
 from shared import date_filter, datetime_filter, time_filter, current_user
 from common.csrf import get_csrf_token
 
-import config
+from shared import config
 
 
 class Table:
diff --git a/wiki.py b/wiki.py
index 4213cfa..95733bc 100644
--- a/wiki.py
+++ b/wiki.py
@@ -1,7 +1,7 @@
 import requests
 from json import JSONDecodeError
 
-import config
+from shared import config
 
 HTTP_STATUS_OK = 200
 HTTP_STATUS_AUTHENTICATE = 401
-- 
GitLab