diff --git a/README.md b/README.md
index 405aa98b76940c1be82eccc9f29c84811f730edf..914694f0c8b1a2b63a31bc4d0a18aca54cedcb82 100644
--- a/README.md
+++ b/README.md
@@ -274,69 +274,283 @@ To build the documentation, run:
 for each of the documentation projects.
 After this, generated documentation are available in `docs/[project slug]/_build/html`.
 
-## Mails
-The `mails` app is used as the mailing processor of SciPost.
-It may be used in one of two possible ways: with or without editor.
+## Templated emails
+The `mails` app is used as the (templated) mailing processor of SciPost. Each email is defined using two files: the template and the configuration file.
 
-The actual mails only have to be written in the html version
-(the text based alternative is automatically generated before sending).
-Creating a new `mail_code` is easily done by creating new files in the `templates/email/<subfolder>` folder called `<mail_code>.html` and `<mail_code>.json` acting respectively as a content and configuration file. Here, `<subfolder>` is named after the main recipient's class (authors, referees, etc.).
+Each mail is defined using certain general configuration possibilities. These options are defined in the json configuration file or are overwritten in the methods described below. These fields are:
 
-##### The config file is configured as follows
-`templates/email/<subfolder>/<mail_code>.json`
+* `subject` {string}
+> The subject of the mail.
 
-* `context_object` - (_required_) Instance of the main object. This instance needs to be passed as `instance` or `<context_object>` in the views and as `<context_object>` in the template file (see description below);
-* `subject` - (_string, required_) Default subject value;
-* `to_address` - (_string or path of properties, required_) Default to address;
-* `bcc_to` - (_string or path of properties, optional_) - A comma-separated bcc list of mail addresses;
-* `from_address` - (_string, optional_) - From address' default value: `no-reply@scipost.org`;
-* `from_address_name` - (_string, optional_) - From address name's default value: `SciPost`.
+* `recipient_list` and `bcc` {list}
+> Both fields are lists of strings. Each string may be either a plain mail address, eg. ` example@scipost.org`, or it may represent a certain relation to the central object. For example, one may define:
+```python
+>>> sub_1 = Submission.objects.first()
+>>> mail_util = DirectMailUtil([...], object=sub_1, recipient_list=['example@scipost.org', 'submitted_by.user.email'])
+```
+
+* `from_email` {string}
+> For this field, the same flexibility and functionality exists as for the `recipient_list` and `bcc` fields. However, this field should always be a single string entry.
+```python
+>>> mail_util = DirectMailUtil([...], from_email='noreply@scipost.org')
+```
 
+* `from_name` {string}
+> The representation of the mail sender.
 
-### Mailing with editor
-Any regular method or class-based view may be used together with the builtin wysiwyg editor. The class-based views inherited from Django's UpdateView are easily extended for use with the editor.
+### Central object
+#### Using a single Model instance
+The "central object" is a *django.db.models*.__Model__ instance that will be used for the email fields if needed and in the template. The mail engine will try to automatically detect a possible Model instance and save this in the template context as `<Model.verbose_name>` and `object`. The keyword you use to send it to the mail engine is not relevant for this method, but will be copied to be used in the template as well.
 
+##### Example
 ```python
-from django.views.generic.edit import UpdateView
-from mails.views import MailEditorMixin
+>>> sub_1 = Submission.object.first()
+>>> mail_util = DirectMailUtil([...], weird_keyword=sub_1)
+```
 
-class AnyUpdateView(MailEditorMixin, UpdateView):
-    mail_code = '<any_valid_mail_code>'
+Now, in the template, the variables `weird_keyword`, `submission` and `object` will all represent the `sub_1` instance. For example:
+
+```html
+<h1>Dear {{ weird_keyword.submitted_by.get_title_display }} {{ object.submitted_by.user.last_name }},</h1>
+<p>Thank you for your submission: {{ submission.title }}.</p>
 ```
 
-For method-based views, one implements the mails construction as:
+#### Using multiple Model instances
+If a certain mail requires more than one Model instance, it is required to pass either a `instance` or `object` parameter for the mail engine to determine the central object.
 
+##### Example
 ```python
-from mails.views import MailEditingSubView
+>>> sub_1 = Submission.object.first()
+>>> report_1 = Report.object.first()
+>>> mail_util = DirectMailUtil([...], submission=sub_1, report=report_1)
+ValueError: "Multiple db instances are given."
+```
 
-def any_method_based_view(request):
-    # Initialize mail view
-    mail_request = MailEditingSubView(request, mail_code='<any_valid_mail_code>', instance=django_model_instance)
-    if mail_request.is_valid():
-        # Send mail
-        mail_request.send()
-        return redirect('reverse:url')
-    else:
-        # Render the wsyiwyg editor
-        return mail_request.return_render()
+Here, it is required to pass either the `instance` or `object` parameter, eg.:
+```python
+>>> mail_util = DirectMailUtil([...], object=sub_1, report=report_1)
+```
+
+#### Configuration file
+
+File: *templates/email/*__<mail_code>.json__
+
+Each mail is configured with a json file, which at least contains a `subject` and `recipient_list` value. The other fields are optional. An example of all available configuration fields are shown:
+```json
+{
+    "subject": "Foo subject",
+    "recipient_list": [
+        "noreply@scipost.org"
+    ],
+    "bcc": [
+        "secret@scipost.org"
+    ],
+    "from_email": "server@scipost.org",
+    "from_name": "SciPost Techsupport"
+}
+```
+
+#### Template file
+
+File: *templates/email/*__<mail_code>.html__
+
+Any mail will be defined in the html file using the conventions as per [Django's default template processor](https://docs.djangoproject.com/en/1.11/topics/templates/).
+
+### Direct mail utility
+The fastest, easiest way to use templated emails is using the `DirectMailUtil` class.
+
+*class* mails.utils.__DirectMailUtil(__*mail_code, delayed_processing=True, subject='', recipient_list=[], bcc=[], from_email='', from_name='', \**template_variables*__)__
+
+##### Attributes
+* `mail_code` {string}
+> The unique code refereeing to a template and configuration file.
+
+* `delayed_processing` {boolean, optional}
+> Execute template rendering in a cronjob to reduce executing time.
+
+* `subject` {string, optional}
+> Overwrite the `subject` field defined in the configuration field.
+
+* `recipient_list` {list, optional}
+> Overwrite the `recipient_list` field defined in the configuration field.
+
+* `bcc` {list, optional}
+> Overwrite the `bcc` field defined in the configuration field.
+
+* `from_email` {string, optional}
+> Overwrite the `from_email` field defined in the configuration field.
+
+* `from_name` {string, optional}
+> Overwrite the `from_name` field defined in the configuration field.
+
+* `**template_variables`
+> Append any keyword argument that may be used in the email template.
+
+##### Methods
+
+* `send_mail()`
+> Send the mail as defined on initialization.
+
+##### Basic example
+```python
+>>> from mails.utils import DirectMailUtil
+>>> mail_util = DirectMailUtil('test_mail_code_1')
+>>> mail_util.send_mail()
 ```
+This utility is protected to prevent double sending. So now, the following has no effect anymore:
+```python
+>>> mail_util.send_mail()
+```
+
+
+### Class-based view editor
+ This acts like a regular Django class-based view, but will intercept the post request to load the email form and submit when positively validated.
+
+This view may be used as a [generic editing view](https://docs.djangoproject.com/en/1.11/ref/class-based-views/generic-editing/) or [DetailView](https://docs.djangoproject.com/en/1.11/ref/class-based-views/generic-display/#detailview).
+
 
-### Direct mailing
-Mailing is also possible without intercepting the request for completing or editing the mail's content. For this, use the `DirectMailUtil` instead.
+*class* mails.views.__MailView__
 
+This view is a basic class-based view, which may be used as basic editor for a specific templated email.
+
+##### Attributes    
+* `mail_code` {string}
+> The unique code refereeing to a template and configuration file.
+
+* `mail_config` {dict, optional}
+> Overwrite any of the configuration fields of the configuration file:
+    * `subject` {string}
+    * `recipient_list` {list}
+    * `bcc` {list}
+    * `from_email` {string}
+    * `from_name` {string}
+
+* `mail_variables` {dict, optional}
+> Append extra variables to the mail template.
+
+* `fail_silently` {boolean, optional}
+> If set to False, raise PermissionDenied is `can_send_mail()` returns False on POST request.
+
+##### Methods
+* `can_send_mail()`
+> Control permission to actually send the mail. Return a __boolean__, returns `True` by default.
+
+* `get_mail_config()`
+> Return an optional explicit mail configuration. Return a __dictionary__, returns `mail_config` by default.
+
+
+*class* mails.views.__MailFormView__
+
+This view may be used as a generic editing view, and will intercept the POST request to let the user edit the email before saving the original form and sending the templated mail.
+
+##### Attributes
+* `form_class` {django.forms.__ModelForm__ | django.forms.__Form__}
+> The original form to use as in any regular Django editing view.
+
+* `mail_code` {string}
+> The unique code refereeing to a template and configuration file.
+
+* `mail_config` {dict, optional}
+> Overwrite any of the configuration fields of the configuration file:
+    * `subject` {string}
+    * `recipient_list` {list}
+    * `bcc` {list}
+    * `from_email` {string}
+    * `from_name` {string}
+
+* `mail_variables` {dict, optional}
+> Append extra variables to the mail template.
+
+* `fail_silently` {boolean, optional}
+> If set to False, raise PermissionDenied is `can_send_mail()` returns False on POST request.
+
+##### Methods
+* `can_send_mail()`
+> Control permission to actually send the mail. Return a __boolean__, returns `True` by default.
+
+* `get_mail_config()`
+> Return an optional explicit mail configuration. Return a __dictionary__, returns `mail_config` by default.
+
+
+##### Basic example
 ```python
-from mails.utils import DirectMailUtil
+# <app>/views.py
+from mails.views import MailView
+
+class FooView(MailView):
+    mail_code = 'test_mail_code_1'
+```
+```python
+# <app>/urls.py
+from django.conf.urls import url
+
+from .views import FooView
 
-def any_python_method_within_django():
-    # Init mailer
-    mail_sender = DirectMailUtil(mail_code='<any_valid_mail_code>', instance=django_model_instance)
+urlpatterns = [
+    url(r'^$', FooView.as_view(), name='foo'),
+]
+```
+
+### Function-based view editor
+Similar as to the `MailView` it is possible to have the user edit a templated email before sending in function-based views, using the `MailEditorSubview`.
+
+*class* mails.views.__MailEditorSubview(__*request, mail_code, header_template='', context={}, subject='', recipient_list=[], bcc=[], from_email='', from_name='', \**template_variables*__)__
+
+##### Attributes
+* `request` {django.http.__HttpResponse__}
+> The HttpResponse which is typically the first parameter in a function-based view.
+
+* `mail_code` {string}
+> The unique code refereeing to a template and configuration file.
+
+* `header_template` {string, optional}
+> Any template that may be used in the header of the edit form.
+
+* `context` {dict, optional}
+> A context dictionary as in any usual Django view, which may be useful combined with `header_template`.
+
+* `subject` {string, optional}
+> Overwrite the `subject` field defined in the configuration field.
+
+* `recipient_list` {list, optional}
+> Overwrite the `recipient_list` field defined in the configuration field.
+
+* `bcc` {list, optional}
+> Overwrite the `bcc` field defined in the configuration field.
+
+* `from_email` {string, optional}
+> Overwrite the `from_email` field defined in the configuration field.
 
-    # Optionally(!) alter from_address from config file
-    mail_sender.set_alternative_sender('SciPost Refereeing', 'refereeing@scipost.org')
+* `from_name` {string, optional}
+> Overwrite the `from_name` field defined in the configuration field.
 
-    # Send the actual mail
-    mail_sender.send()
-    return
+* `**template_variables`
+> Append any keyword argument that may be used in the email template.
+
+##### Methods
+* `is_valid()`
+> See if data is returned and valid, similar to Django forms. Returns a __boolean__.
+
+* `interrupt()`
+> Interrupt request by rendering the templated email form. Returns a [__HttpResponse__](https://docs.djangoproject.com/en/2.1/ref/request-response/#django.http.HttpResponse).
+
+* `send_mail()`
+> Send email as edited by the user in the template.
+
+
+##### Basic example
+```python
+from submissions.models import Submission
+from mails.views import MailEditorSubview
+
+def any_method_based_view(request):
+    submission = Submission.objects.first()
+    mail_request = MailEditorSubview(request, 'test_mail_code_1', object=submission)
+    if mail_request.is_valid():
+        mail_request.send_mail()
+        return redirect('reverse:url')
+    else:
+        return mail_request.interrupt()
 ```
 
 ## Django-extensions
diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py
index cf31b72725a6b791465d876c88b56f0df768a757..3d3df39a80fbf5e3f26f4d06658795b15fd6991c 100644
--- a/SciPost_v1/urls.py
+++ b/SciPost_v1/urls.py
@@ -57,6 +57,7 @@ urlpatterns = [
     url(r'^submission/', include('submissions.urls', namespace="_submissions")),
     url(r'^theses/', include('theses.urls', namespace="theses")),
     url(r'^thesis/', include('theses.urls', namespace="_theses")),
+    url(r'^mails/', include('mails.urls', namespace="mails")),
     url(r'^meetings/', include('virtualmeetings.urls', namespace="virtualmeetings")),
     url(r'^news/', include('news.urls', namespace="news")),
     url(r'^notifications/', include('notifications.urls', namespace="notifications")),
diff --git a/colleges/templates/colleges/base.html b/colleges/templates/colleges/base.html
index b02dd091ee5a212cbd810f90f4554beaba8ee882..aa45f327d61775782936b42a94606ca02bf3de85 100644
--- a/colleges/templates/colleges/base.html
+++ b/colleges/templates/colleges/base.html
@@ -1,9 +1,9 @@
 {% extends 'scipost/base.html' %}
 
 {% block breadcrumb %}
-    <div class="container-outside header">
+    <div class="breadcrumb-container">
         <div class="container">
-            <nav class="breadcrumb hidden-sm-down">
+            <nav class="breadcrumb">
                 {% block breadcrumb_items %}
                     <a href="{% url 'colleges:potential_fellowships' %}" class="breadcrumb-item">Potential Fellowships</a>
                 {% endblock %}
diff --git a/colleges/templates/colleges/potentialfellowship_list.html b/colleges/templates/colleges/potentialfellowship_list.html
index dd3c9b301ad652bd24388f69975320f3ea296637..6cc4b4e90102462f1bb52f3dfbea6e246360a355 100644
--- a/colleges/templates/colleges/potentialfellowship_list.html
+++ b/colleges/templates/colleges/potentialfellowship_list.html
@@ -2,7 +2,6 @@
 
 {% load scipost_extras %}
 {% load colleges_extras %}
-
 {% load bootstrap %}
 
 {% block headsup %}
@@ -15,85 +14,80 @@ $(document).ready(function($) {
 </script>
 {% endblock headsup %}
 
+{% block breadcrumb_items %}
+    <span class="breadcrumb-item">Potential Fellowships</span>
+{% endblock %}
+
 {% block pagetitle %}: Potential Fellowships{% endblock pagetitle %}
 
 {% block content %}
 
+<h1 class="highlight">Potential Fellowships</h1>
 {% if perms.scipost.can_add_potentialfellowship %}
-<div class="row">
-  <div class="col-12">
-    <h3 class="highlight">Nominations</h3>
-    <p>
-      Do you know somebody qualified who could serve as a Fellow?<br/>
-      Nominate them by <a href="{% url 'colleges:potential_fellowship_create' %}">adding a Potential Fellowship</a>.
-    </p>
+    <div class="row">
+      <div class="col-12">
+        <h3 class="highlight">Nominations</h3>
+        <p>
+          Do you know somebody qualified who could serve as a Fellow?<br/>
+          Nominate them by <a href="{% url 'colleges:potential_fellowship_create' %}">adding a Potential Fellowship</a>.
+        </p>
 
-  </div>
-</div>
-
-{% if potfels_to_vote_on or potfels_voted_on %}
-<div class="row">
-  <div class="col-12">
-    <h3 class="highlight">Ongoing elections</h3>
-    {% if potfels_to_vote_on %}
-    <h4>Nominations to vote on:</h4>
-    <div>
-      {% include 'colleges/_potentialfellowship_voting_table.html' with potfels_list=potfels_to_vote_on %}
+      </div>
     </div>
-    {% endif %}
-    {% if potfels_voted_on %}
-    <h4>Nominations you have already voted on (you can revise your vote if you wish):</h4>
-    <div>
-      {% include 'colleges/_potentialfellowship_voting_table.html' with potfels_list=potfels_voted_on %}
+
+    {% if potfels_to_vote_on or potfels_voted_on %}
+    <div class="row">
+      <div class="col-12">
+        <h3 class="highlight">Ongoing elections</h3>
+        {% if potfels_to_vote_on %}
+        <h4>Nominations to vote on:</h4>
+        <div>
+          {% include 'colleges/_potentialfellowship_voting_table.html' with potfels_list=potfels_to_vote_on %}
+        </div>
+        {% endif %}
+        {% if potfels_voted_on %}
+        <h4>Nominations you have already voted on (you can revise your vote if you wish):</h4>
+        <div>
+          {% include 'colleges/_potentialfellowship_voting_table.html' with potfels_list=potfels_voted_on %}
+        </div>
+        {% endif %}
+      </div>
     </div>
     {% endif %}
-  </div>
-</div>
-{% endif %}
 {% endif %}
 
 <div class="row">
   <div class="col-12">
     <h3 class="highlight">List of potential Fellowships</h3>
-    <p>
-      <ul>
-	<li>
-	  <a href="{% url 'colleges:potential_fellowships' %}">View all</a>
-	</li>
-	<li>
-	  View by discipline/subject area:
-	  <ul class="list-inline">
-	    <li class="list-inline-item">
-	    </li>
-	    {% for discipline in subject_areas %}
-	    <li class="list-inline-item">
-	      <div class="dropdown">
-		<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton{{ discipline.0|cut:" " }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ discipline.0 }}</button>
-		<div class="dropdown-menu" aria-labelledby="dropdownMenuButton{{ discipline.0|cut:" " }}">
-		  <a class="dropdown-item" href="{% url 'colleges:potential_fellowships' discipline=discipline.0|cut:' ' %}">View all in {{ discipline.0 }}</a>
-		  {% for area in discipline.1 %}
-		  <a class="dropdown-item" href="{% url 'colleges:potential_fellowships' discipline=discipline.0|cut:' ' expertise=area.0 %}">{{ area.0 }}</a>
-		  {% endfor %}
-		</div>
-	      </div>
-	    </li>
-	    {% endfor %}
-	  </ul>
-	</li>
-	<br/>
-	<li>
-	  <div class="dropdown">
-	    <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButtonStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Filter by status</button>
-	    <div class="dropdown-menu" aria-labelledby="dropdownMenuButtonStatus">
-	      <a class="dropdown-item" href="">View all</a>
-	      {% for status in statuses %}
-	      <a class="dropdown-item" href="?status={{ status.0 }}">{{ status.1 }}</a>
-	      {% endfor %}
-	    </div>
-	  </div>
-	</li>
-      </ul>
-    </p>
+    <a href="{% url 'colleges:potential_fellowships' %}">View all</a>
+    <br>
+    View by discipline/subject area:
+    <ul class="d-inline-block list-inline">
+        <li class="list-inline-item">
+        </li>
+        {% for discipline in subject_areas %}
+            <li class="list-inline-item">
+              <div class="dropdown">
+            <button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton{{ discipline.0|cut:" " }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ discipline.0 }}</button>
+            <div class="dropdown-menu" aria-labelledby="dropdownMenuButton{{ discipline.0|cut:" " }}">
+              <a class="dropdown-item" href="{% url 'colleges:potential_fellowships' discipline=discipline.0|cut:' ' %}">View all in {{ discipline.0 }}</a>
+              {% for area in discipline.1 %}
+                  <a class="dropdown-item" href="{% url 'colleges:potential_fellowships' discipline=discipline.0|cut:' ' expertise=area.0 %}">{{ area.0 }}</a>
+              {% endfor %}
+            </div>
+              </div>
+            </li>
+        {% endfor %}
+    </ul>
+    <div class="dropdown">
+        <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButtonStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Filter by status</button>
+        <div class="dropdown-menu" aria-labelledby="dropdownMenuButtonStatus">
+            <a class="dropdown-item" href="">View all</a>
+            {% for status in statuses %}
+                <a class="dropdown-item" href="?status={{ status.0 }}">{{ status.1 }}</a>
+            {% endfor %}
+        </div>
+    </div>
   </div>
 </div>
 
diff --git a/colleges/views.py b/colleges/views.py
index e5cf6c49720c23b42b7fc6e88b7d08e4eda9d6b4..461df78b60c7668dfb1ba617933d5ba6591f7b85 100644
--- a/colleges/views.py
+++ b/colleges/views.py
@@ -9,17 +9,16 @@ from django.db.models import Count
 from django.http import Http404
 from django.shortcuts import get_object_or_404, render, redirect
 from django.utils import timezone
-from django.utils.decorators import method_decorator
 from django.views.generic.detail import DetailView
 from django.views.generic.edit import CreateView, UpdateView, DeleteView
 from django.views.generic.list import ListView
 
 from submissions.models import Submission
 
-from .constants import     POTENTIAL_FELLOWSHIP_STATUSES,\
-    POTENTIAL_FELLOWSHIP_INVITED, potential_fellowship_statuses_dict,\
-    POTENTIAL_FELLOWSHIP_EVENT_VOTED_ON, POTENTIAL_FELLOWSHIP_EVENT_EMAILED,\
-    POTENTIAL_FELLOWSHIP_EVENT_STATUSUPDATED, POTENTIAL_FELLOWSHIP_EVENT_COMMENT
+from .constants import (
+    POTENTIAL_FELLOWSHIP_STATUSES, POTENTIAL_FELLOWSHIP_EVENT_STATUSUPDATED,
+    POTENTIAL_FELLOWSHIP_INVITED, potential_fellowship_statuses_dict,
+    POTENTIAL_FELLOWSHIP_EVENT_VOTED_ON, POTENTIAL_FELLOWSHIP_EVENT_EMAILED)
 from .forms import FellowshipForm, FellowshipTerminateForm, FellowshipRemoveSubmissionForm,\
     FellowshipAddSubmissionForm, AddFellowshipForm, SubmissionAddFellowshipForm,\
     FellowshipRemoveProceedingsForm, FellowshipAddProceedingsForm, SubmissionAddVotingFellowForm,\
@@ -30,7 +29,6 @@ from .models import Fellowship, PotentialFellowship, PotentialFellowshipEvent
 from scipost.constants import SCIPOST_SUBJECT_AREAS
 from scipost.mixins import PermissionsMixin, PaginationMixin, RequestViewMixin
 
-from mails.forms import EmailTemplateForm
 from mails.views import MailView
 
 
diff --git a/conflicts/migrations/0014_auto_20190209_1127.py b/conflicts/migrations/0014_auto_20190209_1127.py
new file mode 100644
index 0000000000000000000000000000000000000000..119496d85a68bdb0728e021a7ce17964e3b2f954
--- /dev/null
+++ b/conflicts/migrations/0014_auto_20190209_1127.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-02-09 10:27
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('conflicts', '0013_conflictofinterest_related_submissions'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='conflictofinterest',
+            name='related_submissions',
+            field=models.ManyToManyField(blank=True, related_name='conflict_of_interests', to='submissions.Submission'),
+        ),
+    ]
diff --git a/invitations/forms.py b/invitations/forms.py
index 13de0ac4abe2dbb386c2350bb6f21376bd3c84da..6d5b6dddb5da1c0048d0a38ef5c3ba082e8f7428 100644
--- a/invitations/forms.py
+++ b/invitations/forms.py
@@ -238,6 +238,8 @@ class RegistrationInvitationForm(AcceptRequestMixin, forms.ModelForm):
     def save(self, *args, **kwargs):
         if not hasattr(self.instance, 'created_by'):
             self.instance.created_by = self.request.user
+        if not hasattr(self.instance, 'invited_by'):
+            self.instance.invited_by = self.request.user
 
         # Try to associate an existing Profile to invitation:
         profile = Profile.objects.get_unique_from_email_or_None(
diff --git a/invitations/mixins.py b/invitations/mixins.py
index 42b9743081c23ed9572b433645d22aa09787a200..d1cd0315a369f9d0744250bdf1e240f600f583f4 100644
--- a/invitations/mixins.py
+++ b/invitations/mixins.py
@@ -2,77 +2,12 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
 
-from django.db import transaction
-from django.contrib import messages
-
-from .constants import INVITATION_EDITORIAL_FELLOW
-from .models import RegistrationInvitation
-
-
 class RequestArgumentMixin:
     """
     Use the WSGIRequest as an argument in the form.
     """
+
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
         kwargs['request'] = self.request
         return kwargs
-
-
-class BaseFormViewMixin:
-    send_mail = None
-
-    @transaction.atomic
-    def form_valid(self, form):
-        # Communication with the user.
-        model_name = self.object._meta.verbose_name
-        model_name = model_name[:1].upper() + model_name[1:]  # Hack it to capitalize the name
-
-        if self.send_mail:
-            self.object.mail_sent(user=self.request.user)
-            messages.success(self.request, '{} updated and sent'.format(model_name))
-        else:
-            messages.success(self.request, '{} updated'.format(model_name))
-        return super().form_valid(form)
-
-
-class SendMailFormMixin(BaseFormViewMixin):
-    """
-    Send mail out if form is valid.
-    """
-    def post(self, request, *args, **kwargs):
-        # Intercept the specific submit value before validation the form so `MailEditorMixin`
-        # can use this data.
-        if self.send_mail is None:
-            # Communicate with the `MailEditorMixin` whether the mails should go out or not.
-            self.send_mail = request.user.has_perm('scipost.can_manage_registration_invitations')
-            self.has_permission_to_send_mail = self.send_mail
-
-        if isinstance(self.object, RegistrationInvitation):
-            if self.object.invitation_type == INVITATION_EDITORIAL_FELLOW:
-                self.alternative_from_address = ('J-S Caux', 'jscaux@scipost.org')
-        return super().post(request, *args, **kwargs)
-
-
-class SaveAndSendFormMixin(BaseFormViewMixin):
-    """
-    Use the Save or Save and Send option to send the mail out after form is valid.
-    """
-    def post(self, request, *args, **kwargs):
-        # Intercept the specific submit value before validation the form so `MailEditorMixin`
-        # can use this data.
-        if self.send_mail is None:
-            self.send_mail = request.POST.get('save', '') in ['save_and_send', 'send_from_editor']
-            if self.send_mail:
-                self.send_mail = request.user.has_perm('scipost.can_manage_registration_invitations')
-
-        # Communicate with the `MailEditorMixin` whether the mails should go out or not.
-        self.has_permission_to_send_mail = self.send_mail
-        instance = self.get_object()
-        if isinstance(instance, RegistrationInvitation):
-            if instance.invitation_type == INVITATION_EDITORIAL_FELLOW:
-                self.alternative_from_address = ('J-S Caux', 'jscaux@scipost.org')
-            if not instance.invited_by and self.has_permission_to_send_mail:
-                instance.invited_by = self.request.user
-                instance.save()
-        return super().post(request, *args, **kwargs)
diff --git a/invitations/templates/invitations/registrationinvitation_list.html b/invitations/templates/invitations/registrationinvitation_list.html
index 0e01f3655d8839217d9e212e73521d2b7a5b38a4..37668fe39fefc08a1ecf5bd35dc6c2b0c41f605a 100644
--- a/invitations/templates/invitations/registrationinvitation_list.html
+++ b/invitations/templates/invitations/registrationinvitation_list.html
@@ -22,7 +22,7 @@
             {% endif %}
             {% if perms.scipost.can_manage_registration_invitations %}
                 <li><a href="{% url 'invitations:cleanup' %}">Perform a cleanup</a></li>
-                <li><a href="{% url 'invitations:citation_notification_list' %}">List unprocessed Citation Notifications</a></li>
+                <li><a href="{% url 'invitations:citation_notification_list' %}">List unprocessed Citation Notifications ({{ count_unprocessed }})<a/></li>
                 <li><a href="{% url 'invitations:list_contributors' %}">List draft Contributor Invitations (to be sent)</a></li>
             {% endif %}
             {% if perms.scipost.can_invite_fellows %}
diff --git a/invitations/views.py b/invitations/views.py
index efcd198b220afbdb4bd9b1a1180e5823af2430aa..907693ec070d97f55e2d46c1c89f079456ad7d81 100644
--- a/invitations/views.py
+++ b/invitations/views.py
@@ -10,17 +10,18 @@ from django.urls import reverse_lazy, reverse
 from django.views.generic.list import ListView
 from django.views.generic.edit import UpdateView, DeleteView
 
+from .constants import INVITATION_EDITORIAL_FELLOW
 from .forms import RegistrationInvitationForm, RegistrationInvitationReminderForm,\
     RegistrationInvitationMarkForm, RegistrationInvitationMapToContributorForm,\
     CitationNotificationForm, SuggestionSearchForm, RegistrationInvitationFilterForm,\
     CitationNotificationProcessForm, RegistrationInvitationAddCitationForm,\
     RegistrationInvitationMergeForm
-from .mixins import RequestArgumentMixin, SaveAndSendFormMixin, SendMailFormMixin
+from .mixins import RequestArgumentMixin
 from .models import RegistrationInvitation, CitationNotification
 
 from scipost.models import Contributor
 from scipost.mixins import PaginationMixin, PermissionsMixin
-from mails.views import MailEditorMixin
+from mails.views import MailFormView
 
 
 class RegistrationInvitationsView(PaginationMixin, PermissionsMixin, ListView):
@@ -40,6 +41,7 @@ class RegistrationInvitationsView(PaginationMixin, PermissionsMixin, ListView):
         context = super().get_context_data(**kwargs)
         context['count_in_draft'] = RegistrationInvitation.objects.drafts().count()
         context['count_pending'] = RegistrationInvitation.objects.sent().count()
+        context['count_unprocessed'] = CitationNotification.objects.unprocessed().count()
         context['search_form'] = self.search_form
         return context
 
@@ -72,24 +74,29 @@ class CitationNotificationsView(PermissionsMixin, ListView):
         'invitation', 'contributor', 'contributor__user')
 
 
-class CitationNotificationsProcessView(PermissionsMixin, RequestArgumentMixin,
-                                       MailEditorMixin, UpdateView):
+class CitationNotificationsProcessView(PermissionsMixin, RequestArgumentMixin, MailFormView):
     permission_required = 'scipost.can_manage_registration_invitations'
     form_class = CitationNotificationProcessForm
     queryset = CitationNotification.objects.unprocessed()
     success_url = reverse_lazy('invitations:citation_notification_list')
     mail_code = 'citation_notification'
 
+    def can_send_mail(self):
+        """
+        Only send mail if Contributor has not opted-out.
+        """
+        citation = self.get_form().get_all_notifications().filter(contributor__isnull=False).first()
+        if not citation.contributor:
+            return True
+        return citation.contributor.accepts_SciPost_emails
+
     @transaction.atomic
     def form_valid(self, form):
         """
-        Form is valid; use the MailEditorMixin to send out the mail if
+        Form is valid; the MailFormView will send the mail if
         (possible) Contributor didn't opt-out from mails.
         """
-        citation = form.get_all_notifications().filter(contributor__isnull=False).first()
-        contributor = citation.contributor
         form.get_all_notifications().update(processed=True)
-        self.send_mail = (contributor and contributor.accepts_SciPost_emails) or not contributor
         return super().form_valid(form)
 
 
@@ -148,8 +155,7 @@ def create_registration_invitation_or_citation(request):
     return render(request, 'invitations/registrationinvitation_form_add_new.html', context)
 
 
-class RegistrationInvitationsUpdateView(RequestArgumentMixin, PermissionsMixin,
-                                        SaveAndSendFormMixin, MailEditorMixin, UpdateView):
+class RegistrationInvitationsUpdateView(RequestArgumentMixin, PermissionsMixin, MailFormView):
     permission_required = 'scipost.can_create_registration_invitations'
     form_class = RegistrationInvitationForm
     mail_code = 'registration_invitation'
@@ -172,6 +178,18 @@ class RegistrationInvitationsUpdateView(RequestArgumentMixin, PermissionsMixin,
             qs = qs.created_by(self.request.user)
         return qs
 
+    def can_send_mail(self):
+        return self.request.user.has_perm('scipost.can_manage_registration_invitations')
+
+    def get_mail_config(self):
+        config = super().get_mail_config()
+        print('Config', config)
+        print(self.object)
+        if self.object.invitation_type == INVITATION_EDITORIAL_FELLOW:
+            config['from_email'] = 'jscaux@scipost.org'
+            config['from_name'] = 'J-S Caux'
+        return config
+
 
 class RegistrationInvitationsMergeView(RequestArgumentMixin, PermissionsMixin, UpdateView):
     permission_required = 'scipost.can_manage_registration_invitations'
@@ -206,8 +224,7 @@ class RegistrationInvitationsMapToContributorView(RequestArgumentMixin, Permissi
     success_url = reverse_lazy('invitations:list')
 
 
-class RegistrationInvitationsReminderView(RequestArgumentMixin, PermissionsMixin,
-                                          SendMailFormMixin, MailEditorMixin, UpdateView):
+class RegistrationInvitationsReminderView(RequestArgumentMixin, PermissionsMixin, MailFormView):
     permission_required = 'scipost.can_manage_registration_invitations'
     queryset = RegistrationInvitation.objects.sent()
     success_url = reverse_lazy('invitations:list')
@@ -215,6 +232,13 @@ class RegistrationInvitationsReminderView(RequestArgumentMixin, PermissionsMixin
     template_name = 'invitations/registrationinvitation_reminder_form.html'
     mail_code = 'registration_invitation_reminder'
 
+    def get_mail_config(self):
+        config = super().get_mail_config()
+        if self.object.invitation_type == INVITATION_EDITORIAL_FELLOW:
+            config['from_email'] = 'jscaux@scipost.org'
+            config['from_name'] = 'J-S Caux'
+        return config
+
 
 class RegistrationInvitationsDeleteView(PermissionsMixin, DeleteView):
     permission_required = 'scipost.can_manage_registration_invitations'
diff --git a/journals/views.py b/journals/views.py
index 4d54c502bfe8a41e9cf2df064148393af428c879..75888a824c81595f6b250da777c8806b9ae782c1 100644
--- a/journals/views.py
+++ b/journals/views.py
@@ -45,7 +45,6 @@ from .utils import JournalUtils
 from comments.models import Comment
 from funders.forms import FunderSelectForm, GrantSelectForm
 from funders.models import Grant
-from mails.views import MailEditingSubView
 from ontology.models import Topic
 from ontology.forms import SelectTopicForm
 from organizations.models import Organization
@@ -853,7 +852,7 @@ def request_pubfrac_check(request, doi_label):
     been confirmed.
     """
     publication = get_object_or_404(Publication, doi_label=doi_label)
-    mail_request = MailEditingSubView(
+    mail_request =  MailEditorSubview(
         request, mail_code='authors/request_pubfrac_check', instance=publication)
     if mail_request.is_valid():
         messages.success(request, 'The corresponding author has been emailed.')
diff --git a/mails/admin.py b/mails/admin.py
index c351e617f114fc2b5be48d03a4d0d5cb6540a5d9..29efe75097de851260434edd21a6cf1f11d7580f 100644
--- a/mails/admin.py
+++ b/mails/admin.py
@@ -4,13 +4,18 @@ __license__ = "AGPL v3"
 
 from django.contrib import admin
 
-from .models import MailLog
+from .models import MailLog, MailLogRelation
+
+
+class MailLogRelationInline(admin.TabularInline):
+    model = MailLogRelation
 
 
 class MailLogAdmin(admin.ModelAdmin):
     list_display = ['__str__', 'to_recipients', 'created', 'status']
     list_filter = ['status']
     readonly_fields = ('created', 'latest_activity')
+    inlines = (MailLogRelationInline,)
 
 
 admin.site.register(MailLog, MailLogAdmin)
diff --git a/mails/backends/filebased.py b/mails/backends/filebased.py
index 3e1fad5a718eda2b05a22d9487656bcdcea4b181..b9ccf5e2f3821e891cce3ec5a6100b82ff668471 100644
--- a/mails/backends/filebased.py
+++ b/mails/backends/filebased.py
@@ -1,8 +1,9 @@
 from django.conf import settings
 from django.core.mail.backends.filebased import EmailBackend as FileBackend
 from django.core.mail.message import sanitize_address
+from django.db import models
 
-from ..models import MailLog
+from ..models import MailLog, MailLogRelation
 
 
 class EmailBackend(FileBackend):
@@ -49,17 +50,16 @@ class ModelEmailBackend(FileBackend):
         except AttributeError:
             pass
 
-        content_object = None
         mail_code = ''
-        if 'delayed_processing' in email_message.extra_headers \
-           and email_message.extra_headers['delayed_processing']:
+        if email_message.extra_headers.get('delayed_processing', False):
             status = 'not_rendered'
-            content_object = email_message.extra_headers.get('content_object', None)
+            context = email_message.extra_headers.get('context', {})
             mail_code = email_message.extra_headers.get('mail_code', '')
         else:
             status = 'rendered'
+            context = {}
 
-        MailLog.objects.create(
+        mail_log = MailLog.objects.create(
             body=body,
             subject=subject,
             body_html=body_html,
@@ -67,6 +67,16 @@ class ModelEmailBackend(FileBackend):
             bcc_recipients=bcc_recipients,
             from_email=from_email,
             status=status,
-            content_object=content_object,
             mail_code=mail_code)
+
+        for key, var in context.items():
+            if isinstance(var, models.Model):
+                context_object = var
+                value = ''
+            else:
+                context_object = None
+                value = str(var)
+            rel = MailLogRelation.objects.create(
+                mail=mail_log, name=key, value=value, content_object=context_object)
+
         return True
diff --git a/mails/core.py b/mails/core.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc9154903b91b6462d0d75617959d25a4236e89c
--- /dev/null
+++ b/mails/core.py
@@ -0,0 +1,223 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+from html2text import HTML2Text
+import json
+import re
+import inspect
+
+from django.conf import settings
+from django.core.mail import EmailMultiAlternatives
+from django.db import models
+from django.template.loader import get_template
+
+from .exceptions import ConfigurationError
+
+
+class MailEngine:
+    """
+    This engine processes the configuration and template files to be saved into the database in
+    the MailLog table.
+    """
+
+    _required_parameters = ['recipient_list', 'subject', 'from_email']
+    _possible_parameters = ['recipient_list', 'subject', 'from_email', 'from_name', 'bcc']
+    _email_fields = ['recipient_list', 'from_email', 'bcc']
+    _processed_template = False
+    _mail_sent = False
+
+    def __init__(self, mail_code, subject='', recipient_list=[], bcc=[], from_email='',
+            from_name='', **kwargs):
+        """
+        Start engine with specific mail_code. Any other keyword argument that is passed will
+        be used as a variable in the mail template.
+
+        @Arguments
+        -- mail_code (str)
+
+        @Keyword arguments
+        The following arguments overwrite the default values, set in the configuration files:
+        -- subject (str, optional)
+        -- recipient_list (str, optional): List of email addresses or db-relations.
+        -- bcc (str, optional): List of email addresses or db-relations.
+        -- from_email (str, optional): Plain email address.
+        -- from_name (str, optional): Display name for from address.
+        """
+        self.mail_code = mail_code
+        self.extra_config = {
+            'bcc': bcc,
+            'subject': subject,
+            'from_name': from_name,
+            'from_email': from_email,
+            'recipient_list': recipient_list,
+        }
+        self.template_variables = kwargs
+
+    def __repr__(self):
+        return '<%(cls)s code="%(code)s", validated=%(validated)s sent=%(sent)s>' % {
+            'cls': self.__class__.__name__,
+            'code': self.mail_code,
+            'validated': hasattr(self, 'mail_data'),
+            'sent': self._mail_sent,
+        }
+
+    def validate(self, render_template=False):
+        """Check if MailEngine is valid and ready for sending."""
+        self._read_configuration_file()
+        self._detect_and_save_object()
+        self._check_template_exists()
+        self._validate_configuration()
+        if render_template:
+            self.render_template()
+
+    def render_template(self, html_message=None):
+        """
+        Render the template associated with the mail_code. If html_message is given,
+        use this as a template instead.
+        """
+        if html_message:
+            self.mail_data['html_message'] = html_message
+        else:
+            mail_template = get_template('email/%s.html' % self.mail_code)
+            self.mail_data['html_message'] = mail_template.render(self.template_variables)  # Damn slow.
+
+        # Transform to non-HTML version.
+        handler = HTML2Text()
+        self.mail_data['message'] = handler.handle(self.mail_data['html_message'])
+        self._processed_template = True
+
+    def send_mail(self):
+        """Send the mail."""
+        if self._mail_sent:
+            # Prevent double sending when using a Django form.
+            return
+        elif not hasattr(self, 'mail_data'):
+            raise ValueError(
+                "The mail: %s could not be sent because the data didn't validate." % self.mail_code)
+        email = EmailMultiAlternatives(
+            self.mail_data['subject'],
+            self.mail_data.get('message', ''),
+            '%s <%s>' % (
+                self.mail_data.get('from_name', 'SciPost'),
+                self.mail_data.get('from_email', 'noreply@scipost.org')),
+            self.mail_data['recipient_list'],
+            bcc=self.mail_data['bcc'],
+            reply_to=[
+                self.mail_data.get('from_email', 'noreply@scipost.org')
+            ],
+            headers={
+                'delayed_processing': not self._processed_template,
+                'context': self.template_variables,
+                'mail_code': self.mail_code,
+            })
+
+        # Send html version if available
+        if 'html_message' in self.mail_data:
+            email.attach_alternative(self.mail_data['html_message'], 'text/html')
+
+        email.send(fail_silently=False)
+        self._mail_sent = True
+
+        if self.template_variables['object'] and hasattr(self.template_variables['object'], 'mail_sent'):
+            self.template_variables['object'].mail_sent()
+
+    def _detect_and_save_object(self):
+        """
+        Detect if less than or equal to one object exists and save it, else raise exception.
+        Stick to Django's convention of saving it as a central `object` variable.
+        """
+        object = None
+        context_object_name = None
+
+        if 'object' in self.template_variables:
+            object = self.template_variables['object']
+            context_object_name = self.template_variables['object']._meta.model_name
+        elif 'instance' in self.template_variables:
+            object = self.template_variables['instance']
+            context_object_name = self.template_variables['instance']._meta.model_name
+        else:
+            for key, var in self.template_variables.items():
+                if isinstance(var, models.Model):
+                    if object:
+                        raise ValueError('Multiple db instances are given. Please specify which object to use.')
+                    else:
+                        object = var
+        self.template_variables['object'] = object
+
+        if context_object_name and object and context_object_name not in self.template_variables:
+            self.template_variables[context_object_name] = object
+
+
+    def _read_configuration_file(self):
+        """Retrieve default configuration for specific mail_code."""
+        json_location = '%s/templates/email/%s.json' % (settings.BASE_DIR, self.mail_code)
+
+        try:
+            self.mail_data = json.loads(open(json_location).read())
+        except OSError:
+            raise ImportError('No configuration file found. Mail code: %s' % self.mail_code)
+
+        # Check if configuration file is valid.
+        if 'subject' not in self.mail_data:
+            raise ConfigurationError('key "subject" is missing.')
+        if 'recipient_list' not in self.mail_data:
+            raise ConfigurationError('key "recipient_list" is missing.')
+
+        # Overwrite mail data if parameters are given.
+        for key, val in self.extra_config.items():
+            if val or key not in self.mail_data:
+                self.mail_data[key] = val
+
+    def _check_template_exists(self):
+        """Save template or raise TemplateDoesNotExist."""
+        self._template = get_template('email/%s.html' % self.mail_code)
+
+    def _validate_configuration(self):
+        """Check if all required data is given via either configuration or extra parameters."""
+
+        # Check data is complete
+        if not all(key in self.mail_data for key in self._required_parameters):
+            txt = 'Not all required parameters are given in the configuration file or on instantiation.'
+            txt += ' Check required parameters: {}'.format(self._required_parameters)
+            raise ConfigurationError(txt)
+
+        # Check all configuration value types
+        for email_key in ['subject', 'from_email', 'from_name']:
+            if email_key in self.mail_data and self.mail_data[email_key]:
+                if not isinstance(self.mail_data[email_key], str):
+                    raise ConfigurationError('"%(key)s" argument must be a string' % {
+                        'key': email_key,
+                    })
+        for email_key in ['recipient_list', 'bcc']:
+            if email_key in self.mail_data and self.mail_data[email_key]:
+                if not isinstance(self.mail_data[email_key], list):
+                    raise ConfigurationError('"%(key)s" argument must be a list' % {
+                        'key': email_key,
+                    })
+
+        # Validate all email addresses
+        for email_key in self._email_fields:
+            if email_key in self.mail_data:
+                if isinstance(self.mail_data[email_key], list):
+                    for i, email in enumerate(self.mail_data[email_key]):
+                        self.mail_data[email_key][i] = self._validate_email_addresses(email)
+                else:
+                    self.mail_data[email_key] = self._validate_email_addresses(self.mail_data[email_key])
+
+    def _validate_email_addresses(self, entry):
+        """Return email address given raw email or database relation given in `entry`."""
+        if re.match("[^@]+@[^@]+\.[^@]+", entry):
+            # Email string
+            return entry
+        elif self.template_variables['object']:
+            mail_to = self.template_variables['object']
+            for attr in entry.split('.'):
+                try:
+                    mail_to = getattr(mail_to, attr)
+                    if inspect.ismethod(mail_to):
+                        mail_to = mail_to()
+                except AttributeError:
+                    # Invalid property/mail
+                    raise KeyError('The property (%s) does not exist.' % entry)
+            return mail_to
+        raise KeyError('Neither an email adress nor db instance is given.')
diff --git a/mails/exceptions.py b/mails/exceptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a765a71ac5ba34a6f3aa127adbbc1527f4c9a89
--- /dev/null
+++ b/mails/exceptions.py
@@ -0,0 +1,10 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+class ConfigurationError(Exception):
+    def __init__(self, name):
+        self.name = name
+
+    def __str__(self):
+        return 'Configuration error: {}'.format(self.name)
diff --git a/mails/factories.py b/mails/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6ef3aa050aca7c4ddee32cb4eed7ea240ec9940
--- /dev/null
+++ b/mails/factories.py
@@ -0,0 +1,34 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+import factory
+# import pytz
+# import random
+
+from .models import MailLog, MAIL_NOT_RENDERED, MAIL_RENDERED
+
+# from faker import Faker
+
+
+class MailLogFactory(factory.django.DjangoModelFactory):
+    processed = False
+    status = MAIL_NOT_RENDERED
+    body = ''
+    body_html = ''
+
+    from_email = factory.Faker('ascii_safe_email')
+    mail_code = factory.Faker('slug')
+    subject = factory.Faker('word')
+    to_recipients = factory.List([factory.Faker('ascii_safe_email') for _ in range(2)])
+    bcc_recipients = factory.List([factory.Faker('ascii_safe_email') for _ in range(2)])
+
+    class Meta:
+        model = MailLog
+
+
+class RenderedMailLogFactory(MailLogFactory):
+    processed = True
+    status = MAIL_RENDERED
+    body = factory.Faker('text')
+    body_html = factory.Faker('text')
diff --git a/mails/forms.py b/mails/forms.py
index 472ae255b8594ec19449317df834a35ce395e4e8..870918b178e5bbe9e8f8ab30ec0be4185b61ea88 100644
--- a/mails/forms.py
+++ b/mails/forms.py
@@ -4,78 +4,96 @@ __license__ = "AGPL v3"
 
 from django import forms
 
-from .mixins import MailUtilsMixin
+from .core import MailEngine
+from .exceptions import ConfigurationError
 from .widgets import SummernoteEditor
 
 
-class EmailTemplateForm(forms.Form, MailUtilsMixin):
-    subject = forms.CharField(max_length=250, label="Subject*")
+class EmailForm(forms.Form):
+    """
+    This form is prefilled with data from a mail_code and is used by any user to send out
+    the mail after editing.
+    """
+
+    subject = forms.CharField(max_length=255, label="Subject*")
     text = forms.CharField(widget=SummernoteEditor, label="Text*")
-    extra_recipient = forms.EmailField(label="Optional: bcc this email to", required=False)
+    mail_field = forms.EmailField(label="Optional: bcc this email to", required=False)
     prefix = 'mail_form'
+    extra_config = {}
 
     def __init__(self, *args, **kwargs):
-        self.pre_validation(*args, **kwargs)
+        self.mail_code = kwargs.pop('mail_code')
+
+        # Check if all exta configurations are valid.
+        self.extra_config.update(kwargs.pop('mail_config', {}))
+
+        if not all(key in MailEngine._possible_parameters for key, val in self.extra_config.items()):
+            raise KeyError('Not all `extra_config` parameters are accepted.')
 
         # This form shouldn't be is_bound==True is there is any non-relavant POST data given.
-        data = args[0] if args else {}
-        if not data:
+        if len(args) > 0 and args[0]:
+            data = args[0]
+        elif 'data' in kwargs:
+            data = kwargs.pop('data')
+        else:
             data = {}
         if '%s-subject' % self.prefix in data.keys():
             data = {
                 '%s-subject' % self.prefix: data.get('%s-subject' % self.prefix),
                 '%s-text' % self.prefix: data.get('%s-text' % self.prefix),
-                '%s-extra_recipient' % self.prefix: data.get('%s-extra_recipient' % self.prefix),
-            }
-        elif kwargs.get('data', False):
-            data = {
-                '%s-subject' % self.prefix: kwargs['data'].get('%s-subject' % self.prefix),
-                '%s-text' % self.prefix: kwargs['data'].get('%s-text' % self.prefix),
-                '%s-extra_recipient' % self.prefix: kwargs['data'].get('%s-extra_recipient' % self.prefix),
+                '%s-mail_field' % self.prefix: data.get('%s-mail_field' % self.prefix),
             }
         else:
-            data = None
+            # Reset to prevent having a false-bound form.
+            data = {}
         super().__init__(data or None)
 
-        if not self.original_recipient:
-            self.fields['extra_recipient'].label = "Send this email to"
-            self.fields['extra_recipient'].required = True
-
         # Set the data as initials
-        self.fields['text'].initial = self.mail_template
-        self.fields['subject'].initial = self.mail_data['subject']
-
-    def save_data(self):
-        # Get text and html
-        self.html_message = self.cleaned_data['text']
-        self.subject = self.cleaned_data['subject']
-        self.validate_message()
-        self.validate_bcc_list()
-
-        # Get recipients list. Try to send through BCC to prevent privacy issues!
-        if self.cleaned_data.get('extra_recipient') and self.original_recipient:
-            self.bcc_list.append(self.cleaned_data.get('extra_recipient'))
-        elif self.cleaned_data.get('extra_recipient') and not self.original_recipient:
-            self.original_recipient = [self.cleaned_data.get('extra_recipient')]
-        elif not self.original_recipient:
-            self.add_error('extra_recipient', 'Please fill the bcc field to send the mail.')
-
-        self.validate_recipients()
-        self.save_mail_data()
-
-    def clean(self):
-        data = super().clean()
-        self.save_data()
-        return data
+        self.engine = MailEngine(self.mail_code, **self.extra_config, **kwargs)
+        self.engine.validate(render_template=True)
+        self.fields['text'].initial = self.engine.mail_data['html_message']
+        self.fields['subject'].initial = self.engine.mail_data['subject']
+
+    def is_valid(self):
+        """Fallback used in CBVs."""
+        if super().is_valid():
+            try:
+                self.engine.validate(render_template=False)
+                return True
+            except (ImportError, KeyError, ConfigurationError):
+                return False
+        return False
 
     def save(self):
-        """Because Django uses .save() by default..."""
-        self.send()
-        return self.instance
+        self.engine.render_template(self.cleaned_data['text'])
+        self.engine.mail_data['subject'] = self.cleaned_data['subject']
+        if self.cleaned_data['mail_field']:
+            self.engine.mail_data['bcc'].append(self.cleaned_data['mail_field'])
+        self.engine.send_mail()
+        return self.engine.template_variables['object']
+
 
+class FakeForm(forms.Form):
+    """
+    Fake form for testing purposes.
+    """
+
+    field1 = forms.CharField(label='Field 1')
+
+    def save(self):
+        """Dummy method."""
+        print('Save', self.cleaned_data)
+        return
 
 
 class HiddenDataForm(forms.Form):
+    """
+    Regular Django form which tranforms all fields to hidden fields.
+
+    BE AWARE: This form may only be used for non-sensitive data!
+        Any data that may not be interceptedby the used should NEVER be added to this form.
+    """
+
     def __init__(self, form, *args, **kwargs):
         super().__init__(form.data, *args, **kwargs)
         for name, field in form.fields.items():
diff --git a/mails/management/commands/send_mails.py b/mails/management/commands/send_mails.py
index a810d2fcd96d88ce9ee67246875f352c8acdb801..24332c8f3079d00be9fbab08d457c0421c52b24c 100644
--- a/mails/management/commands/send_mails.py
+++ b/mails/management/commands/send_mails.py
@@ -4,6 +4,7 @@ from django.conf import settings
 from ...models import MailLog
 from ...utils import DirectMailUtil
 
+
 class Command(BaseCommand):
     """
     This sends the mails that are not processed, written to the database.
@@ -17,13 +18,11 @@ class Command(BaseCommand):
         """
         Render the templates for the mail if not done yet.
         """
-        mail_util = DirectMailUtil(
-            mail_code=mail.mail_code,
-            instance=mail.content_object)  # This will process the mail, but: not send yet!
+        mail_util = DirectMailUtil(mail.mail_code, delayed_processing=False, **mail.get_full_context())
 
         MailLog.objects.filter(id=mail.id).update(
-            body=mail_util.mail_data['message'],
-            body_html=mail_util.mail_data['html_message'],
+            body=mail_util.engine.mail_data['message'],
+            body_html=mail_util.engine.mail_data['html_message'],
             status='rendered')
 
     def send_mails(self, mails):
diff --git a/mails/migrations/0006_auto_20190220_1633.py b/mails/migrations/0006_auto_20190220_1633.py
new file mode 100644
index 0000000000000000000000000000000000000000..b2e48d65ea43f8fa69ecf7b7d060698c2eca5bc1
--- /dev/null
+++ b/mails/migrations/0006_auto_20190220_1633.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-02-20 15:33
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('mails', '0005_auto_20181217_1051'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='MailLogRelation',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=254)),
+                ('value', models.CharField(blank=True, max_length=254)),
+                ('object_id', models.PositiveIntegerField(blank=True, null=True)),
+                ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+            ],
+        ),
+        migrations.RemoveField(
+            model_name='maillog',
+            name='content_type',
+        ),
+        migrations.RemoveField(
+            model_name='maillog',
+            name='object_id',
+        ),
+        migrations.AddField(
+            model_name='maillogrelation',
+            name='mail',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='context', to='mails.MailLog'),
+        ),
+    ]
diff --git a/mails/mixins.py b/mails/mixins.py
index 902707b1adeff763992068f2d24b35f2063c1e5f..268f3756fe97294cdf9398ad2eb414cd4bfbfff7 100644
--- a/mails/mixins.py
+++ b/mails/mixins.py
@@ -17,7 +17,11 @@ from scipost.models import Contributor
 
 
 class MailUtilsMixin:
-    """This mixin takes care of inserting the default data into the Utils or Form."""
+    """
+    This mixin takes care of inserting the default data into the Utils or Form.
+
+    DEPRECATED
+    """
 
     instance = None
     mail_data = {}
diff --git a/mails/models.py b/mails/models.py
index 5911812cdc316b0161168d09188eb74226a745a7..fbf417e66c879bd743c4f914bfb2772f4b3e7e9d 100644
--- a/mails/models.py
+++ b/mails/models.py
@@ -28,9 +28,6 @@ class MailLog(models.Model):
     status = models.CharField(max_length=16, choices=MAIL_STATUSES, default=MAIL_RENDERED)
 
     mail_code = models.CharField(max_length=254, blank=True)
-    content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE)
-    object_id = models.PositiveIntegerField(blank=True, null=True)
-    content_object = GenericForeignKey('content_type', 'object_id')
 
     body = models.TextField()
     body_html = models.TextField(blank=True)
@@ -55,3 +52,35 @@ class MailLog(models.Model):
             id=self.id,
             subject=self.subject[:30],
             count=len(self.to_recipients) + len(self.bcc_recipients))
+
+    def get_full_context(self):
+        """Get the full template context needed to render the template."""
+        if hasattr(self, '_context'):
+            return self._context
+        self._context = {}
+        for relation in self.context.all():
+            self._context[relation.name] = relation.get_item()
+        return self._context
+
+
+class MailLogRelation(models.Model):
+    """
+    A template context item for the MailLog in case the a mail has delayed rendering.
+    This may be plain text or any relation within the database.
+    """
+
+    mail = models.ForeignKey('mails.MailLog', on_delete=models.CASCADE, related_name='context')
+
+    name = models.CharField(max_length=254)
+    value = models.CharField(max_length=254, blank=True)
+
+    content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE)
+    object_id = models.PositiveIntegerField(blank=True, null=True)
+    content_object = GenericForeignKey('content_type', 'object_id')
+
+    def get_item(self):
+        if self.value:
+            return self.value
+        elif self.content_object:
+            return self.content_object
+        return None
diff --git a/mails/tests/__init__.py b/mails/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/mails/tests/test_mail_engine.py b/mails/tests/test_mail_engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..2cef79d7099eeb66cbb465f4d4281fa23b8af89f
--- /dev/null
+++ b/mails/tests/test_mail_engine.py
@@ -0,0 +1,113 @@
+from django.template.exceptions import TemplateDoesNotExist
+from django.test import TestCase
+
+from mails.core import MailEngine
+from mails.exceptions import ConfigurationError
+
+
+class MailLogModelTests(TestCase):
+    """
+    Test the MailEngine object.
+    """
+
+    def test_valid_instantiation(self):
+        """Test if init method of the engine works properly."""
+        # Test no mail_code given fails.
+        with self.assertRaises(TypeError):
+            MailEngine()
+
+        # Test only mail_code given works.
+        try:
+            MailEngine('tests/test_mail_code_1')
+        except:
+            # For whatever reason possible...
+            self.fail('MailEngine() raised unexpectedly!')
+
+        # Test all extra arguments are accepted.
+        try:
+            MailEngine(
+                'tests/test_mail_code_1',
+                subject='Test subject A',
+                recipient_list=['test_A@example.org', 'test_B@example.org'],
+                bcc=['test_C@example.com', 'test_D@example.com'],
+                from_email='test@example.org',
+                from_name='John Doe')
+        except KeyError:
+            self.fail('MailEngine() does not accept all keyword arguments!')
+
+        # Test if only proper arguments are accepted.
+        with self.assertRaises(ConfigurationError):
+            engine = MailEngine('tests/test_mail_code_1', recipient_list='test_A@example.org')
+            engine.validate()
+        with self.assertRaises(ConfigurationError):
+            engine = MailEngine('tests/test_mail_code_1', bcc='test_A@example.org')
+            engine.validate()
+        with self.assertRaises(ConfigurationError):
+            engine = MailEngine('tests/test_mail_code_1', from_email=['test_A@example.org'])
+            engine.validate()
+
+        # See if any other keyword argument is accepted and saved as template variable.
+        try:
+            engine = MailEngine(
+                'tests/test_mail_code_1',
+                fake='Test subject A',
+                extra=['test_A@example.org'])
+        except KeyError:
+            self.fail('MailEngine() does not accept extra keyword arguments!')
+
+        self.assertIs(engine.template_variables['fake'], 'Test subject A')
+        self.assertListEqual(engine.template_variables['extra'], ['test_A@example.org'])
+
+    def test_invalid_mail_code(self):
+        """Test if invalid configuration files are handled properly."""
+        with self.assertRaises(ImportError):
+            engine = MailEngine('tests/fake_mail_code_1')
+            engine.validate()
+        with self.assertRaises(ConfigurationError):
+            engine = MailEngine('tests/test_mail_code_fault_1')
+            engine.validate()
+        with self.assertRaises(TemplateDoesNotExist):
+            engine = MailEngine('tests/test_mail_code_no_template_1')
+            engine.validate()
+
+    def test_positive_validation_delayed_rendering(self):
+        """Test if validation works and rendering is delayed."""
+        engine = MailEngine('tests/test_mail_code_1')
+        engine.validate()  # Should validate without rendering
+        self.assertIn('subject', engine.mail_data)
+        self.assertIn('recipient_list', engine.mail_data)
+        self.assertIn('from_email', engine.mail_data)
+        self.assertNotIn('message', engine.mail_data)
+        self.assertNotIn('html_message', engine.mail_data)
+        self.assertEqual(engine.mail_data['subject'], 'SciPost Test')
+        self.assertIn('test@scipost.org', engine.mail_data['recipient_list'])
+        self.assertEqual(engine.mail_data['from_email'], 'admin@scipost.org')
+
+    def test_positive_direct_validation(self):
+        """Test if validation and rendering works as required."""
+        engine = MailEngine('tests/test_mail_code_1')
+        engine.validate(render_template=True)  # Should validate and render
+        self.assertIn('message', engine.mail_data)
+        self.assertIn('html_message', engine.mail_data)
+        self.assertNotEqual(engine.mail_data['message'], '')
+        self.assertNotEqual(engine.mail_data['html_message'], '')
+
+    def test_additional_parameters(self):
+        """Test if validation and rendering works as required if given extra parameters."""
+        engine = MailEngine(
+            'tests/test_mail_code_1',
+            subject='Test Subject 2',
+            recipient_list=['test1@scipost.org'],
+            bcc=['test2@scipost.org'],
+            from_email='test3@scipost.org',
+            from_name='Test Name',
+            weird_variable_name='John Doe')
+        engine.validate()
+        self.assertEqual(engine.mail_data['subject'], 'Test Subject 2')
+        self.assertIn('test1@scipost.org', engine.mail_data['recipient_list'])
+        self.assertIn('test2@scipost.org', engine.mail_data['bcc'])
+        self.assertEqual(engine.mail_data['from_email'], 'test3@scipost.org')
+        self.assertEqual(engine.mail_data['from_name'], 'Test Name')
+        self.assertNotIn('weird_variable_name', engine.mail_data)
+        self.assertIn('weird_variable_name', engine.template_variables)
+        self.assertEqual(engine.template_variables['weird_variable_name'], 'John Doe')
diff --git a/mails/tests/test_model_based_backend.py b/mails/tests/test_model_based_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..317e69c40c84a8e89cdeeb3c8d37cee55945a7cc
--- /dev/null
+++ b/mails/tests/test_model_based_backend.py
@@ -0,0 +1,90 @@
+from django.core.management import call_command
+from django.test import TestCase
+
+from mails.models import MailLog, MAIL_RENDERED, MAIL_NOT_RENDERED, MAIL_SENT
+from mails.utils import DirectMailUtil
+from submissions.factories import SubmissionFactory
+
+
+class ModelEmailBackendTests(TestCase):
+    """
+    Test the ModelEmailBackend object assuming the MailEngine and DirectMailUtil work properly.
+    """
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.submission = SubmissionFactory.create()
+
+    def test_non_rendered_database_entries(self):
+        """Test non rendered mail database entries are correct after sending email."""
+        with self.settings(EMAIL_BACKEND='mails.backends.filebased.ModelEmailBackend'):
+            mail_util = DirectMailUtil(
+                'tests/test_mail_code_1',
+                subject='Test Subject Unique For Testing 93872',
+                recipient_list=['test1@scipost.org'],
+                bcc=['test2@scipost.org'],
+                from_email='test3@scipost.org',
+                from_name='Test Name',
+                weird_variable_name='John Doe')
+            self.assertFalse(mail_util.engine._mail_sent)
+            mail_util.send_mail()
+            self.assertTrue(mail_util.engine._mail_sent)
+
+        mail_log = MailLog.objects.last()
+        self.assertFalse(mail_log.processed)
+        self.assertEqual(mail_log.status, MAIL_NOT_RENDERED)
+        self.assertEqual(mail_log.mail_code, 'tests/test_mail_code_1')
+        self.assertEqual(mail_log.subject, 'Test Subject Unique For Testing 93872')
+        self.assertEqual(mail_log.body, '')
+        self.assertEqual(mail_log.body_html, '')
+        self.assertIn('test1@scipost.org', mail_log.to_recipients)
+        self.assertIn('test2@scipost.org', mail_log.bcc_recipients)
+        self.assertEqual('Test Name <test3@scipost.org>', mail_log.from_email)
+
+    def test_rendered_database_entries(self):
+        """Test rendered mail database entries are correct after sending email."""
+        with self.settings(EMAIL_BACKEND='mails.backends.filebased.ModelEmailBackend'):
+            mail_util = DirectMailUtil(
+                'tests/test_mail_code_1',
+                delayed_processing=False,
+                subject='Test Subject Unique For Testing 786234')  # Use weird subject to confirm right instance.
+            mail_util.send_mail()
+
+        mail_log = MailLog.objects.last()
+        self.assertEqual(mail_log.status, MAIL_RENDERED)
+        self.assertEqual(mail_log.subject, 'Test Subject Unique For Testing 786234')
+        self.assertNotEqual(mail_log.body, '')
+        self.assertNotEqual(mail_log.body_html, '')
+
+    def test_context_saved_to_database(self):
+        """Test mail database entries have relations with their context items."""
+        with self.settings(EMAIL_BACKEND='mails.backends.filebased.ModelEmailBackend'):
+            mail_util = DirectMailUtil(
+                'tests/test_mail_code_1',
+                subject='Test Subject Unique For Testing 786234',
+                weird_variable_name='TestValue1',
+                random_submission_relation=self.submission)
+            mail_util.send_mail()
+
+        mail_log = MailLog.objects.last()
+        context = mail_log.get_full_context()
+        self.assertEqual(mail_log.status, MAIL_NOT_RENDERED)
+        self.assertEqual(mail_log.subject, 'Test Subject Unique For Testing 786234')
+        self.assertIn('random_submission_relation', context)
+        self.assertEqual(context['random_submission_relation'], self.submission)
+        self.assertIn('weird_variable_name', context)
+        self.assertEqual(context['weird_variable_name'], 'TestValue1')
+
+    def test_management_command(self):
+        """Test if management command does the updating of the mail."""
+        with self.settings(EMAIL_BACKEND='mails.backends.filebased.ModelEmailBackend'):
+            mail_util = DirectMailUtil('tests/test_mail_code_1', object=self.submission)
+            mail_util.send_mail()
+
+        mail_log = MailLog.objects.last()
+        call_command('send_mails', id=mail_log.id)
+
+        mail_log.refresh_from_db()
+        self.assertNotEqual(mail_log.body, '')
+        self.assertNotEqual(mail_log.body_html, '')
+        self.assertEqual(mail_log.status, MAIL_SENT)
diff --git a/mails/tests/test_views.py b/mails/tests/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..faa191753be7d6bd0e650e601b64652c59782c75
--- /dev/null
+++ b/mails/tests/test_views.py
@@ -0,0 +1,42 @@
+from django.test import TestCase
+
+# from mails.models import MailLog, MAIL_RENDERED, MAIL_NOT_RENDERED, MAIL_SENT
+# from mails.utils import DirectMailUtil
+from mails.views import MailView, MailEditorSubview
+# from submissions.factories import SubmissionFactory
+
+
+class MailDetailViewTest(TestCase):
+    """
+    Test the mails.views.MailView CBV.
+    """
+
+    # @classmethod
+    # def setUpTestData(cls):
+    #     cls.submission = SubmissionFactory.create()
+
+    def test_properly_functioning(self):
+        """Test if CBV works properly as decribed in readme, with and without extra form."""
+        pass
+
+    def test_fails_properly(self):
+        """Test if CBV fails gently if not used properly."""
+        pass
+
+
+class MailEditorSubviewTest(TestCase):
+    """
+    Test the mails.views.MailEditorSubview FBV.
+    """
+
+    # @classmethod
+    # def setUpTestData(cls):
+    #     cls.submission = SubmissionFactory.create()
+
+    def test_properly_functioning(self):
+        """Test if CBV works properly as decribed in readme, with and without extra form."""
+        pass
+
+    def test_fails_properly(self):
+        """Test if CBV fails gently if not used properly."""
+        pass
diff --git a/mails/urls.py b/mails/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..d79388013a145bf2b241dcab055143124375f2ff
--- /dev/null
+++ b/mails/urls.py
@@ -0,0 +1,12 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+    url(r'^test/(?P<pk>\d+)/$', views.TestView.as_view(), name='test'),
+    url(r'^test/(?P<pk>\d+)/edit$', views.TestUpdateView.as_view(), name='test_edit'),
+]
diff --git a/mails/utils.py b/mails/utils.py
index a1c49158e249ddb54c527e654e187c312b56d3a3..6fa1ad19a89ba9a9fb0946152455960467b0b700 100644
--- a/mails/utils.py
+++ b/mails/utils.py
@@ -2,18 +2,16 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
 
-from .mixins import MailUtilsMixin
+from .core import MailEngine
 
 
-class DirectMailUtil(MailUtilsMixin):
-    """
-    Same templates and json files as the form EmailTemplateForm, but this will directly send
-    the mails out, without intercepting and showing the mail editor to the user.
-    """
+class DirectMailUtil:
+    """Send a templated email directly; easiest possible way."""
 
-    def __init__(self, mail_code, *args, **kwargs):
-        kwargs['mail_code'] = mail_code
-        kwargs['instance'] = kwargs.pop('instance', None)
-        self.delayed_processing = kwargs.pop('delayed_processing', False)
-        super().__init__(*args, **kwargs)
-        self.validate()
+    def __init__(self, mail_code, delayed_processing=True, **kwargs):
+        # Set the data as initials
+        self.engine = MailEngine(mail_code, **kwargs)
+        self.engine.validate(render_template=not delayed_processing)
+
+    def send_mail(self):
+        return self.engine.send_mail()
diff --git a/mails/views.py b/mails/views.py
index 9aa031d86ea309074213fc11f82097f2d64aefc0..acdbf57bbcdc184afcd795dc4ebd049129d9e32b 100644
--- a/mails/views.py
+++ b/mails/views.py
@@ -1,132 +1,216 @@
 __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
-
+from django import forms
+from django.core.exceptions import ImproperlyConfigured, PermissionDenied
 from django.contrib import messages
+from django.http import HttpResponseRedirect
 from django.shortcuts import render
+from django.utils.encoding import force_text
 from django.views.generic.edit import UpdateView
 
-from .forms import EmailTemplateForm, HiddenDataForm
-
-
-class MailEditingSubView(object):
-    alternative_from_address = None  # Tuple: ('from_name', 'from_address')
-
-    def __init__(self, request, mail_code, **kwargs):
-        self.request = request
-        self.context = kwargs.get('context', {})
-        self.template_name = kwargs.get('template', 'mails/mail_form.html')
-        self.header_template = kwargs.get('header_template', '')
-        self.mail_form = EmailTemplateForm(request.POST or None, mail_code=mail_code, **kwargs)
+from submissions.models import Submission
+from .forms import EmailForm, HiddenDataForm, FakeForm
 
-    @property
-    def recipients_string(self):
-        return ', '.join(getattr(self.mail_form, 'mail_data', {}).get('recipients', ['']))
 
-    def add_form(self, form):
-        self.context['transfer_data_form'] = HiddenDataForm(form)
+class MailViewBase:
+    """Send a templated email after being edited by user."""
 
-    def set_alternative_sender(self, from_name, from_address):
-        self.alternative_from_address = (from_name, from_address)
+    form_class = None
+    mail_code = None
+    mail_config = {}
+    mail_variables = {}
+    fail_silently = True
 
-    def is_valid(self):
-        return self.mail_form.is_valid()
+    def __init__(self, *args, **kwargs):
+        if not self.mail_code:
+            raise AttributeError(self.__class__.__name__ + ' object has no attribute `mail_code`')
+        super().__init__(*args, **kwargs)
+        self.mail_form = None
 
-    def send(self):
-        if self.alternative_from_address:
-            self.mail_form.set_alternative_sender(
-                self.alternative_from_address[0], self.alternative_from_address[1])
-        return self.mail_form.send()
+    def can_send_mail(self):
+        """Overwrite method to control permissions for sending mails."""
+        return True
 
-    def return_render(self):
-        self.context['form'] = self.mail_form
-        self.context['header_template'] = self.header_template
-        if hasattr(self.mail_form, 'instance') and self.mail_form.instance:
-            self.context['object'] = self.mail_form.instance
-        else:
-            self.context['object'] = None
-        return render(self.request, self.template_name, self.context)
+    def get_mail_config(self):
+        return self.mail_config
 
 
-class MailEditorMixin:
+class MailFormView(MailViewBase, UpdateView):
     """
-    Use MailEditorMixin in edit CBVs to automatically implement the mail editor as
-    a post-form_valid hook.
-
-    The view must specify the `mail_code` variable.
+    MailUpdateView acts as a base class-based form view, but will intercept the POST request
+    of the original form. It'll render the email edit form and save/send both after validation.
     """
-    object = None
-    mail_form = None
-    has_permission_to_send_mail = True
-    alternative_from_address = None  # Tuple: ('from_name', 'from_address')
-
-    def __init__(self, *args, **kwargs):
-        if not self.mail_code:
-            raise AttributeError(self.__class__.__name__ + ' object has no attribute `mail_code`')
-        super().__init__(*args, **kwargs)
 
     def get_template_names(self):
-        """
-        The mail editor form has its own template.
-        """
+        """The mail editor form has its own template."""
         if self.mail_form and not self.mail_form.is_valid():
             return ['mails/mail_form.html']
         return super().get_template_names()
 
+    # def get_form_kwargs(self):
+    #     kwargs = {
+    #         'initial': self.get_initial(),
+    #         'prefix': self.get_prefix(),
+    #     }
+    #
+    #     if self.request.method in ('POST', 'PUT'):
+    #         kwargs.update({
+    #             'data': self.request.POST,
+    #             'files': self.request.FILES,
+    #         })
+    #
+    #     if isinstance(self.form_class, forms.ModelForm) and hasattr(self, 'object'):
+    #         kwargs.update({'instance': self.object})
+    #     return kwargs
+
     def post(self, request, *args, **kwargs):
-        """
-        Handle POST requests, but interpect the data if the mail form data isn't valid.
-        """
-        if not self.has_permission_to_send_mail:
-            # Don't use the mail form; don't send out the mail.
-            return super().post(request, *args, **kwargs)
-        self.object = self.get_object()
+        """Save forms or intercept the request."""
+        self.object = None
+        if hasattr(self, 'get_object'):
+            self.object = self.get_object()
         form = self.get_form()
+
         if form.is_valid():
-            self.mail_form = EmailTemplateForm(request.POST or None, mail_code=self.mail_code,
-                                               instance=self.object)
+            self.mail_form = EmailForm(
+                request.POST or None, mail_code=self.mail_code,
+                instance=self.object, **self.get_mail_config(), **self.mail_variables)
             if self.mail_form.is_valid():
                 return self.form_valid(form)
 
             return self.render_to_response(
-                self.get_context_data(form=self.mail_form,
-                                      transfer_data_form=HiddenDataForm(form)))
+                self.get_context_data(
+                    form=self.mail_form, transfer_data_form=HiddenDataForm(form)))
         else:
             return self.form_invalid(form)
 
     def form_valid(self, form):
-        """
-        If both the regular form and mailing form are valid, save the form and run the mail form.
-        """
-        # Don't use the mail form; don't send out the mail.
-        if not self.has_permission_to_send_mail:
-            return super().form_valid(form)
-
-        if self.alternative_from_address:
-            # Set different from address if given.
-            self.mail_form.set_alternative_sender(
-                self.alternative_from_address[0], self.alternative_from_address[1])
-
+        """If both the regular form and mailing form are valid, save both."""
         response = super().form_valid(form)
         try:
-            self.mail_form.send()
+            if not self.can_send_mail():
+                if self.fail_silently:
+                    return response
+                else:
+                    raise PermissionDenied("You are not allowed to send mail: %s." % self.mail_code)
+            self.mail_form.save()
         except AttributeError:
             # self.mail_form is None
-            raise AttributeError('Did you check the order in which MailEditorMixin is used?')
+            raise AttributeError('Did you check the order in which %(cls)s inherits MailView?' % {
+                'cls': self.__class__.__name__,
+            })
         messages.success(self.request, 'Mail sent')
         return response
 
+    def get_success_url(self):
+        """
+        Returns the supplied URL.
+        """
+        if self.success_url:
+            if hasattr(self, 'object') and self.object:
+                url = self.success_url.format(**self.object.__dict__)
+            else:
+                url = force_text(self.success_url)
+        elif hasattr(self, 'object') and self.object:
+            try:
+                url = self.object.get_absolute_url()
+            except AttributeError:
+                raise ImproperlyConfigured(
+                    "No URL to redirect to.  Either provide a url or define"
+                    " a get_absolute_url method on the Model.")
+        else:
+            raise ImproperlyConfigured(
+                "No URL to redirect to. Provide a success_url.")
+        return url
+
 
-class MailView(UpdateView):
+class MailView(MailViewBase, UpdateView):
+    form_class = EmailForm
     template_name = 'mails/mail_form.html'
-    form_class = EmailTemplateForm
 
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
         kwargs['mail_code'] = self.mail_code
+        kwargs['instance'] = self.get_object()
+        kwargs.update(**self.get_mail_config())
+        kwargs.update(**self.mail_variables)
         return kwargs
 
     def form_valid(self, form):
-        response = super().form_valid(form)
-        form.send()
-        return response
+        """If both the regular form and mailing form are valid, save both."""
+        if not self.can_send_mail():
+            if self.fail_silently:
+                return HttpResponseRedirect(self.get_success_url())
+            else:
+                raise PermissionDenied("You are not allowed to send mail: %s." % self.mail_code)
+        messages.success(self.request, 'Mail sent')
+        return super().form_valid(form)
+
+
+class TestView(MailView):
+    """To be removed; exists for testing purposes only."""
+    mail_code = 'tests/test_mail_code_1'
+    model = Submission
+    success_url = '/'
+
+
+class TestUpdateView(MailFormView):
+    """To be removed; exists for testing purposes only."""
+    mail_code = 'tests/test_mail_code_1'
+    model = Submission
+    success_url = '/'
+    form_class = FakeForm
+
+
+class MailEditorSubview:
+    """
+    This subview works as an interrupter for function based views.
+
+    If a FBV is completed, the MailEditingSubview will interrupt the request and
+    provide a form that give the user the possibility to edit a template based email before
+    sending it.
+    """
+
+    template_name = 'mails/mail_form.html'
+
+    def __init__(self, request, mail_code, context=None, header_template=None, **kwargs):
+        self.mail_code = mail_code
+        self.context = context or {}
+        self.request = request
+        self.header_template = header_template
+        self.mail_form = EmailForm(request.POST or None, mail_code=mail_code, **kwargs)
+        self._is_valid = False
+
+    def interrupt(self):
+        """
+        Interrupt request by rendering the templated email form.
+
+        The `request` should be an HttpRequest instance that should be captured
+        and be included into the response of the interrupted response. Currently only
+        POST requests are supported.
+        """
+        self.context['form'] = self.mail_form
+        self.context['header_template'] = self.header_template
+        if 'object' in self.mail_form.engine.template_variables:
+            self.context['object'] = self.mail_form.engine.template_variables['object']
+        else:
+            self.context['object'] = None
+        return render(self.request, self.template_name, self.context)
+
+    def is_valid(self):
+        """See if data is returned and valid."""
+        self._is_valid = self.mail_form.is_valid()
+        return self._is_valid
+
+    def send_mail(self):
+        """Send email as returned by user."""
+        if not self._is_valid:
+            raise ValueError(
+                "The mail: %s could not be sent because the data didn't validate." % self.mail_code)
+        return self.mail_form.save()
+
+
+
+class MailEditorMixin:
+    """Deprecated."""
+    pass
diff --git a/notifications/migrations/0003_notification_url_code.py b/notifications/migrations/0003_notification_url_code.py
index 3c3e177b32c61d7f7c2830f1a4096b661a09ea31..9290c40f8608cffe1c087e3a63cbb54d668539b1 100644
--- a/notifications/migrations/0003_notification_url_code.py
+++ b/notifications/migrations/0003_notification_url_code.py
@@ -12,9 +12,9 @@ class Migration(migrations.Migration):
     ]
 
     operations = [
-        migrations.AddField(
-            model_name='notification',
-            name='url_code',
-            field=models.CharField(blank=True, max_length=16),
-        ),
+        # migrations.AddField(
+        #     model_name='notification',
+        #     name='url_code',
+        #     field=models.CharField(blank=True, max_length=255),
+        # ),
     ]
diff --git a/notifications/models.py b/notifications/models.py
index 65b4d5055513e63ba96de5147a21df4a1cbe9dc2..80c550d95b2ba202ec8634acbf28d51a2c59f073 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -61,7 +61,7 @@ class Notification(models.Model):
     target_object_id = models.CharField(max_length=255, blank=True, null=True)
     target = GenericForeignKey('target_content_type', 'target_object_id')
 
-    url_code = models.CharField(max_length=16, blank=True)
+    url_code = models.CharField(max_length=255, blank=True)
 
     action_object_content_type = models.ForeignKey(ContentType, blank=True, null=True,
                                                    related_name='notify_action_object')
diff --git a/partners/views.py b/partners/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2babdbefca983b67a7db0f970873509400f3852
--- /dev/null
+++ b/partners/views.py
@@ -0,0 +1,422 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+import mimetypes
+
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.db import transaction
+from django.forms import modelformset_factory
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404, render, reverse, redirect
+from django.utils import timezone
+
+from guardian.decorators import permission_required
+
+from mails.views import MailEditorSubview
+
+from .constants import PROSPECTIVE_PARTNER_REQUESTED,\
+    PROSPECTIVE_PARTNER_APPROACHED, PROSPECTIVE_PARTNER_ADDED,\
+    PROSPECTIVE_PARTNER_EVENT_REQUESTED, PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,\
+    PROSPECTIVE_PARTNER_FOLLOWED_UP
+from .models import Partner, ProspectivePartner, ProspectiveContact, ContactRequest,\
+    ProspectivePartnerEvent, MembershipAgreement, Contact, PartnersAttachment
+from .forms import ProspectivePartnerForm, ProspectiveContactForm,\
+    PromoteToPartnerForm,\
+    ProspectivePartnerEventForm, MembershipQueryForm,\
+    PartnerForm, ContactForm, ContactFormset, ContactModelFormset,\
+    NewContactForm, ActivationForm, PartnerEventForm,\
+    MembershipAgreementForm, RequestContactForm, RequestContactFormSet,\
+    ProcessRequestContactForm, PartnersAttachmentFormSet, PartnersAttachmentForm
+
+
+def supporting_partners(request):
+    current_agreements = MembershipAgreement.objects.now_active()
+    context = {
+        'current_agreements': current_agreements
+    }
+    if request.user.groups.filter(name='Editorial Administrators').exists():
+        # Show Agreements to Administrators only!
+        prospective_agreements = MembershipAgreement.objects.submitted().order_by('date_requested')
+        context['prospective_partners'] = prospective_agreements
+    return render(request, 'partners/supporting_partners.html', context)
+
+
+@login_required
+@permission_required('scipost.can_read_partner_page', return_403=True)
+def dashboard(request):
+    """Administration page for Partners and Prospective Partners.
+
+    This page is meant as a personal page for Partners, where they will for example be able
+    to read their personal data and agreements.
+    """
+    context = {}
+    try:
+        context['personal_agreements'] = (MembershipAgreement.objects.open_to_partner()
+                                          .filter(partner__contact=request.user.partner_contact))
+    except Contact.DoesNotExist:
+        pass
+
+    if request.user.has_perm('scipost.can_manage_SPB'):
+        context['contact_requests_count'] = ContactRequest.objects.awaiting_processing().count()
+        context['inactivate_contacts_count'] = Contact.objects.filter(user__is_active=False).count()
+        context['partners'] = Partner.objects.all()
+        context['prospective_partners'] = ProspectivePartner.objects.order_by(
+            'country', 'institution_name')
+        context['ppevent_form'] = ProspectivePartnerEventForm()
+        context['agreements'] = MembershipAgreement.objects.order_by('date_requested')
+    return render(request, 'partners/dashboard.html', context)
+
+
+@transaction.atomic
+def membership_request(request):
+    query_form = MembershipQueryForm(request.POST or None)
+    if query_form.is_valid():
+        prospartner = ProspectivePartner(
+            kind=query_form.cleaned_data['partner_kind'],
+            institution_name=query_form.cleaned_data['institution_name'],
+            country=query_form.cleaned_data['country'],
+            date_received=timezone.now(),
+            status=PROSPECTIVE_PARTNER_REQUESTED,
+        )
+        prospartner.save()
+        contact = ProspectiveContact(
+            prospartner=prospartner,
+            title=query_form.cleaned_data['title'],
+            first_name=query_form.cleaned_data['first_name'],
+            last_name=query_form.cleaned_data['last_name'],
+            email=query_form.cleaned_data['email'],
+        )
+        contact.save()
+        prospartnerevent = ProspectivePartnerEvent(
+            prospartner=prospartner,
+            event=PROSPECTIVE_PARTNER_EVENT_REQUESTED)
+        prospartnerevent.save()
+        ack_message = ('Thank you for your SPB Membership query. '
+                       'We will get back to you in the very near future '
+                       'with further details.')
+        context = {'ack_message': ack_message}
+        return render(request, 'scipost/acknowledgement.html', context)
+    context = {'query_form': query_form}
+    return render(request, 'partners/membership_request.html', context)
+
+
+@permission_required('scipost.can_manage_organizations', return_403=True)
+@transaction.atomic
+def promote_prospartner(request, prospartner_id):
+    prospartner = get_object_or_404(ProspectivePartner.objects.not_yet_partner(),
+                                    pk=prospartner_id)
+    form = PromoteToPartnerForm(request.POST or None, instance=prospartner)
+    contact_formset = ContactModelFormset(request.POST or None,
+                                          queryset=prospartner.prospective_contacts.all())
+    if form.is_valid() and contact_formset.is_valid():
+        partner = form.promote_to_partner(request.user)
+        contacts = contact_formset.promote_contacts(partner, request.user)
+        messages.success(request, ('<h3>Upgraded Partner %s</h3>'
+                                   '%i contacts have received a validation mail.') %
+                                  (str(partner), len(contacts)))
+        return redirect(reverse('partners:dashboard'))
+    context = {'form': form, 'contact_formset': contact_formset}
+    return render(request, 'partners/promote_prospartner.html', context)
+
+
+###############
+# Partner views
+###############
+@permission_required('scipost.can_view_own_partner_details', return_403=True)
+def partner_view(request, partner_id):
+    partner = get_object_or_404(Partner.objects.my_partners(request.user), id=partner_id)
+    form = PartnerEventForm(request.POST or None)
+    if form.is_valid():
+        event = form.save(commit=False)
+        event.partner = partner
+        event.noted_by = request.user
+        event.save()
+        messages.success(request, 'Added a new event to Partner.')
+        return redirect(partner.get_absolute_url())
+    context = {
+        'partner': partner,
+        'form': form
+    }
+    return render(request, 'partners/partners_detail.html', context)
+
+
+@permission_required('scipost.can_manage_SPB', return_403=True)
+@transaction.atomic
+def partner_edit(request, partner_id):
+    partner = get_object_or_404(Partner, id=partner_id)
+
+    # Start/fill forms
+    form = PartnerForm(request.POST or None, instance=partner)
+    ContactModelFormset = modelformset_factory(Contact, ContactForm, can_delete=True, extra=0,
+                                               formset=ContactFormset)
+    contact_formset = ContactModelFormset(request.POST or None, partner=partner,
+                                          queryset=partner.contact_set.all())
+
+    # Validate forms for POST request
+    if form.is_valid() and contact_formset.is_valid():
+        form.save()
+        contact_formset.save()
+        messages.success(request, 'Partner saved')
+        return redirect(reverse('partners:partner_view', args=(partner.id,)))
+    context = {
+        'form': form,
+        'contact_formset': contact_formset
+    }
+    return render(request, 'partners/partner_edit.html', context)
+
+
+@permission_required('scipost.can_manage_SPB', return_403=True)
+def partner_add_contact(request, partner_id):
+    partner = get_object_or_404(Partner, id=partner_id)
+    form = NewContactForm(request.POST or None, partner=partner)
+    if form.is_valid():
+        contact = form.save(current_user=request.user)
+        messages.success(request, '<h3>Created contact: %s</h3>Email has been sent.'
+                                  % str(contact))
+        return redirect(reverse('partners:dashboard'))
+    context = {
+        'partner': partner,
+        'form': form
+    }
+    return render(request, 'partners/partner_add_contact.html', context)
+
+
+@permission_required('scipost.can_view_own_partner_details', return_403=True)
+def partner_request_contact(request, partner_id):
+    partner = get_object_or_404(Partner.objects.my_partners(request.user), id=partner_id)
+    form = RequestContactForm(request.POST or None)
+    if form.is_valid():
+        contact_request = form.save(commit=False)
+        contact_request.partner = partner
+        contact_request.save()
+        messages.success(request, ('<h3>Request sent</h3>'
+                                   'We will process your request as soon as possible.'))
+        return redirect(partner.get_absolute_url())
+    context = {
+        'partner': partner,
+        'form': form
+    }
+    return render(request, 'partners/partner_request_contact.html', context)
+
+
+@permission_required('scipost.can_manage_SPB', return_403=True)
+def process_contact_requests(request):
+    form = RequestContactForm(request.POST or None)
+
+    RequestContactModelFormSet = modelformset_factory(ContactRequest, ProcessRequestContactForm,
+                                                      formset=RequestContactFormSet, extra=0)
+    formset = RequestContactModelFormSet(request.POST or None,
+                                         queryset=ContactRequest.objects.awaiting_processing())
+    if formset.is_valid():
+        formset.process_requests(current_user=request.user)
+        messages.success(request, 'Processing completed')
+        return redirect(reverse('partners:process_contact_requests'))
+    context = {
+        'form': form,
+        'formset': formset
+    }
+    return render(request, 'partners/process_contact_requests.html', context)
+
+
+
+###########################
+# Prospective Partner Views
+###########################
+
+@permission_required('scipost.can_manage_SPB', return_403=True)
+def add_prospective_partner(request):
+    form = ProspectivePartnerForm(request.POST or None)
+    if form.is_valid():
+        pp = form.save()
+        messages.success(request, 'Prospective Partner successfully added')
+        return redirect(reverse('partners:add_prospartner_contact',
+                                kwargs={'prospartner_id': pp.id}))
+    context = {'form': form}
+    return render(request, 'partners/add_prospective_partner.html', context)
+
+
+@permission_required('scipost.can_manage_SPB', return_403=True)
+def add_prospartner_contact(request, prospartner_id):
+    prospartner = get_object_or_404(ProspectivePartner, pk=prospartner_id)
+    form = ProspectiveContactForm(request.POST or None, initial={'prospartner': prospartner})
+    if form.is_valid():
+        form.save()
+        messages.success(request, 'Contact successfully added to Prospective Partner')
+        return redirect(reverse('partners:dashboard'))
+    context = {'form': form, 'prospartner': prospartner}
+    return render(request, 'partners/add_prospartner_contact.html', context)
+
+
+@permission_required('scipost.can_email_prospartner_contact', return_403=True)
+@transaction.atomic
+def email_prospartner_contact(request, contact_id, mail=None):
+    contact = get_object_or_404(ProspectiveContact, pk=contact_id)
+
+    suffix = ''
+    if mail == 'followup':
+        code = 'partners_followup_mail'
+        suffix = ' (followup)'
+        new_status = PROSPECTIVE_PARTNER_FOLLOWED_UP
+    else:
+        code = 'partners_initial_mail'
+        new_status = PROSPECTIVE_PARTNER_APPROACHED
+
+    mail_request = MailEditorSubview(request, mail_code=code, contact=contact)
+    if mail_request.is_valid():
+        comments = 'Email{suffix} sent to {name}.'.format(suffix=suffix, name=contact)
+        prospartnerevent = ProspectivePartnerEvent(
+            prospartner=contact.prospartner,
+            event=PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,
+            comments=comments,
+            noted_on=timezone.now(),
+            noted_by=request.user.contributor)
+        prospartnerevent.save()
+        if contact.prospartner.status in [PROSPECTIVE_PARTNER_REQUESTED,
+                                          PROSPECTIVE_PARTNER_ADDED,
+                                          PROSPECTIVE_PARTNER_APPROACHED]:
+            contact.prospartner.status = new_status
+            contact.prospartner.save()
+
+        messages.success(request, 'Email successfully sent.')
+        mail_request.send_mail()
+        return redirect(reverse('partners:dashboard'))
+    else:
+        return mail_request.interrupt()
+
+
+@permission_required('scipost.can_email_prospartner_contact', return_403=True)
+@transaction.atomic
+def email_prospartner_generic(request, prospartner_id, mail=None):
+    prospartner = get_object_or_404(ProspectivePartner, pk=prospartner_id)
+
+    suffix = ''
+
+    if mail == 'followup':
+        code = 'partners_followup_mail'
+        suffix = ' (followup)'
+        new_status = PROSPECTIVE_PARTNER_FOLLOWED_UP
+    else:
+        code = 'partners_initial_mail'
+        new_status = PROSPECTIVE_PARTNER_APPROACHED
+    mail_request = MailEditorSubview(request, mail_code=code)
+    if mail_request.is_valid():
+        comments = 'Email{suffix} sent to {name}.'.format(
+            suffix=suffix, name=mail_request.recipients_string)
+        prospartnerevent = ProspectivePartnerEvent(
+            prospartner=prospartner,
+            event=PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,
+            comments=comments,
+            noted_on=timezone.now(),
+            noted_by=request.user.contributor)
+        prospartnerevent.save()
+        if prospartner.status in [PROSPECTIVE_PARTNER_REQUESTED,
+                                  PROSPECTIVE_PARTNER_ADDED,
+                                  PROSPECTIVE_PARTNER_APPROACHED]:
+            prospartner.status = new_status
+            prospartner.save()
+
+        messages.success(request, 'Email successfully sent.')
+        mail_request.send_mail()
+        return redirect(reverse('partners:dashboard'))
+    else:
+        return mail_request.interrupt()
+
+
+@permission_required('scipost.can_manage_SPB', return_403=True)
+@transaction.atomic
+def add_prospartner_event(request, prospartner_id):
+    prospartner = get_object_or_404(ProspectivePartner, pk=prospartner_id)
+    if request.method == 'POST':
+        ppevent_form = ProspectivePartnerEventForm(request.POST)
+        if ppevent_form.is_valid():
+            ppevent = ppevent_form.save(commit=False)
+            ppevent.prospartner = prospartner
+            ppevent.noted_by = request.user.contributor
+            ppevent.save()
+            prospartner.update_status_from_event(ppevent.event)
+            prospartner.save()
+            return redirect(reverse('partners:dashboard'))
+        else:
+            errormessage = 'The form was invalidly filled.'
+            return render(request, 'scipost/error.html', {'errormessage': errormessage})
+    errormessage = 'This view can only be posted to.'
+    return render(request, 'scipost/error.html', {'errormessage': errormessage})
+
+
+############
+# Agreements
+############
+@permission_required('scipost.can_manage_SPB', return_403=True)
+def add_agreement(request):
+    form = MembershipAgreementForm(request.POST or None, initial=request.GET)
+    if request.POST and form.is_valid():
+        agreement = form.save(request.user)
+        messages.success(request, 'Membership Agreement created.')
+        return redirect(agreement.get_absolute_url())
+    context = {
+        'form': form
+    }
+    return render(request, 'partners/agreements_add.html', context)
+
+
+@permission_required('scipost.can_view_own_partner_details', return_403=True)
+def agreement_details(request, agreement_id):
+    agreement = get_object_or_404(MembershipAgreement, id=agreement_id)
+    context = {}
+
+    if request.user.has_perm('scipost.can_manage_SPB'):
+        form = MembershipAgreementForm(request.POST or None, instance=agreement)
+        PartnersAttachmentFormSet
+
+        PartnersAttachmentFormset = modelformset_factory(PartnersAttachment,
+                                                         PartnersAttachmentForm,
+                                                         formset=PartnersAttachmentFormSet)
+        attachment_formset = PartnersAttachmentFormset(request.POST or None, request.FILES or None,
+                                                       queryset=agreement.attachments.all())
+
+        context['form'] = form
+        context['attachment_formset'] = attachment_formset
+        if form.is_valid() and attachment_formset.is_valid():
+            agreement = form.save(request.user)
+            attachment_formset.save(agreement)
+            messages.success(request, 'Membership Agreement updated.')
+            return redirect(agreement.get_absolute_url())
+
+    context['agreement'] = agreement
+    return render(request, 'partners/agreements_details.html', context)
+
+
+@permission_required('scipost.can_view_own_partner_details', return_403=True)
+def agreement_attachments(request, agreement_id, attachment_id):
+    attachment = get_object_or_404(PartnersAttachment.objects.my_attachments(request.user),
+                                   agreement__id=agreement_id, id=attachment_id)
+
+    content_type, encoding = mimetypes.guess_type(attachment.attachment.path)
+    content_type = content_type or 'application/octet-stream'
+    response = HttpResponse(attachment.attachment.read(), content_type=content_type)
+    response["Content-Encoding"] = encoding
+    response['Content-Disposition'] = ('filename=%s' % attachment.name)
+    return response
+
+
+#########
+# Account
+#########
+def activate_account(request, activation_key):
+    contact = get_object_or_404(Contact, user__is_active=False,
+                                activation_key=activation_key,
+                                user__email__icontains=request.GET.get('email', None))
+
+    # TODO: Key Expires fallback
+    form = ActivationForm(request.POST or None, instance=contact.user)
+    if form.is_valid():
+        form.activate_user()
+        messages.success(request, '<h3>Thank you for registration</h3>')
+        return redirect(reverse('partners:dashboard'))
+    context = {
+        'contact': contact,
+        'form': form
+    }
+    return render(request, 'partners/activate_account.html', context)
diff --git a/preprints/factories.py b/preprints/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..70bd6341820f0fcdfd2d8fe630fc4f53a771ce46
--- /dev/null
+++ b/preprints/factories.py
@@ -0,0 +1,27 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+import factory
+
+from common.helpers import random_arxiv_identifier_without_version_number
+
+from .models import Preprint
+
+
+class PreprintFactory(factory.django.DjangoModelFactory):
+    """
+    Generate random Preprint instances.
+    """
+
+    vn_nr = 1
+    identifier_wo_vn_nr = factory.Sequence(
+        lambda n: random_arxiv_identifier_without_version_number())
+    identifier_w_vn_nr = factory.lazy_attribute(lambda o: '%sv%i' % (
+        o.identifier_wo_vn_nr, o.vn_nr))
+    url = factory.lazy_attribute(lambda o: (
+        'https://arxiv.org/abs/%s' % o.identifier_wo_vn_nr))
+    scipost_preprint_identifier = factory.Sequence(lambda n: Preprint.objects.count() + 1)
+
+    class Meta:
+        model = Preprint
diff --git a/production/views.py b/production/views.py
index b8e04de72bb5a86f4d0ab78e5f0dd5ce34ae26e1..a433aa2fb421de4c04d40b6c53a6f618e0d08d2b 100644
--- a/production/views.py
+++ b/production/views.py
@@ -19,7 +19,7 @@ from guardian.core import ObjectPermissionChecker
 from guardian.shortcuts import assign_perm, remove_perm
 
 from finances.forms import WorkLogForm
-from mails.views import MailEditingSubView
+from mails.views import MailEditorSubview
 
 from . import constants
 from .models import ProductionUser, ProductionStream, ProductionEvent, Proofs,\
@@ -687,13 +687,12 @@ def send_proofs(request, stream_id, version):
         stream.status = constants.PROOFS_SENT
         stream.save()
 
-    mail_request = MailEditingSubView(request, mail_code='production_send_proofs',
-                                      proofs=proofs)
+    mail_request = MailEditorSubview(request, mail_code='production_send_proofs', proofs=proofs)
     if mail_request.is_valid():
         proofs.save()
         stream.save()
         messages.success(request, 'Proofs have been sent.')
-        mail_request.send()
+        mail_request.send_mail()
         prodevent = ProductionEvent(
             stream=stream,
             event='status',
@@ -703,7 +702,7 @@ def send_proofs(request, stream_id, version):
         prodevent.save()
         return redirect(stream.get_absolute_url())
     else:
-        return mail_request.return_render()
+        return mail_request.interrupt()
 
     messages.success(request, 'Proofs have been sent.')
     return redirect(stream.get_absolute_url())
diff --git a/requirements.txt b/requirements.txt
index ac91ae8017bc6b275489df1ee254ba3b972cac05..1cd64c7dbc652fb2112be9f9e2bb1cdfb168bab8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -33,7 +33,7 @@ sphinx-rtd-theme==0.1.9  # Sphinx theme
 
 # Testing
 factory-boy==2.10.0
-Faker==0.8.1
+Faker==1.0.2
 
 
 # Django Utils
diff --git a/scipost/factories.py b/scipost/factories.py
index 27ef1f8ff3b04a98a93802792e52d33f1ce161c8..193dd6a1071e41facef8c66fc67f49105e8925c2 100644
--- a/scipost/factories.py
+++ b/scipost/factories.py
@@ -12,13 +12,13 @@ from common.helpers import generate_orcid
 from submissions.models import Submission
 
 from .models import Contributor, EditorialCollege, EditorialCollegeFellowship, Remark
-from .constants import TITLE_CHOICES, SCIPOST_SUBJECT_AREAS
+from .constants import TITLE_CHOICES, SCIPOST_SUBJECT_AREAS, NORMAL_CONTRIBUTOR
 
 
 class ContributorFactory(factory.django.DjangoModelFactory):
     title = factory.Iterator(TITLE_CHOICES, getter=lambda c: c[0])
     user = factory.SubFactory('scipost.factories.UserFactory', contributor=None)
-    status = 'normal'  # normal user
+    status = NORMAL_CONTRIBUTOR  # normal user
     vetted_by = factory.Iterator(Contributor.objects.all())
     personalwebpage = factory.Faker('uri')
     expertises = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: [c[0]])
@@ -32,6 +32,12 @@ class ContributorFactory(factory.django.DjangoModelFactory):
         model = Contributor
         django_get_or_create = ('user',)
 
+    @classmethod
+    def create(cls, **kwargs):
+        if Contributor.objects.count() < 1 and kwargs.get('vetted_by', False) is not None:
+            ContributorFactory.create(vetted_by=None)
+        return super().create(**kwargs)
+
     @factory.post_generation
     def add_to_vetting_editors(self, create, extracted, **kwargs):
         if create:
@@ -44,7 +50,7 @@ class VettingEditorFactory(ContributorFactory):
     def add_to_vetting_editors(self, create, extracted, **kwargs):
         if not create:
             return
-        self.user.groups.add(Group.objects.get(name="Vetting Editors"))
+        self.user.groups.add(Group.objects.get_or_create(name="Vetting Editors")[0])
 
 
 class UserFactory(factory.django.DjangoModelFactory):
@@ -70,7 +76,7 @@ class UserFactory(factory.django.DjangoModelFactory):
             for group in extracted:
                 self.groups.add(group)
         else:
-            self.groups.add(Group.objects.get(name="Registered Contributors"))
+            self.groups.add(Group.objects.get_or_create(name="Registered Contributors")[0])
 
 
 class EditorialCollegeFactory(factory.django.DjangoModelFactory):
diff --git a/scipost/templates/scipost/register.html b/scipost/templates/scipost/register.html
index f8901f6fcf477d00619a0b5babc584f02b6d4795..1a77c273be8194416e99a555dd65adeaba6229ff 100644
--- a/scipost/templates/scipost/register.html
+++ b/scipost/templates/scipost/register.html
@@ -4,18 +4,11 @@
 
 {% block pagetitle %}: register{% endblock pagetitle %}
 
-{% block breadcrumb %}
-    <div class="container-outside header">
-        <div class="container">
-            <h1>Register to SciPost</h1>
-        </div>
-    </div>
-{% endblock %}
-
 {% block content %}
 
 <div class="row">
     <div class="col-12">
+        <h1 class="highlight">Register to SciPost</h1>
         {% if invitation %}
             <h2>Welcome {{invitation.get_title_display}} {{invitation.last_name}} and thanks in advance for registering (by completing this form)</h2>
         {% endif %}
diff --git a/submissions/factories.py b/submissions/factories.py
index ceb5c664bdc11326bee5aef059db30abe2efa214..a0a309fcfb9a84a992e34b6b411d540bf034f939 100644
--- a/submissions/factories.py
+++ b/submissions/factories.py
@@ -9,9 +9,9 @@ import random
 from comments.factories import SubmissionCommentFactory
 from scipost.constants import SCIPOST_SUBJECT_AREAS
 from scipost.models import Contributor
+from journals.models import Journal
 from journals.constants import SCIPOST_JOURNALS_DOMAINS
-from common.helpers import random_arxiv_identifier_without_version_number, random_scipost_journal,\
-    random_scipost_report_doi_label
+from common.helpers import random_scipost_report_doi_label
 
 from .constants import (
     STATUS_UNASSIGNED, STATUS_EIC_ASSIGNED, STATUS_INCOMING, STATUS_PUBLISHED, SUBMISSION_TYPE,
@@ -23,50 +23,59 @@ from faker import Faker
 
 
 class SubmissionFactory(factory.django.DjangoModelFactory):
+    """
+    Generate random basic Submission instances.
+    """
+
     author_list = factory.Faker('name')
     submitted_by = factory.Iterator(Contributor.objects.all())
     submission_type = factory.Iterator(SUBMISSION_TYPE, getter=lambda c: c[0])
-    submitted_to = factory.Sequence(lambda n: Journal.objects.get(doi_label=random_scipost_journal()))
+    submitted_to = factory.Iterator(Journal.objects.all())
     title = factory.Faker('sentence')
     abstract = factory.Faker('paragraph', nb_sentences=10)
-    identifier_wo_vn_nr = factory.Sequence(
-        lambda n: random_arxiv_identifier_without_version_number())
+    list_of_changes = factory.Faker('paragraph', nb_sentences=10)
     subject_area = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: c[0])
     domain = factory.Iterator(SCIPOST_JOURNALS_DOMAINS, getter=lambda c: c[0])
     abstract = factory.Faker('paragraph')
     author_comments = factory.Faker('paragraph')
     remarks_for_editors = factory.Faker('paragraph')
+    thread_hash = factory.Faker('uuid4')
     is_current = True
-    vn_nr = 1
-    url = factory.lazy_attribute(lambda o: (
-        'https://arxiv.org/abs/%s' % o.preprint.identifier_wo_vn_nr))
-    preprint__identifier_w_vn_nr = factory.lazy_attribute(lambda o: '%sv%i' % (
-        o.preprint.identifier_wo_vn_nr, o.preprint.vn_nr))
     submission_date = factory.Faker('date_this_decade')
     latest_activity = factory.LazyAttribute(lambda o: Faker().date_time_between(
         start_date=o.submission_date, end_date="now", tzinfo=pytz.UTC))
+    preprint = factory.SubFactory('preprints.factories.PreprintFactory')
 
     class Meta:
         model = Submission
 
+    @classmethod
+    def create(cls, **kwargs):
+        if Contributor.objects.count() < 5:
+            from scipost.factories import ContributorFactory
+            ContributorFactory.create_batch(5)
+        if Journal.objects.count() < 3:
+            from journals.factories import JournalFactory
+            JournalFactory.create_batch(3)
+        return super().create(**kwargs)
+
     @factory.post_generation
     def contributors(self, create, extracted, **kwargs):
-        contributors = Contributor.objects.all()
+        contribs = Contributor.objects.all()
         if self.editor_in_charge:
-            contributors = contributors.exclude(id=self.editor_in_charge.id)
-        contributors = contributors.order_by('?')[:random.randint(1, 6)]
+            contribs = contribs.exclude(id=self.editor_in_charge.id)
+        contribs = contribs.order_by('?')[:random.randint(1, 6)]
 
         # Auto-add the submitter as an author
-        self.submitted_by = contributors[0]
+        self.submitted_by = contribs[0]
         self.author_list = ', '.join([
-            '%s %s' % (c.user.first_name, c.user.last_name) for c in contributors])
+            '%s %s' % (c.user.first_name, c.user.last_name) for c in contribs])
 
         if not create:
             return
 
         # Add three random authors
-        self.authors.add(*contributors)
-        self.cycle.update_deadline()
+        self.authors.add(*contribs)
 
 
 class UnassignedSubmissionFactory(SubmissionFactory):
@@ -125,7 +134,6 @@ class ResubmittedSubmissionFactory(EICassignedSubmissionFactory):
     open_for_commenting = False
     open_for_reporting = False
     is_current = False
-    is_resubmission = False
 
     @factory.post_generation
     def successive_submission(self, create, extracted, **kwargs):
@@ -180,8 +188,7 @@ class ResubmissionFactory(EICassignedSubmissionFactory):
     status = STATUS_INCOMING
     open_for_commenting = True
     open_for_reporting = True
-    is_resubmission = True
-    preprint__vn_nr = 2
+    vn_nr = 2
 
     @factory.post_generation
     def previous_submission(self, create, extracted, **kwargs):
@@ -252,6 +259,7 @@ class PublishedSubmissionFactory(EICassignedSubmissionFactory):
 class ReportFactory(factory.django.DjangoModelFactory):
     status = factory.Iterator(REPORT_STATUSES, getter=lambda c: c[0])
     submission = factory.Iterator(Submission.objects.all())
+    report_nr = 1
     date_submitted = factory.Faker('date_time_this_decade')
     vetted_by = factory.Iterator(Contributor.objects.all())
     author = factory.Iterator(Contributor.objects.all())
diff --git a/submissions/migrations/0052_merge_20190209_1124.py b/submissions/migrations/0052_merge_20190209_1124.py
new file mode 100644
index 0000000000000000000000000000000000000000..a5c3680e5e2d2e849da7b6cbb60f2c1e1a60deda
--- /dev/null
+++ b/submissions/migrations/0052_merge_20190209_1124.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-02-09 10:24
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('submissions', '0051_auto_20181218_2201'),
+        ('submissions', '0051_auto_20190126_2058'),
+    ]
+
+    operations = [
+    ]
diff --git a/submissions/views.py b/submissions/views.py
index d9d15c3301356488b2558e8df790e68da65c98da..3550f60c6ec74767eebfd4806054b679a9d4c4d4 100644
--- a/submissions/views.py
+++ b/submissions/views.py
@@ -54,7 +54,7 @@ from invitations.constants import STATUS_SENT
 from invitations.models import RegistrationInvitation
 from journals.models import Journal
 from mails.utils import DirectMailUtil
-from mails.views import MailEditingSubView
+from mails.views import MailEditorSubview
 from ontology.models import Topic
 from ontology.forms import SelectTopicForm
 from production.forms import ProofsDecisionForm
@@ -809,10 +809,10 @@ def assignment_failed(request, identifier_w_vn_nr):
     submission = get_object_or_404(Submission.objects.pool(request.user).unassigned(),
                                    preprint__identifier_w_vn_nr=identifier_w_vn_nr)
 
-    mail_request = MailEditingSubView(
+    mail_editor_view = MailEditorSubview(
         request, mail_code='submissions_assignment_failed', instance=submission,
         header_template='partials/submissions/admin/editorial_assignment_failed.html')
-    if mail_request.is_valid():
+    if mail_editor_view.is_valid():
         # Deprecate old Editorial Assignments
         EditorialAssignment.objects.filter(submission=submission).invited().update(
             status=STATUS_DEPRECATED)
@@ -826,10 +826,9 @@ def assignment_failed(request, identifier_w_vn_nr):
             request, 'Submission {arxiv} has failed pre-screening and been rejected.'.format(
                 arxiv=submission.preprint.identifier_w_vn_nr))
         messages.success(request, 'Authors have been informed by email.')
-        mail_request.send()
+        mail_editor_view.send_mail()
         return redirect(reverse('submissions:pool'))
-    else:
-        return mail_request.return_render()
+    return mail_editor_view.interrupt()
 
 
 @login_required
@@ -1022,9 +1021,9 @@ def invite_referee(request, identifier_w_vn_nr, profile_id, auto_reminders_allow
                             'Please go back and select another referee.')
             return render(request, 'scipost/error.html', {'errormessage': errormessage})
 
-        mail_request = MailEditingSubView(request,
-                                          mail_code='referees/invite_contributor_to_referee',
-                                          invitation=referee_invitation)
+        mail_request = MailEditorSubview(
+            request, mail_code='referees/invite_contributor_to_referee',
+            invitation=referee_invitation)
     else: # no Contributor, so registration invitation
         registration_invitation, reginv_created = RegistrationInvitation.objects.get_or_create(
             profile=profile,
@@ -1036,9 +1035,9 @@ def invite_referee(request, identifier_w_vn_nr, profile_id, auto_reminders_allow
             created_by=request.user,
             invited_by=request.user,
             invitation_key=referee_invitation.invitation_key)
-        mail_request = MailEditingSubView(request,
-                                          mail_code='referees/invite_unregistered_to_referee',
-                                          invitation=referee_invitation)
+        mail_request = MailEditorSubview(
+            request, mail_code='referees/invite_unregistered_to_referee',
+            invitation=referee_invitation)
 
     if mail_request.is_valid():
         referee_invitation.date_invited = timezone.now()
@@ -1050,11 +1049,11 @@ def invite_referee(request, identifier_w_vn_nr, profile_id, auto_reminders_allow
         submission.add_event_for_author('A referee has been invited.')
         submission.add_event_for_eic('Referee %s has been invited.' % profile.last_name)
         messages.success(request, 'Invitation sent')
-        mail_request.send()
+        mail_request.send_mail()
         return redirect(reverse('submissions:editorial_page',
                                 kwargs={'identifier_w_vn_nr': identifier_w_vn_nr}))
     else:
-        return mail_request.return_render()
+        return mail_request.interrupt()
 
 
 @login_required
diff --git a/templates/email/_footer.html b/templates/email/_footer.html
index 0ddecd32c06c5cc829db01b92d3157d572442ede..1751c46c89a21b4893ec5e1de3f781c406c29e0b 100644
--- a/templates/email/_footer.html
+++ b/templates/email/_footer.html
@@ -1,21 +1,13 @@
 {% load staticfiles %}
 
 <a href="https://scipost.org"><img src="https://scipost.org/static/scipost/images/logo_scipost_with_bgd_small.png" width="64px"></a>
-<br/>
-<div style="background-color: #f0f0f0; color: #002B49; align-items: center;">
-    <div style="display: inline-block; padding: 8px;">
-        <a href="https://scipost.org/journals/">Journals</a>
-    </div>
-    <div style="display: inline-block; padding: 8px;">
-        <a href="https://scipost.org/submissions/">Submissions</a>
-    </div>
-    <div style="display: inline-block; padding: 8px;">
-        <a href="https://scipost.org/commentaries/">Commentaries</a>
-    </div>
-    <div style="display: inline-block; padding: 8px;">
-        <a href="https://scipost.org/theses/">Theses</a>
-    </div>
-    <div style="display: inline-block; padding: 8px;">
-        <a href="https://scipost.org/login/">Login</a>
-    </div>
-</div>
+&nbsp;&nbsp;
+<a href="https://scipost.org/journals/">Journals</a>
+&nbsp;&middot;&nbsp;
+<a href="https://scipost.org/submissions/">Submissions</a>
+&nbsp;&middot;&nbsp;
+<a href="https://scipost.org/commentaries/">Commentaries</a>
+&nbsp;&middot;&nbsp;
+<a href="https://scipost.org/theses/">Theses</a>
+&nbsp;&middot;&nbsp;
+<a href="https://scipost.org/login/">Login</a>
diff --git a/templates/email/citation_notification.html b/templates/email/citation_notification.html
index 6ae8b2d2d90831846a9e82877020ada2b71acffc..495bff1cf98cbe07b1e076553e9028ab405ce903 100644
--- a/templates/email/citation_notification.html
+++ b/templates/email/citation_notification.html
@@ -1,5 +1,5 @@
 
-Dear {{ notification.get_title }} {{ notification.last_name }},
+Dear {{ object.get_title }} {{ object.last_name }},
 
 <br>
 <br>
@@ -7,42 +7,42 @@ Dear {{ notification.get_title }} {{ notification.last_name }},
 <p>
     We would like to notify you that your work has been cited in
 
-    {% if notification.related_notifications.for_publications %}
-        {% if notification.related_notifications.for_publications|length > 1 %}{{ notification.related_notifications.for_publications|length }} papers{% else %}a paper{% endif %}
+    {% if object.related_notifications.for_publications %}
+        {% if object.related_notifications.for_publications|length > 1 %}{{ object.related_notifications.for_publications|length }} papers{% else %}a paper{% endif %}
         published by SciPost:
 
         <ul>
-            {% for notification in notification.related_notifications.for_publications %}
+            {% for notification in object.related_notifications.for_publications %}
                 <li>
-                    <a href="https://doi.org/{{ notification.publication.doi_string }}">{{ notification.publication.citation }}</a>
+                    <a href="https://doi.org/{{ object.publication.doi_string }}">{{ object.publication.citation }}</a>
                     <br>
-                    {{ notification.publication.title }}
+                    {{ object.publication.title }}
                     <br>
-                    <i>by {{ notification.publication.author_list }}</i>
+                    <i>by {{ object.publication.author_list }}</i>
                 </li>
             {% endfor %}
         </ul>
     {% endif %}
 
-    {% if notification.related_notifications.for_submissions %}
-        {% if notification.related_notifications.for_submissions|length > 1 %}{{ notification.related_notifications.for_submissions|length }} manuscripts{% else %}a manuscript{% endif %}
+    {% if object.related_notifications.for_submissions %}
+        {% if object.related_notifications.for_submissions|length > 1 %}{{ object.related_notifications.for_submissions|length }} manuscripts{% else %}a manuscript{% endif %}
         submitted to SciPost,
 
         <ul>
-            {% for notification in notification.related_notifications.for_submissions %}
+            {% for notification in object.related_notifications.for_submissions %}
                 <li>
-                    {{ notification.submission.title }}
+                    {{ object.submission.title }}
                     <br>
-                    <i>by {{ notification.submission.author_list }}</i>
+                    <i>by {{ object.submission.author_list }}</i>
                     <br>
-                    <a href="https://scipost.org/{{ notification.submission.get_absolute_url }}">View the submission's page</a>
+                    <a href="https://scipost.org/{{ object.submission.get_absolute_url }}">View the submission's page</a>
                 </li>
             {% endfor %}
         </ul>
     {% endif %}
 </p>
 
-{% if notification.related_notifications.for_publications %}
+{% if object.related_notifications.for_publications %}
     <p>We hope you will find this paper of interest to your own research.</p>
 {% else %}
     <p>You might for example consider reporting or commenting on the above submission before the refereeing deadline.</p>
@@ -54,9 +54,9 @@ Dear {{ notification.get_title }} {{ notification.last_name }},
     The SciPost Team
 </p>
 
-{% if notification.get_first_related_contributor and notification.get_first_related_contributor.activation_key %}
+{% if object.get_first_related_contributor and object.get_first_related_contributor.activation_key %}
     <p style="font-size: 10px;">
-        Don\'t want to receive such emails? <a href="https://scipost.org/{% url 'scipost:unsubscribe' notification.get_first_related_contributor.id notification.get_first_related_contributor.activation_key %}">Unsubscribe</a>
+        Don\'t want to receive such emails? <a href="https://scipost.org/{% url 'scipost:unsubscribe' object.get_first_related_contributor.id object.get_first_related_contributor.activation_key %}">Unsubscribe</a>
     </p>
 {% endif %}
 
diff --git a/templates/email/citation_notification.json b/templates/email/citation_notification.json
index a9314819355439cff7eb54d7fae2942f9b647524..0bf1884a8fbe721323fb12d56fab6d7d09622093 100644
--- a/templates/email/citation_notification.json
+++ b/templates/email/citation_notification.json
@@ -1,8 +1,11 @@
 {
     "subject": "SciPost: citation notification",
-    "to_address": "email",
-    "bcc_to": "admin@scipost.org",
-    "from_address_name": "SciPost Admin",
-    "from_address": "admin@scipost.org",
-    "context_object": "notification"
+    "recipient_list": [
+        "email"
+    ],
+    "bcc": [
+        "admin@scipost.org"
+    ],
+    "from_name": "SciPost Admin",
+    "from_email": "admin@scipost.org"
 }
diff --git a/templates/email/potentialfellowships/invite_potential_fellow_initial.html b/templates/email/potentialfellowships/invite_potential_fellow_initial.html
index 8c54bc307825d84575af609fe48ab257692f04d1..50419128e31e6debe1f04e26bc4e9b3ba4424d68 100644
--- a/templates/email/potentialfellowships/invite_potential_fellow_initial.html
+++ b/templates/email/potentialfellowships/invite_potential_fellow_initial.html
@@ -1,4 +1,4 @@
-<p>Dear {{ potfel.profile.get_title_display }} {{ potfel.profile.last_name }},</p>
+<p>Dear {{ object.profile.get_title_display }} {{ object.profile.last_name }},</p>
 
 <p>Hopefully you've already come across <a href="https://scipost.org{% url 'scipost:index' %}">SciPost</a> and are aware of our mission to establish a community-based infrastructure for scientific publishing.</p>
 
diff --git a/templates/email/potentialfellowships/invite_potential_fellow_initial.json b/templates/email/potentialfellowships/invite_potential_fellow_initial.json
index ae3649f39978634bd3c1d0ff80576a68c2f148a2..35a85dbf0128e0d76278bc1d557e9d0763f009dd 100644
--- a/templates/email/potentialfellowships/invite_potential_fellow_initial.json
+++ b/templates/email/potentialfellowships/invite_potential_fellow_initial.json
@@ -1,8 +1,11 @@
 {
     "subject": "Invitation to become a Fellow at SciPost",
-    "to_address": "profile.email",
-    "bcc_to": "admin@scipost.org",
-    "from_address_name": "SciPost Admin",
-    "from_address": "admin@scipost.org",
-    "context_object": "potfel"
+    "recipient_list": [
+        "profile.email"
+    ],
+    "bcc": [
+        "admin@scipost.org"
+    ],
+    "from_name": "SciPost Admin",
+    "from_email": "admin@scipost.org"
 }
diff --git a/templates/email/registration_invitation.html b/templates/email/registration_invitation.html
index ae584bfa4e86d454a2bd505ca18d612753721c94..8a96130be8f8dd5bbbfa09c85d239946b6dfb623 100644
--- a/templates/email/registration_invitation.html
+++ b/templates/email/registration_invitation.html
@@ -1,23 +1,23 @@
-{% if invitation.invitation_type == 'F' %}
+{% if object.invitation_type == 'F' %}
     <strong>RE: Invitation to join the Editorial College of SciPost</strong>
     <br>
 {% endif %}
 
-Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }} {{ invitation.last_name }}{% else %}{{ invitation.first_name }}{% endif %},
+Dear {% if object.message_style == 'F' %}{{ object.get_title_display }} {{ object.last_name }}{% else %}{{ object.first_name }}{% endif %},
 
 <br><br>
 
-{% if invitation.personal_message %}
-    {{ invitation.personal_message|linebreaksbr }}
+{% if object.personal_message %}
+    {{ object.personal_message|linebreaksbr }}
     <br>
 {% endif %}
 
 
-{% if invitation.invitation_type == 'R' %}
+{% if object.invitation_type == 'R' %}
     {# Referee invite #}
     <p>
         We would hereby like to cordially invite you to become a Contributor on SciPost (this is required in order to deliver reports; our records show that you are not yet registered);
-        for your convenience, we have prepared a pre-filled <a href="https://scipost.org/invitation/{{ invitation.invitation_key }}">registration form</a> for you.
+        for your convenience, we have prepared a pre-filled <a href="https://scipost.org/invitation/{{ object.invitation_key }}">registration form</a> for you.
         After activation of your registration, you will be allowed to contribute, in particular by providing referee reports.
     </p>
     <p>
@@ -25,7 +25,7 @@ Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }}
         we would appreciate a quick accept/decline response from you, ideally within the next 2 days.
     </p>
     <p>
-        If you are <strong>not</strong> able to provide a Report, you can let us know by simply <a href="https://scipost.org/submissions/decline_ref_invitation/{{ invitation.invitation_key }}"> clicking here</a>.
+        If you are <strong>not</strong> able to provide a Report, you can let us know by simply <a href="https://scipost.org/submissions/decline_ref_invitation/{{ object.invitation_key }}"> clicking here</a>.
     </p>
     <p>
         If you are able to provide a Report, you can confirm this after registering and logging in (you will automatically be prompted for a confirmation).
@@ -38,17 +38,17 @@ Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }}
         The SciPost Team
     </p>
 
-{% elif invitation.invitation_type == 'C' %}
+{% elif object.invitation_type == 'C' %}
     {# "Regular" invite #}
-    {% if invitation.citation_notifications.for_publications %}
+    {% if object.citation_notifications.for_publications %}
         <p>
             Your work has been cited in
-            {% if invitation.citation_notifications.for_publications|length > 1 %}{{ invitation.citation_notifications.for_publications|length }} papers{% else %}a paper{% endif %}
+            {% if object.citation_notifications.for_publications|length > 1 %}{{ object.citation_notifications.for_publications|length }} papers{% else %}a paper{% endif %}
             published by SciPost:
         </p>
 
         <ul>
-            {% for notification in invitation.citation_notifications.for_publications %}
+            {% for notification in object.citation_notifications.for_publications %}
                 <li>
                     <a href="https://doi.org/{{ notification.publication.doi_string }}">{{ notification.publication.citation }}</a>
                     <br>
@@ -60,15 +60,15 @@ Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }}
         </ul>
     {% endif %}
 
-    {% if invitation.citation_notifications.for_submissions %}
+    {% if object.citation_notifications.for_submissions %}
         <p>
             Your work has been cited in
-            {% if invitation.citation_notifications.for_submissions|length > 1 %}{{ invitation.citation_notifications.for_submissions|length }} manuscripts{% else %}a manuscript{% endif %}
+            {% if object.citation_notifications.for_submissions|length > 1 %}{{ object.citation_notifications.for_submissions|length }} manuscripts{% else %}a manuscript{% endif %}
             submitted to SciPost,
         </p>
 
         <ul>
-            {% for notification in invitation.citation_notifications.for_submissions %}
+            {% for notification in object.citation_notifications.for_submissions %}
                 <li>
                     {{ notification.submission.title }}
                     <br>
@@ -100,7 +100,7 @@ Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }}
         enabling you to contribute to the site's contents, for example by offering submissions, reports and comments.
     </p>
     <p>
-        For your convenience, a partly pre-filled <a href="https://scipost.org/invitation/{{ invitation.invitation_key }}">registration form</a>
+        For your convenience, a partly pre-filled <a href="https://scipost.org/invitation/{{ object.invitation_key }}">registration form</a>
         has been prepared for you (you can in any case still register at the
         <a href="https://scipost.org/register">registration page</a>).
     </p>
@@ -122,7 +122,7 @@ Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }}
         <br>1098 XH Amsterdam
         <br>The Netherlands
     </p>
-{% elif invitation.invitation_type == 'F' %}
+{% elif object.invitation_type == 'F' %}
     {# Fellow invite #}
     <p>
         You will perhaps have already heard about SciPost, a publication
@@ -160,7 +160,7 @@ Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }}
         Besides looking around the site, you can also personally register
         (to become a Contributor, without necessarily committing to membership
         of the Editorial College, this to be discussed separately) by visiting
-        the following <a href="https://scipost.org/invitation/{{ invitation.invitation_key }}">
+        the following <a href="https://scipost.org/invitation/{{ object.invitation_key }}">
         single-use link</a>, containing a partly pre-filled form for your convenience.
     </p>
     <p>
@@ -192,7 +192,7 @@ Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }}
         <br>
         <br>Prof. dr Jean-Sébastien Caux
         <br>---------------------------------------------
-        <br>Institute for Theoretial Physics
+        <br>Institute for Theoretical Physics
         <br>University of Amsterdam
         <br>Science Park 904
         <br>1098 XH Amsterdam
diff --git a/templates/email/registration_invitation.json b/templates/email/registration_invitation.json
index 4c7426326117b5a762f1fc57575eb9f1954c3fa3..8f69af55581edda8250df25133c1cb3416d6bada 100644
--- a/templates/email/registration_invitation.json
+++ b/templates/email/registration_invitation.json
@@ -1,8 +1,12 @@
 {
     "subject": "SciPost: invitation",
-    "to_address": "email",
-    "bcc_to": "invited_by.email,admin@scipost.org",
-    "from_address_name": "SciPost Registration",
-    "from_address": "registration@scipost.org",
-    "context_object": "invitation"
+    "recipient_list": [
+        "email"
+    ],
+    "bcc": [
+        "invited_by.email",
+        "admin@scipost.org"
+    ],
+    "from_name": "SciPost Registration",
+    "from_email": "registration@scipost.org"
 }
diff --git a/templates/email/tests/test_mail_code_1.html b/templates/email/tests/test_mail_code_1.html
new file mode 100644
index 0000000000000000000000000000000000000000..045ef365826e3319f8999c98141e722c2b8ab8e3
--- /dev/null
+++ b/templates/email/tests/test_mail_code_1.html
@@ -0,0 +1,10 @@
+<h3>Super test title</h3>
+<h4>Object: {{ object }}</h4>
+
+<p>
+    Dear reader,
+    <br>
+    <strong>Thanks for reading this test mail.</strong>
+    <br>
+    <em>¡Salud!</em>
+</p>
diff --git a/templates/email/tests/test_mail_code_1.json b/templates/email/tests/test_mail_code_1.json
new file mode 100644
index 0000000000000000000000000000000000000000..43cae8f8bdafe878a5aed5b91b5b247732e05871
--- /dev/null
+++ b/templates/email/tests/test_mail_code_1.json
@@ -0,0 +1,5 @@
+{
+    "subject": "SciPost Test",
+    "recipient_list": ["test@scipost.org"],
+    "from_email": "admin@scipost.org"
+}
diff --git a/templates/email/tests/test_mail_code_fault_1.json b/templates/email/tests/test_mail_code_fault_1.json
new file mode 100644
index 0000000000000000000000000000000000000000..48ed00ad03592db18fc74d2b1dc7fbec47d8850b
--- /dev/null
+++ b/templates/email/tests/test_mail_code_fault_1.json
@@ -0,0 +1,4 @@
+{
+    "subject": "SciPost Test",
+    "from_email": "admin@scipost.org"
+}
diff --git a/templates/email/tests/test_mail_code_no_template_1.json b/templates/email/tests/test_mail_code_no_template_1.json
new file mode 100644
index 0000000000000000000000000000000000000000..43cae8f8bdafe878a5aed5b91b5b247732e05871
--- /dev/null
+++ b/templates/email/tests/test_mail_code_no_template_1.json
@@ -0,0 +1,5 @@
+{
+    "subject": "SciPost Test",
+    "recipient_list": ["test@scipost.org"],
+    "from_email": "admin@scipost.org"
+}