summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMagnus Hagander2025-06-02 12:54:46 +0000
committerMagnus Hagander2025-06-02 12:54:46 +0000
commit197fb34fa458178d9bde9f11f304c60f02579243 (patch)
tree82e31cd86b808b712bab151f4f3b26603ac869cc
parentc16a71968443c094312334641da11290bef51d83 (diff)
Add community account to mailboxes, and self service password change
Instead of just having pattern based permissions, also make it possible to attach a community account to an individual mailbox. If this is done, that account can then manage their own password without having pattern based access, as a simple self-service. The only thing they can change is the password, other attributes are still managed by the admins.
-rw-r--r--pgmailmgr/mailmgr/forms.py64
-rw-r--r--pgmailmgr/mailmgr/models.py5
-rw-r--r--pgmailmgr/mailmgr/templates/home.html22
-rw-r--r--pgmailmgr/mailmgr/templates/mailbox.html26
-rw-r--r--pgmailmgr/mailmgr/views.py34
-rw-r--r--pgmailmgr/urls.py3
6 files changed, 151 insertions, 3 deletions
diff --git a/pgmailmgr/mailmgr/forms.py b/pgmailmgr/mailmgr/forms.py
index b541291..2b6521a 100644
--- a/pgmailmgr/mailmgr/forms.py
+++ b/pgmailmgr/mailmgr/forms.py
@@ -1,19 +1,67 @@
from django import forms
from django.forms import ValidationError
from django.db import connection
+from django.contrib.auth.password_validation import validate_password, MinimumLengthValidator, UserAttributeSimilarityValidator
+from django.contrib.auth.password_validation import CommonPasswordValidator, NumericPasswordValidator
+
+from pgmailmgr.auth import user_search, user_import
from .models import *
+class AccountEmailWidget(forms.TextInput):
+ def __init__(self, account):
+ self.account = account
+ super().__init__()
+
+ def get_context(self, name, value, attrs):
+ c = super().get_context(name, value, attrs)
+ c['widget']['value'] = self.account and self.account.email or ''
+ c['widget']['attrs']['placeholder'] = 'Enter email address of a community account to connect to'
+ return c
+
+
+class password_validator:
+ def __init__(self, mailbox):
+ self.mailbox = mailbox
+
+ def __call__(self, password):
+ return validate_password(password, self.mailbox, password_validators=[
+ MinimumLengthValidator(10),
+ CommonPasswordValidator(),
+ NumericPasswordValidator(),
+ UserAttributeSimilarityValidator(user_attributes=['local_part', 'full_name']),
+ ])
+
+
+class PasswordChangeForm(forms.Form):
+ password = forms.CharField(widget=forms.PasswordInput)
+ verify_password = forms.CharField(widget=forms.PasswordInput)
+
+ def __init__(self, *args, **kwargs):
+ mailbox = kwargs.pop('mailbox')
+ super().__init__(*args, **kwargs)
+
+ self.fields['password'].validators = [password_validator(mailbox)]
+
+ def clean(self):
+ d = super().clean()
+ if 'password' in d and d.get('password') != d.get('verify_password', None):
+ self.add_error('verify_password', "Passwords don't match")
+ return d
+
+
class VirtualUserForm(forms.ModelForm):
class Meta:
model = VirtualUser
- fields = ('local_domain', 'local_part', 'mail_quota', 'passwd', 'full_name')
+ fields = ('local_domain', 'local_part', 'mail_quota', 'passwd', 'full_name', 'account')
def __init__(self, data=None, instance=None, user=None):
super(VirtualUserForm, self).__init__(data=data, instance=instance)
self.user = user
+ self.fields['account'] = forms.CharField(max_length=200, widget=AccountEmailWidget(instance.account), required=False)
+
def clean_local_domain(self):
if not self.instance.pk:
return self.cleaned_data['local_domain']
@@ -44,6 +92,20 @@ class VirtualUserForm(forms.ModelForm):
return self.cleaned_data['passwd']
+ def clean_account(self):
+ a = self.cleaned_data['account']
+ if a == '':
+ return None
+
+ try:
+ return User.objects.get(email=a)
+ except User.DoesNotExist:
+ # Import the user if we can
+ users = user_search(searchterm=a)
+ if len(users) == 1 and users[0]['e'] == a:
+ return user_import(users[0]['u'])
+ raise ValidationError("User not found")
+
def clean(self):
if 'local_part' not in self.cleaned_data:
return {}
diff --git a/pgmailmgr/mailmgr/models.py b/pgmailmgr/mailmgr/models.py
index e8d944f..61c1cbd 100644
--- a/pgmailmgr/mailmgr/models.py
+++ b/pgmailmgr/mailmgr/models.py
@@ -45,10 +45,15 @@ class VirtualUser(models.Model):
mail_quota = models.IntegerField(null=False, default=500)
passwd = models.CharField(max_length=100, null=False, blank=False, verbose_name="Password")
full_name = models.CharField(max_length=200, null=False, blank=True)
+ account = models.ForeignKey(User, null=True, blank=True, verbose_name="Connected user", on_delete=models.SET_NULL)
def __str__(self):
return "%s@%s (%s)" % (self.local_part, self.local_domain.domain_name, self.full_name or '')
+ @property
+ def fulladdr(self):
+ return "%s@%s" % (self.local_part, self.local_domain.domain_name)
+
trigger_update = True
class Meta:
diff --git a/pgmailmgr/mailmgr/templates/home.html b/pgmailmgr/mailmgr/templates/home.html
new file mode 100644
index 0000000..8dff238
--- /dev/null
+++ b/pgmailmgr/mailmgr/templates/home.html
@@ -0,0 +1,22 @@
+{%extends "base.html" %}
+
+{%block content%}
+
+<h2>Mailboxes</h2>
+{% if mailboxes %}
+<ul>
+{% for m in mailboxes %}
+ <li><a href="{{m.virtual_user_id}}/">{{m.fulladdr}}</a></li>
+{% endfor %}
+</ul>
+{% else %}
+<p>
+ You have no mailbox here.
+</p>
+{% endif %}
+
+{%if admperm %}
+<a href="/https/git.postgresql.org/adm/" class="btn btn-primary">Administer other users and forwarders</a>
+{%endif%}
+
+{%endblock%}
diff --git a/pgmailmgr/mailmgr/templates/mailbox.html b/pgmailmgr/mailmgr/templates/mailbox.html
new file mode 100644
index 0000000..28a0235
--- /dev/null
+++ b/pgmailmgr/mailmgr/templates/mailbox.html
@@ -0,0 +1,26 @@
+{%extends "base.html" %}
+{%block content%}
+<h1>{{mailbox.fulladdr}}</h1>
+
+<h2>Change password</h2>
+
+<form method="post" action=".">
+{% csrf_token %}
+{%if form.non_field_errors%}
+ <div class="alert alert-danger">{{form.non_field_errors}}</div>
+{%endif%}
+{%for field in form%}
+{%include "form_field.html"%}
+{%endfor%}
+
+ <div class="form-group">
+ <div class="col-lg-12">
+ <div class="control">
+ <input type="submit" name="submit" class="col-md-2 mb-2 mr-2 btn btn-primary" value="Change password">
+ <a class="col-md-2 mb-2 mr-2 btn btn-secondary" href="/">Cancel</a>
+ </div>
+ </div>
+ </div>
+
+</form>
+{%endblock%}
diff --git a/pgmailmgr/mailmgr/views.py b/pgmailmgr/mailmgr/views.py
index 41b6df6..343bdb1 100644
--- a/pgmailmgr/mailmgr/views.py
+++ b/pgmailmgr/mailmgr/views.py
@@ -18,7 +18,39 @@ def log(user, what):
@login_required
def home(request):
- return None
+ admperm = VirtualUser.objects.extra(where=["EXISTS (SELECT 1 FROM mailmgr_userpermissions p WHERE p.user_id=%s AND p.domain_id = local_domain_id AND local_part ~* ('^'||p.pattern||'$'))" % request.user.id]).exists() or \
+ Forwarder.objects.extra(where=["EXISTS (SELECT 1 FROM mailmgr_userpermissions p WHERE p.user_id=%s AND p.domain_id = local_domain_id AND local_part ~* ('^'||p.pattern||'$'))" % request.user.id]).exists()
+ mailboxes = VirtualUser.objects.filter(account=request.user.id)
+
+ return render(request, 'home.html', {
+ 'admperm': admperm,
+ 'mailboxes': mailboxes,
+ })
+
+
+@login_required
+def mailbox(request, virtualid):
+ mailbox = get_object_or_404(VirtualUser, virtual_user_id=virtualid, account=request.user.id)
+
+ if request.method == 'POST':
+ form = PasswordChangeForm(data=request.POST, mailbox=mailbox)
+ if form.is_valid():
+ with transaction.atomic():
+ curs = connection.cursor()
+ curs.execute("UPDATE mail.virtual_user SET passwd=public.crypt(%(pwd)s, public.gen_salt('md5')) WHERE virtual_user_id=%(id)s", {
+ 'id': mailbox.virtual_user_id,
+ 'pwd': form.cleaned_data['password'],
+ })
+ log(request.user, "Changed password for own mailbox {}".format(mailbox.fulladdr))
+ messages.info(request, "Changed password of {}".format(mailbox.fulladdr))
+ return HttpResponseRedirect('/')
+ else:
+ form = PasswordChangeForm(mailbox=mailbox)
+
+ return render(request, 'mailbox.html', {
+ 'mailbox': mailbox,
+ 'form': form,
+ })
@login_required
diff --git a/pgmailmgr/urls.py b/pgmailmgr/urls.py
index 8876ef2..882cf21 100644
--- a/pgmailmgr/urls.py
+++ b/pgmailmgr/urls.py
@@ -8,7 +8,8 @@ from django.contrib import admin
admin.autodiscover()
urlpatterns = [
- re_path(r'^/$', views.home),
+ re_path(r'^$', views.home),
+ re_path(r'^(\d+)/$', views.mailbox),
re_path(r'^adm/$', views.adm_home),
re_path(r'^adm/user/(\d+|add)/$', views.userform),
re_path(r'^adm/forwarder/(\d+|add)/$', views.forwarderform),