diff --git a/migrations/versions/515d261a624b_.py b/migrations/versions/515d261a624b_.py
new file mode 100644
index 0000000000000000000000000000000000000000..2b5f42eb6e83a5937ea39f00a29557c2e77fcf62
--- /dev/null
+++ b/migrations/versions/515d261a624b_.py
@@ -0,0 +1,28 @@
+"""empty message
+
+Revision ID: 515d261a624b
+Revises: 77bf71eef07f
+Create Date: 2017-02-26 00:33:13.555804
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '515d261a624b'
+down_revision = '77bf71eef07f'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('protocoltypes', sa.Column('usual_time', sa.Time(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('protocoltypes', 'usual_time')
+    # ### end Alembic commands ###
diff --git a/migrations/versions/77bf71eef07f_.py b/migrations/versions/77bf71eef07f_.py
new file mode 100644
index 0000000000000000000000000000000000000000..71827d3377ee44516c47eeb45eb44f3609e8fffe
--- /dev/null
+++ b/migrations/versions/77bf71eef07f_.py
@@ -0,0 +1,28 @@
+"""empty message
+
+Revision ID: 77bf71eef07f
+Revises: f91d760158dc
+Create Date: 2017-02-26 00:26:48.499578
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '77bf71eef07f'
+down_revision = 'f91d760158dc'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('meetingreminders', sa.Column('additional_text', sa.String(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('meetingreminders', 'additional_text')
+    # ### end Alembic commands ###
diff --git a/models/database.py b/models/database.py
index 5c180270fb6dc1df0e571b0d9b437fb8e605745d..2e709c69753b730586a725221327d1a7f4a0fa64 100644
--- a/models/database.py
+++ b/models/database.py
@@ -21,6 +21,7 @@ class ProtocolType(db.Model):
     name = db.Column(db.String, unique=True)
     short_name = db.Column(db.String, unique=True)
     organization = db.Column(db.String)
+    usual_time = db.Column(db.Time)
     is_public = db.Column(db.Boolean)
     private_group = db.Column(db.String)
     public_group = db.Column(db.String)
@@ -36,12 +37,13 @@ class ProtocolType(db.Model):
     reminders = relationship("MeetingReminder", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="MeetingReminder.days_before")
     todos = relationship("Todo", backref=backref("protocoltype"), order_by="Todo.id")
 
-    def __init__(self, name, short_name, organization,
+    def __init__(self, name, short_name, organization, usual_time,
             is_public, private_group, public_group, private_mail, public_mail,
             use_wiki, wiki_category, wiki_only_public, printer):
         self.name = name
         self.short_name = short_name
         self.organization = organization
+        self.usual_time = usual_time
         self.is_public = is_public
         self.private_group = private_group
         self.public_group = public_group
@@ -56,11 +58,11 @@ class ProtocolType(db.Model):
         return ("<ProtocolType(id={}, short_name={}, name={}, "
                 "organization={}, is_public={}, private_group={}, "
                 "public_group={}, use_wiki={}, wiki_category='{}', "
-                "wiki_only_public={}, printer={})>".format(
+                "wiki_only_public={}, printer={}, usual_time={})>".format(
             self.id, self.short_name, self.name,
             self.organization, self.is_public, self.private_group,
             self.public_group, self.use_wiki, self.wiki_category,
-            self.wiki_only_public, self.printer))
+            self.wiki_only_public, self.printer, self.usual_time))
 
     def get_latest_protocol(self):
         candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.date, reverse=True)
@@ -385,12 +387,14 @@ class MeetingReminder(db.Model):
     days_before = db.Column(db.Integer)
     send_public = db.Column(db.Boolean)
     send_private = db.Column(db.Boolean)
+    additional_text = db.Column(db.String)
 
-    def __init__(self, protocoltype_id, days_before, send_public, send_private):
+    def __init__(self, protocoltype_id, days_before, send_public, send_private, additional_text):
         self.protocoltype_id = protocoltype_id
         self.days_before = days_before
         self.send_public = send_public
         self.send_private = send_private
+        self.additional_text = additional_text
 
     def __repr__(self):
         return "<MeetingReminder(id={}, protocoltype_id={}, days_before={}, send_public={}, send_private={})>".format(
diff --git a/requirements.txt b/requirements.txt
index 78fd29d3b2f7a85a018f12b226677f767f69a521..73034a329f1f111288cb4eb183fdea310aaf1ccb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,7 @@
 alembic==0.8.10
 amqp==2.1.4
 appdirs==1.4.0
+APScheduler==3.3.1
 argh==0.26.2
 billiard==3.5.0.2
 blessings==1.6
@@ -37,6 +38,7 @@ regex==2017.2.8
 requests==2.13.0
 six==1.10.0
 SQLAlchemy==1.1.5
+tzlocal==1.3
 vine==1.1.3
 watchdog==0.8.3
 wcwidth==0.1.7
diff --git a/server.py b/server.py
index 57bfc4ff335f998198e76eff54cc832f4c9ce90c..203d9dd160f586a7e750c2764174f0c20e5c367e 100755
--- a/server.py
+++ b/server.py
@@ -9,6 +9,10 @@ from flask_migrate import Migrate, MigrateCommand
 #from flask_socketio import SocketIO
 from celery import Celery
 from sqlalchemy import or_, and_
+from apscheduler.schedulers.background import BackgroundScheduler
+from apscheduler.triggers.cron import CronTrigger
+from apscheduler.triggers.interval import IntervalTrigger
+import atexit
 from io import StringIO, BytesIO
 import os
 from datetime import datetime
@@ -39,6 +43,17 @@ celery = make_celery(app, config)
 #    return socketio
 #socketio = make_socketio(app, config)
 
+def make_scheduler(app, config, function):
+    scheduler = BackgroundScheduler()
+    scheduler.start()
+    scheduler.add_job(
+        func=function,
+        trigger=CronTrigger(hour='*'),
+        id="scheduler",
+        name="Do an action regularly",
+        replace_existing=True)
+    atexit.register(scheduler.shutdown)
+
 app.jinja_env.trim_blocks = True
 app.jinja_env.lstrip_blocks = True
 app.jinja_env.filters["datify"] = date_filter
@@ -87,7 +102,7 @@ def new_type():
             flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
         else:
             protocoltype = ProtocolType(form.name.data, form.short_name.data,
-                form.organization.data, form.is_public.data,
+                form.organization.data, form.usual_time.data, form.is_public.data,
                 form.private_group.data, form.public_group.data,
                 form.private_mail.data, form.public_mail.data,
                 form.use_wiki.data, form.wiki_category.data,
@@ -148,7 +163,7 @@ def new_reminder(type_id):
         return redirect(request.args.get("next") or url_for("index"))
     form = MeetingReminderForm()
     if form.validate_on_submit():
-        reminder = MeetingReminder(protocoltype.id, form.days_before.data, form.send_public.data, form.send_private.data)
+        reminder = MeetingReminder(protocoltype.id, form.days_before.data, form.send_public.data, form.send_private.data, form.additional_text.data)
         db.session.add(reminder)
         db.session.commit()
         return redirect(request.args.get("next") or url_for("show_type", type_id=protocoltype.id))
@@ -365,7 +380,7 @@ def list_protocols():
                     for text, matched in parts
                 ]))
             search_results[protocol] = "<br />\n".join(formatted_lines)
-        protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
+    protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
     page = _get_page()
     page_count = int(math.ceil(len(protocols)) / config.PAGE_LENGTH)
     if page >= page_count:
@@ -809,6 +824,20 @@ def logout():
         flash("You are not logged in.", "alert-error")
     return redirect(url_for(".index"))
 
+def check_and_send_reminders():
+    with app.app_context():
+        current_time = datetime.now()
+        current_day = current_time.date()
+        for protocol in Protocol.query.filter(Protocol.done == False).all():
+            day_difference = (protocol.date - current_day).days
+            usual_time = protocol.protocoltype.usual_time
+            protocol_time = datetime(1, 1, 1, usual_time.hour, usual_time.minute)
+            hour_difference = (protocol_time - current_time).seconds // 3600
+            print(protocol.get_identifier(), day_difference, hour_difference)
+            for reminder in protocol.protocoltype.reminders:
+                if day_difference == reminder.days_before and hour_difference == 0:
+                    tasks.send_reminder(reminder, protocol)
 
 if __name__ == "__main__":
+    make_scheduler(app, config, check_and_send_reminders)
     manager.run()
diff --git a/tasks.py b/tasks.py
index 88fdebfd5359541ae9f729c750b816b675d9fb44..e82b2d4ab5b6a00b8b35d8acbda2923bafe4704d 100644
--- a/tasks.py
+++ b/tasks.py
@@ -5,7 +5,7 @@ import subprocess
 import shutil
 import tempfile
 
-from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP
+from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder
 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
@@ -308,21 +308,33 @@ def print_file_async(filename, protocol_id):
             db.session.add(error)
             db.session.commit()
 
-def send_mail(mail):
-    send_mail_async.delay(mail.id)
+def send_reminder(reminder, protocol):
+    send_reminder_async.delay(reminder.id, protocol.id)
 
 @celery.task
-def send_mail_async(mail_id):
+def send_reminder_async(reminder_id, protocol_id):
     with app.app_context():
-        mail = Mail.query.filter_by(id=mail_id).first()
-        if mail is None:
-            return False
-        mail.ready = False
-        mail.error = False
-        db.session.commit()
-        result = mail_manager.send(mail.to_addr, mail.subject, mail.content)
-        mail.ready = True
-        mail.error = not result
-        db.session.commit()
+        reminder = MeetingReminder.query.filter_by(id=reminder_id).first()
+        protocol = Protocol.query.filter_by(id=protocol_id).first()
+        reminder_text = render_template("reminder.txt", reminder=reminder, protocol=protocol)
+        if reminder.send_public:
+            send_mail(protocol, protocol.protocoltype.public_mail, "Tagesordnung der {}".format(protocol.protocoltype.name), reminder_text)
+        if reminder.send_private:
+            send_mail(protocol, protocol.protocoltype.private_mail, "Tagesordnung der {}".format(protocol.protocoltype.name), reminder_text)
+
+def send_mail(protocol, to_addr, subject, content):
+    if to_addr is not None and len(to_addr.strip()) > 0:
+        send_mail_async.delay(protocol.id, to_addr, subject, content)
 
+@celery.task
+def send_mail_async(protocol_id, to_addr, subject, content):
+    with app.app_context():
+        protocol = Protocol.query.filter_by(id=protocol_id).first()
+        try:
+            print("sending {} to {}".format(subject, to_addr))
+            mail_manager.send(to_addr, subject, content)
+        except Exception as exc:
+            error = protocol.create_error("Sending Mail", "Sending mail failed", str(exc))
+            db.session.add(error)
+            db.session.commit()
 
diff --git a/templates/reminder.txt b/templates/reminder.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c78c32df6b9cc8712f1ce37ee0ef479135ec2fa6
--- /dev/null
+++ b/templates/reminder.txt
@@ -0,0 +1,24 @@
+Die nächste {{protocol.protocoltype.name}} findet am {{protocol.date|datify}} um {{protocol.protocoltype.usual_time|timify}} statt.
+
+Die vorläufige Tagesordnung ist:
+{% if not protocol.has_nonplanned_tops() %}
+    {% for default_top in protocol.protocoltype.default_tops %}
+        {% if not default_top.is_at_end() %}
+* {{default_top.name}}
+        {% endif %}
+    {% endfor %}
+{% endif %}
+{% for top in protocol.tops %}
+* {{top.name }}
+{% endfor %}
+{% if not protocol.has_nonplanned_tops() %}
+    {% for default_top in protocol.protocoltype.default_tops %}
+        {% if default_top.is_at_end() %}
+* {{default_top.name}}
+        {% endif %}
+    {% endfor %}
+{% endif %}
+
+{% if reminder.additional_text is not none %}
+{{reminder.additional_text}}
+{% endif %}
diff --git a/utils.py b/utils.py
index bd6f3ce79704fbe8865d20311e8e52fd69bda7b7..cc020e5dcba17ae0cd7e80fa0c68d0351af08550 100644
--- a/utils.py
+++ b/utils.py
@@ -67,7 +67,6 @@ class MailManager:
         self.hostname = getattr(config, "MAIL_HOST", "")
         self.username = getattr(config, "MAIL_USER", "")
         self.password = getattr(config, "MAIL_PASSWORD", "")
-        self.prefix = getattr(config, "MAIL_PREFIX", "")
 
     def send(self, to_addr, subject, content):
         if (not self.active
@@ -75,21 +74,16 @@ class MailManager:
             or not self.username
             or not self.password
             or not self.from_addr):
-            return True
-        try:
-            msg = MIMEMultipart("alternative")
-            msg["From"] = self.from_addr
-            msg["To"] = to_addr
-            msg["Subject"] = "[{}] {}".format(self.prefix, subject) if self.prefix else subject
-            msg.attach(MIMEText(content, _charset="utf-8"))
-            server = smtplib.SMTP_SSL(self.hostname)
-            server.login(self.username, self.password)
-            server.sendmail(self.from_addr, to_addr, msg.as_string())
-            server.quit()
-        except Exception as e:
-            print(e)
-            return False
-        return True
+            return
+        msg = MIMEMultipart("alternative")
+        msg["From"] = self.from_addr
+        msg["To"] = to_addr
+        msg["Subject"] = subject
+        msg.attach(MIMEText(content, _charset="utf-8"))
+        server = smtplib.SMTP_SSL(self.hostname)
+        server.login(self.username, self.password)
+        server.sendmail(self.from_addr, to_addr, msg.as_string())
+        server.quit()
 
 mail_manager = MailManager(config)
 
diff --git a/views/forms.py b/views/forms.py
index 10ba631f53292059ec115bf744613ba0b89e93ca..90f4ddc80d9d55e6316c4324de8f7741a28fa200 100644
--- a/views/forms.py
+++ b/views/forms.py
@@ -1,5 +1,5 @@
 from flask_wtf import FlaskForm
-from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField, FileField, DateTimeField
+from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField, FileField, DateTimeField, TextAreaField
 from wtforms.validators import InputRequired, Optional
 
 import config
@@ -12,6 +12,7 @@ class ProtocolTypeForm(FlaskForm):
     name = StringField("Name", validators=[InputRequired("Du musst einen Namen angeben.")])
     short_name = StringField("Abkürzung", validators=[InputRequired("Du musst eine Abkürzung angebene.")])
     organization = StringField("Organisation", validators=[InputRequired("Du musst eine zugehörige Organisation angeben.")])
+    usual_time = DateTimeField("Üblicher Beginn", validators=[InputRequired("Bitte gib die Zeit an, zu der die Sitzung beginnt.")], format="%H:%M")
     is_public = BooleanField("Öffentlich sichtbar")
     private_group = StringField("Interne Gruppe")
     public_group = StringField("Öffentliche Gruppe")
@@ -30,6 +31,7 @@ class MeetingReminderForm(FlaskForm):
     days_before = IntegerField("Tage vor Sitzung", validators=[InputRequired("Du musst eine Dauer angeben.")])
     send_public = BooleanField("Öffentlich einladen")
     send_private = BooleanField("Intern einladen")
+    additional_text = TextAreaField("Zusätzlicher Mailinhalt")
 
 class NewProtocolForm(FlaskForm):
     protocoltype = SelectField("Typ", choices=[], coerce=int)
diff --git a/views/tables.py b/views/tables.py
index 4a44cbb589844a58b7658979426f340b64eab987..ce3389f5e2375b570527bfd0ba533f385c5ff33d 100644
--- a/views/tables.py
+++ b/views/tables.py
@@ -105,8 +105,8 @@ class ProtocolTypeTable(SingleValueTable):
         super().__init__(protocoltype.name, protocoltype, newlink=url_for("edit_type", type_id=protocoltype.id))
 
     def headers(self):
-        headers = ["Name", "Abkürzung", "Organisation", "Öffentlich",
-            "Interne Gruppe", "Öffentliche Gruppe",
+        headers = ["Name", "Abkürzung", "Organisation", "Beginn",
+            "Öffentlich", "Interne Gruppe", "Öffentliche Gruppe",
             "Interner Verteiler", "Öffentlicher Verteiler",
             "Drucker", "Wiki"]
         if self.value.use_wiki:
@@ -118,6 +118,7 @@ class ProtocolTypeTable(SingleValueTable):
             self.value.name,
             self.value.short_name,
             self.value.organization,
+            self.value.usual_time.strftime("%H:%M") if self.value.usual_time is not None else "", # todo: remove if, this field is required
             Table.bool(self.value.is_public),
             self.value.private_group,
             self.value.public_group,
@@ -156,12 +157,13 @@ class MeetingRemindersTable(Table):
         self.protocoltype = protocoltype
 
     def headers(self):
-        return ["Zeit", "Einladen", ""]
+        return ["Zeit", "Einladen", "Zusätzlicher Mailinhalt", ""]
 
     def row(self, reminder):
         return [
             "{} Tage".format(reminder.days_before),
             self.get_send_summary(reminder),
+            reminder.additional_text or "",
             Table.concat([
                 Table.link(url_for("edit_reminder", type_id=self.protocoltype.id, reminder_id=reminder.id), "Ändern"),
                 Table.link(url_for("delete_reminder", type_id=self.protocoltype.id, reminder_id=reminder.id), "Löschen", confirm="Bist du dir sicher, dass du die Einladungsmail {} Tage vor der Sitzung löschen willst?".format(reminder.days_before))