From 679cda71af2938140b82abb3a563ea05d304700a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 6 Apr 2013 16:19:48 +0200 Subject: [PATCH] * Improved parameters management (see #392) * Supporting mailboxes rename and removal only with Dovecot 2.x (see #389) --- modoboa/admin/__init__.py | 190 ++++----- modoboa/admin/fixtures/test_content.json | 2 +- modoboa/admin/forms.py | 29 +- ...l_field_mailbox_gid__del_field_mailbox_.py | 126 ++++++ modoboa/admin/models.py | 162 ++----- modoboa/admin/static/admin/css/admin.css | 2 +- modoboa/admin/static/admin/css/settings.css | 10 + modoboa/admin/templates/admin/parameters.html | 2 +- .../templates/admin/settings_header.html | 1 + modoboa/admin/templatetags/admin_extras.py | 12 +- modoboa/admin/tests.py | 26 +- modoboa/admin/views.py | 13 +- modoboa/auth/__init__.py | 6 - modoboa/auth/lib.py | 4 +- modoboa/extensions/amavis/__init__.py | 47 +-- modoboa/extensions/amavis/forms.py | 30 +- .../extensions/postfix_autoreply/__init__.py | 32 +- .../postfix_autoreply/app_settings.py | 15 + modoboa/extensions/sievefilters/__init__.py | 29 +- .../extensions/sievefilters/app_settings.py | 59 +++ modoboa/extensions/stats/__init__.py | 23 +- modoboa/extensions/stats/app_settings.py | 26 ++ modoboa/extensions/webmail/__init__.py | 62 +-- modoboa/extensions/webmail/app_settings.py | 141 +++++++ modoboa/extensions/webmail/forms.py | 1 + modoboa/extensions/webmail/imaputils.py | 2 +- modoboa/lib/formutils.py | 45 +- .../lib/migrations/0003_clean_parameters.py | 90 ++++ modoboa/lib/parameters.py | 396 +++++++++--------- modoboa/lib/sysutils.py | 9 +- modoboa/lib/tables.py | 9 +- modoboa/lib/templatetags/libextras.py | 14 + modoboa/lib/tests.py | 68 +++ modoboa/lib/webutils.py | 7 +- modoboa/static/css/custom.css | 3 +- modoboa/static/js/twocols_nav.js | 72 +++- modoboa/templates/common/generic_field.html | 4 +- modoboa/templates/common/parameters.html | 26 +- modoboa/urls.py | 1 - modoboa/userprefs/__init__.py | 26 +- modoboa/userprefs/app_settings.py | 17 + .../static/userprefs/css/userprefs.css | 9 + .../userprefs/templates/userprefs/index.html | 1 + modoboa/userprefs/views.py | 19 +- 44 files changed, 1176 insertions(+), 692 deletions(-) create mode 100644 modoboa/admin/migrations/0022_auto__del_field_mailbox_uid__del_field_mailbox_gid__del_field_mailbox_.py create mode 100644 modoboa/admin/static/admin/css/settings.css create mode 100644 modoboa/extensions/postfix_autoreply/app_settings.py create mode 100644 modoboa/extensions/sievefilters/app_settings.py create mode 100644 modoboa/extensions/stats/app_settings.py create mode 100644 modoboa/extensions/webmail/app_settings.py create mode 100644 modoboa/lib/migrations/0003_clean_parameters.py create mode 100644 modoboa/userprefs/app_settings.py create mode 100644 modoboa/userprefs/static/userprefs/css/userprefs.css diff --git a/modoboa/admin/__init__.py b/modoboa/admin/__init__.py index 85571fdad..a0070d33a 100644 --- a/modoboa/admin/__init__.py +++ b/modoboa/admin/__init__.py @@ -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") diff --git a/modoboa/admin/fixtures/test_content.json b/modoboa/admin/fixtures/test_content.json index e824e2a27..0a7c82282 100644 --- a/modoboa/admin/fixtures/test_content.json +++ b/modoboa/admin/fixtures/test_content.json @@ -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"}}] diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index cd0907271..9b63253dd 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -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() diff --git a/modoboa/admin/migrations/0022_auto__del_field_mailbox_uid__del_field_mailbox_gid__del_field_mailbox_.py b/modoboa/admin/migrations/0022_auto__del_field_mailbox_uid__del_field_mailbox_gid__del_field_mailbox_.py new file mode 100644 index 000000000..13dc4fcd0 --- /dev/null +++ b/modoboa/admin/migrations/0022_auto__del_field_mailbox_uid__del_field_mailbox_gid__del_field_mailbox_.py @@ -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'] \ No newline at end of file diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index f35c65b72..caa8075a6 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -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() diff --git a/modoboa/admin/static/admin/css/admin.css b/modoboa/admin/static/admin/css/admin.css index 831e03a6b..7a48f9cda 100644 --- a/modoboa/admin/static/admin/css/admin.css +++ b/modoboa/admin/static/admin/css/admin.css @@ -22,4 +22,4 @@ table td[name="actions"] { width: 85%; -webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px; padding-left: 14px; -} \ No newline at end of file +} diff --git a/modoboa/admin/static/admin/css/settings.css b/modoboa/admin/static/admin/css/settings.css new file mode 100644 index 000000000..3a226bcce --- /dev/null +++ b/modoboa/admin/static/admin/css/settings.css @@ -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; +} \ No newline at end of file diff --git a/modoboa/admin/templates/admin/parameters.html b/modoboa/admin/templates/admin/parameters.html index ec810251c..f26edcec5 100644 --- a/modoboa/admin/templates/admin/parameters.html +++ b/modoboa/admin/templates/admin/parameters.html @@ -1,4 +1,4 @@ -{% load i18n %}{% load url from future %} +{% load i18n libextras %}{% load url from future %} {% url "modoboa.admin.views.saveparameters" as action %}

{% trans "Parameters" %} {% trans "Configure Modoboa" %}


diff --git a/modoboa/admin/templates/admin/settings_header.html b/modoboa/admin/templates/admin/settings_header.html index 9be226e95..9e68f3e24 100644 --- a/modoboa/admin/templates/admin/settings_header.html +++ b/modoboa/admin/templates/admin/settings_header.html @@ -2,6 +2,7 @@ {% load i18n libextras admin_extras %} {% block pagetitle %}{% trans "Modoboa settings" %}{% endblock %} {% block html_head %} +