From 6c67aa7093b9e3bdf48fd30288b1ee21a0d70618 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 26 Mar 2009 18:20:19 +0100 Subject: [PATCH] Models modification for better integration with third party softwares (ie. Postfix and others). Some bugfixes. Added documentation. --- INSTALL | 19 ++++- admin/forms.py | 4 +- admin/models.py | 64 +++++++++++++++- admin/views.py | 127 +++++++++++++++++++++---------- lib/__init__.py | 34 +++++++++ lib/fsops.py | 69 ----------------- templates/admin/aliases.html | 2 +- templates/admin/editalias.html | 1 + templates/admin/editdomain.html | 1 + templates/admin/editmailbox.html | 1 + templates/admin/newalias.html | 1 + 11 files changed, 203 insertions(+), 120 deletions(-) delete mode 100644 lib/fsops.py diff --git a/INSTALL b/INSTALL index f42717d87..25d894cd2 100644 --- a/INSTALL +++ b/INSTALL @@ -36,19 +36,28 @@ http://:8000/mailng/admin/. Tips to link Mailng with postfix -------------------------------- -First, define following maps on your server:: +First, define following maps on your server (this should work with +Postfix versions >= 2.2):: # /etc/postfix/sql-domains.cf user = password = dbname = - query = SELECT name FROM domains WHERE name='%s' AND enabled='1' + query = SELECT name FROM admin_domain WHERE name='%s' AND enabled='1' # /etc/postfix/sql-mailboxes.cf user = password = dbname = - query = SELECT path FROM mailboxes WHERE address = '%s' AND enabled = '1' + query = SELECT path FROM admin_mailbox WHERE full_address = '%s' AND enabled = '1' + +# /etc/postfix/sql-aliases.cf + user = + password = + dbname = + query = SELECT t2.full_address FROM admin_alias AS t1, admin_mailbox AS t2 + WHERE t1.full_address='%s' AND t1.mbox=t2.id + AND t1.enabled='1' Then, use the following configuration in your /etc/postfix/main.cf file (this is one possible configuration):: @@ -65,6 +74,8 @@ Then, use the following configuration in your /etc/postfix/main.cf file relay_domains = mysql:/etc/postfix/sql-domains.cf virtual_mailbox_domains = mysql:/etc/postfix/sql-domains.cf virtual_mailbox_maps = mysql:/etc/postfix/sql-mailboxes.cf - virtual_alias_domains = + virtual_alias_maps = mysql:/etc/postfix/sql-aliases.cf # Stuff after +NB: Mailng only supports maildir format for now. Others will surely be +added soon. diff --git a/admin/forms.py b/admin/forms.py index d24370960..b2fb7dc5c 100644 --- a/admin/forms.py +++ b/admin/forms.py @@ -6,13 +6,14 @@ class DomainForm(forms.ModelForm): model = Domain class MailboxForm(forms.ModelForm): + quota = forms.IntegerField(label="Quota", required=False) password1 = forms.CharField(label="Password", widget=forms.PasswordInput) password2 = forms.CharField(label="Confirmation", widget=forms.PasswordInput) enabled = forms.BooleanField(label="Enabled", required=False) class Meta: model = Mailbox - fields = ('name', 'address', 'quota') + fields = ('name', 'address') def clean_password2(self): if self.cleaned_data["password1"] != self.cleaned_data["password2"]: @@ -31,4 +32,5 @@ class AliasForm(forms.ModelForm): class Meta: model = Alias + fields = ('address', 'mbox', 'enabled') diff --git a/admin/models.py b/admin/models.py index 1ab12639a..39a21e2a6 100644 --- a/admin/models.py +++ b/admin/models.py @@ -1,30 +1,86 @@ from django.db import models from django.contrib.auth.models import User - -# Create your models here. +from django.conf import settings +from mailng.lib import exec_pipe +import os class Domain(models.Model): name = models.CharField(max_length=100) quota = models.IntegerField() enabled = models.BooleanField() + def create_dir(self): + code, output = exec_pipe("sudo -u %s mkdir %s/%s" \ + % (settings.VIRTUAL_UID, settings.STORAGE_PATH, + self.name)) + if code: + os.system("echo '%s' >> /tmp/vmail.log" % output) + return False + return True + + def rename_dir(self, newname): + code, output = exec_pipe("sudo -u %s mv %s/%s %s/%s" \ + % (settings.VIRTUAL_UID, + settings.STORAGE_PATH, self.name, + settings.STORAGE_PATH, newname)) + if code: + os.system("echo '%s' >> /tmp/vmail.log" % output) + return False + return True + + def delete_dir(self): + code, output = exec_pipe("sudo -u %s rm -r %s/%s" \ + % (settings.VIRTUAL_UID, settings.STORAGE_PATH, + self.name)) + if code: + return False + return True + class Mailbox(models.Model): name = models.CharField(max_length=100) address = models.CharField(max_length=100) -# password = models.CharField(max_length=100) + full_address = models.CharField(max_length=150) quota = models.IntegerField() -# enabled = models.BooleanField() uid = models.IntegerField() gid = models.IntegerField() path = models.CharField(max_length=200) domain = models.ForeignKey(Domain) user = models.ForeignKey(User) + path = models.CharField(max_length=255) def __str__(self): return "%s" % (self.address) + def create_dir(self, domain): + path = "%s/%s/%s" % (settings.STORAGE_PATH, domain.name, self.address) + code, output = exec_pipe("sudo -u %s mkdir -p %s/.maildir" \ + % (settings.VIRTUAL_UID, path)) + if code: + return False + self.path = "%s/.maildir/" % path + return True + + def rename_dir(self, domain, newaddress): + path = "%s/%s" % (settings.STORAGE_PATH, domain) + code, output = exec_pipe("sudo -u %s mv %s/%s %s/%s" \ + % (settings.VIRTUAL_UID, + path, self.address, path, newaddress)) + if code: + os.system("echo '%s' >> /tmp/vmail.log" % output) + return False + self.path = "%s/%s/.maildir/" % (path, newaddress) + return True + + def delete_dir(self): + code, output = exec_pipe("sudo -u %s rm -r %s/%s/%s" \ + % (settings.VIRTUAL_UID, settings.STORAGE_PATH, + self.domain.name, self.name)) + if code: + return False + return True class Alias(models.Model): address = models.CharField(max_length=100) + full_address = models.CharField(max_length=150) mbox = models.ForeignKey(Mailbox) enabled = models.BooleanField() diff --git a/admin/views.py b/admin/views.py index 18a900e36..a3b3c8aed 100644 --- a/admin/views.py +++ b/admin/views.py @@ -8,7 +8,6 @@ from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from mailng import admin -from mailng.lib.fsops import * from mailng.admin.models import Domain, Mailbox, Alias from forms import MailboxForm, DomainForm, AliasForm @@ -41,7 +40,8 @@ def newdomain(request): if Domain.objects.filter(name=request.POST["name"]): error = "Domain with this name already defined" else: - if create_domain(request.POST["name"]): + domain = form.save(commit=False) + if domain.create_dir(): form.save() ctx = _ctx_ok(reverse(admin.views.domains)) return HttpResponse(simplejson.dumps(ctx), @@ -62,12 +62,20 @@ def editdomain(request, dom_id): domain = Domain.objects.get(pk=dom_id) if request.method == "POST": form = DomainForm(request.POST, instance=domain) + error = None if form.is_valid(): - form.save() - ctx = _ctx_ok(reverse(admin.views.domains)) - return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + if domain.name != request.POST["name"]: + if Domain.objects.filter(name=request.POST["name"]): + error = "Domain with this name already defined" + else: + if not domain.rename_dir(request.POST["name"]): + error = "Failed to rename domain, check permissions" + if not error: + form.save() + ctx = _ctx_ok(reverse(admin.views.domains)) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") ctx = _ctx_ko("admin/editdomain.html", { - "domain" : domain, "form" : form + "domain" : domain, "form" : form, "error" : error }) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @@ -79,7 +87,7 @@ def editdomain(request, dom_id): @login_required def deldomain(request, dom_id): domain = Domain.objects.get(pk=dom_id) - destroy_domain(domain.name) + domain.delete_dir() domain.delete() return HttpResponseRedirect('/mailng/admin/') @@ -102,8 +110,8 @@ def newmailbox(request, dom_id=None): domain=dom_id): error = "Mailbox with this address already exists" else: - if create_mailbox(domain.name, request.POST["address"]): - mb = form.save(commit=False) + mb = form.save(commit=False) + if mb.create_dir(domain): user = User() user.username = user.email = "%s@%s" % (mb.address, domain.name) user.set_password(request.POST["password1"]) @@ -111,13 +119,16 @@ def newmailbox(request, dom_id=None): and True or False fname, lname = mb.name.split() user.first_name = fname - user.last_name = lname user.save() mb.user = user - + mb.uid = mb.gid = 500 mb.domain = domain + mb.quota = request.POST["quota"] + if not mb.quota: + mb.quota = domain.quota + mb.full_address = "%s@%s" % (mb.address, domain.name) mb.save() ctx = _ctx_ok(reverse(admin.views.mailboxes, args=[domain.id])) return HttpResponse(simplejson.dumps(ctx), @@ -138,22 +149,39 @@ def editmailbox(request, dom_id, mbox_id=None): mb = Mailbox.objects.get(pk=mbox_id) if request.method == "POST": form = MailboxForm(request.POST, instance=mb) + error = None if form.is_valid(): - if request.POST["enabled"] != mb.user.is_active \ - or request.POST["password1"] != mb.user.password: - mb.user.is_active = request.POST["enabled"] - mb.user.password = request.POST["password1"] - mb.user.save() - form.save() - ctx = _ctx_ok(reverse(admin.views.mailboxes, args=[dom_id])) - else: - ctx = _ctx_ko("admin/editmailbox.html", { - "mbox" : mb, "form" : form - }) - json = simplejson.dumps(ctx) - return HttpResponse(json, mimetype="application/json") + if mb.address != request.POST["address"]: + if Mailbox.objects.filter(address=request.POST["address"], + domain=dom_id): + error = "Mailbox with this address already exists" + else: + if not mb.rename_dir(mb.domain.name, request.POST["address"]): + error = "Failed to rename mailbox, check permissions" + if not error: + mb = form.save(commit=False) + enabled = request.POST.has_key("enabled") \ + and request.POST["enabled"] or False + if enabled != mb.user.is_active \ + or request.POST["password1"] != mb.user.password: + mb.user.is_active = enabled + mb.user.password = request.POST["password1"] + mb.user.save() + mb.quota = request.POST["quota"] + if not mb.quota: + mb.quota = mb.domain.quota + mb.full_address = "%s@%s" % (mb.address, mb.domain.name) + mb.save() + ctx = _ctx_ok(reverse(admin.views.mailboxes, args=[dom_id])) + return HttpResponse(simplejson.dumps(ctx), + mimetype="application/json") + ctx = _ctx_ko("admin/editmailbox.html", { + "mbox" : mb, "form" : form, "error" : error + }) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") form = MailboxForm(instance=mb) + form.fields['quota'].initial = mb.quota form.fields['enabled'].initial = mb.user.is_active form.fields['password1'].initial = mb.user.password form.fields['password2'].initial = mb.user.password @@ -164,7 +192,8 @@ def editmailbox(request, dom_id, mbox_id=None): @login_required def delmailbox(request, dom_id, mbox_id=None): mb = Mailbox.objects.get(pk=mbox_id) - destroy_mailbox(mb.domain.name, mb.address) + mb.delete_dir() + mb.user.delete() mb.delete() return HttpResponseRedirect(reverse(admin.views.mailboxes, args=[dom_id])) @@ -188,15 +217,22 @@ def newalias(request, dom_id, mbox_id): mbox = Mailbox.objects.get(pk=mbox_id) if request.method == "POST": form = AliasForm(request.POST) + error = None if form.is_valid(): - form.save() - ctx = _ctx_ok(reverse(admin.views.aliases, args=[mbox.domain.id])) - else: - ctx = _ctx_ko("admin/newalias.html", { - "mbox" : mbox, "form" : form - }) - json = simplejson.dumps(ctx) - return HttpResponse(json, mimetype="application/json") + if Alias.objects.filter(address=request.POST["address"], + mbox=mbox_id): + error = "Alias with this address already exists" + else: + alias = form.save(commit=False) + alias.full_address = "%s@%s" % (alias.address, mbox.domain.name) + alias.save() + ctx = _ctx_ok(reverse(admin.views.aliases, args=[mbox.domain.id])) + return HttpResponse(simplejson.dumps(ctx), + mimetype="application/json") + ctx = _ctx_ko("admin/newalias.html", { + "mbox" : mbox, "form" : form, "error" : error + }) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") form = AliasForm({"mbox" : mbox_id}, domain=mbox.domain) return render_to_response('admin/newalias.html', { @@ -208,15 +244,24 @@ def editalias(request, dom_id, alias_id): alias = Alias.objects.get(pk=alias_id) if request.method == "POST": form = AliasForm(request.POST, instance=alias) + error = None if form.is_valid(): - form.save() - ctx = _ctx_ok(reverse(admin.views.aliases, args=[dom_id])) - else: - ctx = _ctx_ko("admin/editalias.html", { - "alias" : alias, "form" : form - }) - json = simplejson.dumps(ctx) - return HttpResponse(json, mimetype="application/json") + if alias.address != request.POST["address"] \ + and Alias.objects.filter(address=request.POST["address"], + mbox=alias.mbox.id): + error = "Alias with this address already exists" + else: + alias = form.save(commit=False) + alias.full_address = "%s@%s" % (alias.address, + alias.mbox.domain.name) + alias.save() + ctx = _ctx_ok(reverse(admin.views.aliases, args=[dom_id])) + return HttpResponse(simplejson.dumps(ctx), + mimetype="application/json") + ctx = _ctx_ko("admin/editalias.html", { + "alias" : alias, "form" : form, "error" : error + }) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") form = AliasForm(instance=alias) return _render(request, 'admin/editalias.html', { "form" : form, "alias" : alias diff --git a/lib/__init__.py b/lib/__init__.py index e69de29bb..d512117ee 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +import popen2 +import os +import time + +def exec_pipe(cmd): + """Exécute une commande et récupère la sortie générée + + Le module popen2 est utilisé pour l'exécution de la commande. Un + objet Popen4 est créé car il regroupe la sortie standard et la sortie + d'erreur dans le même fd. + De plus, popen2 est le seul à pouvoir capturer retourner le code de + sortie de la commande. + + cmd -- commande à exécuter + + Retourne un couple (code de sortie, texte de sortie) + """ + child = popen2.Popen4(cmd) + child.tochild.close() + while True: + ret = child.poll() + if ret != -1: + break + time.sleep(0.001) + output = child.fromchild.read() + if not ret: + code = 0 + else: + if os.WIFEXITED(ret): + code = os.WEXITSTATUS(ret), output + else: + code = -1 + return code, output diff --git a/lib/fsops.py b/lib/fsops.py deleted file mode 100644 index 8d355f236..000000000 --- a/lib/fsops.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -import popen2 -import os -import time -from django.conf import settings - -def exec_pipe(cmd): - """Exécute une commande et récupère la sortie générée - - Le module popen2 est utilisé pour l'exécution de la commande. Un - objet Popen4 est créé car il regroupe la sortie standard et la sortie - d'erreur dans le même fd. - De plus, popen2 est le seul à pouvoir capturer retourner le code de - sortie de la commande. - - cmd -- commande à exécuter - - Retourne un couple (code de sortie, texte de sortie) - """ - child = popen2.Popen4(cmd) - child.tochild.close() - while True: - ret = child.poll() - if ret != -1: - break - time.sleep(0.001) - output = child.fromchild.read() - if not ret: - code = 0 - else: - if os.WIFEXITED(ret): - code = os.WEXITSTATUS(ret), output - else: - code = -1 - return code, output - - -def create_domain(name): - code, output = exec_pipe("sudo -u %s mkdir %s/%s" \ - % (settings.VIRTUAL_UID, settings.STORAGE_PATH, - name)) - if code: - os.system("echo '%s' >> /tmp/vmail.log" % output) - return False - return True - -def destroy_domain(name): - code, output = exec_pipe("sudo -u %s rm -r %s/%s" \ - % (settings.VIRTUAL_UID, settings.STORAGE_PATH, - name)) - if code: - return False - return True - -def create_mailbox(domain, name): - code, output = exec_pipe("sudo -u %s mkdir -p %s/%s/%s/.maildir" \ - % (settings.VIRTUAL_UID, settings.STORAGE_PATH, - domain, name)) - if code: - return False - return True - -def destroy_mailbox(domain, name): - code, output = exec_pipe("sudo -u %s rm -r %s/%s/%s" \ - % (settings.VIRTUAL_UID, settings.STORAGE_PATH, - domain, name)) - if code: - return False - return True diff --git a/templates/admin/aliases.html b/templates/admin/aliases.html index 9c1cc22bd..8dd9130fd 100644 --- a/templates/admin/aliases.html +++ b/templates/admin/aliases.html @@ -23,6 +23,7 @@ rel="{handler:'iframe',size:{x:300,y:190}}"> + {% url admin.views.delalias domain.id alias.id as del_url %} @@ -30,7 +31,6 @@ {{ alias.address }}@{{ alias.mbox.domain.name }} {{ alias.mbox.address }}@{{ alias.mbox.domain.name }} {{ alias.enabled|yesno }} {% endfor %} diff --git a/templates/admin/editalias.html b/templates/admin/editalias.html index da5299440..3725ae74e 100644 --- a/templates/admin/editalias.html +++ b/templates/admin/editalias.html @@ -4,6 +4,7 @@ {% endblock %} {% block content %}

Alias {{ alias.address }}

+{% if error %}
{{ error }}
{% endif %} {% url admin.views.editalias alias.mbox.domain.id alias.id as edit_url %}
{% include "admin/genericform.html" %} diff --git a/templates/admin/editdomain.html b/templates/admin/editdomain.html index 69ac115de..fe5d2adb4 100644 --- a/templates/admin/editdomain.html +++ b/templates/admin/editdomain.html @@ -5,6 +5,7 @@ {% block content %}

Domain {{ domain.name }}

+{% if error %}
{{ error }}
{% endif %} {% url admin.views.editdomain domain.id as dom_url %} {% include "admin/genericform.html" %} diff --git a/templates/admin/editmailbox.html b/templates/admin/editmailbox.html index fea5ccc50..dc775d226 100644 --- a/templates/admin/editmailbox.html +++ b/templates/admin/editmailbox.html @@ -5,6 +5,7 @@ {% block content %}

Mailbox {{ mbox.name }}

+{% if error %}
{{ error }}
{% endif %} {% url admin.views.editmailbox mbox.domain_id mbox.id as box_url %} {% include "admin/genericform.html" %} diff --git a/templates/admin/newalias.html b/templates/admin/newalias.html index b7ca008e5..645b9e87a 100644 --- a/templates/admin/newalias.html +++ b/templates/admin/newalias.html @@ -4,6 +4,7 @@ {% endblock %} {% block content %}

New alias

+{% if error %}
{{ error }}
{% endif %} {% url admin.views.newalias mbox.domain_id mbox.id as box_url %} {% include "admin/genericform.html" %}