Handle mailbox operations in background.

see #482
This commit is contained in:
Antoine Nguyen
2013-09-14 08:36:44 +02:00
parent a510088034
commit ec8aba2aaa
8 changed files with 243 additions and 29 deletions
+13 -8
View File
@@ -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
@@ -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()
@@ -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']
+15 -18
View File
@@ -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,
+6 -3
View File
@@ -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
+10
View File
@@ -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
}
}
}