From 040b27476030a7aa651c6f9ee1eb8ea8c7db22e8 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Fri, 17 Aug 2012 15:40:31 +0200 Subject: [PATCH] Initial commit --- pgmailmgr/.gitignore | 2 + pgmailmgr/__init__.py | 0 pgmailmgr/mailmgr/__init__.py | 0 pgmailmgr/mailmgr/admin.py | 8 ++ pgmailmgr/mailmgr/forms.py | 121 ++++++++++++++++++++ pgmailmgr/mailmgr/models.py | 56 ++++++++++ pgmailmgr/mailmgr/templates/form.html | 16 +++ pgmailmgr/mailmgr/templates/home.html | 51 +++++++++ pgmailmgr/mailmgr/views.py | 76 +++++++++++++ pgmailmgr/manage.py | 14 +++ pgmailmgr/settings.py | 155 ++++++++++++++++++++++++++ pgmailmgr/urls.py | 14 +++ 12 files changed, 513 insertions(+) create mode 100644 pgmailmgr/.gitignore create mode 100644 pgmailmgr/__init__.py create mode 100644 pgmailmgr/mailmgr/__init__.py create mode 100644 pgmailmgr/mailmgr/admin.py create mode 100644 pgmailmgr/mailmgr/forms.py create mode 100644 pgmailmgr/mailmgr/models.py create mode 100644 pgmailmgr/mailmgr/templates/form.html create mode 100644 pgmailmgr/mailmgr/templates/home.html create mode 100644 pgmailmgr/mailmgr/views.py create mode 100755 pgmailmgr/manage.py create mode 100644 pgmailmgr/settings.py create mode 100644 pgmailmgr/urls.py diff --git a/pgmailmgr/.gitignore b/pgmailmgr/.gitignore new file mode 100644 index 0000000..3ae5d07 --- /dev/null +++ b/pgmailmgr/.gitignore @@ -0,0 +1,2 @@ +*.pyc +settings_local.py diff --git a/pgmailmgr/__init__.py b/pgmailmgr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgmailmgr/mailmgr/__init__.py b/pgmailmgr/mailmgr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgmailmgr/mailmgr/admin.py b/pgmailmgr/mailmgr/admin.py new file mode 100644 index 0000000..467f932 --- /dev/null +++ b/pgmailmgr/mailmgr/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from models import * + +admin.site.register(LocalDomain) +admin.site.register(Forwarder) +admin.site.register(VirtualUser) +admin.site.register(UserPermissions) diff --git a/pgmailmgr/mailmgr/forms.py b/pgmailmgr/mailmgr/forms.py new file mode 100644 index 0000000..4bf44f5 --- /dev/null +++ b/pgmailmgr/mailmgr/forms.py @@ -0,0 +1,121 @@ +from django import forms +from django.forms import ValidationError +from django.db import connection + +from models import * + +class VirtualUserForm(forms.ModelForm): + class Meta: + model = VirtualUser + + def __init__(self, data=None, instance=None, user=None): + super(VirtualUserForm, self).__init__(data=data, instance=instance) + self.user = user + + def clean_local_domain(self): + if not self.instance.pk: return self.cleaned_data['local_domain'] + if self.cleaned_data['local_domain'] != self.instance.local_domain: + raise ValidationError("Can't change local domain!") + return self.cleaned_data['local_domain'] + + def clean_local_part(self): + if not self.instance.pk: return self.cleaned_data['local_part'] + if self.cleaned_data['local_part'] != self.instance.local_part: + raise ValidationError("Renaming accounts is not possible - you have to delete and add!") + return self.cleaned_data['local_part'] + + def clean_mail_quota(self): + if self.cleaned_data['mail_quota'] <= 1: + raise ValidationError("Mail quota must be set") + return self.cleaned_data['mail_quota'] + + def clean_passwd(self): + if self.cleaned_data['passwd'] != self.instance.passwd: + # Changing password requires calling pgcrypto. So let's do that... + curs = connection.cursor() + curs.execute("SELECT public.crypt(%(pwd)s, public.gen_salt('md5'))", { + 'pwd': self.cleaned_data['passwd'] + }) + return curs.fetchall()[0][0] + + return self.cleaned_data['passwd'] + + def clean(self): + if not self.cleaned_data.has_key('local_part'): + return {} + + # Validate that the pattern is allowed + curs = connection.cursor() + curs.execute("SELECT 1 FROM mailmgr_userpermissions WHERE user_id=%(uid)s AND domain_id=%(domain)s AND %(lp)s ~* ('^'||pattern||'$')", { + 'uid': self.user.pk, + 'domain': self.cleaned_data['local_domain'].pk, + 'lp': self.cleaned_data['local_part'], + }) + perms = curs.fetchall() + + if len(perms) < 1: + raise ValidationError("Permission denied to create that user for that domain!") + + # If it's a new user, also check against if it already exists + if not self.instance.pk: + old = VirtualUser.objects.filter(local_part=self.cleaned_data['local_part'], local_domain=self.cleaned_data['local_domain']) + if len(old): + raise ValidationError("A user with that name already exists in that domain!") + + # Make sure we can't get a collision with a forwarding + forwarders = Forwarder.objects.filter(local_part=self.cleaned_data['local_part'], local_domain=self.cleaned_data['local_domain']) + if len(forwarders): + raise ValidationError("A forwarder with that name already exists in that domain!") + + return self.cleaned_data + + + +class ForwarderForm(forms.ModelForm): + class Meta: + model = Forwarder + + def __init__(self, data=None, instance=None, user=None): + super(ForwarderForm, self).__init__(data=data, instance=instance) + self.user = user + + def clean_local_domain(self): + if not self.instance.pk: return self.cleaned_data['local_domain'] + if self.cleaned_data['local_domain'] != self.instance.local_domain: + raise ValidationError("Can't change local domain!") + return self.cleaned_data['local_domain'] + + def clean_local_part(self): + if not self.instance.pk: return self.cleaned_data['local_part'] + if self.cleaned_data['local_part'] != self.instance.local_part: + raise ValidationError("Renaming forwarders is not possible - you have to delete and add!") + return self.cleaned_data['local_part'] + + def clean(self): + if not self.cleaned_data.has_key('local_part'): + return {} + + # Validate that the pattern is allowed + curs = connection.cursor() + curs.execute("SELECT 1 FROM mailmgr_userpermissions WHERE user_id=%(uid)s AND domain_id=%(domain)s AND %(lp)s ~* ('^'||pattern||'$')", { + 'uid': self.user.pk, + 'domain': self.cleaned_data['local_domain'].pk, + 'lp': self.cleaned_data['local_part'], + }) + perms = curs.fetchall() + + if len(perms) < 1: + raise ValidationError("Permission denied to create that forwarder for that domain!") + + # If it's a new user, also check against if it already exists + if not self.instance.pk: + old = Forwarder.objects.filter(local_part=self.cleaned_data['local_part'], local_domain=self.cleaned_data['local_domain']) + if len(old): + raise ValidationError("A forwarder with that name already exists in that domain!") + + # Make sure we can't get a collision with a user + users = VirtualUser.objects.filter(local_part=self.cleaned_data['local_part'], local_domain=self.cleaned_data['local_domain']) + if len(users): + raise ValidationError("A user with that name already exists in that domain!") + + return self.cleaned_data diff --git a/pgmailmgr/mailmgr/models.py b/pgmailmgr/mailmgr/models.py new file mode 100644 index 0000000..14257bd --- /dev/null +++ b/pgmailmgr/mailmgr/models.py @@ -0,0 +1,56 @@ +from django.db import models +from django.contrib.auth.models import User + +class LocalDomain(models.Model): + local_domain_id = models.AutoField(null=False, primary_key=True) + domain_name = models.CharField(max_length=100, null=False, blank=False) + path = models.CharField(max_length=512, null=False, blank=False) + unix_user = models.IntegerField(null=False, blank=False, default=0) + unix_group = models.IntegerField(null=False, blank=False, default=0) + + def __unicode__(self): + return self.domain_name + + class Meta: + ordering=('domain_name',) + db_table='mail"."local_domains' + managed=False + +class Forwarder(models.Model): + forwarder_id = models.AutoField(null=False, primary_key=True) + local_part = models.CharField(max_length=100, null=False, blank=False) + local_domain = models.ForeignKey(LocalDomain, null=False, blank=False, db_column='local_domain_id') + remote_name = models.CharField(max_length=200, null=False, blank=False) + + def __unicode__(self): + return "%s@%s -> %s" % (self.local_part, self.local_domain.domain_name, self.remote_name) + + class Meta: + ordering=('local_part',) + db_table='mail"."forwarder' + managed=False + +class VirtualUser(models.Model): + virtual_user_id = models.AutoField(null=False, primary_key=True) + local_domain = models.ForeignKey(LocalDomain, null=False, blank=False, db_column='local_domain_id') + local_part = models.CharField(max_length=100, null=False, blank=False) + mail_quota = models.IntegerField(null=False) + passwd = models.CharField(max_length=100, null=False, blank=False, verbose_name="Password") + full_name = models.CharField(max_length=200, null=False, blank=True) + + def __unicode__(self): + return "%s@%s (%s)" % (self.local_part, self.local_domain.domain_name, self.full_name or '') + + class Meta: + ordering=('local_part',) + db_table='mail"."virtual_user' + managed=False + unique_together=('local_domain', 'local_part', ) + +class UserPermissions(models.Model): + user = models.ForeignKey(User, null=False) + domain = models.ForeignKey(LocalDomain, null=False) + pattern = models.CharField(max_length=100, null=False, blank=False) + + def __unicode__(self): + return "%s -> %s pattern '%s'" % (self.user, self.domain, self.pattern) diff --git a/pgmailmgr/mailmgr/templates/form.html b/pgmailmgr/mailmgr/templates/form.html new file mode 100644 index 0000000..35f9c95 --- /dev/null +++ b/pgmailmgr/mailmgr/templates/form.html @@ -0,0 +1,16 @@ + + +Edit + + +

Edit

+ +
+{% csrf_token %} + +{{form.as_table}} +
+ +
+ + diff --git a/pgmailmgr/mailmgr/templates/home.html b/pgmailmgr/mailmgr/templates/home.html new file mode 100644 index 0000000..514edd8 --- /dev/null +++ b/pgmailmgr/mailmgr/templates/home.html @@ -0,0 +1,51 @@ + + + Mail manager + + +

Mail manager

+ +{%if messages%} + +{%endif%} + +

Users

+ + + + + + +{%for u in users%} + + + + + +{%endfor%} +
Local partDomainFull name
{{u.local_part}}{{u.local_domain}}{{u.full_name|default:''}}
+Add new. + +

Forwardings

+ + + + + + +{%for f in forwarders%} + + + + + +{%endfor%} +
Local partDomainRemote name
{{f.local_part}}{{f.local_domain}}{{f.remote_name}}
+Add new. + + + diff --git a/pgmailmgr/mailmgr/views.py b/pgmailmgr/mailmgr/views.py new file mode 100644 index 0000000..468472b --- /dev/null +++ b/pgmailmgr/mailmgr/views.py @@ -0,0 +1,76 @@ +from django.shortcuts import render_to_response, get_object_or_404 +from django.http import HttpResponseRedirect +from django.contrib.auth.decorators import login_required +from django.template import RequestContext +from django.contrib import messages + + +from models import * +from forms import * + +@login_required +def home(request): + users = 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]) + forwards = 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]) + + return render_to_response('home.html', { + 'users': users, + 'forwarders': forwards, + }, RequestContext(request)) + +@login_required +def userform(request, userparam): + if userparam == 'add': + vu = VirtualUser() + else: + vulist = VirtualUser.objects.filter(pk=userparam).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]) + if len(vulist) != 1: + raise Http404("Not found or no permissions!") + vu = vulist[0] + + if request.method == 'POST': + form = VirtualUserForm(data=request.POST, instance=vu, user=request.user) + if request.POST['passwd'] != vu.passwd: + password_changed=True + else: + password_changed=False + if form.is_valid(): + form.save() + messages.add_message(request, messages.INFO, 'User %s updated' % vu) + if password_changed: + messages.add_message(request, messages.INFO, 'Password changed for user %s' % vu) + return HttpResponseRedirect('/') + else: + # Generate a new form + form = VirtualUserForm(instance=vu, user=request.user) + + return render_to_response('form.html', { + 'form': form, + 'savebutton': (userparam == 'new') and "New" or "Save" + }, RequestContext(request)) + +@login_required +def forwarderform(request, userparam): + if userparam == 'add': + fwd = Forwarder() + else: + fwdlist = Forwarder.objects.filter(pk=userparam).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]) + if len(fwdlist) != 1: + raise Http404("Not found or no permissions!") + fwd = fwdlist[0] + + if request.method == 'POST': + form = ForwarderForm(data=request.POST, instance=fwd, user=request.user) + if form.is_valid(): + form.save() + messages.add_message(request, messages.INFO, 'Forwarder %s updated' % fwd) + return HttpResponseRedirect('/') + else: + # Generate a new form + form = ForwarderForm(instance=fwd, user=request.user) + + return render_to_response('form.html', { + 'form': form, + 'savebutton': (userparam == 'new') and "New" or "Save" + }, RequestContext(request)) + diff --git a/pgmailmgr/manage.py b/pgmailmgr/manage.py new file mode 100755 index 0000000..3e4eedc --- /dev/null +++ b/pgmailmgr/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +import imp +try: + imp.find_module('settings') # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) + sys.exit(1) + +import settings + +if __name__ == "__main__": + execute_manager(settings) diff --git a/pgmailmgr/settings.py b/pgmailmgr/settings.py new file mode 100644 index 0000000..9e1edf8 --- /dev/null +++ b/pgmailmgr/settings.py @@ -0,0 +1,155 @@ +# Django settings for pgmailmgr project. + +ADMINS = ( + ('PostgreSQL webmaster', 'webmaster@postgresql.org'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'pgmail', # Or path to database file if using sqlite3. + 'USER': 'pgmail', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'GMT' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = False + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = False + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +#SECRET_KEY lives in settings_local.py + + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'pgmailmgr.urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + # Uncomment the next line to enable the admin: + 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', + 'pgmailmgr.mailmgr', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +AUTHENTICATION_BACKENDS = ( + 'util.auth.AuthBackend', +) + +SESSION_COOKIE_SECURE= True +SESSION_COOKIE_DOMAIN="webmail.postgresql.org" +from settings_local import * diff --git a/pgmailmgr/urls.py b/pgmailmgr/urls.py new file mode 100644 index 0000000..d4c3d29 --- /dev/null +++ b/pgmailmgr/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls.defaults import patterns, include, url + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + url(r'^$', 'pgmailmgr.mailmgr.views.home'), + url(r'^user/(\d+|add)/$', 'pgmailmgr.mailmgr.views.userform'), + url(r'^forwarder/(\d+|add)/$', 'pgmailmgr.mailmgr.views.forwarderform'), + + # Uncomment the next line to enable the admin: + url(r'^admin/', include(admin.site.urls)), +) -- 2.39.5