* Improved parameters management (see #392)

* Supporting mailboxes rename and removal only with Dovecot 2.x (see #389)
This commit is contained in:
Antoine Nguyen
2013-04-06 16:19:48 +02:00
parent d64a61ac55
commit 679cda71af
44 changed files with 1176 additions and 692 deletions
+93 -97
View File
@@ -1,85 +1,10 @@
# coding: utf-8
from modoboa.lib import parameters, events
from django import forms
from django.utils.translation import ugettext as _, ugettext_lazy
from django.db.utils import DatabaseError
from models import Extension, Mailbox
def update_uid(value):
"""VIRTUAL_UID modification callback
Update all mailboxes with the new UID value
:param value: the new UID
"""
Mailbox.objects.all().update(uid=Mailbox.resolve_uid(value))
def update_gid(value):
"""VIRTUAL_GID modification callback
Update all mailboxes with the new GID value
:param value: the new GID
"""
Mailbox.objects.all().update(gid=Mailbox.resolve_gid(value))
parameters.register_admin(
"AUTHENTICATION_TYPE", type="list", deflt="local",
values=[('local', ugettext_lazy("Local")),
('ldap', "LDAP")],
help=ugettext_lazy("The backend used for authentication")
)
parameters.register_admin(
"LDA", type="list", deflt="dovecot",
values=[('dovecot', "Dovecot"), ('postfix', 'Postfix')],
help=ugettext_lazy("Local Delivery Agent")
)
parameters.register_admin(
"STORAGE_PATH", type="string", deflt="/var/vmail",
visible_if="LDA=postfix",
help=ugettext_lazy("Path to the root directory where messages are stored")
)
parameters.register_admin(
"VIRTUAL_UID", type="string", deflt="vmail", modify_cb=update_uid,
visible_if="LDA=postfix",
help=ugettext_lazy("UID of the virtual user which owns domains/mailboxes/messages on the filesystem")
)
parameters.register_admin(
"VIRTUAL_GID", type="string", deflt="vmail", modify_cb=update_gid,
visible_if="LDA=postfix",
help=ugettext_lazy("GID of the virtual user which owns domains/mailboxes/messages on the filesystem")
)
parameters.register_admin(
"MAILBOX_TYPE", type="list", deflt="maildir",
values=[("maildir", "maildir"), ("mbox", "mbox")],
visible_if="LDA=postfix",
help=ugettext_lazy("Mailboxes storage format")
)
parameters.register_admin(
"MAILDIR_ROOT", type="string", deflt=".maildir",
visible_if="MAILBOX_TYPE=maildir",
help=ugettext_lazy("Sub-directory (inside the mailbox) where messages are stored when using the maildir format")
)
parameters.register_admin(
"AUTO_ACCOUNT_REMOVAL", type="list_yesno", deflt="no",
help=ugettext_lazy("When a mailbox is removed, also remove the associated account")
)
parameters.register_admin(
"PASSWORD_SCHEME", type="list", deflt="md5crypt",
values=[("crypt", "crypt"),
("md5", "md5"),
("md5crypt", "md5crypt"),
("sha256", "sha256"),
("plain", "plain")],
help=ugettext_lazy("Scheme used to crypt mailbox passwords")
)
parameters.register_admin(
"ITEMS_PER_PAGE", type="int", deflt=30,
help=ugettext_lazy("Number of displayed items per page")
)
from modoboa.lib import parameters, events
from modoboa.lib.formutils import YesNoField, SeparatorField
from modoboa.lib.sysutils import exec_cmd
def enabled_applications():
@@ -91,6 +16,7 @@ def enabled_applications():
:return: a list
"""
from modoboa.admin.models import Extension
from modoboa.lib.dbutils import db_table_exists
result = [("admin", "admin"), ("userprefs", "userprefs")]
@@ -100,12 +26,92 @@ def enabled_applications():
return sorted(result, key=lambda e: e[0])
parameters.register_admin(
"DEFAULT_TOP_REDIRECTION", type="list", deflt="admin",
app="general",
values=enabled_applications(),
help=ugettext_lazy("The default redirection used when no application is specified")
)
class GeneralParametersForm(parameters.AdminParametersForm):
app = "admin"
sep1 = SeparatorField(label=ugettext_lazy("Authentication"))
authentication_type = forms.ChoiceField(
label=ugettext_lazy("Authentication type"),
choices=[('local', ugettext_lazy("Local")),
('ldap', "LDAP")],
initial="local",
help_text=ugettext_lazy("The backend used for authentication")
)
password_scheme = forms.ChoiceField(
label=ugettext_lazy("Default password scheme"),
choices=[("crypt", "crypt"),
("md5", "md5"),
("md5crypt", "md5crypt"),
("sha256", "sha256"),
("plain", "plain")],
initial="md5crypt",
help_text=ugettext_lazy("Scheme used to crypt mailbox passwords")
)
secret_key = forms.CharField(
label=ugettext_lazy("Secret key"),
initial="abcdefghijklmnop",
help_text=ugettext_lazy("Key used to encrypt/decrypt passwords")
)
sep2 = SeparatorField(label=ugettext_lazy("Mailboxes"))
handle_mailboxes = YesNoField(
label=ugettext_lazy("Handle mailboxes on filesystem"),
initial="no",
help_text=ugettext_lazy("Rename or remove mailboxes on the filesystem when they get renamed or removed within Modoboa")
)
mailboxes_owner = forms.CharField(
label=ugettext_lazy("Mailboxes ower"),
initial="vmail",
help_text=ugettext_lazy("The UNIX account who owns mailboxes on the filesystem")
)
auto_account_removal = YesNoField(
label=ugettext_lazy("Automatic account removal"),
initial="no",
help_text=ugettext_lazy("When a mailbox is removed, also remove the associated account"),
)
sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous"))
items_per_page = forms.IntegerField(
label=ugettext_lazy("Items per page"),
initial=30,
help_text=ugettext_lazy("Number of displayed items per page")
)
default_top_redirection = forms.ChoiceField(
label=ugettext_lazy("Default top redirection"),
choices=[],
initial="admin",
help_text=ugettext_lazy("The default redirection used when no application is specified")
)
def __init__(self, *args, **kwargs):
super(GeneralParametersForm, self).__init__(*args, **kwargs)
self.fields["default_top_redirection"].choices = enabled_applications()
hide_fields = False
try:
code, version = exec_cmd("dovecot --version")
except OSError, e:
hide_fields = True
else:
if code or not version.strip().startswith("2"):
hide_fields = True
if hide_fields:
del self.fields["handle_mailboxes"]
del self.fields["mailboxes_owner"]
# Visibility rules
def visibility_mailboxes_owner(self):
return "handle_mailboxes=yes"
parameters.register(GeneralParametersForm, ugettext_lazy("General"))
@events.observe("ExtDisabled")
@@ -114,16 +120,6 @@ def unset_default_topredirection(extension):
Simple callback to change the default redirection if the
corresponding extension is being disabled.
"""
topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="general")
topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION")
if topredirection == extension.name:
parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs", app="general")
@events.observe("ExtDisabled", "ExtEnabled")
def update_available_applications(extension):
"""Simple callback to update the list of available applications
Must be called each time an extension is disabled/enabled.
"""
parameters.update_admin("DEFAULT_TOP_REDIRECTION", app="general",
values=enabled_applications())
parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs")
+1 -1
View File
@@ -1 +1 @@
[{"pk": 2, "model": "admin.user", "fields": {"username": "user@test.com", "is_active": "True", "is_superuser": "False", "password": "{PLAIN}toto", "groups": [2]}},{"pk": 3, "model": "admin.user", "fields": {"username": "admin@test.com", "is_active": "True", "is_superuser": "False", "password": "{PLAIN}toto", "groups": [1]}},{"pk": 1, "model": "admin.ObjectDates", "fields": {"creation": "2011-09-07 09:27:07.579603", "last_modification": "2011-09-07 09:27:07.579603"}}, {"pk": 2, "model": "admin.ObjectDates", "fields": {"creation": "2011-09-07 09:27:07.579603", "last_modification": "2011-09-07 09:27:07.579603"}}, {"pk": 1, "model": "admin.Domain", "fields": {"name" : "test.com", "quota" : "100", "dates": "1"}}, {"pk": 1, "model": "admin.Mailbox", "fields": {"address": "user", "domain": "1", "dates": "1", "uid": "1001", "gid": "1001", "path": "user", "quota": "10", "user": "2"}}, {"pk": 2, "model": "admin.Mailbox", "fields": {"address": "admin", "domain": "1", "dates": "2", "uid": "1001", "gid": "1001", "path": "admin", "quota": "10", "user": "3"}}]
[{"pk": 2, "model": "admin.user", "fields": {"username": "user@test.com", "is_active": "True", "is_superuser": "False", "password": "{PLAIN}toto", "groups": [2]}},{"pk": 3, "model": "admin.user", "fields": {"username": "admin@test.com", "is_active": "True", "is_superuser": "False", "password": "{PLAIN}toto", "groups": [1]}},{"pk": 1, "model": "admin.ObjectDates", "fields": {"creation": "2011-09-07 09:27:07.579603", "last_modification": "2011-09-07 09:27:07.579603"}}, {"pk": 2, "model": "admin.ObjectDates", "fields": {"creation": "2011-09-07 09:27:07.579603", "last_modification": "2011-09-07 09:27:07.579603"}}, {"pk": 1, "model": "admin.Domain", "fields": {"name" : "test.com", "quota" : "100", "dates": "1"}}, {"pk": 1, "model": "admin.Mailbox", "fields": {"address": "user", "domain": "1", "dates": "1", "quota": "10", "user": "2"}}, {"pk": 2, "model": "admin.Mailbox", "fields": {"address": "admin", "domain": "1", "dates": "2", "quota": "10", "user": "3"}}]
+21 -8
View File
@@ -75,10 +75,15 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
def save(self, user, commit=True):
d = super(DomainFormGeneral, self).save(commit=False)
if commit:
if self.oldname is not None and d.name != self.oldname:
if not d.rename_dir(self.oldname):
raise AdminError(_("Failed to rename domain, check permissions"))
old_mail_homes = None
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
if hm == "yes":
if self.oldname is not None and d.name != self.oldname:
old_mail_homes = {mb.id: mb.mail_home for mb in d.mailbox_set.all()}
d.save()
if old_mail_homes is not None:
for mb in d.mailbox_set.all():
mb.rename_dir(old_mail_homes[mb.id])
for k, v in self.cleaned_data.iteritems():
if not k.startswith("aliases"):
continue
@@ -561,14 +566,22 @@ class AccountFormMail(forms.Form, DynamicForm):
grant_access_to_object(admin, self.mb.user)
else:
old_mail_home = None
if self.cleaned_data["email"] != self.mb.full_address:
if self.mb.rename_dir(domname, locpart):
self.mb.domain = domain
self.mb.address = locpart
else:
raise AdminError(_("Failed to rename mailbox, check permissions"))
old_mail_home = self.mb.mail_home
self.mb.domain = domain
self.mb.address = locpart
self.mb.set_quota(self.cleaned_data["quota"], user.has_perm("admin.add_domain"))
self.mb.save()
if old_mail_home is not None:
# Problem here: the mailbox has already been saved so,
# if the rename fails, we can't rollback!
# Maybe django-reversion could be the solution?
try:
self.mb.rename_dir(old_mail_home)
except AdminError:
# do rollback
pass
account.email = self.cleaned_data["email"]
account.save()
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting field 'Mailbox.uid'
db.delete_column('admin_mailbox', 'uid')
# Deleting field 'Mailbox.gid'
db.delete_column('admin_mailbox', 'gid')
# Deleting field 'Mailbox.path'
db.delete_column('admin_mailbox', 'path')
def backwards(self, orm):
# User chose to not deal with backwards NULL issues for 'Mailbox.uid'
raise RuntimeError("Cannot reverse this migration. 'Mailbox.uid' and its values cannot be restored.")
# User chose to not deal with backwards NULL issues for 'Mailbox.gid'
raise RuntimeError("Cannot reverse this migration. 'Mailbox.gid' and its values cannot be restored.")
# User chose to not deal with backwards NULL issues for 'Mailbox.path'
raise RuntimeError("Cannot reverse this migration. 'Mailbox.path' and its values cannot be restored.")
models = {
'admin.alias': {
'Meta': {'ordering': "['domain__name', 'address']", 'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'},
'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'})
},
'admin.domain': {
'Meta': {'ordering': "['name']", 'object_name': 'Domain'},
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'quota': ('django.db.models.fields.IntegerField', [], {})
},
'admin.domainalias': {
'Meta': {'object_name': 'DomainAlias'},
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"})
},
'admin.extension': {
'Meta': {'object_name': 'Extension'},
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})
},
'admin.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'admin.objectaccess': {
'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'admin.objectdates': {
'Meta': {'object_name': 'ObjectDates'},
'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['admin']
+44 -118
View File
@@ -547,27 +547,6 @@ class Domain(DatesAware):
@property
def admins(self):
return [oa.user for oa in self.owners.filter(user__is_superuser=False)]
def create_dir(self):
if parameters.get_admin("LDA") == "postfix":
path = "%s/%s" % (parameters.get_admin("STORAGE_PATH"), self.name)
return exec_as_vuser("mkdir -p %s" % path)
return True
def rename_dir(self, oldname):
if parameters.get_admin("LDA") == "postfix":
stpath = parameters.get_admin("STORAGE_PATH")
return exec_as_vuser("mv %s/%s %s/%s" \
% (stpath, oldname, stpath, self.name))
return True
def delete_dir(self):
if parameters.get_admin("LDA") == "postfix":
return exec_as_vuser("rm -r %s/%s" \
% (parameters.get_admin("STORAGE_PATH"),
self.name))
return True
def delete(self, fromuser, keepdir=False):
from modoboa.lib.permissions import ungrant_access_to_object, ungrant_access_to_objects
@@ -578,6 +557,10 @@ class Domain(DatesAware):
if self.mailbox_set.count():
events.raiseEvent("DeleteMailbox", self.mailbox_set.all())
ungrant_access_to_objects(self.mailbox_set.all())
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
if hm == "yes" and not keepdir:
for mb in self.mailbox_set.all():
mb.delete_dir()
if self.alias_set.count():
events.raiseEvent("MailboxAliasDelete", self.alias_set.all())
ungrant_access_to_objects(self.alias_set.all())
@@ -587,13 +570,6 @@ class Domain(DatesAware):
events.raiseEvent("DeleteDomain", self)
ungrant_access_to_object(self)
super(Domain, self).delete()
if not keepdir:
self.delete_dir()
def save(self, *args, **kwargs):
if not self.create_dir():
raise AdminError(_("Failed to initialise domain, check permissions"))
super(Domain, self).save(*args, **kwargs)
def __str__(self):
return self.name
@@ -665,30 +641,27 @@ class Mailbox(DatesAware):
help_text=ugettext_lazy("Mailbox address (without the @domain.tld part)")
)
quota = models.IntegerField()
uid = models.IntegerField()
gid = models.IntegerField()
path = models.CharField(max_length=255)
domain = models.ForeignKey(Domain)
user = models.ForeignKey(User)
class Meta:
permissions = (
("view_mailboxes", "View mailboxes"),
)
)
def __init__(self, *args, **kwargs):
super(Mailbox, self).__init__(*args, **kwargs)
self.mbtype = parameters.get_admin("MAILBOX_TYPE")
self.mdirroot = parameters.get_admin("MAILDIR_ROOT")
if not self.mdirroot.endswith('/'):
self.mdirroot += '/'
self.__mail_home = None
def __str__(self):
return self.full_address
def __full_address(self, localpart):
return "%s@%s" % (localpart, self.domain.name)
@property
def full_address(self):
return "%s@%s" % (self.address, self.domain.name)
return self.__full_address(self.address)
@property
def name_and_address(self):
@@ -701,92 +674,48 @@ class Mailbox(DatesAware):
def enabled(self):
return self.user.is_active
@property
def full_path(self):
path = os.path.join(parameters.get_admin("STORAGE_PATH"),
self.domain.name, self.address)
return os.path.abspath(path)
@property
def alias_count(self):
return self.alias_set.count()
def create_dir(self):
if self.mbtype == "mbox":
self.path = "%s/Inbox" % self.address
else:
self.path = "%s/" % self.address
if parameters.get_admin("LDA") == "postfix":
relpath = "%s/%s" % (self.domain.name, self.address)
abspath = os.path.join(parameters.get_admin("STORAGE_PATH"), relpath)
if os.path.exists(abspath):
return True
if not exec_as_vuser("mkdir -p %s" % abspath):
return False
if self.mbtype == "mbox":
template = ["Inbox", "Drafts", "Sent", "Trash", "Junk"]
for f in template:
exec_as_vuser("touch %s/%s" % (abspath, f))
else:
template = [".Drafts/", ".Sent/", ".Trash/", ".Junk/"]
for dir in template:
for sdir in ["cur", "new", "tmp"]:
exec_as_vuser("mkdir -p %s/%s/%s/%s" \
% (abspath, self.mdirroot, dir, sdir))
return True
@property
def mail_home(self):
"""
def rename_dir(self, newdomain, newaddress):
if parameters.get_admin("LDA") == "postfix":
if self.domain.name == newdomain and newaddress == self.address:
return True
oldpath = "%s/%s" % (parameters.get_admin("STORAGE_PATH"), self.domain.name)
newpath = "%s/%s" % (parameters.get_admin("STORAGE_PATH"), newdomain)
oldpath = os.path.join(oldpath, self.address)
newpath = os.path.join(newpath, newaddress)
code = exec_as_vuser("mv %s %s" % (oldpath, newpath))
"""
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
if hm is None or hm == "no":
return None
if self.__mail_home is None:
code, output = exec_cmd("doveadm user %s -f home" % self.full_address,
sudo_user=parameters.get_admin("MAILBOXES_OWNER"))
if code:
self.path = "%s/" % newaddress
return code
else:
self.path ="%s/" % newaddress
raise AdminError(_("Failed to retrieve mailbox location"))
self.__mail_home = output.strip()
return self.__mail_home
return True
def rename_dir(self, old_mail_home):
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
if hm is None or hm == "no":
return
self.__mail_home = None
if not os.path.exists(old_mail_home):
return
code, output = exec_cmd("mv %s %s" % (old_mail_home, self.mail_home),
sudo_user=parameters.get_admin("MAILBOXES_OWNER"))
if code:
raise AdminError(_("Failed to rename mailbox: %s" % output))
def delete_dir(self):
if parameters.get_admin("LDA") == "postfix":
return exec_as_vuser("rm -r %s/%s/%s" \
% (parameters.get_admin("STORAGE_PATH"),
self.domain.name, self.address))
return True
@staticmethod
def resolve_id(value, idname, errmsg):
if value.isdigit():
return value
try:
rid = getattr(pwd.getpwnam(value), idname)
except KeyError:
raise AdminError(errmsg)
return rid
@staticmethod
def resolve_uid(value):
err = _("%s is not a valid uid/username" % value)
return Mailbox.resolve_id(value, "pw_uid", err)
@staticmethod
def resolve_gid(value):
err = _("%s is not a valid gid/groupname" % value)
return Mailbox.resolve_id(value, "pw_gid", err)
def set_ownership(self):
"""Set uid and gid for this mailbox
Values come from global parameters. They are resolved
(converted to integers) if needed.
"""
self.uid = self.resolve_uid(parameters.get_admin("VIRTUAL_UID"))
self.gid = self.resolve_gid(parameters.get_admin("VIRTUAL_GID"))
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
if hm is None or hm == "no":
return
if not os.path.exists(self.mail_home):
return
code, output = exec_cmd("rm -r %s" % self.mail_home,
sudo_user=parameters.get_admin("MAILBOXES_OWNER"))
if code:
raise AdminError(_("Failed to remove mailbox: %s" % output))
def set_quota(self, value, override_domain=False):
if value is None or (int(value) > self.domain.quota and not override_domain):
@@ -804,9 +733,6 @@ class Mailbox(DatesAware):
self.address = localpart
self.domain = domain
self.user = user
if not self.create_dir():
raise AdminError(_("Failed to initialise mailbox, check permissions"))
self.set_ownership()
self.set_quota(quota, True if (owner and owner.has_perm("admin.add_domain")) else False)
super(Mailbox, self).save()
+1 -1
View File
@@ -22,4 +22,4 @@ table td[name="actions"] {
width: 85%;
-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;
padding-left: 14px;
}
}
@@ -0,0 +1,10 @@
#settings_content .form-horizontal .control-label {
margin-left: 20px;
text-align: left;
width: 200px;
}
#settings_content > .form-horizontal .controls {
margin-left: 240px;
}
@@ -1,4 +1,4 @@
{% load i18n %}{% load url from future %}
{% load i18n libextras %}{% load url from future %}
{% url "modoboa.admin.views.saveparameters" as action %}
<h2>{% trans "Parameters" %} <small>{% trans "Configure Modoboa" %}</small></h2><hr />
<form id="settings_form" method="POST" action="{% url 'modoboa.admin.views.saveparameters' %}" class="form-horizontal">
@@ -2,6 +2,7 @@
{% load i18n libextras admin_extras %}
{% block pagetitle %}{% trans "Modoboa settings" %}{% endblock %}
{% block html_head %}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}admin/css/settings.css" />
<script type="text/javascript" src="{{ STATIC_URL }}js/history.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/twocols_nav.js"></script>
<script type="text/javascript">
+9 -3
View File
@@ -224,6 +224,9 @@ def param(app, definition):
value = definition.has_key("value") \
and definition["value"] or definition["deflt"]
if definition["type"] == "separator":
return "<h5>%s</h5>" % (unicode(definition["label"]))
extratags = ""
if "visible_if" in definition:
vfield, vvalue = definition["visible_if"].split("=")
@@ -270,9 +273,12 @@ def param(app, definition):
"""
if definition.has_key("help"):
result += """
<span class="help-block">%s</span>
""" % _(definition["help"])
result += """<a class="help" rel="popover" data-content="%s" data-original-title="%s">
<i class="icon-info-sign"></i>
</a>""" % (unicode(definition["help"]), _("Help"))
# result += """
# <span class="help-block">%s</span>
# """ % _(definition["help"])
result += """
</div>
+13 -13
View File
@@ -3,8 +3,13 @@
Basic tests for the 'admin' application
=======================================
TODO:
* Add new tests to check domain/mailbox manipulations when
create_directories == yes
"""
import os
from django.test import TestCase
from django.test.client import Client
from django.utils import simplejson
@@ -14,7 +19,6 @@ from modoboa.admin.lib import *
from modoboa.lib import parameters
from modoboa.lib.tests import ModoTestCase
import os
class DomainTestCase(TestCase):
fixtures = ["initial_users.json"]
@@ -26,18 +30,12 @@ class DomainTestCase(TestCase):
self.assertEqual(dom.quota, 100)
self.assertEqual(dom.enabled, False)
self.assertEqual(dom.create_dir(), True)
path = os.path.join(parameters.get_admin("STORAGE_PATH"), dom.name)
self.assertEqual(os.path.exists(path), True)
dom.name = "toto.com"
dom.save()
self.assertEqual(dom.name, "toto.com")
path = os.path.join(parameters.get_admin("STORAGE_PATH"), dom.name)
self.assertEqual(os.path.exists(path), True)
dom.delete(User.objects.get(pk=1))
self.assertEqual(os.path.exists(path), False)
class DomainAliasTestCase(ModoTestCase):
fixtures = ["initial_users.json", "test_content.json"]
@@ -68,9 +66,14 @@ class DomainAliasTestCase(ModoTestCase):
self.check_ajax_post(reverse("modoboa.admin.views.editdomain", args=[1]), values)
self.assertEqual(dom.domainalias_set.count(), 1)
class AccountTestCase(ModoTestCase):
fixtures = ["initial_users.json", "test_content.json",]
def setUp(self):
super(AccountTestCase, self).setUp()
from modoboa import admin
def test(self):
values = dict(username="tester@test.com", first_name="Tester", last_name="Toto",
password1="toto", password2="toto", role="SimpleUsers",
@@ -80,7 +83,6 @@ class AccountTestCase(ModoTestCase):
account = User.objects.get(username="tester@test.com")
mb = account.mailbox_set.all()[0]
self.assertEqual(mb.full_address, "tester@test.com")
self.assertEqual(os.path.exists(mb.full_path), True)
self.assertEqual(mb.quota, 100)
self.assertEqual(mb.enabled, True)
self.assertEqual(account.username, mb.full_address)
@@ -93,14 +95,10 @@ class AccountTestCase(ModoTestCase):
self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[account.id]), values)
mb = Mailbox.objects.get(pk=mb.id)
self.assertEqual(mb.path, "%s/" % mb.address)
path = mb.full_path
self.assertEqual(mb.full_address, "pouet@test.com")
self.assertEqual(os.path.exists(path), True)
self.check_ajax_get(reverse("modoboa.admin.views.delaccount") + "?selection=%d" \
% account.id, {})
self.assertEqual(os.path.exists(path), False)
def test_update_password(self):
"""Password update
@@ -124,6 +122,7 @@ class AccountTestCase(ModoTestCase):
self.clt.logout()
self.assertEqual(self.clt.login(username="user@test.com", password="tutu"), True)
class AliasTestCase(ModoTestCase):
fixtures = ["initial_users.json", "test_content.json"]
@@ -181,6 +180,7 @@ class AliasTestCase(ModoTestCase):
self.assertRaises(Alias.DoesNotExist, Alias.objects.get,
address="forward", domain__name="test.com")
class PermissionsTestCase(ModoTestCase):
fixtures = ["initial_users.json", "test_content.json"]
+7 -6
View File
@@ -481,17 +481,18 @@ def viewparameters(request, tplname='admin/parameters.html'):
return ajax_simple_response({
"status" : "ok",
"left_selection" : "parameters",
"content" : render_to_string(tplname, {"gparams" : parameters.get_all_admin_parameters()})
"content" : render_to_string(tplname, {"forms" : parameters.get_admin_forms})
})
@login_required
@user_passes_test(lambda u: u.is_superuser)
def saveparameters(request):
for pname, v in request.POST.iteritems():
if pname == "update":
for formdef in parameters.get_admin_forms(request.POST):
form = formdef["form"]
if form.is_valid():
form.save()
continue
app, name = pname.split('.')
parameters.save_admin(name, v, app=app)
return ajax_simple_response({"status": "ko", "prefix": form.app, "errors": form.errors})
return ajax_simple_response(dict(status="ok", respmsg=_("Parameters saved")))
@login_required
@@ -510,7 +511,7 @@ def viewextensions(request, tplname='admin/extensions.html'):
dbext.enabled = False
dbext.save()
ext["selection"] = False
tbl = ExtensionsTable(request, exts)
return ajax_simple_response({
"status" : "ok",
-6
View File
@@ -1,6 +0,0 @@
# coding: utf-8
from modoboa.lib import parameters
from django.utils.translation import ugettext_lazy
parameters.register_admin("SECRET_KEY", type="string", deflt="abcdefghijklmnop",
help=ugettext_lazy("Key used to encrypt/decrypt passwords"))
+2 -2
View File
@@ -4,7 +4,7 @@ import base64
from modoboa.lib import parameters
def encrypt(clear):
key = parameters.get_admin("SECRET_KEY")
key = parameters.get_admin("SECRET_KEY", app="admin")
obj = AES.new(key, AES.MODE_ECB)
clear = str(clear)
if len(clear) % AES.block_size:
@@ -14,7 +14,7 @@ def encrypt(clear):
return ciph
def decrypt(ciph):
obj = AES.new(parameters.get_admin("SECRET_KEY"), AES.MODE_ECB)
obj = AES.new(parameters.get_admin("SECRET_KEY", app="admin"), AES.MODE_ECB)
ciph = base64.b64decode(ciph)
clear = obj.decrypt(ciph)
return clear.rstrip(' ')
+5 -42
View File
@@ -40,50 +40,13 @@ class Amavis(ModoExtension):
priority=7, policy=p)
def load(self):
parameters.register_admin(
"MAX_MESSAGES_AGE", type="int",
deflt=14,
help=_("Quarantine messages maximum age (in days) before deletion")
)
parameters.register_admin(
"RELEASED_MSGS_CLEANUP", type="list_yesno", deflt="no",
help=_("Remove messages marked as released while cleaning up the database")
)
parameters.register_admin("AM_PDP_MODE", type="list",
deflt="unix",
values=[("inet", "inet"), ("unix", "unix")],
help=ugettext_lazy("Mode used to access the PDP server"))
parameters.register_admin("AM_PDP_HOST", type="string",
deflt="localhost",
visible_if="AM_PDP_MODE=inet",
help=ugettext_lazy("PDP server address (if inet mode)"))
parameters.register_admin("AM_PDP_PORT", type="int",
deflt=9998,
visible_if="AM_PDP_MODE=inet",
help=ugettext_lazy("PDP server port (if inet mode)"))
parameters.register_admin("AM_PDP_SOCKET", type="string",
deflt="/var/amavis/amavisd.sock",
visible_if="AM_PDP_MODE=unix",
help=ugettext_lazy("Path to the PDP server socket (if unix mode)"))
parameters.register_admin("CHECK_REQUESTS_INTERVAL", type="int",
deflt=30,
help=ugettext_lazy("Interval between two release requests checks"))
parameters.register_admin("USER_CAN_RELEASE", type="list_yesno", deflt="no",
help=ugettext_lazy("Allow users to directly release their messages"))
parameters.register_admin("SELF_SERVICE", type="list_yesno", deflt="no",
help=ugettext_lazy("Activate the 'self-service' mode"))
parameters.register_admin("NOTIFICATIONS_SENDER", type="string", deflt="notification@modoboa.org",
help=ugettext_lazy("The e-mail address used to send notitications"))
parameters.register_user(
"MESSAGES_PER_PAGE", type="int", deflt=40,
label="Number of displayed emails per page",
help=ugettext_lazy("Sets the maximum number of messages displayed in a page")
)
from app_settings import ParametersForm, UserSettings
parameters.register(ParametersForm, "Amavis")
parameters.register(UserSettings, _("Quarantine"))
def destroy(self):
events.unregister("UserMenuDisplay", menu)
parameters.unregister_app("amavis")
parameters.unregister()
exts_pool.register_extension(Amavis)
+1 -29
View File
@@ -1,35 +1,7 @@
# coding: utf-8
from django.utils.translation import ugettext_lazy as _
from django import forms
from models import Policy, Users
from django.utils.translation import ugettext_lazy
from django.forms.widgets import RadioSelect, RadioInput
from django.utils.html import conditional_escape
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
class CustomRadioInput(RadioInput):
def __unicode__(self):
if 'id' in self.attrs:
label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
else:
label_for = ''
choice_label = conditional_escape(force_unicode(self.choice_label))
return mark_safe(u'<label class="radio inline" %s>%s %s</label>' % (label_for, self.tag(), choice_label))
class InlineRadioRenderer(RadioSelect.renderer):
def __iter__(self):
for i, choice in enumerate(self.choices):
yield CustomRadioInput(self.name, self.value, self.attrs.copy(), choice, i)
def render(self):
return mark_safe(u'\n'.join([u'%s\n' % force_unicode(w) for w in self]))
class InlineRadioSelect(RadioSelect):
renderer = InlineRadioRenderer
class DomainPolicyForm(forms.ModelForm):
@@ -12,6 +12,7 @@ from modoboa.lib import events, parameters
from modoboa.extensions import ModoExtension, exts_pool
from models import *
class PostfixAutoreply(ModoExtension):
name = "postfix_autoreply"
label = "Postfix autoreply"
@@ -20,7 +21,7 @@ class PostfixAutoreply(ModoExtension):
def init(self):
from modoboa.admin.models import Domain
for dom in Domain.objects.all():
try:
trans = Transport.objects.get(domain="autoreply.%s" % dom.name)
@@ -36,11 +37,8 @@ class PostfixAutoreply(ModoExtension):
onCreateMailbox(None, mb)
def load(self):
parameters.register_admin(
"AUTOREPLIES_TIMEOUT",
type="int", deflt=86400,
help=ugettext_lazy("Timeout in seconds between two auto-replies to the same recipient")
)
from app_settings import ParametersForm
parameters.register(ParametersForm, _("Automatic replies"))
def destroy(self):
events.unregister("CreateDomain", onCreateDomain)
@@ -49,14 +47,16 @@ class PostfixAutoreply(ModoExtension):
events.unregister("DeleteMailbox", onDeleteMailbox)
events.unregister("ModifyMailbox", onModifyMailbox)
events.unregister("UserMenuDisplay", menu)
parameters.unregister_app("postfix_autoreply")
parameters.unregister()
exts_pool.register_extension(PostfixAutoreply)
@events.observe("ExtraUprefsRoutes")
def extra_routes():
return [(r'^autoreply/$', 'modoboa.extensions.postfix_autoreply.views.autoreply'),]
@events.observe("ExtraUprefsJS")
def extra_js():
return ["""function autoreply_cb() {
@@ -64,20 +64,20 @@ def extra_js():
}
""" % 'en']
@events.observe("UserMenuDisplay")
def menu(target, user):
import views
if target != "uprefs_menu":
return []
if not user.has_mailbox:
return []
return [
{"name" : "autoreply",
"class" : "ajaxlink",
"url" : "autoreply/",
"label" : ugettext_lazy("Auto-reply message")}
]
{"name": "autoreply",
"class": "ajaxlink",
"url": "autoreply/",
"label": ugettext_lazy("Auto-reply message")}
]
@events.observe("CreateDomain")
def onCreateDomain(user, domain):
@@ -86,11 +86,13 @@ def onCreateDomain(user, domain):
transport.method = "autoreply:"
transport.save()
@events.observe("DeleteDomain")
def onDeleteDomain(domain):
trans = Transport.objects.get(domain="autoreply.%s" % domain.name)
trans.delete()
@events.observe("CreateMailbox")
def onCreateMailbox(user, mailbox):
alias = Alias()
@@ -99,6 +101,7 @@ def onCreateMailbox(user, mailbox):
"%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name)
alias.save()
@events.observe("DeleteMailbox")
def onDeleteMailbox(mailboxes):
from modoboa.admin.models import Mailbox
@@ -113,6 +116,7 @@ def onDeleteMailbox(mailboxes):
else:
alias.delete()
@events.observe("ModifyMailbox")
def onModifyMailbox(mailbox, oldmailbox):
if oldmailbox.full_address == mailbox.full_address:
@@ -0,0 +1,15 @@
# coding: utf-8
from django.utils.translation import ugettext_lazy
from django import forms
from modoboa.lib.parameters import AdminParametersForm
class ParametersForm(AdminParametersForm):
app = "postfix_autoreply"
autoreplies_timeout = forms.IntegerField(
label=ugettext_lazy("Automatic reply timeout"),
initial=86400,
help_text=ugettext_lazy("Timeout in seconds between two auto-replies to the same recipient")
)
+5 -24
View File
@@ -3,7 +3,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from django.core.urlresolvers import reverse
from modoboa.lib import events, parameters
from modoboa.extensions import ModoExtension, exts_pool
from sievelib.managesieve import SUPPORTED_AUTH_MECHS
class SieveFilters(ModoExtension):
name = "sievefilters"
@@ -13,32 +13,13 @@ class SieveFilters(ModoExtension):
url = "sfilters"
def load(self):
parameters.register_app(uparams_opts=dict(needs_mailbox=True))
parameters.register_admin("SERVER", type="string",
deflt="127.0.0.1",
help=ugettext_lazy("Address of your MANAGESIEVE server"))
parameters.register_admin("PORT", type="int", deflt="2000",
help=ugettext_lazy("Listening port of your MANAGESIEVE server"))
parameters.register_admin("STARTTLS", type="list_yesno", deflt="no",
help=ugettext_lazy("Use the STARTTLS extension"))
from app_settings import ParametersForm, UserSettings
parameters.register(ParametersForm, _("Sieve filters"))
parameters.register(UserSettings, _("Message filters"))
values = [('AUTO', 'auto')]
for m in SUPPORTED_AUTH_MECHS:
values += [(m, m.lower())]
parameters.register_admin("AUTHENTICATION_MECH", type="list", deflt="auto",
values=values,
help=ugettext_lazy("Prefered authentication mechanism"))
# User parameters
parameters.register_user("EDITOR_MODE", type="list", deflt="raw",
label=ugettext_lazy("Editor mode"),
values=[("raw", "raw"), ("gui", "simplified")],
help=ugettext_lazy("Select the mode you want the editor to work in"))
def destroy(self):
events.unregister("UserMenuDisplay", menu)
parameters.unregister_app("sievefilters")
parameters.unregister()
exts_pool.register_extension(SieveFilters)
@@ -0,0 +1,59 @@
# coding: utf-8
from django.utils.translation import ugettext_lazy as _
from django import forms
from sievelib.managesieve import SUPPORTED_AUTH_MECHS
from modoboa.lib.parameters import AdminParametersForm, UserParametersForm
from modoboa.lib.formutils import SeparatorField, YesNoField
def supported_auth_mechs():
values = [('AUTO', 'auto')]
for m in SUPPORTED_AUTH_MECHS:
values += [(m, m.lower())]
return values
class ParametersForm(AdminParametersForm):
app = "sievefilters"
sep1 = SeparatorField(label=_("ManageSieve settings"))
server = forms.CharField(
label=_("Server address"),
initial="127.0.0.1",
help_text=_("Address of your MANAGESIEVE server")
)
port = forms.IntegerField(
label=_("Server port"),
initial=2000,
help_text=_("Listening port of your MANAGESIEVE server")
)
starttls = YesNoField(
label=_("Connect using STARTTLS"),
initial="no",
help_text=_("Use the STARTTLS extension")
)
authentication_mech = forms.ChoiceField(
label=_("Authentication mechanism"),
choices=supported_auth_mechs(),
initial="auto",
help_text=_("Prefered authentication mechanism")
)
class UserSettings(UserParametersForm):
app = "sievefilters"
editor_mode = forms.ChoiceField(
initial="raw",
label=_("Editor mode"),
choices=[("raw", "raw"), ("gui", "simplified")],
help_text=_("Select the mode you want the editor to work in")
)
@staticmethod
def has_access(user):
return user.has_mailbox
+9 -14
View File
@@ -12,6 +12,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events, parameters
from modoboa.extensions import ModoExtension, exts_pool
class Stats(ModoExtension):
name = "stats"
label = "Statistics"
@@ -20,32 +21,26 @@ class Stats(ModoExtension):
needs_media = True
def load(self):
from app_settings import ParametersForm
events.registerEvent("GetGraphSets")
parameters.register_admin("LOGFILE", type="string",
deflt="/var/log/mail.log",
help=ugettext_lazy("Path to log file used to collect statistics"))
parameters.register_admin("RRD_ROOTDIR", type="string",
deflt="/tmp/modoboa",
help=ugettext_lazy("Path to directory where RRD files are stored"))
parameters.register_admin("IMG_ROOTDIR", type="string",
deflt="/tmp/modoboa",
help=ugettext_lazy("Path to directory where PNG files are stored"))
parameters.register(ParametersForm, ugettext_lazy("Graphical statistics"))
def destroy(self):
events.unregister("AdminMenuDisplay", menu)
exts_pool.register_extension(Stats)
@events.observe("UserMenuDisplay")
def menu(target, user):
if target != "top_menu" or user.group == "SimpleUsers":
return []
return [
{"name" : "stats",
"label" : _("Statistics"),
"url" : reverse('fullindex')}
]
{"name": "stats",
"label": _("Statistics"),
"url": reverse('fullindex')}
]
@events.observe("GetGraphSets")
def get_default_graph_sets():
+26
View File
@@ -0,0 +1,26 @@
# coding: utf-8
from django.utils.translation import ugettext_lazy
from django import forms
from modoboa.lib.parameters import AdminParametersForm
class ParametersForm(AdminParametersForm):
app = "stats"
logfile = forms.CharField(
label=ugettext_lazy("Path to the log file"),
initial="/var/log/mail.log",
help_text=ugettext_lazy("Path to log file used to collect statistics")
)
rrd_rootdir = forms.CharField(
label=ugettext_lazy("Directory to store RRD files"),
initial="/tmp/modoboa",
help_text=ugettext_lazy("Path to directory where RRD files are stored")
)
img_rootdir = forms.CharField(
label=ugettext_lazy("Directory to store PNG files"),
initial="/tmp/modoboa",
help_text=ugettext_lazy("Path to directory where PNG files are stored")
)
+5 -57
View File
@@ -4,6 +4,7 @@ from django.core.urlresolvers import reverse
from modoboa.lib import events, parameters
from modoboa.extensions import ModoExtension, exts_pool
class Webmail(ModoExtension):
name = "webmail"
label = "Webmail"
@@ -12,67 +13,14 @@ class Webmail(ModoExtension):
needs_media = True
def load(self):
parameters.register_app(uparams_opts=dict(needs_mailbox=True))
from app_settings import ParametersForm, UserSettings
parameters.register_admin("IMAP_SERVER", type="string",
deflt="127.0.0.1",
help=ugettext_lazy("Address of your IMAP server"))
parameters.register_admin("IMAP_SECURED", type="list_yesno",
deflt="no",
help=ugettext_lazy("Use a secured connection to access IMAP server"))
parameters.register_admin("IMAP_PORT", type="int", deflt="143",
help=ugettext_lazy("Listening port of your IMAP server"))
parameters.register_admin("SMTP_SERVER", type="string",
deflt="127.0.0.1",
help=ugettext_lazy("Address of your SMTP server"))
parameters.register_admin("SMTP_SECURED_MODE", type="list",
values=[("none", ugettext_lazy("None")),
("starttls", "STARTTLS"),
("ssl", "SSL/TLS")],
deflt="none",
help=ugettext_lazy("Use a secured connection to access SMTP server"))
parameters.register_admin("SMTP_PORT", type="int", deflt="25",
help=ugettext_lazy("Listening port of your SMTP server"))
parameters.register_admin("SMTP_AUTHENTICATION", type="list_yesno",
deflt="no",
help=ugettext_lazy("Server needs authentication"))
parameters.register_admin("MAX_ATTACHMENT_SIZE", type="int", deflt=2048,
help=ugettext_lazy("Maximum attachment size in bytes (or KB, MB, GB if specified)"))
parameters.register_user("MESSAGES_PER_PAGE", type="int", deflt=40,
label=ugettext_lazy("Number of displayed emails per page"),
help=ugettext_lazy("Sets the maximum number of messages displayed in a page"))
parameters.register_user("REFRESH_INTERVAL", type="int", deflt=300,
label=ugettext_lazy("Listing refresh rate"),
help=ugettext_lazy("Automatic folder refresh rate (in seconds)"))
parameters.register_user("TRASH_FOLDER", type="string", deflt="Trash",
label=ugettext_lazy("Trash folder"),
help=ugettext_lazy("Folder where deleted messages go"))
parameters.register_user("SENT_FOLDER", type="string", deflt="Sent",
label=ugettext_lazy("Sent folder"),
help=ugettext_lazy("Folder where copies of sent messages go"))
parameters.register_user("DRAFTS_FOLDER", type="string", deflt="Drafts",
label=ugettext_lazy("Drafts folder"),
help=ugettext_lazy("Folder where drafts go"))
parameters.register_user("SIGNATURE", type="text", deflt="",
label=ugettext_lazy("Signature text"),
help=ugettext_lazy("User defined email signature"))
parameters.register_user("EDITOR", type="list", deflt="plain",
label=ugettext_lazy("Default editor"),
values=[("html", "html"), ("plain", "text")],
help=ugettext_lazy("The default editor to use when composing a message"))
parameters.register_user("DISPLAYMODE", type="list", deflt="plain",
label=ugettext_lazy("Default message display mode"),
values=[("html", "html"), ("plain", "text")],
help=ugettext_lazy("The default mode used when displaying a message"))
parameters.register_user("ENABLE_LINKS", type="list_yesno", deflt="no",
label=ugettext_lazy("Enable HTML links display"),
visible_if="DISPLAYMODE=html",
help=ugettext_lazy("Enable/Disable HTML links display"))
parameters.register(ParametersForm, "Webmail")
parameters.register(UserSettings, "Webmail")
def destroy(self):
events.unregister("UserMenuDisplay", menu)
parameters.unregister_app("webmail")
parameters.unregister()
exts_pool.register_extension(Webmail)
+141
View File
@@ -0,0 +1,141 @@
# coding: utf-8
from django import forms
from django.utils.translation import ugettext_lazy as _
from modoboa.lib.parameters import AdminParametersForm, UserParametersForm
from modoboa.lib.formutils import SeparatorField, YesNoField
class ParametersForm(AdminParametersForm):
app = "webmail"
sep3 = SeparatorField(label=_("General"))
max_attachment_size = forms.CharField(
label=_("Maximum attachment size"),
initial="2048",
help_text=_("Maximum attachment size in bytes (or KB, MB, GB if specified)")
)
sep1 = SeparatorField(label=_("IMAP settings"))
imap_server = forms.CharField(
label=_("Server address"),
initial="127.0.0.1",
help_text=_("Address of your IMAP server")
)
imap_secured = YesNoField(
label=_("Use a secured connection"),
initial="no",
help_text=_("Use a secured connection to access IMAP server")
)
imap_port = forms.IntegerField(
label=_("Server port"),
initial=143,
help_text=_("Listening port of your IMAP server")
)
sep2 = SeparatorField(label=_("SMTP settings"))
smtp_server = forms.CharField(
label=_("Server address"),
initial="127.0.0.1",
help_text=_("Address of your SMTP server")
)
smtp_secured_mode = forms.ChoiceField(
label=_("Secured connection mode"),
choices=[("none", _("None")),
("starttls", "STARTTLS"),
("ssl", "SSL/TLS")],
initial="none",
help_text=_("Use a secured connection to access SMTP server")
)
smtp_port = forms.IntegerField(
label=_("Server port"),
initial=25,
help_text=_("Listening port of your SMTP server")
)
smtp_authentication = YesNoField(
label=_("Authentication required"),
initial="no",
help_text=_("Server needs authentication")
)
class UserSettings(UserParametersForm):
app = "webmail"
sep1 = SeparatorField(label=_("Display"))
displaymode = forms.ChoiceField(
initial="plain",
label=_("Default message display mode"),
choices=[("html", "html"), ("plain", "text")],
help_text=_("The default mode used when displaying a message")
)
enable_links = YesNoField(
initial="no",
label=_("Enable HTML links display"),
help_text=_("Enable/Disable HTML links display")
)
messages_per_page = forms.IntegerField(
initial=40,
label=_("Number of displayed emails per page"),
help_text=_("Sets the maximum number of messages displayed in a page")
)
refresh_interval = forms.IntegerField(
initial=300,
label=_("Listing refresh rate"),
help_text=_("Automatic folder refresh rate (in seconds)")
)
sep2 = SeparatorField(label=_("Mailboxes"))
trash_folder = forms.CharField(
initial="Trash",
label=_("Trash folder"),
help_text=_("Folder where deleted messages go")
)
sent_folder = forms.CharField(
initial="Sent",
label=_("Sent folder"),
help_text=_("Folder where copies of sent messages go")
)
drafts_folder = forms.CharField(
initial="Drafts",
label=_("Drafts folder"),
help_text=_("Folder where drafts go")
)
sep3 = SeparatorField(label=_("Composing messages"))
editor = forms.ChoiceField(
initial="plain",
label=_("Default editor"),
choices=[("html", "html"), ("plain", "text")],
help_text=_("The default editor to use when composing a message")
)
signature = forms.CharField(
initial="",
label=_("Signature text"),
help_text=_("User defined email signature"),
widget=forms.widgets.Textarea,
required=False
)
def visibility_enable_links(self):
return "displaymode=html"
@staticmethod
def has_access(user):
return user.has_mailbox
+1
View File
@@ -2,6 +2,7 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
class ComposeMailForm(forms.Form):
from_ = forms.CharField(label=_("From"))
to = forms.CharField(label=_("To"))
+1 -1
View File
@@ -554,7 +554,7 @@ class IMAPconnector(object):
return
quotadef = data[1][0]
m = re.match("[^\s]+ \(STORAGE (\d+) (\d+)\)", quotadef)
m = re.search("\(STORAGE (\d+) (\d+)\)", quotadef)
if not m:
print "Problem while parsing quota def"
return
+40 -5
View File
@@ -1,9 +1,8 @@
# coding: utf-8
import re
from django.utils.translation import ugettext as _
from django import forms
from django.forms.widgets import MultiWidget, TextInput
from django.forms.fields import MultiValueField
from django.forms.widgets import RadioSelect, RadioInput
from django.forms.fields import CharField, Field, ChoiceField
from django.core.exceptions import ValidationError
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
@@ -149,7 +148,6 @@ def is_valid_host(host):
"""
host = host.encode('idna').lower()
if not hasattr(is_valid_host, '_re'):
import re
is_valid_host._re = re.compile(r'^([0-9a-z][-\w]*[0-9a-z]\.)+[a-z0-9\-]{2,15}$')
return bool(is_valid_host._re.match(host))
@@ -157,7 +155,8 @@ def validate_domain_name(value):
if not is_valid_host(value):
raise ValidationError(_('Enter a valid domain name'), 'invalid')
class DomainNameField(forms.CharField):
class DomainNameField(CharField):
"""
A subclass of CharField that only accepts a valid domain name.
"""
@@ -170,3 +169,39 @@ class DomainNameField(forms.CharField):
def clean(self, value):
value = self.to_python(value).strip()
return super(DomainNameField, self).clean(value)
class CustomRadioInput(RadioInput):
def __unicode__(self):
if 'id' in self.attrs:
label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
else:
label_for = ''
choice_label = conditional_escape(force_unicode(self.choice_label))
return mark_safe(u'<label class="radio inline" %s>%s %s</label>' % (label_for, self.tag(), choice_label))
class InlineRadioRenderer(RadioSelect.renderer):
def __iter__(self):
for i, choice in enumerate(self.choices):
yield CustomRadioInput(self.name, self.value, self.attrs.copy(), choice, i)
def render(self):
return mark_safe(u'\n'.join([u'%s\n' % force_unicode(w) for w in self]))
class InlineRadioSelect(RadioSelect):
renderer = InlineRadioRenderer
class SeparatorField(Field):
def __init__(self, *args, **kwargs):
kwargs["required"] = False
super(SeparatorField, self).__init__(*args, **kwargs)
class YesNoField(ChoiceField):
def __init__(self, *args, **kwargs):
kwargs["choices"] = [("yes", _("Yes")), ("no", _("No"))]
kwargs["widget"] = InlineRadioSelect
super(YesNoField, self).__init__(*args, **kwargs)
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
class Migration(DataMigration):
def forwards(self, orm):
"Write your forwards methods here."
orm["lib.Parameter"].objects.filter(name__in=[
"admin.STORAGE_PATH", "admin.VIRTUAL_GID", "admin.MAILBOX_TYPE",
"admin.MAILDIR_ROOT"
]).delete()
try:
orm["lib.parameter"].objects.filter(name="general.DEFAULT_TOP_REDICTION").update(name="admin.DEFAULT_TOP_REDICTION")
except ObjectDoesNotExist:
pass
try:
orm["lib.parameter"].objects.filter(name="admin.CREATE_DIRECTORIES").update(name="admin.HANDLE_MAILBOXES")
except ObjectDoesNotExist:
pass
try:
orm["lib.parameter"].objects.filter(name="admin.VIRTUAL_UID").update(name="admin.MAILBOXES_OWNER")
except ObjectDoesNotExist:
pass
try:
orm["lib.parameter"].objects.filter(name="auth.SECRET_KEY").update(name="admin.SECRET_KEY")
except ObjectDoesNotExist:
pass
def backwards(self, orm):
"Write your backwards methods here."
raise RuntimeError("Cannot revert this migration")
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'lib.parameter': {
'Meta': {'object_name': 'Parameter'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'lib.userparameter': {
'Meta': {'object_name': 'UserParameter'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
}
}
complete_apps = ['lib']
symmetrical = True
+204 -192
View File
@@ -12,12 +12,14 @@ Only super users will be able to access this part of the web interface.
import inspect
import re
import copy
from django import forms
from exceptions import ModoboaException
_params = {}
_params_order = {}
_levels = {'A' : 'admin', 'U' : 'user'}
class NotDefined(Exception):
_params = {'A': {}, 'U': {}}
class NotDefined(ModoboaException):
def __init__(self, app, name):
self.app = app
self.name = name
@@ -26,37 +28,142 @@ class NotDefined(Exception):
return "Application '%s' and/or parameter '%s' not defined" \
% (self.app, self.name)
def register_app(app=None, aparams_opts=None, uparams_opts=None):
"""Manually register an application
You don't need to call this function unless you want to provide
specific options for your application parameters.
class GenericParametersForm(forms.Form):
app = None
For example, you can indicate that user-level parameters are only
accessible for users that own a mailbox.
def __init__(self, *args, **kwargs):
if self.app is None:
raise NotImplementedError
:param app: the application's name
:param aparams_opts: a dict containing options applicable to admin-level params
:param uparams_opts: a dict containing options applicable to user-level params
kwargs["prefix"] = self.app
super(GenericParametersForm, self).__init__(*args, **kwargs)
self.visirules = {}
for param in self.fields.keys():
fname = "visibility_%s" % param
if not hasattr(self, fname):
continue
field, value = getattr(self, fname)().split("=")
visibility = {"field": "id_%s-%s" % (self.app, field), "value": value}
self.visirules["%s-%s" % (self.app, param)] = visibility
if not args:
self._load_initial_values()
def _load_initial_values(self):
raise NotImplementedError
def _save_parameter(self, p, name, value):
if p.value == value:
return
name = name.lower()
if hasattr(self, "update_%s" % name):
getattr(self, "update_%s" % name)(value)
if type(value) is unicode:
p.value = value.encode("unicode_escape").strip()
else:
p.value = str(value)
p.save()
def save(self):
raise NotImplementedError
class AdminParametersForm(GenericParametersForm):
def _load_initial_values(self):
from models import Parameter
for name in self.fields.keys():
fullname = "%s.%s" % (self.app, name.upper())
try:
p = Parameter.objects.get(name=fullname)
except Parameter.DoesNotExist:
continue
self.fields[name].initial = p.value
def save(self):
from models import Parameter
from modoboa.lib.formutils import SeparatorField
for name, value in self.cleaned_data.items():
if type(self.fields[name]) is SeparatorField:
continue
fullname = "%s.%s" % (self.app, name.upper())
try:
p = Parameter.objects.get(name=fullname)
except Parameter.DoesNotExist:
p = Parameter()
p.name = fullname
self._save_parameter(p, name, value)
class UserParametersForm(GenericParametersForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user") if "user" in kwargs else None
super(UserParametersForm, self).__init__(*args, **kwargs)
def _load_initial_values(self):
if self.user is None:
return
from models import UserParameter
for name in self.fields.keys():
fullname = "%s.%s" % (self.app, name.upper())
try:
p = UserParameter.objects.get(user=self.user, name=fullname)
except UserParameter.DoesNotExist:
continue
self.fields[name].initial = p.value
@staticmethod
def has_access(user):
return True
def save(self):
from models import UserParameter
from modoboa.lib.formutils import SeparatorField
for name, value in self.cleaned_data.items():
if type(self.fields[name]) is SeparatorField:
continue
fullname = "%s.%s" % (self.app, name.upper())
try:
p = UserParameter.objects.get(user=self.user, name=fullname)
except UserParameter.DoesNotExist:
p = UserParameter()
p.user = self.user
p.name = fullname
self._save_parameter(p, name, value)
def register(formclass, label):
"""Register a form class containing parameters
formclass must inherit from ``AdminParametersForm`` of
``UserParametersForm``.
:param formclass: a form class
:param label:
"""
if app is None:
app = __guess_extension()
from models import Parameter
from modoboa.lib.formutils import SeparatorField
if _params.has_key(app):
return
_params[app] = {}
_params[app]['options'] = {}
_params_order[app] = {}
for lvl in _levels.keys():
_params[app][lvl] = {}
_params_order[app][lvl] = []
if issubclass(formclass, AdminParametersForm):
level = 'A'
elif issubclass(formclass, UserParametersForm):
level = 'U'
else:
raise RuntimeError("Unknown parameter class")
_params[level][formclass.app] = {"label": label, "form": formclass, "defaults": {}}
form = formclass()
for name, field in form.fields.items():
if type(field) is SeparatorField:
continue
_params[level][formclass.app]["defaults"][name.upper()] = field.initial
if aparams_opts:
_params[app]['options']['A'] = aparams_opts
if uparams_opts:
_params[app]['options']['U'] = uparams_opts
def unregister_app(app):
def unregister(app=None):
"""Unregister an application
All parameters associated to this application will also be
@@ -64,51 +171,19 @@ def unregister_app(app):
:param app: the application's name (string)
"""
if not _params.has_key(app):
return False
del _params[app]
return True
if app is None:
app = __guess_extension()
for lvlparams in _params.values():
if app in lvlparams:
del lvlparams[app]
def __is_defined(app, level, name):
if not level in _levels.keys() \
or not app in _params.keys() \
or not name in _params[app][level].keys():
if not level in ['A', 'U'] \
or not app in _params[level] \
or not name in _params[level][app]["defaults"]:
raise NotDefined(app, name)
def __register(app, level, name, **kwargs):
"""Register a new parameter.
``app`` corresponds to a core component (admin, main) or to an
extension.
:param name: the application's name
:param level: the level this parameter is available from
:param name: the parameter's name
"""
if not app in _params.keys():
register_app(app)
if not level in _levels.keys():
return
if _params[app][level].has_key(name):
return
_params[app][level][name] = {}
_params_order[app][level] += [name]
for k, v in kwargs.iteritems():
_params[app][level][name][k] = v
def __update(app, level, name, **kwargs):
"""Update a parameter's definition
:param app: the application's name
:param level: the level this parameter is available from
:param name: the parameter's name
"""
if not app in _params.keys() or not level in _levels.keys() \
or not _params[app][level].has_key(name):
return
for k, v in kwargs.iteritems():
_params[app][level][name][k] = v
def __guess_extension():
"""Tries to guess the application's name by inspecting the stack
@@ -121,58 +196,6 @@ def __guess_extension():
return m.group(1)
return None
def register_admin(name, **kwargs):
"""Register a new parameter (admin level)
Each parameter is associated to one application. If no application
is provided, the function tries to guess the appropriate one.
:param name: the parameter's name
"""
if kwargs.has_key("app"):
app = kwargs["app"]
del kwargs["app"]
else:
app = __guess_extension()
return __register(app, 'A', name, **kwargs)
def update_admin(name, **kwargs):
"""Update a parameter's definition (admin level)
Each parameter is associated to one application. If no application
is provided, the function tries to guess the appropriate one.
:param name: the parameter's name
"""
if kwargs.has_key("app"):
app = kwargs["app"]
del kwargs["app"]
else:
app = __guess_extension()
return __update(app, 'A', name, **kwargs)
def register_user(name, **kwargs):
"""Register a new user-level parameter
:param name: the parameter's name
"""
if kwargs.has_key("app"):
app = kwargs["app"]
del kwargs["app"]
else:
app = __guess_extension()
return __register(app, 'U', name, **kwargs)
def get_param_def(app, level, name):
"""Return the definition of a given parameter
:param app: the application's name
:param level: the required level (A or U)
:param name: the parameter's name
:return: a dictionnary
"""
__is_defined(app, level, name)
return _params[app][level][name]
def save_admin(name, value, app=None):
from models import Parameter
@@ -186,13 +209,10 @@ def save_admin(name, value, app=None):
except Parameter.DoesNotExist:
p = Parameter()
p.name = fullname
if p.value != value:
pdef = get_param_def(app, 'A', name)
if "modify_cb" in pdef:
pdef["modify_cb"](value)
p.value = value.encode("unicode_escape").strip()
p.save()
return True
p.value = None
f = get_parameter_form('A', name, app)
f()._save_parameter(p, name, value)
def save_user(user, name, value, app=None):
from models import UserParameter
@@ -207,96 +227,88 @@ def save_user(user, name, value, app=None):
p = UserParameter()
p.user = user
p.name = fullname
if p.value != value:
pdef = get_param_def(app, 'U', name)
if "modify_cb" in pdef:
pdef["modify_cb"](value)
p.value = value.encode("unicode_escape").strip()
p.save()
return True
f = get_parameter_form('U', name, app)
f()._save_parameter(p, name, value)
def get_admin(name, app=None):
def get_admin(name, app=None, raise_error=True):
"""Return an administrative parameter
A ``NotDefined`` exception if the parameter doesn't exist.
:param name: the parameter's name
:param app: the application owning the parameter
:return: the corresponding value as a string
"""
from models import Parameter
if app is None:
app = __guess_extension()
__is_defined(app, "A", name)
try:
__is_defined(app, "A", name)
except NotDefined:
if raise_error:
raise
return None
try:
p = Parameter.objects.get(name="%s.%s" % (app, name))
except Parameter.DoesNotExist:
return _params[app]["A"][name]["deflt"]
return _params["A"][app]["defaults"][name]
return p.value.decode("unicode_escape")
def get_all_admin_parameters():
"""Retrieve all administrative parameters
Returned parameters are sorted by application
:return: a list of dictionaries
"""
result = []
for app in sorted(_params.keys()):
tmp = {"name" : app, "params" : []}
for p in _params_order[app]['A']:
newdef = copy.deepcopy(_params[app]['A'][p])
newdef["name"] = p
newdef["value"] = get_admin(p, app=app)
tmp["params"] += [newdef]
result += [tmp]
return result
def get_user(user, name, app=None):
"""Return a parameter for a specific user
A ``NotDefined`` exception if the parameter doesn't exist.
:param ``User`` user: the desired user
:param name: the parameter's name
:param app: the application owning the parameter
:return: the corresponding value as a string
"""
from models import UserParameter
if app is None:
app = __guess_extension()
__is_defined(app, "U", name)
try:
__is_defined(app, "U", name)
except NotDefined:
if raise_error:
raise
return None
try:
p = UserParameter.objects.get(user=user, name="%s.%s" % (app, name))
except UserParameter.DoesNotExist:
return _params[app]["U"][name]["deflt"]
return _params["U"][app]["defaults"][name]
return p.value.decode("unicode_escape")
def get_all_user_params(user):
"""Retrieve all the parameters of the given user
Returned parameters are sorted by application.
def get_admin_forms(*args, **kwargs):
for formdef in _params['A'].values():
yield {"label": formdef["label"], "form": formdef["form"](*args, **kwargs)}
:param user: a ``User`` object
:return: a list of dictionaries
"""
result = []
for app in sorted(_params.keys()):
if not len(_params[app]['U']):
continue
if get_app_option('U', 'needs_mailbox', False, app=app) \
and not user.has_mailbox:
continue
tmp = {"name" : app, "params" : []}
for p in _params_order[app]['U']:
param_def = _params[app]['U'][p]
newdef = copy.deepcopy(param_def)
newdef["name"] = p
newdef["value"] = get_user(user, p, app=app)
tmp["params"] += [newdef]
result += [tmp]
return result
def get_app_option(lvl, name, dflt, app=None):
"""Retrieve a specific option for a given application
def get_user_forms(user, *args, **kwargs):
kwargs["user"] = user
def realfunc():
for formdef in _params['U'].values():
if not formdef["form"].has_access(user):
continue
yield {"label": formdef["label"], "form": formdef["form"](*args, **kwargs)}
If the option is not found, a default value is returned.
return realfunc
:param lvl: the option's level (U or A)
:param name: the option's name
:param dflt: the default value to return
:param app: the application name
:return: the option's value
def get_parameter_form(level, name, app=None):
"""Return the form containing a specific parameter
:param string level: associated level
:param string name: paremeter's name
:param string app: parameter's application
:return: a form class
"""
if app is None:
app = __guess_extension()
if not lvl in _params[app]['options']:
return dflt
if not _params[app]['options'][lvl].has_key(name):
return dflt
return _params[app]['options'][lvl][name]
__is_defined(app, level, name)
return _params[level][app]["form"]
+6 -3
View File
@@ -8,16 +8,19 @@ import logging
import logging.handlers
from modoboa.lib import parameters
def exec_cmd(cmd, **kwargs):
def exec_cmd(cmd, sudo_user=None, **kwargs):
import subprocess
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, **kwargs)
if sudo_user is not None:
cmd = "sudo -u %s %s" % (sudo_user, cmd)
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, **kwargs)
output = p.communicate()[0]
return p.returncode, output
def exec_as_vuser(cmd):
code, output = exec_cmd("sudo -u %s %s" \
% (parameters.get_admin("VIRTUAL_UID", app="admin"), cmd))
% (parameters.get_admin("MAILBOXES_OWNER", app="admin"), cmd))
if code:
exec_cmd("echo '%s' >> /tmp/vmail.log" % output)
return False
+5 -4
View File
@@ -57,17 +57,18 @@ class SelectionColumn(Column):
A 'selection' column contains only checkboxes (one per row). It is
usefull to select rows for grouped actions (modify, delete, etc.)
"""
def __str__(self):
def __unicode__(self):
if self.header:
return "<input type='checkbox' name='selectall' id='selectall' />"
return ""
def render(self, value, selection):
return "<input type='checkbox' id='%s' name='select_%s' value='%s' %s/>" \
% (self.name, value, value, selection and "checked" or "")
return "<input type='checkbox' id='%(id)s' name='select_%(value)s' value='%(value)s' %(checked)s/>" \
% {'id': self.name, 'value': value,
"checked": "checked=''" if selection else ""}
def transform(self, row, col, table):
selection = row.has_key(self.name) and row[self.name] or False
selection = row[self.name] if self.name in row else False
col["value"] = self.render(row[table.idkey], selection)
col["safe"] = True
+14
View File
@@ -13,6 +13,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events
from modoboa.lib.sysutils import exec_cmd
from modoboa.lib.webutils import static_url
from modoboa.lib.formutils import SeparatorField
register = template.Library()
@@ -90,6 +91,9 @@ def render_form(form, tpl=None):
@register.simple_tag
def render_field(field, help_display_mode="tooltip"):
if type(field.form.fields[field.name]) is SeparatorField:
return "<h5>%s</h5>" % unicode(field.label)
return render_to_string("common/generic_field.html", dict(
field=field, help_display_mode=help_display_mode
))
@@ -175,6 +179,16 @@ def progress_color(value):
def fromunix(value):
return datetime.datetime.fromtimestamp(int(value))
@register.simple_tag
def visirule(field):
if not hasattr(field.form, "visirules") or not field.html_name in field.form.visirules:
return ""
rule = field.form.visirules[field.html_name]
return " data-visibility-field='%s' data-visibility-value='%s' " \
% (rule["field"], rule["value"])
class ConnectedUsers(template.Node):
def __init__(self, varname):
self.varname = varname
+68
View File
@@ -1,7 +1,11 @@
# coding: utf-8
import unittest
from django.test import TestCase
from django.test.client import Client
from django.utils import simplejson
from django import forms
import parameters
class ModoTestCase(TestCase):
@@ -24,6 +28,7 @@ class ModoTestCase(TestCase):
def check_ajax_get(self, *args, **kwargs):
self.check_ajax_request("get", *args, **kwargs)
class ExtTestCase(ModoTestCase):
name = None
@@ -32,3 +37,66 @@ class ExtTestCase(ModoTestCase):
self.clt.get("/modoboa/admin/settings/extensions/")
self.clt.post("/modoboa/admin/settings/extensions/save/",
{"select_%s" % self.name : "1"})
class TestParams(parameters.AdminParametersForm):
app = "test"
param1 = forms.CharField(label="Test", initial="toto")
param2 = forms.IntegerField(label="Int", initial=42)
def update_param2(self, value):
raise RuntimeError("new value %d" % value)
class TestUserParams(parameters.UserParametersForm):
app = "test"
param1 = forms.CharField(label="Test", initial="titi")
param2 = forms.IntegerField(label="Int", initial=42)
def update_param2(self, value):
raise RuntimeError("new value %d" % value)
class ParameterTestCase(TestCase):
"""Simple test cases for ``modoboa.lib.parameters`` module.
"""
def setUp(self):
from modoboa.admin.models import User
parameters.register(TestParams, "Test")
parameters.register(TestUserParams, "TestUser")
self.user = User.objects.create(username="tester")
def test_register_form(self):
self.assertIn("test", parameters._params['A'])
def test_unregister(self):
parameters.unregister("test")
self.assertNotIn("test", parameters._params['A'])
def test_get_parameter_form(self):
self.assertIs(parameters.get_parameter_form('A', 'PARAM1', 'test'), TestParams)
def test_save_admin(self):
parameters.save_admin("PARAM1", "45", app="test")
self.assertEqual(parameters.get_admin("PARAM1", app="test"), "45")
def test_update_param(self):
with self.assertRaisesRegexp(RuntimeError, "new value 35"):
parameters.save_admin("PARAM2", 35, app="test")
def test_get_admin(self):
self.assertEqual(parameters.get_admin("PARAM1", "test"), "toto")
with self.assertRaises(parameters.NotDefined):
parameters.get_admin("TOTO", "test")
def test_get_user(self):
self.assertEqual(parameters.get_user(self.user, "PARAM1", "test"), "titi")
with self.assertRaises(parameters.NotDefined):
parameters.get_user(self.user, "TOTO", "test")
def test_save_user(self):
parameters.save_user(self.user, "PARAM1", "pouet", "test")
self.assertEqual(parameters.get_user(self.user, "PARAM1", "test"), "pouet")
+4 -3
View File
@@ -93,14 +93,15 @@ def ajax_response(request, status="ok", respmsg=None,
jsonctx["norefresh"] = norefresh
return HttpResponse(simplejson.dumps(jsonctx), mimetype="application/json")
def ajax_simple_response(content):
def ajax_simple_response(content, **response_kwargs):
"""Simple AJAX response
No extra formatting is done. The content is passed directly to simplejon.
:param content: the response's content (list, dict, string)
"""
return HttpResponse(simplejson.dumps(content), mimetype="application/json")
response_kwargs["content_type"] = "application/json"
return HttpResponse(simplejson.dumps(content), **response_kwargs)
def static_url(path):
"""Returns the correct static url for a given file
@@ -150,7 +151,7 @@ def topredirection(request):
from modoboa.lib import parameters
from modoboa.extensions import exts_pool
topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="general")
topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="admin")
if not topredir in ["admin", "userprefs"]:
infos = exts_pool.get_extension_infos(topredir)
path = infos["url"] if infos["url"] else infos["name"]
+2 -1
View File
@@ -37,7 +37,8 @@ body {
float: left;
padding-top: 5px;
text-align: left;
width: 200px;
width: 250px;
margin-left: 20px;
}
.form-horizontal .param-controls {
+59 -13
View File
@@ -26,12 +26,35 @@ TwocolsNav.prototype = {
formid: this.options.formid,
modal: false,
reload_on_success: false,
error_cb: $.proxy(this.update_content, this),
error_cb: $.proxy(this.display_errors, this),
success_cb: $.proxy(this.save_cb, this)
});
}, this));
},
display_errors: function(data) {
$.each(data.errors, function(id, value) {
var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id;
var $widget = $("#" + fullid);
var spanid = fullid + "-error";
var $span = $("#" + spanid);
if (!$widget.parents(".control-group").hasClass("error")) {
$widget.parents(".control-group").addClass("error");
}
if (!$span.length) {
$span = $("<span />", {
"class": "help-inline",
"html": value[0],
"id": spanid
});
$widget.parents(".controls").append($span);
} else {
$span.html(value[0]);
}
});
},
update_content: function(data) {
if (data.content) {
$('#' + this.options.divid).html(data.content);
@@ -43,15 +66,37 @@ TwocolsNav.prototype = {
$("body").notify("error", data.respmsg);
}
$('#' + this.options.divid + ' select').change($.proxy(this.select_change, this));
$(".help").popover().click(function(e) {e.preventDefault();});
$('#' + this.options.divid + ' input[type=radio]').click($.proxy(this.radio_clicked, this));
$(".help").popover({
placement: 'bottom',
trigger: 'click'
}).click(function(e) {e.preventDefault();});
$('#' + this.options.divid + ' select').change();
$('#' + this.options.divid + ' input[type=radio]:checked').click();
},
propagate_change: function($node) {
var $select = $node.find("select");
var $radio = $node.find("input[type=radio]");
if ($select.length) {
$select.change();
}
if ($radio.length) {
$radio.click();
}
},
toggle_field_visibility: function($field, $parent, value) {
if ($parent.attr("idsabled") === undefined &&
$field.attr("data-visibility-value") == value) {
$field.attr("disabled", null);
$field.show();
this.propagate_change($field);
} else {
$field.hide();
$field.attr("disabled", "disabled");
this.propagate_change($field);
}
},
select_change: function(e) {
@@ -60,18 +105,19 @@ TwocolsNav.prototype = {
var $parent = $target.parents("div.control-group");
$('div[data-visibility-field="' + $target.attr("id") + '"]').each(function(idx) {
var $this = $(this);
instance.toggle_field_visibility($(this), $parent, $target.attr("value"));
});
},
if ($parent.attr("disabled") === undefined &&
$this.attr("data-visibility-value") == $target.attr("value")) {
$this.attr("disabled", null);
$this.show();
instance.propagate_change($this)
} else {
$this.hide();
$this.attr("disabled", "disabled");
instance.propagate_change($this);
}
radio_clicked: function(e) {
var instance = this;
var $target = $(e.target);
var $parent = $target.parents("div.control-group");
var realid = $target.attr("id");
realid = realid.substr(0, realid.length - 2);
$('div[data-visibility-field="' + realid + '"]').each(function(idx) {
instance.toggle_field_visibility($(this), $parent, $target.attr("value"));
});
},
+2 -2
View File
@@ -1,5 +1,5 @@
{% load i18n %}
<div class="control-group {% if field.errors %}error{% endif %}"{% if hidden %}style="display: none"{% endif %}>
{% load i18n libextras %}
<div class="control-group {% if field.errors %}error{% endif %}"{% visirule field %}{% if hidden %}style="display: none"{% endif %}>
{% if field.label %}<label class="control-label" for="id_{{ field.html_name }}">{{ field.label }}</label>{% endif %}{% if help_display_mode == "tooltip" and field.help_text %}<a href="#" class="help pull-left" data-original-title="{% trans 'Help:' %} {{ field.label }}" data-content="{{ field.help_text }}" rel="popover">?</a>{% endif %}
<div class="controls spaced">
{% if appended_text %}<div class="input-append">{% endif %}
+11 -15
View File
@@ -1,16 +1,12 @@
{% load i18n admin_extras %}
{% if gparams %}
<div class="tabbable">
<ul class="nav nav-tabs">
{% for app in gparams %}<li{% if forloop.first %} class="active"{% endif %}><a href="#{{ forloop.counter }}" data-toggle="tab">{{ app.name }}</a></li>
{% endfor %}
</ul>
<div class="tab-content">{% for app in gparams %}
<div class="tab-pane{% if forloop.first %} active{% endif %}" id="{{ forloop.counter }}">
{% for p in app.params %}{% param app.name p %}{% endfor %}
</div>{% endfor %}
</div>
{% load libextras %}
<div class="tabbable">
<ul class="nav nav-tabs">
{% for form in forms %}<li{% if forloop.first %} class="active"{% endif %}><a href="#{{ forloop.counter }}" data-toggle="tab">{{ form.label }}</a></li>
{% endfor %}
</ul>
<div class="tab-content">{% for form in forms %}
<div class="tab-pane{% if forloop.first %} active{% endif %}" id="{{ forloop.counter }}">
{% render_form form.form %}
</div>{% endfor %}
</div>
{% else %}
<div class="info">{% trans "No parameters available" %}</div>
{% endif %}
</div>
-1
View File
@@ -6,7 +6,6 @@ from modoboa.lib import parameters
urlpatterns = patterns('',
(r'^$', 'modoboa.lib.webutils.topredirection'),
(r'^admin/', include('modoboa.admin.urls')),
(r'^userprefs/', include('modoboa.userprefs.urls')),
(r'^accounts/login/$', 'modoboa.auth.views.dologin'),
(r'^accounts/logout/$', 'modoboa.auth.views.dologout'),
(r'^jsi18n/$', 'django.views.i18n.javascript_catalog',
+18 -8
View File
@@ -1,11 +1,21 @@
# coding: utf-8
from django.utils.translation import ugettext_lazy
from modoboa.lib import parameters
from modoboa.extensions import ModoExtension, exts_pool
parameters.register_user("LANG", type="list", deflt="en",
label=ugettext_lazy("Prefered language"),
values=[("de", "deutsch"), ("en", "english"),
("es", "español"), ("fr", "français"),
("pt", "português"), ("sv", "svenska"),],
help=ugettext_lazy("Prefered language to display pages"),
app="general")
class UserPreferences(ModoExtension):
name = "userprefs"
label = ugettext_lazy("User preferences")
version = "1.0"
description = ugettext_lazy("Per-user settings")
always_active = True
def load(self):
from modoboa.lib import parameters
from app_settings import UserSettings
parameters.register(UserSettings, ugettext_lazy("General"))
def destroy(self):
parameters.unregister()
exts_pool.register_extension(UserPreferences, show=False)
+17
View File
@@ -0,0 +1,17 @@
# coding: utf-8
from django import forms
from django.utils.translation import ugettext_lazy
from modoboa.lib.parameters import UserParametersForm
class UserSettings(UserParametersForm):
app = "general"
lang = forms.ChoiceField(
initial="en",
label=ugettext_lazy("Prefered language"),
choices=[("de", "deutsch"), ("en", "english"),
("es", "español"), ("fr", "français"),
("pt", "português"), ("sv", "svenska"),],
help_text=ugettext_lazy("Prefered language to display pages")
)
@@ -0,0 +1,9 @@
#uprefs_content .form-horizontal .control-label {
margin-left: 20px;
text-align: left;
width: 200px;
}
#uprefs_content > .form-horizontal .controls {
margin-left: 240px;
}
@@ -1,6 +1,7 @@
{% extends "twocols.html" %}{% load i18n uprefs_extras libextras %}
{% block pagetitle %}{% trans "Settings" %}{% endblock %}
{% block html_head %}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}userprefs/css/userprefs.css" />
<script type="text/javascript" src="{{ STATIC_URL }}js/history.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/twocols_nav.js"></script>
<script type="text/javascript">
+11 -8
View File
@@ -102,19 +102,22 @@ def profile(request, tplname='userprefs/profile.html'):
@login_required
def preferences(request):
if request.method == "POST":
for pname, v in request.POST.iteritems():
if pname == "update":
for formdef in parameters.get_user_forms(request.user, request.POST)():
form = formdef["form"]
if form.is_valid():
form.save()
continue
app, name = pname.split('.')
parameters.save_user(request.user, name, v, app=app)
return ajax_simple_response({
"status": "ko", "prefix": form.app, "errors": form.errors
})
return ajax_simple_response(dict(
status="ok", respmsg=_("Preferences saved")
))
return ajax_simple_response({
"status": "ok", "respmsg": _("Preferences saved")
})
return ajax_simple_response({
"status" : "ok",
"content" : render_to_string("userprefs/preferences.html", {
"gparams" : parameters.get_all_user_params(request.user)
"forms" : parameters.get_user_forms(request.user)
})
})