diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index db716e541..f5e2a217d 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -90,6 +90,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): perfomant at all as it will generate one query per quota record to update. """ + Mailbox.objects.filter(domain=domain, use_domain_quota=True) \ + .update(quota=d.quota) for q in Quota.objects.filter(username__contains="@%s" % self.oldname).values('username'): username = q['username'].replace('@%s' % self.oldname, '@%s' % domain.name) Quota.objects.filter(username=q['username']).update(username=username) @@ -99,21 +101,16 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): Updating a domain may have consequences on other objects (domain alias, mailbox, quota). The most tricky part concerns - quotas update. - + quotas update. """ d = super(DomainFormGeneral, self).save(commit=False) if commit: old_mail_homes = None if self.oldname is not None and d.name != self.oldname: - self.update_mailbox_quotas(d) - old_mail_homes = dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) + old_mail_homes = \ + dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) d.save() - Mailbox.objects.filter(domain=d, use_domain_quota=True).update(quota=d.quota) - 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 @@ -133,6 +130,14 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if not len(filter(lambda name: self.cleaned_data[name] == dalias.name, self.cleaned_data.keys())): dalias.delete() + + self.update_mailbox_quotas(d) + if old_mail_homes is not None: + for mb in d.mailbox_set.all(): + MailboxOperation.objects.create( + mailbox=mb, type='rename', argument=old_mail_homes[mb.id] + ) + return d diff --git a/modoboa/admin/management/__init__.py b/modoboa/admin/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/management/commands/__init__.py b/modoboa/admin/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py new file mode 100644 index 000000000..419766023 --- /dev/null +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -0,0 +1,62 @@ +import os +import logging +from optparse import make_option +from django.core.management.base import BaseCommand +from modoboa.lib import parameters +from modoboa.admin.models import MailboxOperation + + +class OperationError(Exception): + pass + + +class Command(BaseCommand): + args = '' + help = 'Handles rename and delete operations on mailboxes' + + def __init__(self, *args, **kwargs): + super(Command, self).__init__(*args, **kwargs) + self.logger = logging.getLogger('modoboa.cron') + + def rename_mailbox(self, operation): + if not os.path.exists(operation.argument): + return + new_mail_home = operation.mailbox.mail_home.strip(os.sep) + self.logger.info("Renaming %s to %s", operation.argument, new_mail_home) + curpath = '/' + for part in new_mail_home.split(os.sep)[:-1]: + if not part: + continue + curpath = os.path.join(curpath, part) + if not os.path.exists(curpath): + os.mkdir(curpath) + code, output = exec_cmd( + "mv %s %s" % (old_mail_home, new_mail_home) + ) + if code: + raise OperationError(output) + + def delete_mailbox(self, operation): + if not os.path.exists(operation.argument): + return + self.logger.info("Deleting %s", operation.argument) + code, output = exec_cmd( + "rm -r %s" % operation.argument + ) + if code: + raise OperationError(output) + + def handle(self): + if parameters.get_admin("HANDLE_MAILBOXES") == 'no': + return + for ope in MailboxOperation.objects.all(): + try: + f = getattr(self, '%s_mailbox' % ope.optype) + except AttributeError: + continue + try: + f(ope) + except OperationError as e: + self.logger.critical('Operation failed: %s', str(e)) + else: + ope.delete() diff --git a/modoboa/admin/migrations/0036_auto__add_mailboxoperation.py b/modoboa/admin/migrations/0036_auto__add_mailboxoperation.py new file mode 100644 index 000000000..f59c90677 --- /dev/null +++ b/modoboa/admin/migrations/0036_auto__add_mailboxoperation.py @@ -0,0 +1,137 @@ +# -*- 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): + # Adding model 'MailboxOperation' + db.create_table(u'admin_mailboxoperation', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('mailbox', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['admin.Mailbox'], null=True, blank=True)), + ('type', self.gf('django.db.models.fields.CharField')(max_length=20)), + ('argument', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal(u'admin', ['MailboxOperation']) + + + def backwards(self, orm): + # Deleting model 'MailboxOperation' + db.delete_table(u'admin_mailboxoperation') + + + models = { + u'admin.alias': { + 'Meta': {'ordering': "['domain__name', 'address']", 'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'aliases': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['admin.Alias']", 'symmetrical': 'False'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + u'admin.domain': { + 'Meta': {'ordering': "['name']", 'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'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', [], {}) + }, + u'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'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': u"orm['admin.Domain']"}) + }, + u'admin.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + u'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Domain']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'quota': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.User']"}) + }, + u'admin.mailboxoperation': { + 'Meta': {'object_name': 'MailboxOperation'}, + 'argument': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + u'admin.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'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': u"orm['admin.User']"}) + }, + u'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'admin.quota': { + 'Meta': {'object_name': 'Quota'}, + 'bytes': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'mbox': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'quota_value'", 'unique': 'True', 'null': 'True', 'to': u"orm['admin.Mailbox']"}), + 'messages': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'username': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'primary_key': 'True'}) + }, + u'admin.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_local': ('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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'}) + }, + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'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'}), + u'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 8b395d800..8619781b4 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -702,7 +702,9 @@ class Domain(DatesAware): 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() + MailboxOperation.objects.create( + type='delete', argument=mb.mail_home + ) if self.alias_set.count(): events.raiseEvent("MailboxAliasDelete", self.alias_set.all()) ungrant_access_to_objects(self.alias_set.all()) @@ -873,15 +875,9 @@ class Mailbox(DatesAware): 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") + MailboxOperation.objects.create( + mailbox=self, type='rename', argument=old_mail_home ) - if code: - raise AdminError(_("Failed to rename mailbox: %s" % output)) def rename(self, address, domain): """Rename the mailbox @@ -893,21 +889,14 @@ class Mailbox(DatesAware): qs = Quota.objects.filter(username=self.full_address) self.address = address self.domain = domain - self.rename_dir(old_mail_home) qs.update(username=self.full_address) + self.rename_dir(old_mail_home) def delete_dir(self): 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)) + MailboxOperation.objects.create(type='delete', argument=self.mail_home) def set_quota(self, value=None, override_rules=False): """Set or update quota's value for this mailbox. @@ -1017,6 +1006,14 @@ class Quota(models.Model): mbox = models.OneToOneField(Mailbox, related_name="quota_value", null=True) +class MailboxOperation(models.Model): + mailbox = models.ForeignKey(Mailbox, blank=True, null=True) + type = models.CharField( + max_length=20, choices=(('rename', 'rename'), ('delete', 'delete')) + ) + argument = models.TextField() + + class Alias(DatesAware): address = models.CharField( ugettext_lazy('address'), max_length=254, diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index b34136387..3582e9b33 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -125,6 +125,7 @@ def newdomain(request, tplname="common/wizard_forms.html"): @login_required @permission_required("admin.view_domains") +@transaction.commit_on_success def editdomain(request, dom_id, tplname="admin/editdomainform.html"): domain = Domain.objects.get(pk=dom_id) if not request.user.can_access(domain): @@ -157,11 +158,13 @@ def editdomain(request, dom_id, tplname="admin/editdomainform.html"): error = str(e) else: events.raiseEvent("DomainModified", domain) - return ajax_simple_response({"status": "ok", "respmsg": _("Domain modified")}) + return ajax_simple_response({"status": "ok", "respmsg": _("Domain modified")}) commonctx["tabs"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) + return ajax_simple_response({ + "status": "ko", "respmsg": error, + "content": _render_to_string(request, tplname, commonctx) + }) commonctx["tabs"] = DomainForm(request.user, instances=instances) commonctx["domadmins"] = domadmins diff --git a/modoboa/core/templates/settings.py.tpl b/modoboa/core/templates/settings.py.tpl index 4f926f712..67aa8c691 100644 --- a/modoboa/core/templates/settings.py.tpl +++ b/modoboa/core/templates/settings.py.tpl @@ -208,6 +208,11 @@ LOGGING = { 'facility': SysLogHandler.LOG_AUTH, 'formatter': 'syslog' }, + 'syslog-cron': { + 'class': 'logging.handlers.SysLogHandler', + 'facility': SysLogHandler.LOG_CRON, + 'formatter': 'syslog' + } 'modoboa': { 'class': 'modoboa.lib.logutils.SQLHandler', } @@ -227,6 +232,11 @@ LOGGING = { 'handlers': ['modoboa'], 'level': 'INFO', 'propagate': False + }, + 'modoboa.cron': { + 'handlers': ['syslog-cron'], + 'level': 'INFO', + 'propagate': False } } }