diff --git a/scipost_django/common/utils/__init__.py b/scipost_django/common/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..38fada4d15613d37971a24a2cb11cc2f750f86f0
--- /dev/null
+++ b/scipost_django/common/utils/__init__.py
@@ -0,0 +1,3 @@
+from .models import *
+from .text import *
+from .mail import *
diff --git a/scipost_django/common/utils/mail.py b/scipost_django/common/utils/mail.py
new file mode 100644
index 0000000000000000000000000000000000000000..b90c65c6e9af9199f152b81fa8b91cbd14e2ebde
--- /dev/null
+++ b/scipost_django/common/utils/mail.py
@@ -0,0 +1,50 @@
+# MARKED FOR DEPRECATION
+from django.core.mail import EmailMultiAlternatives
+from common.utils.models import get_current_domain
+
+
+class BaseMailUtil(object):
+    mail_sender = "no-reply@%s" % get_current_domain()
+    mail_sender_title = ""
+
+    @classmethod
+    def load(cls, _dict, request=None):
+        cls._context = _dict
+        cls._context["request"] = request
+        cls._context["domain"] = get_current_domain()
+        for var_name in _dict:
+            setattr(cls, var_name, _dict[var_name])
+
+    def _send_mail(
+        cls, template_name, recipients, subject, extra_bcc=None, extra_context={}
+    ):
+        """
+        Call this method from a classmethod to send emails.
+        The template will have context variables defined appended from the `load` method.
+
+        Arguments:
+        template_name -- The .html template to use in the mail. The name be used to get the
+                         following two templates:
+                            `email/<template_name>.txt` (non-HTML)
+                            `email/<template_name>.html`
+        recipients -- List of mailaddresses to send to mail to.
+        subject -- The subject of the mail.
+        """
+        template = loader.get_template("email/%s.txt" % template_name)
+        html_template = loader.get_template("email/%s.html" % template_name)
+        cls._context.update(extra_context)
+        message = template.render(cls._context)
+        html_message = html_template.render(cls._context)
+        bcc_list = [cls.mail_sender]
+        if extra_bcc:
+            bcc_list += extra_bcc
+        email = EmailMultiAlternatives(
+            subject,
+            message,
+            "%s <%s>" % (cls.mail_sender_title, cls.mail_sender),
+            recipients,
+            bcc=bcc_list,
+            reply_to=[cls.mail_sender],
+        )
+        email.attach_alternative(html_message, "text/html")
+        email.send(fail_silently=False)
diff --git a/scipost_django/common/utils/models.py b/scipost_django/common/utils/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..6ed59b7eb893017ea3137e6d9262fd156a129712
--- /dev/null
+++ b/scipost_django/common/utils/models.py
@@ -0,0 +1,78 @@
+from django.contrib.sites.models import Site
+from django.db.models import Field, ForeignObjectRel
+from django.db.models.fields.related import RelatedField
+
+
+def get_current_domain():
+    try:
+        return Site.objects.get_current().domain
+    except:
+        return "fake.domain"
+
+
+def merge(old, new):
+    """
+    Merge two model instances, `old` and `new`, by:
+    - copying all the fields from `old` to `new` if they are not already set
+    - updating all (reverse) relations from `old` to `new`
+    """
+    model = old.__class__
+
+    for field in model._meta.get_fields():
+        accessor = field.name or field.get_accessor_name()
+        old_value = getattr(old, accessor, None)
+
+        if isinstance(field, Field):
+            # If new object has a value for the field, skip it
+            # otherwise, set the value from the old object
+            if getattr(new, accessor, None) is None:
+                setattr(new, accessor, old_value)
+        elif isinstance(field, RelatedField) or isinstance(field, ForeignObjectRel):
+            # Handle object relations
+            related_object = field
+            manager = related_object.related_model.objects
+
+            # Guard against missing related object field names
+            if not hasattr(related_object, "field"):
+                continue
+
+            field_name = related_object.field.name
+
+            if related_object.one_to_one:
+                # For one-to-one relations, we get the related objects from the manager
+                # and anull (let go) the attribute of the new object
+                # so that it can be attached it to the old object
+                # =====================
+                # Equivalent to:
+                # new.field_name = None
+                # old.field_name = new
+                manager.filter(**{field_name: new}).update(**{field_name: None})
+                manager.filter(**{field_name: old}).update(**{field_name: new})
+            elif related_object.many_to_many:
+                # For many-to-many relations, `old_value` is a manager
+                # and we can add the related objects to the new object
+                if accessor is not None and old_value is not None:
+                    getattr(new, accessor).add(*old_value.all())
+                else:
+                    for related_queryset in manager.filter(**{field_name: old}):
+                        getattr(related_queryset, accessor).remove(old)
+                        getattr(related_queryset, accessor).add(new)
+            elif related_object.one_to_many:
+                # For one-to-many relations, we get the related objects from the manager
+                # and update the foreign key to the new object
+                manager.filter(**{field_name: old}).update(**{field_name: new})
+            else:
+                # Handle many-to-one relations by setting the attribute
+                # of the new object if it is not already set
+                if getattr(new, accessor) is None:
+                    setattr(new, accessor, old_value)
+
+        else:
+            # Handle fields by setting the attribute of the new object
+            # if it is not already set
+            if getattr(new, accessor) is None:
+                setattr(new, accessor, old_value)
+
+    # Save both objects
+    new.save()
+    old.save()
diff --git a/scipost_django/common/utils.py b/scipost_django/common/utils/text.py
similarity index 70%
rename from scipost_django/common/utils.py
rename to scipost_django/common/utils/text.py
index 550b3aa03426fc3a1c45696f605edf82da85222b..25265f257628f6957fd8b035b8ee7c89e95e67bd 100644
--- a/scipost_django/common/utils.py
+++ b/scipost_django/common/utils/text.py
@@ -4,12 +4,9 @@ __license__ = "AGPL v3"
 
 import datetime
 
-from django.contrib.sites.models import Site
-from django.core.mail import EmailMultiAlternatives
 from django.db.models import Q
-from django.template import loader
 
-from .constants import CHARACTER_ALTERNATIVES, CHARACTER_LATINISATIONS
+from ..constants import CHARACTER_ALTERNATIVES, CHARACTER_LATINISATIONS
 
 import unicodedata
 
@@ -170,63 +167,8 @@ def jatsify_tags(text):
     return jatsified
 
 
-def get_current_domain():
-    try:
-        return Site.objects.get_current().domain
-    except:
-        return "fake.domain"
-
-
 def remove_extra_spacing(text):
     """
     Remove extra spacing from text in the form of multiple spaces.
     """
     return " ".join(text.strip().split())
-
-
-# MARKED FOR DEPRECATION
-class BaseMailUtil(object):
-    mail_sender = "no-reply@%s" % get_current_domain()
-    mail_sender_title = ""
-
-    @classmethod
-    def load(cls, _dict, request=None):
-        cls._context = _dict
-        cls._context["request"] = request
-        cls._context["domain"] = get_current_domain()
-        for var_name in _dict:
-            setattr(cls, var_name, _dict[var_name])
-
-    def _send_mail(
-        cls, template_name, recipients, subject, extra_bcc=None, extra_context={}
-    ):
-        """
-        Call this method from a classmethod to send emails.
-        The template will have context variables defined appended from the `load` method.
-
-        Arguments:
-        template_name -- The .html template to use in the mail. The name be used to get the
-                         following two templates:
-                            `email/<template_name>.txt` (non-HTML)
-                            `email/<template_name>.html`
-        recipients -- List of mailaddresses to send to mail to.
-        subject -- The subject of the mail.
-        """
-        template = loader.get_template("email/%s.txt" % template_name)
-        html_template = loader.get_template("email/%s.html" % template_name)
-        cls._context.update(extra_context)
-        message = template.render(cls._context)
-        html_message = html_template.render(cls._context)
-        bcc_list = [cls.mail_sender]
-        if extra_bcc:
-            bcc_list += extra_bcc
-        email = EmailMultiAlternatives(
-            subject,
-            message,
-            "%s <%s>" % (cls.mail_sender_title, cls.mail_sender),
-            recipients,
-            bcc=bcc_list,
-            reply_to=[cls.mail_sender],
-        )
-        email.attach_alternative(html_message, "text/html")
-        email.send(fail_silently=False)