diff --git a/scipost_django/mailing_lists/admin.py b/scipost_django/mailing_lists/admin.py
index 869cb63f00746339dc914cac744a09038508f870..b83dc198c3c5a126770045d0f5df423d7ee22c48 100644
--- a/scipost_django/mailing_lists/admin.py
+++ b/scipost_django/mailing_lists/admin.py
@@ -4,7 +4,7 @@ __license__ = "AGPL v3"
 
 from django.contrib import admin
 
-from .models import MailchimpList
+from .models import *
 
 
 @admin.register(MailchimpList)
@@ -16,3 +16,19 @@ class MailchimpListAdmin(admin.ModelAdmin):
         return False
 
 
+@admin.register(MailingList)
+class MailingListAdmin(admin.ModelAdmin):
+    list_display = ["__str__", "is_opt_in", "eligible_count", "subscribed_count"]
+    list_filter = ["is_opt_in"]
+    autocomplete_fields = ["eligible_subscribers", "subscribed"]
+
+    readonly_fields = ["_email_list"]
+
+    def eligible_count(self, obj):
+        return obj.eligible_subscribers.count()
+
+    def subscribed_count(self, obj):
+        return obj.subscribed.count()
+
+    def _email_list(self, obj):
+        return ", ".join(obj.email_list)
diff --git a/scipost_django/mailing_lists/migrations/0003_mailinglist.py b/scipost_django/mailing_lists/migrations/0003_mailinglist.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a5dca347fb774b8ffa1e22a1b4d0ced18c268e6
--- /dev/null
+++ b/scipost_django/mailing_lists/migrations/0003_mailinglist.py
@@ -0,0 +1,49 @@
+# Generated by Django 4.2.10 on 2024-04-17 10:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        (
+            "scipost",
+            "0041_alter_remark_contributor_alter_remark_recommendation_and_more",
+        ),
+        ("mailing_lists", "0002_auto_20171229_1435"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="MailingList",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=255)),
+                ("slug", models.SlugField(max_length=255, unique=True)),
+                ("is_opt_in", models.BooleanField(default=False)),
+                (
+                    "eligible_subscribers",
+                    models.ManyToManyField(
+                        blank=True,
+                        related_name="eligible_mailing_lists",
+                        to="scipost.contributor",
+                    ),
+                ),
+                (
+                    "subscribed",
+                    models.ManyToManyField(
+                        blank=True,
+                        related_name="subscribed_mailing_lists",
+                        to="scipost.contributor",
+                    ),
+                ),
+            ],
+        ),
+    ]
diff --git a/scipost_django/mailing_lists/models.py b/scipost_django/mailing_lists/models.py
index 21fd08aeb60a794460070eed6d2e2cdc956854e5..f5ffbef84e392e097a73ca835dbdb35005ccc5b5 100644
--- a/scipost_django/mailing_lists/models.py
+++ b/scipost_django/mailing_lists/models.py
@@ -20,7 +20,7 @@ from .constants import (
 )
 from .managers import MailListManager
 
-from profiles.models import Profile
+from profiles.models import Profile, ProfileEmail
 from scipost.behaviors import TimeStampedModel
 from scipost.constants import NORMAL_CONTRIBUTOR
 from scipost.models import Contributor
@@ -160,3 +160,58 @@ class MailchimpSubscription(TimeStampedModel):
             "active_list",
             "contributor",
         )
+
+
+class MailingList(models.Model):
+    name = models.CharField(max_length=255)
+    slug = models.SlugField(max_length=255, unique=True)
+
+    is_opt_in = models.BooleanField(default=False)
+    eligible_subscribers = models.ManyToManyField(
+        Contributor,
+        blank=True,
+        related_name="eligible_mailing_lists",
+    )
+    subscribed = models.ManyToManyField(
+        Contributor,
+        blank=True,
+        related_name="subscribed_mailing_lists",
+    )
+
+    @property
+    def email_list(self):
+        """
+        Returns a list of email addresses of all eligible subscribers who should receive emails from this list.
+        This is calculated as the set of eligible subscribers minus the set of unsubscribed subscribers.
+        """
+        return list(
+            self.subscribed.annotate(
+                primary_email=models.Subquery(
+                    ProfileEmail.objects.filter(
+                        profile=models.OuterRef("profile"), primary=True
+                    ).values("email")[:1]
+                )
+            ).values_list("primary_email", flat=True)
+        )
+
+    def add_eligible_subscriber(self, contributor):
+        """Adds the contributor to the list of eligible subscribers."""
+        self.eligible_subscribers.add(contributor)
+        # If the list is not opt-in, automatically subscribe the contributor
+        if not self.is_opt_in:
+            self.subscribe(contributor)
+
+    def subscribe(self, contributor):
+        """Subscribes the contributor to the list."""
+        if contributor not in self.eligible_subscribers.all():
+            raise ValueError("Contributor is not eligible to subscribe to this list.")
+        self.subscribed.add(contributor)
+
+    def unsubscribe(self, contributor):
+        """Unsubscribes the contributor from the list."""
+        if contributor not in self.subscribed.all():
+            raise ValueError("Contributor is not subscribed to this list.")
+        self.subscribed.remove(contributor)
+
+    def __str__(self):
+        return self.name