Initial commit
authorMagnus Hagander <[email protected]>
Fri, 17 Aug 2012 13:40:31 +0000 (15:40 +0200)
committerMagnus Hagander <[email protected]>
Fri, 17 Aug 2012 13:40:31 +0000 (15:40 +0200)
12 files changed:
pgmailmgr/.gitignore[new file with mode: 0644]
pgmailmgr/__init__.py[new file with mode: 0644]
pgmailmgr/mailmgr/__init__.py[new file with mode: 0644]
pgmailmgr/mailmgr/admin.py[new file with mode: 0644]
pgmailmgr/mailmgr/forms.py[new file with mode: 0644]
pgmailmgr/mailmgr/models.py[new file with mode: 0644]
pgmailmgr/mailmgr/templates/form.html[new file with mode: 0644]
pgmailmgr/mailmgr/templates/home.html[new file with mode: 0644]
pgmailmgr/mailmgr/views.py[new file with mode: 0644]
pgmailmgr/manage.py[new file with mode: 0755]
pgmailmgr/settings.py[new file with mode: 0644]
pgmailmgr/urls.py[new file with mode: 0644]

diff --git a/pgmailmgr/.gitignore b/pgmailmgr/.gitignore
new file mode 100644 (file)
index 0000000..3ae5d07
--- /dev/null
@@ -0,0 +1,2 @@
+*.pyc
+settings_local.py
diff --git a/pgmailmgr/__init__.py b/pgmailmgr/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pgmailmgr/mailmgr/__init__.py b/pgmailmgr/mailmgr/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pgmailmgr/mailmgr/admin.py b/pgmailmgr/mailmgr/admin.py
new file mode 100644 (file)
index 0000000..467f932
--- /dev/null
@@ -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 (file)
index 0000000..4bf44f5
--- /dev/null
@@ -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 (file)
index 0000000..14257bd
--- /dev/null
@@ -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 (file)
index 0000000..35f9c95
--- /dev/null
@@ -0,0 +1,16 @@
+<html>
+<head>
+<title>Edit</title>
+</head>
+<body>
+<h1>Edit</h1>
+
+<form method="post" action=".">
+{% csrf_token %}
+<table>
+{{form.as_table}}
+</table>
+<input type="submit" value="{{savebutton}}">
+</form>
+</body>
+</html>
diff --git a/pgmailmgr/mailmgr/templates/home.html b/pgmailmgr/mailmgr/templates/home.html
new file mode 100644 (file)
index 0000000..514edd8
--- /dev/null
@@ -0,0 +1,51 @@
+<html>
+<head>
+ <title>Mail manager</title>
+</head>
+<body>
+ <h1>Mail manager</h1>
+
+{%if messages%}
+<ul class="messages">
+    {% for message in messages %}
+    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
+    {% endfor %}
+</ul>
+{%endif%}
+
+ <h2>Users</h2>
+<table border="1" cellspacing="0" cellpadding="1">
+<tr>
+ <th>Local part</th>
+ <th>Domain</th>
+ <th>Full name</th>
+</tr>
+{%for u in users%}
+<tr>
+ <td><a href="/user/{{u.pk}}/">{{u.local_part}}</a></td>
+ <td>{{u.local_domain}}</td>
+ <td>{{u.full_name|default:''}}</td>
+</tr>
+{%endfor%}
+</table>
+<a href="/user/add/">Add</a> new.
+
+<h2>Forwardings</h2>
+<table border="1" cellspacing="0" cellpadding="1">
+<tr>
+ <th>Local part</th>
+ <th>Domain</th>
+ <th>Remote name</th>
+</tr>
+{%for f in forwarders%}
+<tr>
+ <td><a href="/forwarder/{{f.pk}}/">{{f.local_part}}</a></td>
+ <td>{{f.local_domain}}</td>
+ <td>{{f.remote_name}}</td>
+</tr>
+{%endfor%}
+</table>
+<a href="/forwarder/add/">Add</a> new.
+
+</body>
+</html>
diff --git a/pgmailmgr/mailmgr/views.py b/pgmailmgr/mailmgr/views.py
new file mode 100644 (file)
index 0000000..468472b
--- /dev/null
@@ -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 (executable)
index 0000000..3e4eedc
--- /dev/null
@@ -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 (file)
index 0000000..9e1edf8
--- /dev/null
@@ -0,0 +1,155 @@
+# Django settings for pgmailmgr project.
+
+ADMINS = (
+       ('PostgreSQL webmaster', '[email protected]'),
+)
+
+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 (file)
index 0000000..d4c3d29
--- /dev/null
@@ -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)),
+)