--- /dev/null
+*.pyc
+settings_local.py
--- /dev/null
+from django.contrib import admin
+
+from models import *
+
+admin.site.register(LocalDomain)
+admin.site.register(Forwarder)
+admin.site.register(VirtualUser)
+admin.site.register(UserPermissions)
--- /dev/null
+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
--- /dev/null
+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)
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+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))
+
--- /dev/null
+#!/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)
--- /dev/null
+# Django settings for pgmailmgr project.
+
+ADMINS = (
+)
+
+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 *
--- /dev/null
+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)),
+)