diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index fb7b302fb9dc90f83ead0c4a407ea26845143523..e5bbd752f65866e203629307d56e12045d9f5312 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,11 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Added
+-----
+
+* Info mailings
+
 Fixed
 -----
 
diff --git a/aleksis/apps/paweljong/forms.py b/aleksis/apps/paweljong/forms.py
index 2d38289db8ce18dd09b8771e35cd9484458ffea2..46dade3deda9d17afca981ec628b68591465ef85 100644
--- a/aleksis/apps/paweljong/forms.py
+++ b/aleksis/apps/paweljong/forms.py
@@ -11,7 +11,7 @@ from phonenumber_field.formfields import PhoneNumberField
 from aleksis.core.mixins import ExtensibleForm
 from aleksis.core.models import Group, Person
 
-from .models import Event, EventRegistration, Terms, Voucher
+from .models import Event, EventRegistration, InfoMailing, Terms, Voucher
 
 COMMENT_CHOICES = [
     ("first", _("Only first name")),
@@ -49,6 +49,7 @@ class EditEventForm(ExtensibleForm):
             Fieldset(_("Date data"), Row("date_event", "date_registration", "date_retraction")),
             Fieldset(_("Event details"), Row("cost", "max_participants"), "information"),
             Fieldset(_("Terms"), "terms"),
+            Fieldset(_("Info mailings"), "info_mailings"),
         ),
     )
 
@@ -68,6 +69,7 @@ class EditEventForm(ExtensibleForm):
             "max_participants",
             "terms",
             "information",
+            "info_mailings",
         ]
         widgets = {
             "linked_group": ModelSelect2Widget(
@@ -78,6 +80,10 @@ class EditEventForm(ExtensibleForm):
                 search_fields=["aspect__icontains"],
                 attrs={"data-minimum-input-length": 0, "class": "browser-default"},
             ),
+            "info_mailings": ModelSelect2MultipleWidget(
+                search_fields=["subject__icontains"],
+                attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+            ),
         }
 
 
@@ -456,3 +462,17 @@ class RegisterEventAccount(SignupForm, ExtensibleForm):
             "The username must only contain lower case letters and numbers, "
             "and must begin with a letter."
         )
+
+
+class EditInfoMailingForm(forms.ModelForm):
+
+    layout = Layout(
+        Row("sender", "reply_to", "active"),
+        Row("send_to_person", "send_to_guardians"),
+        Row("subject"),
+        Row("text"),
+    )
+
+    class Meta:
+        model = InfoMailing
+        exclude = ["sent_to"]
diff --git a/aleksis/apps/paweljong/menus.py b/aleksis/apps/paweljong/menus.py
index 4655f1535de29effb42a1a143c93acf6b601d60a..afe3056df07a741f0bd94d2bb1db4a015c532907 100644
--- a/aleksis/apps/paweljong/menus.py
+++ b/aleksis/apps/paweljong/menus.py
@@ -44,6 +44,17 @@ MENUS = {
                         )
                     ],
                 },
+                {
+                    "name": _("Info mailings"),
+                    "url": "info_mailings",
+                    "icon": "info",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "paweljong.view_info_mailings_rule",
+                        )
+                    ],
+                },
                 {
                     "name": _("Generate participant list"),
                     "url": "generate_lists",
diff --git a/aleksis/apps/paweljong/migrations/0013_info_mailings.py b/aleksis/apps/paweljong/migrations/0013_info_mailings.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9bc02aa4ce93962f24dc134d7c1f21bf9e7bc94
--- /dev/null
+++ b/aleksis/apps/paweljong/migrations/0013_info_mailings.py
@@ -0,0 +1,50 @@
+# Generated by Django 3.2.12 on 2022-03-01 15:19
+
+import ckeditor.fields
+import django.contrib.sites.managers
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0037_alter_personinvitation_id'),
+        ('sites', '0002_alter_domain_unique'),
+        ('paweljong', '0012_event_slug'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='eventregistration',
+            name='event',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='paweljong.event', verbose_name='Event'),
+        ),
+        migrations.CreateModel(
+            name='InfoMailing',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
+                ('subject', models.CharField(max_length=255, verbose_name='subject')),
+                ('text', ckeditor.fields.RichTextField(verbose_name='Text')),
+                ('reply_to', models.EmailField(blank=True, max_length=254, verbose_name='Request replies to')),
+                ('active', models.BooleanField(default=False, verbose_name='Mailing is active')),
+                ('sender', models.EmailField(blank=True, max_length=254, verbose_name='Sender')),
+                ('send_to_person', models.BooleanField(default=True, verbose_name='Send to registered person')),
+                ('send_to_guardians', models.BooleanField(default=False, verbose_name='Send to guardians')),
+                ('sent_to', models.ManyToManyField(blank=True, editable=False, related_name='received_info_mailings', to='core.Person', verbose_name='Sent to persons')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
+            ],
+            options={
+                'abstract': False,
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.AddField(
+            model_name='event',
+            name='info_mailings',
+            field=models.ManyToManyField(blank=True, related_name='events', to='paweljong.InfoMailing', verbose_name='Info mailings'),
+        ),
+    ]
diff --git a/aleksis/apps/paweljong/models.py b/aleksis/apps/paweljong/models.py
index 72b02714f240f533867c736922fef638ad2fa1e4..55a067ad82e33466938c75670b2e2d1bd2955087 100644
--- a/aleksis/apps/paweljong/models.py
+++ b/aleksis/apps/paweljong/models.py
@@ -11,7 +11,8 @@ from django_iban.fields import IBANField
 
 from aleksis.core.mixins import ExtensibleModel
 from aleksis.core.models import Group, Person
-from aleksis.core.util.core_helpers import generate_random_code
+from aleksis.core.util.core_helpers import generate_random_code, get_site_preferences
+from aleksis.core.util.email import send_email
 
 
 class Terms(ExtensibleModel):
@@ -23,6 +24,75 @@ class Terms(ExtensibleModel):
         return self.title
 
 
+class InfoMailing(ExtensibleModel):
+    subject = models.CharField(max_length=255, verbose_name=_("subject"))
+    text = RichTextField(verbose_name=_("Text"))
+    reply_to = models.EmailField(verbose_name=_("Request replies to"), blank=True)
+
+    active = models.BooleanField(verbose_name=_("Mailing is active"), default=False)
+
+    sender = models.EmailField(verbose_name=_("Sender"), blank=True)
+    send_to_person = models.BooleanField(verbose_name=_("Send to registered person"), default=True)
+    send_to_guardians = models.BooleanField(verbose_name=_("Send to guardians"), default=False)
+
+    sent_to = models.ManyToManyField(
+        Person,
+        verbose_name=_("Sent to persons"),
+        related_name="received_info_mailings",
+        editable=False,
+        blank=True,
+    )
+
+    def __str__(self) -> str:
+        return self.subject
+
+    @classmethod
+    def get_active_mailings(cls):
+        return cls.objects.filter(active=True)
+
+    def send(self):
+        sent_to = self.sent_to.all()
+
+        for event in self.events.all():
+            for registration in event.registrations.all():
+                if registration.person in sent_to:
+                    continue
+
+                subject = self.subject.format(
+                    event=event, registration=registration, person=registration.person
+                )
+                body = self.text.format(
+                    event=event, registration=registration, person=registration.person
+                )
+
+                if self.send_to_person:
+                    to = [registration.person.email]
+                    if self.send_to_guardians:
+                        cc = registration.person.guardians.values_list("email", flat=True).all()
+                    else:
+                        cc = []
+                elif self.send_to_guardians:
+                    to = registration.person.guardians.values_list("email", flat=True).all()
+                    cc = []
+
+                sender = self.sender or get_site_preferences()["mail__address"]
+                reply_to = self.reply_to or sender
+
+                context = {"subject": subject, "body": body}
+                send_email(
+                    template_name="info_mailing",
+                    context=context,
+                    from_email=sender,
+                    recipient_list=to,
+                    cc=cc,
+                    headers={
+                        "Reply-To": reply_to,
+                    },
+                )
+
+                self.sent_to.add(registration.person)
+
+
 class Event(ExtensibleModel):
     # Event details
     display_name = models.CharField(verbose_name=_("Display name"), max_length=255)
@@ -44,6 +114,9 @@ class Event(ExtensibleModel):
     max_participants = models.PositiveSmallIntegerField(verbose_name=_("Maximum participants"))
     information = RichTextField(verbose_name=_("Information about the event"))
     terms = models.ManyToManyField(Terms, verbose_name=_("Terms"), related_name="event", blank=True)
+    info_mailings = models.ManyToManyField(
+        InfoMailing, verbose_name=_("Info mailings"), related_name="events", blank=True
+    )
 
     def save(self, *args, **kwargs):
         if not self.slug:
@@ -142,7 +215,9 @@ class Voucher(ExtensibleModel):
 
 class EventRegistration(ExtensibleModel):
 
-    event = models.ForeignKey(Event, on_delete=models.CASCADE, verbose_name=_("Event"))
+    event = models.ForeignKey(
+        Event, on_delete=models.CASCADE, verbose_name=_("Event"), related_name="registrations"
+    )
     person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name=_("Person"))
     date_registered = models.DateTimeField(auto_now_add=True, verbose_name=_("Registration date"))
 
diff --git a/aleksis/apps/paweljong/rules.py b/aleksis/apps/paweljong/rules.py
index 99254b87447353cbc6c437845bf3e7d61ebdcad5..056af3de4f17ea5c2fed2fce93b168a7c5fd8f74 100644
--- a/aleksis/apps/paweljong/rules.py
+++ b/aleksis/apps/paweljong/rules.py
@@ -109,3 +109,11 @@ view_terms_predicate = has_person & (
     has_global_perm("paweljong.view_term") | has_any_object("paweljong.view_term", Terms)
 )
 rules.add_perm("paweljong.view_terms_rule", view_terms_predicate)
+
+
+# View info_mailings
+view_info_mailings_predicate = has_person & (
+    has_global_perm("paweljong.view_info_mailing")
+    | has_any_object("paweljong.view_info_mailing", Terms)
+)
+rules.add_perm("paweljong.view_info_mailings_rule", view_info_mailings_predicate)
diff --git a/aleksis/apps/paweljong/tables.py b/aleksis/apps/paweljong/tables.py
index 9dd0416c66601d415869104842983d8b0c7ad03a..2e7ac691d402d6c72ac1437eb5a7666146fe1e20 100644
--- a/aleksis/apps/paweljong/tables.py
+++ b/aleksis/apps/paweljong/tables.py
@@ -76,3 +76,23 @@ class TermsTable(tables.Table):
         verbose_name=_("Edit"),
         text=_("Edit"),
     )
+
+
+class InfoMailingsTable(tables.Table):
+    class Meta:
+        attrs = {"class": "responsive-table highlight"}
+
+    subject = tables.Column()
+
+    edit = tables.LinkColumn(
+        "edit_info_mailing_by_pk",
+        args=[A("id")],
+        verbose_name=_("Edit"),
+        text=_("Edit"),
+    )
+    delete = tables.LinkColumn(
+        "delete_info_mailing_by_pk",
+        args=[A("id")],
+        verbose_name=_("Delete"),
+        text=_("Delete"),
+    )
diff --git a/aleksis/apps/paweljong/tasks.py b/aleksis/apps/paweljong/tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1f34f10ec72dcc996aef7d85a9db5571a3a41bb
--- /dev/null
+++ b/aleksis/apps/paweljong/tasks.py
@@ -0,0 +1,11 @@
+from datetime import timedelta
+
+from aleksis.core.celery import app
+
+
+@app.task(run_every=timedelta(hours=1))
+def send_info_mailings() -> None:
+    from .models import InfoMailing  # noqa
+
+    for mailing in InfoMailing.get_active_mailings():
+        mailing.send()
diff --git a/aleksis/apps/paweljong/templates/paweljong/info_mailing/create.html b/aleksis/apps/paweljong/templates/paweljong/info_mailing/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..9c6952492f8a256e8e97bd5f17a7b56b6a00fbd8
--- /dev/null
+++ b/aleksis/apps/paweljong/templates/paweljong/info_mailing/create.html
@@ -0,0 +1,19 @@
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block page_title %}{% blocktrans %}Create info mailing{% endblocktrans %}{% endblock %}
+{% block browser_title %}{% blocktrans %}Create info mailing{% endblocktrans %}{% endblock %}
+
+{% block extra_head %}
+    {{ form.media.css }}
+{% endblock %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+  {{ form.media.js }}
+{% endblock %}
diff --git a/aleksis/apps/paweljong/templates/paweljong/info_mailing/edit.html b/aleksis/apps/paweljong/templates/paweljong/info_mailing/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..d243a39ee8a5a9a0cbcd1a63a61ac5b5444a7157
--- /dev/null
+++ b/aleksis/apps/paweljong/templates/paweljong/info_mailing/edit.html
@@ -0,0 +1,18 @@
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block page_title %}{% blocktrans %}Edit info mailing{% endblocktrans %}{% endblock %}
+{% block browser_title %}{% blocktrans %}Edit info mailing{% endblocktrans %}{% endblock %}
+
+{% block extra_head %}
+    {{ form.media.css }}
+{% endblock %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+  {{ form.media.js }}
+{% endblock %}
diff --git a/aleksis/apps/paweljong/templates/paweljong/info_mailing/list.html b/aleksis/apps/paweljong/templates/paweljong/info_mailing/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..28b1e1714120f774798cdb36851f83a12bed64ac
--- /dev/null
+++ b/aleksis/apps/paweljong/templates/paweljong/info_mailing/list.html
@@ -0,0 +1,14 @@
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% load render_table from django_tables2 %}
+
+{% block page_title %}{% blocktrans %}Info mailings{% endblocktrans %}{% endblock %}
+{% block browser_title %}{% blocktrans %}Info mailings{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+    <a class="btn colour-primary waves-effect waves-light" href="{% url 'create_info_mailing' %}">{% trans "Create info mailing" %}</a>
+    {% render_table table %}
+
+{% endblock %}
diff --git a/aleksis/apps/paweljong/templates/templated_email/info_mailing.email b/aleksis/apps/paweljong/templates/templated_email/info_mailing.email
new file mode 100644
index 0000000000000000000000000000000000000000..946f635afbdfca45318bf3dda4232cbdf11a3c6c
--- /dev/null
+++ b/aleksis/apps/paweljong/templates/templated_email/info_mailing.email
@@ -0,0 +1,3 @@
+{% block subject %}{{ subject }}{% endblock %}
+
+{% block html %}{{ body|safe }}{% endblock %}
diff --git a/aleksis/apps/paweljong/urls.py b/aleksis/apps/paweljong/urls.py
index 0be09dfc82b789b40636088edae650f9308c4371..e90bde07f8ef1bbc3ebcb2575bbf58c9f100a55c 100644
--- a/aleksis/apps/paweljong/urls.py
+++ b/aleksis/apps/paweljong/urls.py
@@ -102,4 +102,24 @@ urlpatterns = [
         views.TermEditView.as_view(),
         name="edit_term_by_pk",
     ),
+    path(
+        "info_mailings/list",
+        views.InfoMailingListView.as_view(),
+        name="info_mailings",
+    ),
+    path(
+        "info_mailings/create",
+        views.InfoMailingCreateView.as_view(),
+        name="create_info_mailing",
+    ),
+    path(
+        "info_mailings/<int:pk>/edit",
+        views.InfoMailingEditView.as_view(),
+        name="edit_info_mailing_by_pk",
+    ),
+    path(
+        "info_mailings/<int:pk>/delete",
+        views.InfoMailingDeleteView.as_view(),
+        name="delete_info_mailing_by_pk",
+    ),
 ]
diff --git a/aleksis/apps/paweljong/views.py b/aleksis/apps/paweljong/views.py
index 972e2cabf863b1d6af8349708acf94a79675054f..efe2fee8e039f6022e85d02c56cad1ceaeb64477 100644
--- a/aleksis/apps/paweljong/views.py
+++ b/aleksis/apps/paweljong/views.py
@@ -32,12 +32,19 @@ from .filters import EventFilter, EventRegistrationFilter, VoucherFilter
 from .forms import (
     EditEventForm,
     EditEventRegistrationForm,
+    EditInfoMailingForm,
     EditTermForm,
     EditVoucherForm,
     GenerateListForm,
 )
-from .models import Event, EventRegistration, Terms, Voucher
-from .tables import EventRegistrationsTable, ManageEventsTable, TermsTable, VouchersTable
+from .models import Event, EventRegistration, InfoMailing, Terms, Voucher
+from .tables import (
+    EventRegistrationsTable,
+    InfoMailingsTable,
+    ManageEventsTable,
+    TermsTable,
+    VouchersTable,
+)
 
 User = get_user_model()
 
@@ -769,3 +776,46 @@ class UpcomingEventsRSSFeed(Feed):
 class AccountRegisterStart(TemplateView):
 
     template_name = "paweljong/register_start.html"
+
+
+class InfoMailingListView(PermissionRequiredMixin, SingleTableView):
+    """Table of all info mailings."""
+
+    model = InfoMailing
+    table_class = InfoMailingsTable
+    permission_required = "paweljong.view_info_mailing"
+    template_name = "paweljong/info_mailing/list.html"
+
+
+@method_decorator(never_cache, name="dispatch")
+class InfoMailingCreateView(PermissionRequiredMixin, AdvancedCreateView):
+    """Create view for info mailings."""
+
+    model = InfoMailing
+    form_class = EditInfoMailingForm
+    permission_required = "paweljong.add_info_mailing"
+    template_name = "paweljong/info_mailing/create.html"
+    success_url = reverse_lazy("info_mailings")
+    success_message = _("The info mailing has been created.")
+
+
+@method_decorator(never_cache, name="dispatch")
+class InfoMailingEditView(PermissionRequiredMixin, AdvancedEditView):
+    """Edit view for info mailings."""
+
+    model = InfoMailing
+    form_class = EditInfoMailingForm
+    permission_required = "paweljong.edit_info_mailing"
+    template_name = "paweljong/info_mailing/edit.html"
+    success_url = reverse_lazy("info_mailings")
+    success_message = _("The info mailing has been saved.")
+
+
+class InfoMailingDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
+    """Delete view for info mailings."""
+
+    model = InfoMailing
+    permission_required = "paweljong.delete_info_mailing"
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("info_mailings")
+    success_message = _("The info mailing has been deleted.")