From ae38729f5dad885fe96bfbd4031033ad77db857f Mon Sep 17 00:00:00 2001 From: Louis-Dominique Dubeau Date: Sun, 11 Aug 2013 16:09:09 -0400 Subject: [PATCH 01/61] This change prevents http://dev.modoboa.org/issues/464 from happening. --- modoboa/extensions/amavis/views.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/modoboa/extensions/amavis/views.py b/modoboa/extensions/amavis/views.py index 46f8f4155..e33cc26e8 100644 --- a/modoboa/extensions/amavis/views.py +++ b/modoboa/extensions/amavis/views.py @@ -19,14 +19,18 @@ from sql_listing import * def __get_current_url(request): + params = [] if "page" in request.session: - res = "listing?page=%s" % request.session["page"] + res = "listing" + params.append("page=%s" % request.session["page"]) else: - res = "" - params = "&".join(map(lambda p: "%s=%s" % (p, request.session[p]), - filter(request.session.has_key, ["criteria", "pattern"]))) - if params != "": - res += "?%s" % (params) + res = "listing" + + params += map(lambda p: "%s=%s" % (p, request.session[p]), + filter(request.session.has_key, ["criteria", "pattern"])) + + if len(params): + res += "?%s" % ("&".join(params)) return res From ef826224c4f2ac224163efbbd03cf6fc78f546bc Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 25 Aug 2013 16:36:41 +0200 Subject: [PATCH 02/61] Don't forget to update the transport to! fixes #459 --- modoboa/extensions/postfix_autoreply/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/modoboa/extensions/postfix_autoreply/__init__.py b/modoboa/extensions/postfix_autoreply/__init__.py index 8ef596627..6434a339e 100644 --- a/modoboa/extensions/postfix_autoreply/__init__.py +++ b/modoboa/extensions/postfix_autoreply/__init__.py @@ -88,10 +88,17 @@ def onCreateDomain(user, domain): transport.save() +@events.observe("DomainModified") +def onDomainModified(domain): + if domain.oldname == domain.name: + return + Transport.objects.filter(domain="autoreply.%s" % domain.oldname) \ + .update(domain="autoreply.%s" % domain.name) + + @events.observe("DeleteDomain") def onDeleteDomain(domain): - trans = Transport.objects.get(domain="autoreply.%s" % domain.name) - trans.delete() + Transport.objects.filter(domain="autoreply.%s" % domain.name).delete() @events.observe("CreateMailbox") From 48f70e98cb47344835bf0ebe7507d78bc85d61b0 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 25 Aug 2013 16:49:04 +0200 Subject: [PATCH 03/61] Adding a unique constraint to the armessage field. fixes #467 --- ...3_auto__add_unique_arhistoric_armessage.py | 120 ++++++++++++++++++ .../extensions/postfix_autoreply/models.py | 4 +- 2 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_armessage.py diff --git a/modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_armessage.py b/modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_armessage.py new file mode 100644 index 000000000..a1bd293e0 --- /dev/null +++ b/modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_armessage.py @@ -0,0 +1,120 @@ +# -*- 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 unique constraint on 'ARhistoric', fields ['armessage'] + db.create_unique(u'postfix_autoreply_arhistoric', ['armessage_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'ARhistoric', fields ['armessage'] + db.delete_unique(u'postfix_autoreply_arhistoric', ['armessage_id']) + + + models = { + 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.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.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.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'}) + }, + u'postfix_autoreply.alias': { + 'Meta': {'object_name': 'Alias'}, + 'autoreply_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'full_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'postfix_autoreply.arhistoric': { + 'Meta': {'object_name': 'ARhistoric'}, + 'armessage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['postfix_autoreply.ARmessage']", 'unique': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_sent': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'sender': ('django.db.models.fields.TextField', [], {}) + }, + u'postfix_autoreply.armessage': { + 'Meta': {'object_name': 'ARmessage'}, + 'content': ('django.db.models.fields.TextField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mbox': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Mailbox']"}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'untildate': ('django.db.models.fields.DateTimeField', [], {}) + }, + u'postfix_autoreply.transport': { + 'Meta': {'object_name': 'Transport'}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'method': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['postfix_autoreply'] \ No newline at end of file diff --git a/modoboa/extensions/postfix_autoreply/models.py b/modoboa/extensions/postfix_autoreply/models.py index 5381f6a19..db1c4c44b 100644 --- a/modoboa/extensions/postfix_autoreply/models.py +++ b/modoboa/extensions/postfix_autoreply/models.py @@ -18,9 +18,9 @@ class ARmessage(models.Model): help_text=_("The content that will appear in sent emails")) enabled = models.BooleanField(_('enabled'), help_text=_("Activate/Deactivate your auto reply")) - untildate = models.DateField() + untildate = models.DateTimeField() class ARhistoric(models.Model): - armessage = models.ForeignKey(ARmessage) + armessage = models.ForeignKey(ARmessage, unique=True) last_sent = models.DateTimeField(auto_now=True) sender = models.TextField() From 6fac43655da676dde619e41780d1175e65329e2f Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 25 Aug 2013 17:08:21 +0200 Subject: [PATCH 04/61] Queries update. fixes #447 --- modoboa/core/commands/postfix_maps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py index 140de1cb2..f697db4e2 100644 --- a/modoboa/core/commands/postfix_maps.py +++ b/modoboa/core/commands/postfix_maps.py @@ -37,8 +37,8 @@ class DomainsMap(MapFile): class DomainsAliasesMap(MapFile): filename = 'sql-domain-aliases.cf' - mysql = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" - postgres = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled AND dom.enabled" + mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT concat(al.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" + postgres = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND al.address='%u')" class MailboxesMap(MapFile): From 1816b393f6d5654f4982aaaf81f5598c462a7623 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 27 Aug 2013 09:05:55 +0200 Subject: [PATCH 05/61] Don't prevent domain admins to use local mailboxes they don't own. fixes #469 --- modoboa/admin/forms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index 1459d4593..18e878c75 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -6,6 +6,7 @@ from django.http import QueryDict from modoboa.admin.templatetags.admin_extras import gender from modoboa.admin.exceptions import AdminError from modoboa.lib import events, parameters +from modoboa.lib.exceptions import PermDeniedException from modoboa.lib.emailutils import split_mailbox from modoboa.lib.permissions import get_account_roles from modoboa.lib.formutils import * @@ -272,8 +273,6 @@ class AliasForm(forms.ModelForm, DynamicForm): except Domain.DoesNotExist: domain = None if domain is not None: - if not self.user.can_access(domain): - raise PermDeniedException(v) try: rcpt = Alias.objects.get(domain=domain, address=local_part) if rcpt.full_address == self.cleaned_data["email"]: From 1f6d387907c441a6761bc7da6199f088d4a94ae0 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 27 Aug 2013 20:18:45 +0200 Subject: [PATCH 06/61] Password and adresses can contain non ascii chars... fixes #470 --- modoboa/admin/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index 6a2134c29..2f0b73be7 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -547,7 +547,7 @@ class User(AbstractBaseUser, PermissionsMixin): dom.add_admin(self) def to_csv(self, csvwriter): - row = ["account", self.username.encode("utf-8"), self.password, + row = ["account", self.username.encode("utf-8"), self.password.encode("utf-8"), self.first_name.encode("utf-8"), self.last_name.encode("utf-8"), self.is_active, self.group, self.email] if self.group == "DomainAdmins": @@ -1195,7 +1195,7 @@ class Alias(DatesAware): self.save(int_rcpts, ext_rcpts, creator=user) def to_csv(self, csvwriter): - row = [self.type, self.full_address, self.enabled] + row = [self.type, self.full_address.encode("utf-8"), self.enabled] row += self.get_recipients() csvwriter.writerow(row) From fa6c5ed6c06aa97bd217402c42e13828613f7e0a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 27 Aug 2013 20:21:02 +0200 Subject: [PATCH 07/61] see #470 --- modoboa/admin/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index 2f0b73be7..8b395d800 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -549,7 +549,7 @@ class User(AbstractBaseUser, PermissionsMixin): def to_csv(self, csvwriter): row = ["account", self.username.encode("utf-8"), self.password.encode("utf-8"), self.first_name.encode("utf-8"), self.last_name.encode("utf-8"), - self.is_active, self.group, self.email] + self.is_active, self.group, self.email.encode("utf-8")] if self.group == "DomainAdmins": row += [dom.name for dom in self.get_domains()] csvwriter.writerow(row) From 0b286141514e26bc2c74d1bd119b1b40495055e6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 28 Aug 2013 21:34:49 +0200 Subject: [PATCH 08/61] fixing multiple unicode issues. fixes #471 --- modoboa/extensions/sievefilters/views.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index 8aa7fa06f..a648195cf 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -98,12 +98,13 @@ def submitfilter(request, setname, okmsg, tplname, tplctx, update=False, sc=None if match_type == "all": match_type = "anyof" conditions = [("true",)] + fltname = form.cleaned_data["name"].encode("utf-8") if not update: - fset.addfilter(form.cleaned_data["name"], conditions, actions, + fset.addfilter(fltname, conditions, actions, match_type) else: fset.updatefilter(request.POST["oldname"], - form.cleaned_data["name"], conditions, actions, + fltname, conditions, actions, match_type) sc.pushscript(fset.name, str(fset)) return ajax_response(request, respmsg=okmsg, ajaxnav=True) @@ -146,6 +147,8 @@ def editfilter(request, setname, fname, tplname="sievefilters/filter.html"): update=True, sc=sc) fset = sc.getscript(setname, format="fset") + if type(fname) is unicode: + fname = fname.encode("utf-8") f = fset.getfilter(fname) form = build_filter_form_from_filter(request, fname, f) ctx = build_filter_ctx(ctx, form) @@ -160,7 +163,7 @@ def removefilter(request, setname, fname): sc = SieveClient(user=request.user.username, password=request.session["password"]) fset = sc.getscript(setname, format="fset") - if fset.removefilter(fname): + if fset.removefilter(fname.encode("utf-8")): sc.pushscript(fset.name, str(fset)) return ajax_response(request, respmsg=_("Filter removed")) return ajax_response(request, "ko", respmsg=_("Failed to remove filter")) @@ -261,6 +264,8 @@ def download_filters_set(request, name): def toggle_filter_state(request, setname, fname): sc = SieveClient(user=request.user.username, password=request.session["password"]) + if type(fname) is unicode: + fname = fname.encode("utf-8") try: fset = sc.getscript(setname, format="fset") if fset.is_filter_disabled(fname): @@ -288,7 +293,7 @@ def move_filter(request, setname, fname, direction): password=request.session["password"]) try: fset = sc.getscript(setname, format="fset") - fset.movefilter(fname, direction) + fset.movefilter(fname.encode("utf-8"), direction) sc.pushscript(setname, str(fset)) except (SieveClientError), e: return ajax_response(request, "ko", respmsg=str(e)) From 366371dc592fb86f88783f78d962f3c5c15b91a2 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 28 Aug 2013 21:50:30 +0200 Subject: [PATCH 09/61] try/except statement added. fixes #472 --- modoboa/extensions/sievefilters/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index a648195cf..8253037eb 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -106,7 +106,10 @@ def submitfilter(request, setname, okmsg, tplname, tplctx, update=False, sc=None fset.updatefilter(request.POST["oldname"], fltname, conditions, actions, match_type) - sc.pushscript(fset.name, str(fset)) + try: + sc.pushscript(fset.name, str(fset)) + except SieveClientError as e: + return ajax_response(request, "ko", respmsg=str(e)) return ajax_response(request, respmsg=okmsg, ajaxnav=True) tplctx = build_filter_ctx(tplctx, form) return ajax_response(request, status="ko", template=tplname, **tplctx) From 3b665ecc2234c3129b6a95f1303d6b8cda355a9c Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 29 Aug 2013 19:01:23 +0200 Subject: [PATCH 10/61] Fixing wrong instruction. --- doc/getting_started/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/getting_started/install.rst b/doc/getting_started/install.rst index 3ba00684f..af7d64419 100644 --- a/doc/getting_started/install.rst +++ b/doc/getting_started/install.rst @@ -51,7 +51,7 @@ Fortunately, all major distributions include a ready-to-use package. On Debian/Ubuntu:: $ apt-get install libcairo2-dev libpango1.0-dev librrd-dev - $ pip install python-rrdtool + $ apt-get install python-rrdtool `virtualenv `_ users ========================================================== From 94bb096c9ac6f463840bed6321ddd8df4764a24b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 29 Aug 2013 19:12:50 +0200 Subject: [PATCH 11/61] Documentation fixed. fixes #461 --- doc/getting_started/upgrade.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index 1cc854e15..608951ff7 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -170,7 +170,8 @@ Postfix and Dovecot configuration update It is necessary to update the queries used to retrieve users and mailboxes: -#. Replace all occurences of ``auth_user`` by ``admin_user`` +#. Run ``modoboa-admin.py postfix_maps --dbtype `` and compare the files with those that postfix currently + use. Make necessary updates in light of the differences #. Into *dovecot-sql.conf*, update the ``user_query`` query, refer to :ref:`dovecot_mysql_queries` or :ref:`dovecot_pg_queries` From fdd5eea6cd168b7319f10f8e4cdf3aef6b24ce91 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 31 Aug 2013 09:36:59 +0200 Subject: [PATCH 12/61] fixes #464. --- modoboa/extensions/amavis/views.py | 33 +++++++++++++++++------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/modoboa/extensions/amavis/views.py b/modoboa/extensions/amavis/views.py index e33cc26e8..4f4857dc5 100644 --- a/modoboa/extensions/amavis/views.py +++ b/modoboa/extensions/amavis/views.py @@ -18,20 +18,25 @@ from modoboa.lib.email_listing import parse_search_parameters from sql_listing import * -def __get_current_url(request): +def __back_to_listing(request): + """Return the current listing URL. + + Looks into the user's session and the current request to build the + URL. + + :param request: a ``Request`` object + :return: a string + """ + url = "listing" params = [] if "page" in request.session: - res = "listing" - params.append("page=%s" % request.session["page"]) - else: - res = "listing" + params += ["page=%s" % request.session["page"]] - params += map(lambda p: "%s=%s" % (p, request.session[p]), - filter(request.session.has_key, ["criteria", "pattern"])) - - if len(params): - res += "?%s" % ("&".join(params)) - return res + params += ["%s=%s" % (p, request.session[p]) + for p in ["criteria", "pattern"] if p in request.session] + if params: + url += "?%s" % ("&".join(params)) + return url def empty_quarantine(request): @@ -228,7 +233,7 @@ def delete(request, mail_id): "%(count)d messages deleted successfully", len(mail_id)) % {"count": len(mail_id)} return ajax_response(request, respmsg=message, - url=__get_current_url(request)) + url=__back_to_listing(request)) def release_selfservice(request, mail_id): @@ -276,7 +281,7 @@ def release(request, mail_id): "%(count)d requests sent", len(mail_id)) % {"count": len(mail_id)} return ajax_response(request, "ok", respmsg=message, - url=__get_current_url(request)) + url=__back_to_listing(request)) else: msgrcpts = [] wrapper = get_wrapper() @@ -302,7 +307,7 @@ def release(request, mail_id): else: message = error return ajax_response(request, "ko" if error else "ok", respmsg=message, - url=__get_current_url(request)) + url=__back_to_listing(request)) @login_required From f210178a44a789f39e50f2deb2cf528f13d605fa Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 31 Aug 2013 09:49:37 +0200 Subject: [PATCH 13/61] It seems I forgot the rename case. --- modoboa/extensions/sievefilters/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index 8253037eb..7f1bdd26e 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -103,9 +103,10 @@ def submitfilter(request, setname, okmsg, tplname, tplctx, update=False, sc=None fset.addfilter(fltname, conditions, actions, match_type) else: - fset.updatefilter(request.POST["oldname"], - fltname, conditions, actions, - match_type) + oldname = request.POST["oldname"].encode("utf-8") + fset.updatefilter( + oldname, fltname, conditions, actions, match_type + ) try: sc.pushscript(fset.name, str(fset)) except SieveClientError as e: From 2b3349749bfd44e42a40518223b0aec1ce14848b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 31 Aug 2013 17:25:51 +0200 Subject: [PATCH 14/61] Special case for users migrating from a version < 0.9.5. fixes #460 --- doc/getting_started/upgrade.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index 608951ff7..b9ac53826 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -15,6 +15,16 @@ newest versions of Modoboa. Latest version ************** +.. warning:: + + If you use a version **prior to 0.9.5**, please migrate in two + steps: + + #. first migrate to 0.9.5 + #. then migrate to the latest version + + If you try to migrate directly, the operation will fail. + Starting with version 0.9.1, Modoboa comes as a standard django application. Fetch the latest version (see :ref:`get_modoboa`) and install it. From af97150fbb006f837d6da9593fb5483ef391dedd Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 31 Aug 2013 17:26:36 +0200 Subject: [PATCH 15/61] Updated gunicorn binary. --- doc/integration/web_servers.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index ec4911a63..ff5ca5712 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -113,7 +113,7 @@ file named *gunicorn.conf.py* inside Modoboa's root dir):: To start gunicorn, execute the following commands:: $ cd - $ gunicorn_django -c gunicorn.conf.py + $ gunicorn -c gunicorn.conf.py Now the *nginx* part. Just create a new virtual host and use the following configuration:: @@ -143,7 +143,7 @@ following configuration:: proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Forwarded-Protocol ssl; - proxy_pass http://modoboa; + proxy_pass http://modoboa; } } From b2f5c026e69dcb95ccea46ebaea2c5d316bb6a6b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 1 Sep 2013 16:05:09 +0200 Subject: [PATCH 16/61] Always encode a parameter's value (if needed). fixes #468 --- modoboa/static/js/history.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modoboa/static/js/history.js b/modoboa/static/js/history.js index 562aa4605..72cd2c069 100644 --- a/modoboa/static/js/history.js +++ b/modoboa/static/js/history.js @@ -134,16 +134,12 @@ History.prototype = { } var elems = str.split('&'); for (var i = 0; i < elems.length; i++) { - this.setparamfromstring(elems[i]); + var pdef = elems[i].split('='); + this.setparam(pdef[0], pdef[1]); } return this; }, - setparamfromstring: function(str) { - var def = str.split('='); - this.params[def[0]] = def[1]; - }, - setparam: function(name, value, encode) { if (encode == undefined || encode) { this.params[name] = encodeURIComponent(value); From d1616f53077c7f13311be169357ea5b459bf63cc Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 1 Sep 2013 16:16:33 +0200 Subject: [PATCH 17/61] Unique constraint must be set on both armessage AND sender. fixes #467 --- ..._auto__add_unique_arhistoric_sender_armessage.py} | 12 ++++++------ modoboa/extensions/postfix_autoreply/models.py | 9 ++++++++- 2 files changed, 14 insertions(+), 7 deletions(-) rename modoboa/extensions/postfix_autoreply/migrations/{0003_auto__add_unique_arhistoric_armessage.py => 0003_auto__add_unique_arhistoric_sender_armessage.py} (93%) diff --git a/modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_armessage.py b/modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_sender_armessage.py similarity index 93% rename from modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_armessage.py rename to modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_sender_armessage.py index a1bd293e0..5a06f9029 100644 --- a/modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_armessage.py +++ b/modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_sender_armessage.py @@ -8,13 +8,13 @@ from django.db import models class Migration(SchemaMigration): def forwards(self, orm): - # Adding unique constraint on 'ARhistoric', fields ['armessage'] - db.create_unique(u'postfix_autoreply_arhistoric', ['armessage_id']) + # Adding unique constraint on 'ARhistoric', fields ['sender', 'armessage'] + db.create_unique(u'postfix_autoreply_arhistoric', ['sender', 'armessage_id']) def backwards(self, orm): - # Removing unique constraint on 'ARhistoric', fields ['armessage'] - db.delete_unique(u'postfix_autoreply_arhistoric', ['armessage_id']) + # Removing unique constraint on 'ARhistoric', fields ['sender', 'armessage'] + db.delete_unique(u'postfix_autoreply_arhistoric', ['sender', 'armessage_id']) models = { @@ -94,8 +94,8 @@ class Migration(SchemaMigration): u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) }, u'postfix_autoreply.arhistoric': { - 'Meta': {'object_name': 'ARhistoric'}, - 'armessage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['postfix_autoreply.ARmessage']", 'unique': 'True'}), + 'Meta': {'unique_together': "(('armessage', 'sender'),)", 'object_name': 'ARhistoric'}, + 'armessage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['postfix_autoreply.ARmessage']"}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'last_sent': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 'sender': ('django.db.models.fields.TextField', [], {}) diff --git a/modoboa/extensions/postfix_autoreply/models.py b/modoboa/extensions/postfix_autoreply/models.py index db1c4c44b..4082ed83d 100644 --- a/modoboa/extensions/postfix_autoreply/models.py +++ b/modoboa/extensions/postfix_autoreply/models.py @@ -2,14 +2,17 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from modoboa.admin.models import Domain, Mailbox + class Transport(models.Model): domain = models.CharField(max_length=300) method = models.CharField(max_length=255) + class Alias(models.Model): full_address = models.CharField(max_length=255) autoreply_address = models.CharField(max_length=255) + class ARmessage(models.Model): mbox = models.ForeignKey(Mailbox) subject = models.CharField(_('subject'), max_length=255, @@ -20,7 +23,11 @@ class ARmessage(models.Model): help_text=_("Activate/Deactivate your auto reply")) untildate = models.DateTimeField() + class ARhistoric(models.Model): - armessage = models.ForeignKey(ARmessage, unique=True) + armessage = models.ForeignKey(ARmessage) last_sent = models.DateTimeField(auto_now=True) sender = models.TextField() + + class Meta: + unique_together = ("armessage", "sender") From 12dfcdc3f11f79664d669564ce8a5f42789c9738 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 1 Sep 2013 16:25:58 +0200 Subject: [PATCH 18/61] Here is the good combination! fixes #467 --- .../0003_auto__chg_field_arhistoric_sender.py | 120 ++++++++++++++++++ ...add_unique_arhistoric_sender_armessage.py} | 2 +- .../extensions/postfix_autoreply/models.py | 20 ++- 3 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 modoboa/extensions/postfix_autoreply/migrations/0003_auto__chg_field_arhistoric_sender.py rename modoboa/extensions/postfix_autoreply/migrations/{0003_auto__add_unique_arhistoric_sender_armessage.py => 0004_auto__add_unique_arhistoric_sender_armessage.py} (98%) diff --git a/modoboa/extensions/postfix_autoreply/migrations/0003_auto__chg_field_arhistoric_sender.py b/modoboa/extensions/postfix_autoreply/migrations/0003_auto__chg_field_arhistoric_sender.py new file mode 100644 index 000000000..a760497e9 --- /dev/null +++ b/modoboa/extensions/postfix_autoreply/migrations/0003_auto__chg_field_arhistoric_sender.py @@ -0,0 +1,120 @@ +# -*- 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): + + # Changing field 'ARhistoric.sender' + db.alter_column(u'postfix_autoreply_arhistoric', 'sender', self.gf('django.db.models.fields.CharField')(max_length=254)) + + def backwards(self, orm): + + # Changing field 'ARhistoric.sender' + db.alter_column(u'postfix_autoreply_arhistoric', 'sender', self.gf('django.db.models.fields.TextField')()) + + models = { + 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.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.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.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'}) + }, + u'postfix_autoreply.alias': { + 'Meta': {'object_name': 'Alias'}, + 'autoreply_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'full_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'postfix_autoreply.arhistoric': { + 'Meta': {'object_name': 'ARhistoric'}, + 'armessage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['postfix_autoreply.ARmessage']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_sent': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'sender': ('django.db.models.fields.CharField', [], {'max_length': '254'}) + }, + u'postfix_autoreply.armessage': { + 'Meta': {'object_name': 'ARmessage'}, + 'content': ('django.db.models.fields.TextField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mbox': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Mailbox']"}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'untildate': ('django.db.models.fields.DateTimeField', [], {}) + }, + u'postfix_autoreply.transport': { + 'Meta': {'object_name': 'Transport'}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'method': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['postfix_autoreply'] \ No newline at end of file diff --git a/modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_sender_armessage.py b/modoboa/extensions/postfix_autoreply/migrations/0004_auto__add_unique_arhistoric_sender_armessage.py similarity index 98% rename from modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_sender_armessage.py rename to modoboa/extensions/postfix_autoreply/migrations/0004_auto__add_unique_arhistoric_sender_armessage.py index 5a06f9029..25819da9a 100644 --- a/modoboa/extensions/postfix_autoreply/migrations/0003_auto__add_unique_arhistoric_sender_armessage.py +++ b/modoboa/extensions/postfix_autoreply/migrations/0004_auto__add_unique_arhistoric_sender_armessage.py @@ -98,7 +98,7 @@ class Migration(SchemaMigration): 'armessage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['postfix_autoreply.ARmessage']"}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'last_sent': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'sender': ('django.db.models.fields.TextField', [], {}) + 'sender': ('django.db.models.fields.CharField', [], {'max_length': '254'}) }, u'postfix_autoreply.armessage': { 'Meta': {'object_name': 'ARmessage'}, diff --git a/modoboa/extensions/postfix_autoreply/models.py b/modoboa/extensions/postfix_autoreply/models.py index 4082ed83d..c814a11db 100644 --- a/modoboa/extensions/postfix_autoreply/models.py +++ b/modoboa/extensions/postfix_autoreply/models.py @@ -15,19 +15,25 @@ class Alias(models.Model): class ARmessage(models.Model): mbox = models.ForeignKey(Mailbox) - subject = models.CharField(_('subject'), max_length=255, - help_text=_("The subject that will appear in sent emails")) - content = models.TextField(_('content'), - help_text=_("The content that will appear in sent emails")) - enabled = models.BooleanField(_('enabled'), - help_text=_("Activate/Deactivate your auto reply")) + subject = models.CharField( + _('subject'), max_length=255, + help_text=_("The subject that will appear in sent emails") + ) + content = models.TextField( + _('content'), + help_text=_("The content that will appear in sent emails") + ) + enabled = models.BooleanField( + _('enabled'), + help_text=_("Activate/Deactivate your auto reply") + ) untildate = models.DateTimeField() class ARhistoric(models.Model): armessage = models.ForeignKey(ARmessage) last_sent = models.DateTimeField(auto_now=True) - sender = models.TextField() + sender = models.CharField(max_length=254) class Meta: unique_together = ("armessage", "sender") From 95dec87e0292505c2d6d362f45f0bc1d745c48c7 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 2 Sep 2013 21:48:44 +0200 Subject: [PATCH 19/61] A hierarchy delimiter is not always the same. This code will behave better but it is far from perfect. see #435 --- modoboa/extensions/webmail/imaputils.py | 30 +++++++++++++++---- .../webmail/static/webmail/js/webmail.js | 21 ++++++------- .../webmail/templates/webmail/index.html | 3 +- modoboa/extensions/webmail/views.py | 1 + 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/modoboa/extensions/webmail/imaputils.py b/modoboa/extensions/webmail/imaputils.py index 189351705..e519e1a83 100644 --- a/modoboa/extensions/webmail/imaputils.py +++ b/modoboa/extensions/webmail/imaputils.py @@ -146,6 +146,7 @@ class IMAPconnector(object): unseen_pattern = re.compile(r'[^\(]+\(UNSEEN (\d+)\)') def __init__(self, user=None, password=None): + self.__hdelimiter = None self.criterions = [] self.address = parameters.get_admin("IMAP_SERVER") self.port = int(parameters.get_admin("IMAP_PORT")) @@ -220,6 +221,23 @@ class IMAPconnector(object): bodystruct[5], int(bodystruct[6])) return (None, None, 0) + @property + def hdelimiter(self): + """Return the default hierachy delimiter. + + This is a simple way to retrieve the default delimiter (see + http://www.imapwiki.org/ClientImplementation/MailboxList). + + :return: a string + """ + if self.__hdelimiter is None: + data = self._cmd("LIST", "", "") + m = self.list_response_pattern.match(data[0]) + if m is None: + raise WebmailError(_("Failed to retrieve hierarchy delimiter")) + self.__hdelimiter = m.group('delimiter') + return self.__hdelimiter + def refresh(self, user, password): """Check if current connection needs a refresh @@ -378,7 +396,7 @@ class IMAPconnector(object): newmboxes += [descr] if re.search("\%s" % delimiter, name): - parts = name.split(".") + parts = name.split(delimiter) if not descr.has_key("path"): descr["path"] = parts[0] descr["sub"] = [] @@ -392,7 +410,9 @@ class IMAPconnector(object): @capability('LIST-EXTENDED', '_listmboxes_simple') def _listmboxes(self, topmailbox, mailboxes, until_mailbox=None): - pattern = ("%s.%%" % topmailbox.encode("imap4-utf-7")) if len(topmailbox) else "%" + pattern = "%s%s%%" % (topmailbox.encode("imap4-utf-7"), self.hdelimiter) \ + if len(topmailbox) else "%" + print pattern resp = self._cmd("LIST", "", pattern, "RETURN", "(CHILDREN)") newmboxes = [] for mb in resp: @@ -452,7 +472,7 @@ class IMAPconnector(object): {"name" : parameters.get_user(user, "TRASH_FOLDER"), "class" : "icon-trash"}] if until_mailbox: - name, parent = separate_mailbox(until_mailbox) + name, parent = separate_mailbox(until_mailbox, self.hdelimiter) if parent: until_mailbox = parent self._listmboxes(topmailbox, md_mailboxes, until_mailbox) @@ -536,7 +556,7 @@ class IMAPconnector(object): def create_folder(self, name, parent=None): if parent is not None: - name = "%s.%s" % (parent, name) + name = "%s%s%s" % (parent, self.hdelimiter, name) typ, data = self.m.create(self._encode_mbox_name(name)) if typ == "NO": raise WebmailError(data[0]) @@ -659,7 +679,7 @@ def separate_mailbox(fullname, sep="."): If a separator is found in ``fullname``, this function returns the corresponding name and parent mailbox name. """ - if fullname.count("."): + if sep in fullname: parts = fullname.split(sep) name = parts[-1] parent = sep.join(parts[0:len(parts) - 1]) diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index 8d40667f7..8cea472b2 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -15,7 +15,8 @@ Webmail.prototype = { submboxes_url: "", delattachment_url: "", ro_mboxes: ["INBOX"], - trash: "" + trash: "", + hdelimiter: '.' }, initialize: function(options) { @@ -217,7 +218,7 @@ Webmail.prototype = { } var $link = $('a[href="' + mailbox + '"]'); - var parts = mailbox.split('.'); + var parts = mailbox.split(this.options.hdelimiter); var dname = " " + parts[parts.length - 1]; var $i = $link.children("i"); @@ -285,7 +286,7 @@ Webmail.prototype = { 'class': "block", href: mailbox }); - var parts = mailbox.split("."); + var parts = mailbox.split(this.options.hdelimiter); var linkcontent = " "; var displayname = linkcontent + parts[parts.length - 1]; @@ -401,18 +402,18 @@ Webmail.prototype = { * to access this mailbox are also loaded) */ load_and_select_mailbox: function(mailbox) { - if (mailbox.indexOf(".") == -1) { + if (mailbox.indexOf(this.options.hdelimiter) == -1) { this.select_mailbox(mailbox); return; } this.reset_mb_selection(); - var parts = mailbox.split("."); + var parts = mailbox.split(this.options.hdelimiter); var curmb = parts[0], lastmb = ""; for (var i = 1; i < parts.length; i++) { lastmb = curmb; - curmb += "." + parts[i]; + curmb += this.options.hdelimiter + parts[i]; var $link = $('a[href="' + curmb + '"]'); var $container = $('li[name="' + lastmb + '"]'); @@ -535,7 +536,7 @@ Webmail.prototype = { return; } $parent = $ul; - mailbox = $parent.attr("name") + "." + mailbox; + mailbox = $parent.attr("name") + this.hdelimiter + mailbox; } else { $parent = $("#mboxes_container").children("ul"); } @@ -548,8 +549,8 @@ Webmail.prototype = { * If needed, the mailbox will be moved to its new location. */ rename_mailbox: function(oldname, newname, oldparent, newparent) { - var oldpattern = (oldparent) ? oldparent + "." + oldname : oldname; - var newpattern = (newparent) ? newparent + "." + newparent : newname; + var oldpattern = (oldparent) ? oldparent + this.hdelimiter + oldname : oldname; + var newpattern = (newparent) ? newparent + this.hdelimiter + newparent : newname; var $link = $("#folders").find('a[href="' + oldpattern + '"]'); if (oldname != newname) { @@ -562,7 +563,7 @@ Webmail.prototype = { this.navobject.setparam("mbox", newname).update(false, true); } if (oldparent != newparent) { - var newlocation = (newparent) ? newparent + "." + newname : newname; + var newlocation = (newparent) ? newparent + this.hdelimiter + newname : newname; this.remove_mbox_from_tree($link.parent("li")); this.add_mailbox_to_tree(newparent, newname); if (this.navobject.getparam("action") == "listmailbox") { diff --git a/modoboa/extensions/webmail/templates/webmail/index.html b/modoboa/extensions/webmail/templates/webmail/index.html index b404f7efc..96c5b588f 100644 --- a/modoboa/extensions/webmail/templates/webmail/index.html +++ b/modoboa/extensions/webmail/templates/webmail/index.html @@ -20,7 +20,8 @@ $(document).ready(function() { deflocation: "{{ deflocation }}", defcallback: "{{ defcallback }}", ro_mboxes: {% tolist ro_mboxes %}, - trash: "{{ trash }}" + trash: "{{ trash }}", + hdelimiter: '{{ hdelimiter }}' }); }); diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 8563bcc6b..968ee4436 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -504,6 +504,7 @@ def index(request): if not request.is_ajax(): request.session["lastaction"] = None imapc = get_imapconnector(request) + response["hdelimiter"] = imapc.hdelimiter response["mboxes"] = render_mboxes_list(request, imapc) imapc.getquota(curmbox) response["refreshrate"] = \ From becc1822679b6473502d82c91dd26f19c4228014 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 2 Sep 2013 22:10:31 +0200 Subject: [PATCH 20/61] Fixing the sieve plugin too. see #435 --- modoboa/extensions/sievefilters/forms.py | 2 +- .../sievefilters/static/sievefilters/js/jquery.dynfields.js | 3 ++- .../extensions/sievefilters/templates/sievefilters/index.html | 3 ++- modoboa/extensions/sievefilters/views.py | 3 +++ modoboa/extensions/webmail/imaputils.py | 1 - 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/modoboa/extensions/sievefilters/forms.py b/modoboa/extensions/sievefilters/forms.py index 5ee3724bf..2c51841dd 100644 --- a/modoboa/extensions/sievefilters/forms.py +++ b/modoboa/extensions/sievefilters/forms.py @@ -155,7 +155,7 @@ class FilterForm(forms.Form): for fd in folders: value = fd["path"] if fd.has_key("path") else fd["name"] if parentmb: - ret += [(value, fd["name"].replace("%s." % parentmb, ""))] + ret += [(value, fd["name"].replace("%s%s" % (parentmb, imapc.hdelimiter), ""))] else: ret += [(value, fd["name"])] if fd.has_key("sub"): diff --git a/modoboa/extensions/sievefilters/static/sievefilters/js/jquery.dynfields.js b/modoboa/extensions/sievefilters/static/sievefilters/js/jquery.dynfields.js index 98112a4cc..78c17d1a2 100644 --- a/modoboa/extensions/sievefilters/static/sievefilters/js/jquery.dynfields.js +++ b/modoboa/extensions/sievefilters/static/sievefilters/js/jquery.dynfields.js @@ -178,7 +178,7 @@ value = $this.val(); while (true) { - var id = value.indexOf(".", start); + var id = value.indexOf(options.hdelimiter, start); if (id == -1) { break; } @@ -322,6 +322,7 @@ $.fn.dynaction.defaults = { defaultaction: "fileinto", + hdelimiter: '.', templates: [] }; })(jQuery); \ No newline at end of file diff --git a/modoboa/extensions/sievefilters/templates/sievefilters/index.html b/modoboa/extensions/sievefilters/templates/sievefilters/index.html index 4da8cba6e..2430cafdc 100644 --- a/modoboa/extensions/sievefilters/templates/sievefilters/index.html +++ b/modoboa/extensions/sievefilters/templates/sievefilters/index.html @@ -22,7 +22,8 @@ function filterform_cb() { templates: cond_templates }).data('dyncondition'); var $da = $("#action_0").dynaction({ - templates: action_templates + templates: action_templates, + hdelimiter: '{{ hdelimiter }}' }).data('dynaction'); $("input[name=match_type]").click(function(e) { diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index 7f1bdd26e..d2d8cf962 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -18,6 +18,8 @@ from templatetags.sfilters_tags import fset_menu @login_required @needs_mailbox() def index(request, tplname="sievefilters/index.html"): + from modoboa.extensions.webmail.imaputils import get_imapconnector + try: sc = SieveClient(user=request.user.username, password=request.session["password"]) @@ -39,6 +41,7 @@ def index(request, tplname="sievefilters/index.html"): "active_script" : active_script, "default_script" : default_script, "scripts" : sorted(scripts), + "hdelimiter": get_imapconnector(request).hdelimiter }) @login_required diff --git a/modoboa/extensions/webmail/imaputils.py b/modoboa/extensions/webmail/imaputils.py index e519e1a83..a60145200 100644 --- a/modoboa/extensions/webmail/imaputils.py +++ b/modoboa/extensions/webmail/imaputils.py @@ -412,7 +412,6 @@ class IMAPconnector(object): def _listmboxes(self, topmailbox, mailboxes, until_mailbox=None): pattern = "%s%s%%" % (topmailbox.encode("imap4-utf-7"), self.hdelimiter) \ if len(topmailbox) else "%" - print pattern resp = self._cmd("LIST", "", pattern, "RETURN", "(CHILDREN)") newmboxes = [] for mb in resp: From 70dafb1f9827511f55e0b46ef1382999c40601dd Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 3 Sep 2013 19:00:50 +0200 Subject: [PATCH 21/61] Adding encoding information. fixes #475 --- modoboa/extensions/sievefilters/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index d2d8cf962..d730eff2b 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -261,7 +261,7 @@ def download_filters_set(request, name): return ajax_response(request, "ko", respmsg=str(e)) resp = HttpResponse(script) - resp["Content-Type"] = "text/plain" + resp["Content-Type"] = "text/plain; charset=utf-8" resp["Content-Length"] = len(script) resp["Content-Disposition"] = 'attachment; filename="%s.txt"' % name return resp From 4adcc28a0e330b96a1bfa978042bb4104c6d0bdd Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 3 Sep 2013 19:19:36 +0200 Subject: [PATCH 22/61] Setting right padding to 0. We gain some space but it is still an incomplete solution. fixes #476 --- modoboa/extensions/webmail/static/webmail/css/webmail.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modoboa/extensions/webmail/static/webmail/css/webmail.css b/modoboa/extensions/webmail/static/webmail/css/webmail.css index 891f9e2c9..82adf1cef 100644 --- a/modoboa/extensions/webmail/static/webmail/css/webmail.css +++ b/modoboa/extensions/webmail/static/webmail/css/webmail.css @@ -16,6 +16,10 @@ body, html { overflow: auto; } +#mboxes_container .nav-list .nav-list { + padding-right: 0; +} + #menubar { position: fixed; left: 215px; From 3f53b7551611da99e9ba4d596d40289c61b46257 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 5 Sep 2013 20:45:43 +0200 Subject: [PATCH 23/61] Better support of misencoded headers. --- modoboa/lib/u2u_decode.py | 57 +++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/modoboa/lib/u2u_decode.py b/modoboa/lib/u2u_decode.py index f09304742..751f1022d 100644 --- a/modoboa/lib/u2u_decode.py +++ b/modoboa/lib/u2u_decode.py @@ -1,45 +1,50 @@ #!/usr/bin/env python -# u2u_decode.py -# unstructured rfc2047 header to unicode -# +# coding: utf-8 +""" +u2u_decode.py + +unstructured rfc2047 header to unicode +""" import re from email.header import decode_header, make_header # check spaces between encoded_words (and strip them) sre = re.compile(r'\?=[ \t]+=\?') # re pat for MIME encoded_word (without trailing spaces) -mre = re.compile(r'=\?[^?]*?\?[bq]\?[^? \t]*?\?=', re.I) +mre = re.compile(r'=\?[^?]*?\?[bq]\?[^?\t]*?\?=', re.I) + + +def clean_spaces(m): + """Replace unencoded spaces in string + + :param str m: a match object + :return: the cleaned string + """ + return m.group(0).replace(" ", "=20") -tre = re.compile('=\?[^?]*\?=') def decode_mime(m): - # substitute matching encoded_word with unicode equiv. - h = decode_header(m.group(0)) + """substitute matching encoded_word with unicode equiv. + """ + h = decode_header(clean_spaces(m)) u = unicode(make_header(h)) return u -def clean_spaces(m): - return m.group(0).replace(" ", "=20") def u2u_decode(s): - # utility function for (final) decoding of mime header - # note: resulting string is in one line (no \n within) - # note2: spaces between enc_words are stripped (see RFC2047) + ur"""utility function for (final) decoding of mime header + + note: resulting string is in one line (no \n within) + note2: spaces between enc_words are stripped (see RFC2047) + + >>> u2u_decode('=?ISO-8859-15?Q?=20Profitez de tous les services en ligne sur impots.gouv.fr?=') + u' Profitez de tous les services en ligne sur impots.gouv.fr' + >>> u2u_decode('=?ISO-8859-1?Q?Accus=E9?= de =?ISO-8859-1?Q?r=E9ception?= de votre annonce') + u'Accus\xe9 de r\xe9ception de votre annonce' + >>> u2u_decode('Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord') + u'Sm\xf6rg\xe5sbord' + """ s = ''.join(s.splitlines()) s = sre.sub('?==?', s) - s = tre.sub(clean_spaces, s) u = mre.sub(decode_mime, s) return u - -if __name__ == '__main__': - import sys - if len(sys.argv) > 1: - s = sys.argv[1] - else: - s = """[Apache-Users 7784] ServerAlias =?ISO-2022-JP?B?GyRAGyRCJEsbKEI=?=:( - =?ISO-2022-JP?B?GyRCJTMlbSVzGyhC?=)=?ISO-2022-JP?B?GyRCJHI7SBsoQg==?= - =?ISO-2022-JP?B?GyRAGyRCTVEkOSRrJEg1c0YwJCwkKiQrJDckJBsoQg==?=""" - s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord' - u = u2u_decode(s) - print u - From 525c20dd3280fd0d0cbf523dcd033b4d16a72885 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 5 Sep 2013 20:50:50 +0200 Subject: [PATCH 24/61] Better email attachment support. see #473 --- modoboa/extensions/webmail/fetch_parser.py | 44 ++++++++++++++++------ modoboa/extensions/webmail/lib.py | 4 +- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/modoboa/extensions/webmail/fetch_parser.py b/modoboa/extensions/webmail/fetch_parser.py index 22fd79a4b..c5337d4c0 100644 --- a/modoboa/extensions/webmail/fetch_parser.py +++ b/modoboa/extensions/webmail/fetch_parser.py @@ -10,7 +10,20 @@ retrieve multiple attributes of a message. This simple module tries to fix that *problem*. """ -class ParseError(Exception): pass +class ParseError(Exception): + pass + + +class Token(object): + def __init__(self, value): + self.value = value + + +class Literal(Token): + def __init__(self, value): + super(Literal, self).__init__(value) + self.next_token_len = int(self.value[1:-1]) + def parse_next_token(buf): """Look for the next *token* @@ -23,9 +36,11 @@ def parse_next_token(buf): :return: the position of the token's last caracter into ``buf`` """ end = -1 + klass = Token if buf[0] == '{': # Literal end = buf.find('}') + klass = Literal elif buf[0] == '"': # quoted end = buf.find('"', 1) @@ -36,7 +51,9 @@ def parse_next_token(buf): break if end == -1: raise ParseError("End of buffer reached while looking for a token end") - return end + end += 1 + token = klass(buf[:end]) + return token, end def parse_bodystructure(buf, depth=0, prefix=""): """Special parser for BODYSTRUCTURE response @@ -85,9 +102,13 @@ def parse_bodystructure(buf, depth=0, prefix=""): pos += 1 continue nb_bodystruct = 0 - end = parse_next_token(buf[pos:]) - ret.append(buf[pos:pos + end + 1].strip('"')) - pos += end + 1 + token, end = parse_next_token(buf[pos:]) + pos += end + if isinstance(token, Literal): + ret.append(buf[pos:pos + token.next_token_len].strip('"')) + pos += token.next_token_len + else: + ret.append(token.value.strip('"')) raise ParseError("End of buffer reached while looking for a BODY/BODYSTRUCTURE end") @@ -150,11 +171,10 @@ def parse_fetch_response(data): end = pos + 1 break else: - end = parse_next_token(response) - if response[0] == '{': - datalen = int(response[1:end]) - response = response[end + 1:] - end = datalen + token, end = parse_next_token(response) + if isinstance(token, Literal): + response = response[end:] + end = token.next_token_len msgdef[cmdname] = response[:end] response = response[end + 1:] @@ -195,8 +215,8 @@ if __name__ == "__main__": # (' (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) ((NIL NIL "hautdebit" "freetelecom.fr")) ((NIL NIL "nguyen.antoine" "wanadoo.fr")) NIL NIL NIL "<20061213193125.9DA0919AC@dgroup2-2.proxad.net>") ("text" "plain" ("charset" "iso-8859-1") NIL NIL "8bit" 1428 38 NIL ("inline" NIL) NIL NIL) 76 NIL ("inline" ("filename*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74")) NIL NIL) "mixed" ("boundary" "------------040706080908000209030901") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {266}', # 'Date: Tue, 19 Dec 2006 19:50:13 +0100\r\nFrom: Antoine Nguyen \r\nTo: Antoine Nguyen \r\nSubject: [Fwd: [INSCRIPTION] =?ISO-8859-1?Q?R=E9c=E9ption_de_votre_?=\r\n =?ISO-8859-1?Q?dossier_d=27inscription_Free_Haut_D=E9bit=5D?=\r\n\r\n'), ')'] - resp = [('123 (UID 3 BODYSTRUCTURE (((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 1266 30 NIL NIL NIL NIL)("text" "html" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 8830 227 NIL NIL NIL NIL) "alternative" ("boundary" "_000_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL NIL NIL)("image" "png" ("name" "image005.png") "" "image005.png" "base64" 7464 NIL ("inline" ("filename" "image005.png" "size" "5453" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL)("image" "jpeg" ("name" "image006.jpg") "" "image006.jpg" "base64" 2492 NIL ("inline" ("filename" "image006.jpg" "size" "1819" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL) "related" ("boundary" "_006_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_" "type" "multipart/alternative") NIL NIL NIL)("application" "pdf" ("name" "bilan assurance CIC.PDF") NIL "bilan assurance CIC.PDF" "base64" 459532 NIL ("attachment" ("filename" "bilan assurance CIC.PDF" "size" "335811" "creation-date" "Fri, 16 Sep 2011 12:45:23 GMT" "modification-date" "Fri, 16 Sep 2011 12:45:23 GMT")) NIL NIL)(("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 1389 29 NIL NIL NIL NIL)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 1457 27 NIL NIL NIL NIL) "alternative" ("boundary" "===============0775904800==") ("inline" NIL) NIL NIL) "mixed" ("boundary" "_007_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL ("fr-FR") NIL)',), ')'] + # resp = [('123 (UID 3 BODYSTRUCTURE (((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 1266 30 NIL NIL NIL NIL)("text" "html" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 8830 227 NIL NIL NIL NIL) "alternative" ("boundary" "_000_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL NIL NIL)("image" "png" ("name" "image005.png") "" "image005.png" "base64" 7464 NIL ("inline" ("filename" "image005.png" "size" "5453" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL)("image" "jpeg" ("name" "image006.jpg") "" "image006.jpg" "base64" 2492 NIL ("inline" ("filename" "image006.jpg" "size" "1819" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL) "related" ("boundary" "_006_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_" "type" "multipart/alternative") NIL NIL NIL)("application" "pdf" ("name" "bilan assurance CIC.PDF") NIL "bilan assurance CIC.PDF" "base64" 459532 NIL ("attachment" ("filename" "bilan assurance CIC.PDF" "size" "335811" "creation-date" "Fri, 16 Sep 2011 12:45:23 GMT" "modification-date" "Fri, 16 Sep 2011 12:45:23 GMT")) NIL NIL)(("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 1389 29 NIL NIL NIL NIL)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 1457 27 NIL NIL NIL NIL) "alternative" ("boundary" "===============0775904800==") ("inline" NIL) NIL NIL) "mixed" ("boundary" "_007_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL ("fr-FR") NIL)',), ')'] - resp = [('856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "(26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)',), ')'] + # resp = [('856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "(26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)',), ')'] print parse_fetch_response(resp) diff --git a/modoboa/extensions/webmail/lib.py b/modoboa/extensions/webmail/lib.py index e78463b75..8d39667cb 100644 --- a/modoboa/extensions/webmail/lib.py +++ b/modoboa/extensions/webmail/lib.py @@ -211,7 +211,7 @@ class ImapEmail(Email): self.bs = BodyStructure(msg['BODYSTRUCTURE']) data = None - mformat = self.dformat if self.bs.contents.has_key(self.dformat) else fallback_fmt + mformat = self.dformat if self.dformat in self.bs.contents else fallback_fmt if len(self.bs.contents): bodyc = u'' @@ -613,7 +613,7 @@ def create_mail_attachment(attdef): res.set_payload(fp.read()) fp.close() Encoders.encode_base64(res) - res.add_header("Content-Disposition", "attachment; filename='%s'" % attdef["fname"]) + res.add_header("Content-Disposition", 'attachment', filename=attdef["fname"]) return res From 80e8bd0564a517fbd1f89166511a130a92ec1a31 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 5 Sep 2013 20:51:57 +0200 Subject: [PATCH 25/61] Documentation update. --- README.rst | 4 ++-- doc/getting_started/install.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d98c1dd09..7c17f005f 100644 --- a/README.rst +++ b/README.rst @@ -11,8 +11,8 @@ as an administration panel or a webmail. Modoboa integrates with well known software such as `Postfix `_ or `Dovecot `_. A SQL database (`MySQL `_, `PostgreSQL -`_ or `SQLite `_) -is used as a central point of communication between all components. +`_) is used as a central point of +communication between all components. Modoboa is developed with modularity in mind, expanding it is really easy. Actually, all current features are extensions. diff --git a/doc/getting_started/install.rst b/doc/getting_started/install.rst index af7d64419..426f6b89d 100644 --- a/doc/getting_started/install.rst +++ b/doc/getting_started/install.rst @@ -67,7 +67,7 @@ install the following requirements: On a *Debian* like system, just run the following command:: - $ apt-get instal python-dev libxml2-dev libxslt-dev + $ apt-get install python-dev libxml2-dev libxslt-dev .. _database: From c509cb4edec50709a0dd8bf24b696a63488d6e7d Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 9 Sep 2013 09:29:19 +0200 Subject: [PATCH 26/61] Support only integers for this option. fixes #478 --- .../extensions/webmail/static/webmail/js/webmail.js | 2 +- modoboa/lib/emailutils.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index 8cea472b2..c9c745bea 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -83,7 +83,7 @@ Webmail.prototype = { $(document).on("click", "a[name=forward]", $.proxy(this.reply_loader, this)); $(document).on("click", "a[name=delete]", $.proxy(this.delete_message, this)); $(document).on("click", "a[name=activate_links]", $.proxy(function(e) { this.display_mode(e, "1"); }, this)); - $(document).on("click", "a[name=disable_links]", $.proxy(function(e) { this.disable_mode(e, "0"); }, this)); + $(document).on("click", "a[name=disable_links]", $.proxy(function(e) { this.display_mode(e, "0"); }, this)); $(document).on("click", "a[name=sendmail]", $.proxy(this.sendmail, this)); diff --git a/modoboa/lib/emailutils.py b/modoboa/lib/emailutils.py index bc58ea1b3..66fe831c1 100644 --- a/modoboa/lib/emailutils.py +++ b/modoboa/lib/emailutils.py @@ -23,7 +23,7 @@ class EmailAddress(object): return self.fulladdress class Email(object): - def __init__(self, msg, mformat="plain", dformat="plain", links="0"): + def __init__(self, msg, mformat="plain", dformat="plain", links=0): self.attached_map = {} self.contents = {"html" : "", "plain" : ""} self.headers = [] @@ -78,7 +78,7 @@ class Email(object): sometimes, messages embark inline images, which means they must be displayed and not attached. """ - if self.dformat == "html" and self.links != "0" \ + if self.dformat == "html" and self.links \ and msg.has_key("Content-Disposition"): if msg["Content-Disposition"].startswith("inline"): cid = None @@ -167,9 +167,9 @@ class Email(object): def viewmail_html(self, content, **kwargs): if content is None or content == "": return "" - links = kwargs.has_key("links") and kwargs["links"] or "0" - html = lxml.html.fromstring(content) - if links == "0": + links = kwargs.get("links", 0) + html = lxml.html.fromstring(content) + if not links: html.rewrite_links(lambda x: None) else: html.rewrite_links(self.map_cid) @@ -179,7 +179,6 @@ class Email(object): else: body = lxml.html.tostring(body) body = re.sub("<(/?)body", lambda m: "<%sdiv" % m.group(1), body) - body = Template(body).render(Context({})) return body def split_mailbox(mailbox): From 1036aa31a21d798df70536fe7821a8d6464853f4 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 9 Sep 2013 21:33:17 +0200 Subject: [PATCH 27/61] Mailboxes manipulation fixed. fixes #479 --- .../extensions/webmail/static/webmail/js/webmail.js | 11 +++++------ .../extensions/webmail/templates/webmail/folder.html | 2 +- modoboa/extensions/webmail/templatetags/webextras.py | 6 +++--- modoboa/extensions/webmail/views.py | 10 ++++++---- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index c9c745bea..d60e2cfec 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -536,7 +536,7 @@ Webmail.prototype = { return; } $parent = $ul; - mailbox = $parent.attr("name") + this.hdelimiter + mailbox; + mailbox = $parent.attr("name") + this.options.hdelimiter + mailbox; } else { $parent = $("#mboxes_container").children("ul"); } @@ -549,8 +549,8 @@ Webmail.prototype = { * If needed, the mailbox will be moved to its new location. */ rename_mailbox: function(oldname, newname, oldparent, newparent) { - var oldpattern = (oldparent) ? oldparent + this.hdelimiter + oldname : oldname; - var newpattern = (newparent) ? newparent + this.hdelimiter + newparent : newname; + var oldpattern = (oldparent) ? oldparent + this.options.hdelimiter + oldname : oldname; + var newpattern = (newparent) ? newparent + this.options.hdelimiter + newname : newname; var $link = $("#folders").find('a[href="' + oldpattern + '"]'); if (oldname != newname) { @@ -560,14 +560,13 @@ Webmail.prototype = { $link.parent("li").attr("name", newpattern); $link.prepend($i); $link.attr("href", newpattern); - this.navobject.setparam("mbox", newname).update(false, true); + this.navobject.setparam("mbox", newpattern).update(false, true); } if (oldparent != newparent) { - var newlocation = (newparent) ? newparent + this.hdelimiter + newname : newname; this.remove_mbox_from_tree($link.parent("li")); this.add_mailbox_to_tree(newparent, newname); if (this.navobject.getparam("action") == "listmailbox") { - this.navobject.setparam("mbox", newlocation).update(); + this.navobject.setparam("mbox", newpattern).update(); } else { this.select_mailbox("INBOX"); } diff --git a/modoboa/extensions/webmail/templates/webmail/folder.html b/modoboa/extensions/webmail/templates/webmail/folder.html index 717997f59..e0c03b57e 100644 --- a/modoboa/extensions/webmail/templates/webmail/folder.html +++ b/modoboa/extensions/webmail/templates/webmail/folder.html @@ -4,7 +4,7 @@
{% endblock %} diff --git a/modoboa/extensions/webmail/templatetags/webextras.py b/modoboa/extensions/webmail/templatetags/webextras.py index 7efffbb13..03a85cf2e 100644 --- a/modoboa/extensions/webmail/templatetags/webextras.py +++ b/modoboa/extensions/webmail/templatetags/webextras.py @@ -119,7 +119,7 @@ def listmailbox_menu(selection, folder, user): )) @register.simple_tag -def print_mailboxes(tree, selected=None, withunseen=False, selectonly=False): +def print_mailboxes(tree, selected=None, withunseen=False, selectonly=False, hdelimiter='.'): """Display a tree of mailboxes and sub-mailboxes :param tree: the mailboxes to display @@ -129,7 +129,7 @@ def print_mailboxes(tree, selected=None, withunseen=False, selectonly=False): for mbox in tree: cssclass = "" name = mbox["path"] if mbox.has_key("sub") else mbox["name"] - label = separate_mailbox(mbox["name"])[0] + label = separate_mailbox(mbox["name"], hdelimiter)[0] if selected == name: cssclass = "active" result += "
  • \n" % (name, cssclass) @@ -157,7 +157,7 @@ def print_mailboxes(tree, selected=None, withunseen=False, selectonly=False): 'selectfolder' if selectonly else 'loadfolder', extra_attrs, iclass, label) if mbox.has_key("sub") and len(mbox["sub"]): result += "\n" + + print_mailboxes(mbox["sub"], selected, withunseen, selectonly, hdelimiter) + "\n" result += "
  • \n" return result diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 968ee4436..113b831da 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -134,7 +134,8 @@ def newfolder(request, tplname="webmail/folder.html"): "action_classes": "submit", "withunseen": False, "selectonly": True, - "mboxes": mbc.getmboxes(request.user)} + "mboxes": mbc.getmboxes(request.user), + "hdelimiter": mbc.hdelimiter} if request.method == "POST": form = FolderForm(request.POST) @@ -164,14 +165,15 @@ def editfolder(request, tplname="webmail/folder.html"): "action_label" : _("Update"), "action_classes" : "submit", "withunseen" : False, - "selectonly" : True} + "selectonly" : True, + "hdelimiter": mbc.hdelimiter} if request.method == "POST": form = FolderForm(request.POST) if form.is_valid(): pf = request.POST.get("parent_folder", None) ctx["selected"] = pf - oldname, oldparent = separate_mailbox(request.POST["oldname"]) + oldname, oldparent = separate_mailbox(request.POST["oldname"], sep=mbc.hdelimiter) res = dict(status="ok", respmsg=_("Mailbox updated")) if form.cleaned_data["name"] != oldname \ or (pf != oldparent): @@ -193,7 +195,7 @@ def editfolder(request, tplname="webmail/folder.html"): name = request.GET.get("name", None) if name is None: raise WebmailError(_("Invalid request")) - shortname, parent = separate_mailbox(name) + shortname, parent = separate_mailbox(name, sep=mbc.hdelimiter) ctx["mboxes"] = mbc.getmboxes(request.user, until_mailbox=parent) ctx["form"] = FolderForm() ctx["form"].fields["oldname"].initial = name From 23eb93b063dce17ffe963da94ca9e8776ce64cff Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 9 Sep 2013 21:35:31 +0200 Subject: [PATCH 28/61] The gunicorn command line was wrong. --- doc/integration/web_servers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index ff5ca5712..fbe187256 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -113,7 +113,7 @@ file named *gunicorn.conf.py* inside Modoboa's root dir):: To start gunicorn, execute the following commands:: $ cd - $ gunicorn -c gunicorn.conf.py + $ gunicorn -c gunicorn.conf.py .wsgi:application Now the *nginx* part. Just create a new virtual host and use the following configuration:: From d86733911a11de90064995dc2ee88acf98bfafe3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 10 Sep 2013 18:54:58 +0200 Subject: [PATCH 29/61] Missing import added. see #459 --- modoboa/admin/forms.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index 18e878c75..8c8cd0daf 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -10,7 +10,9 @@ from modoboa.lib.exceptions import PermDeniedException from modoboa.lib.emailutils import split_mailbox from modoboa.lib.permissions import get_account_roles from modoboa.lib.formutils import * -from modoboa.admin.models import User, Domain, DomainAlias, Mailbox, Alias +from modoboa.admin.models import ( + User, Domain, DomainAlias, Mailbox, Alias, Quota +) class DomainFormGeneral(forms.ModelForm, DynamicForm): From a5100880340546d44309923bccfec2a41c071663 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 10 Sep 2013 22:46:36 +0200 Subject: [PATCH 30/61] Don't forget aliases :p see #459 --- modoboa/admin/forms.py | 37 +++++++++++++++---- .../extensions/postfix_autoreply/__init__.py | 5 +++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index 8c8cd0daf..db716e541 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -75,17 +75,40 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): return cleaned_data + def update_mailbox_quotas(self, domain): + """Update all quota records associated to this domain + + This methid must be called only when a domain gets renamed. As + the primary key used for a quota is an email address, rename a + domain will change all associated email addresses, so it will + change the primary keys used for quotas. The consequence is we + can't issue regular UPDATE queries using the .save() method of + a Quota instance (it will trigger an INSERT as the primary key + has changed). + + So, we use this ugly hack to bypass this behaviour. It is not + perfomant at all as it will generate one query per quota + record to update. + """ + 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) + def save(self, user, commit=True): + """Custom save method + + Updating a domain may have consequences on other objects + (domain alias, mailbox, quota). The most tricky part concerns + quotas update. + + + """ d = super(DomainFormGeneral, self).save(commit=False) if commit: 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: - for q in Quota.objects.filter(username__contains="@%s" % self.oldname): - q.username = q.username.replace('@%s' % self.oldname, '@%s' % d.name) - q.save() - old_mail_homes = dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) + 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()) d.save() Mailbox.objects.filter(domain=d, use_domain_quota=True).update(quota=d.quota) if old_mail_homes is not None: diff --git a/modoboa/extensions/postfix_autoreply/__init__.py b/modoboa/extensions/postfix_autoreply/__init__.py index 6434a339e..b86855897 100644 --- a/modoboa/extensions/postfix_autoreply/__init__.py +++ b/modoboa/extensions/postfix_autoreply/__init__.py @@ -94,6 +94,11 @@ def onDomainModified(domain): return Transport.objects.filter(domain="autoreply.%s" % domain.oldname) \ .update(domain="autoreply.%s" % domain.name) + for al in Alias.objects.filter(full_address__contains="@%s" % domain.oldname): + new_address = al.full_address.replace("@%s" % domain.oldname, "@%s" % domain.name) + al.full_address = new_address + al.autoreply_address = "%s@autoreply.%s" % (new_address, domain.name) + al.save() @events.observe("DeleteDomain") From ec8aba2aaa56bc42c6d7d039d95bd41360bb890b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 08:36:44 +0200 Subject: [PATCH 31/61] Handle mailbox operations in background. see #482 --- modoboa/admin/forms.py | 21 ++- modoboa/admin/management/__init__.py | 0 modoboa/admin/management/commands/__init__.py | 0 .../commands/handle_mailbox_operations.py | 62 ++++++++ .../0036_auto__add_mailboxoperation.py | 137 ++++++++++++++++++ modoboa/admin/models.py | 33 ++--- modoboa/admin/views.py | 9 +- modoboa/core/templates/settings.py.tpl | 10 ++ 8 files changed, 243 insertions(+), 29 deletions(-) create mode 100644 modoboa/admin/management/__init__.py create mode 100644 modoboa/admin/management/commands/__init__.py create mode 100644 modoboa/admin/management/commands/handle_mailbox_operations.py create mode 100644 modoboa/admin/migrations/0036_auto__add_mailboxoperation.py 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 } } } From 08b0fd0e5c187eed525c04276a9fdb241b35e5cf Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 16:46:28 +0200 Subject: [PATCH 32/61] Log results into the database. see #482 --- modoboa/admin/app_settings.py | 4 +-- modoboa/admin/forms.py | 14 ++++----- .../commands/handle_mailbox_operations.py | 29 +++++++++---------- modoboa/admin/models.py | 5 ++++ 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index 1f02db899..7b183b15c 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -197,11 +197,11 @@ class GeneralParametersForm(parameters.AdminParametersForm): self.fields["default_top_redirection"].choices = enabled_applications() hide_fields = False try: - code, version = exec_cmd("dovecot --version") + code, output = exec_cmd("which doveadm") except OSError, e: hide_fields = True else: - if code or not version.strip().startswith("2"): + if code: hide_fields = True if hide_fields: del self.fields["handle_mailboxes"] diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index f5e2a217d..75ec95f28 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -78,7 +78,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): def update_mailbox_quotas(self, domain): """Update all quota records associated to this domain - This methid must be called only when a domain gets renamed. As + This method must be called only when a domain gets renamed. As the primary key used for a quota is an email address, rename a domain will change all associated email addresses, so it will change the primary keys used for quotas. The consequence is we @@ -90,8 +90,6 @@ 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) @@ -108,9 +106,13 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if commit: old_mail_homes = None if self.oldname is not None and d.name != self.oldname: + d.name = self.oldname old_mail_homes = \ dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) + d.name = self.cleaned_data['name'] d.save() + Mailbox.objects.filter(domain=d, use_domain_quota=True) \ + .update(quota=d.quota) for k, v in self.cleaned_data.iteritems(): if not k.startswith("aliases"): continue @@ -131,12 +133,10 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): self.cleaned_data.keys())): dalias.delete() - self.update_mailbox_quotas(d) if old_mail_homes is not None: + self.update_mailbox_quotas(d) for mb in d.mailbox_set.all(): - MailboxOperation.objects.create( - mailbox=mb, type='rename', argument=old_mail_homes[mb.id] - ) + mb.rename_dir() return d diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index 419766023..e606be97d 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -3,6 +3,8 @@ import logging from optparse import make_option from django.core.management.base import BaseCommand from modoboa.lib import parameters +from modoboa.lib.sysutils import exec_cmd +from modoboa.admin import AdminConsole from modoboa.admin.models import MailboxOperation @@ -16,22 +18,18 @@ class Command(BaseCommand): def __init__(self, *args, **kwargs): super(Command, self).__init__(*args, **kwargs) - self.logger = logging.getLogger('modoboa.cron') + self.logger = logging.getLogger('modoboa.admin') 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) + new_mail_home = operation.mailbox.mail_home + try: + os.makedirs(os.path.dirname(new_mail_home)) + except os.error as e: + raise OperationError(str(e)) code, output = exec_cmd( - "mv %s %s" % (old_mail_home, new_mail_home) + "mv %s %s" % (ope.argument, new_mail_home) ) if code: raise OperationError(output) @@ -39,24 +37,25 @@ class Command(BaseCommand): 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): + def handle(self, *args, **options): + AdminConsole().load() if parameters.get_admin("HANDLE_MAILBOXES") == 'no': return for ope in MailboxOperation.objects.all(): try: - f = getattr(self, '%s_mailbox' % ope.optype) + f = getattr(self, '%s_mailbox' % ope.type) except AttributeError: continue try: f(ope) except OperationError as e: - self.logger.critical('Operation failed: %s', str(e)) + self.logger.critical('Operation failed: %s. Reason: %s', ope, str(e)) else: + self.logger.info('Operation succeed: %s', ope) ope.delete() diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index 8619781b4..8c03ee7e9 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -1013,6 +1013,11 @@ class MailboxOperation(models.Model): ) argument = models.TextField() + def __str__(self): + if self.type == 'rename': + return 'Rename %s -> %s' % (self.argument, self.mailbox.full_address) + return 'Delete %s' % self.argument + class Alias(DatesAware): address = models.CharField( From 40e37389ad511f1f37850fa0028ef58fbc5234cd Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 16:48:37 +0200 Subject: [PATCH 33/61] Fixed wrong call. see #482 --- modoboa/admin/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index 75ec95f28..d1da8c872 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -136,7 +136,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if old_mail_homes is not None: self.update_mailbox_quotas(d) for mb in d.mailbox_set.all(): - mb.rename_dir() + mb.rename_dir(old_mail_homes[mb.id]) return d From 71856df2dabd09396a26a115982d25e0c48198f6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 17:00:54 +0200 Subject: [PATCH 34/61] Check if sudo is needed before executing the command. see #482 --- .../commands/handle_mailbox_operations.py | 3 ++- modoboa/admin/models.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index e606be97d..b6332d9ac 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -5,6 +5,7 @@ from django.core.management.base import BaseCommand from modoboa.lib import parameters from modoboa.lib.sysutils import exec_cmd from modoboa.admin import AdminConsole +from modoboa.admin.exceptions import AdminError from modoboa.admin.models import MailboxOperation @@ -54,7 +55,7 @@ class Command(BaseCommand): continue try: f(ope) - except OperationError as e: + except (OperationError, AdminError) as e: self.logger.critical('Operation failed: %s. Reason: %s', ope, str(e)) else: self.logger.info('Operation succeed: %s', ope) diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index 8c03ee7e9..5b6a111ef 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -24,6 +24,7 @@ import sys import pwd import re import crypt, hashlib, string, base64 +import pwd from random import Random try: @@ -857,15 +858,27 @@ class Mailbox(DatesAware): @property def mail_home(self): - """ + """Retrieve the home directory of this mailbox. + The home directory refers to the place on the file system + where the mailbox data is stored. + + We ask dovecot to give us this information because there are + several patterns to understand and we don't want to implement + them. """ 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")) + curuser = pwd.getpwuid(os.getuid()).pw_name + mbowner = parameters.get_admin("MAILBOXES_OWNER") + options = {} + if curuser != mbowner: + options['sudo_user'] = mbowner + code, output = exec_cmd( + "doveadm user %s -f home" % self.full_address, **options + ) if code: raise AdminError(_("Failed to retrieve mailbox location (%s)" % output)) self.__mail_home = output.strip() From f2f4e243aea69657cc9bb40cd2c4102d1cdd7298 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 17:05:35 +0200 Subject: [PATCH 35/61] Check if path exists before trying to create it. see #482 --- .../commands/handle_mailbox_operations.py | 13 +++++++------ modoboa/admin/models.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index b6332d9ac..e4da2cb36 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -25,10 +25,11 @@ class Command(BaseCommand): if not os.path.exists(operation.argument): return new_mail_home = operation.mailbox.mail_home - try: - os.makedirs(os.path.dirname(new_mail_home)) - except os.error as e: - raise OperationError(str(e)) + if not os.path.exists(new_mail_home): + try: + os.makedirs(os.path.dirname(new_mail_home)) + except os.error as e: + raise OperationError(str(e)) code, output = exec_cmd( "mv %s %s" % (ope.argument, new_mail_home) ) @@ -56,7 +57,7 @@ class Command(BaseCommand): try: f(ope) except (OperationError, AdminError) as e: - self.logger.critical('Operation failed: %s. Reason: %s', ope, str(e)) + self.logger.critical('%s failed (reason: %s)', ope, e) else: - self.logger.info('Operation succeed: %s', ope) + self.logger.info('%s succeed', ope) ope.delete() diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index 5b6a111ef..2e2084857 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -1028,7 +1028,7 @@ class MailboxOperation(models.Model): def __str__(self): if self.type == 'rename': - return 'Rename %s -> %s' % (self.argument, self.mailbox.full_address) + return 'Rename %s -> %s' % (self.argument, self.mailbox.mail_home) return 'Delete %s' % self.argument From 8ea423173cba143f68a94d65ad5f3b5ce650d434 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 17:09:01 +0200 Subject: [PATCH 36/61] Check the container, not the leaf :p see #482 --- .../admin/management/commands/handle_mailbox_operations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index e4da2cb36..663bf3919 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -25,9 +25,10 @@ class Command(BaseCommand): if not os.path.exists(operation.argument): return new_mail_home = operation.mailbox.mail_home - if not os.path.exists(new_mail_home): + dirname = os.path.dirname(new_mail_home) + if not os.path.exists(dirname): try: - os.makedirs(os.path.dirname(new_mail_home)) + os.makedirs(dirname) except os.error as e: raise OperationError(str(e)) code, output = exec_cmd( From 21d6aaecd9657c50c03fb4d1f6666964eba5717e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 17:57:08 +0200 Subject: [PATCH 37/61] Prevent multiple executions. see #482 --- .../commands/handle_mailbox_operations.py | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index 663bf3919..03b899e37 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -14,9 +14,15 @@ class OperationError(Exception): class Command(BaseCommand): - args = '' help = 'Handles rename and delete operations on mailboxes' + option_list = BaseCommand.option_list + ( + make_option( + '--pidfile', default='/tmp/handle_mailbox_operations.pid', + help='Path to the file that will contain the PID of this process' + ), + ) + def __init__(self, *args, **kwargs): super(Command, self).__init__(*args, **kwargs) self.logger = logging.getLogger('modoboa.admin') @@ -32,7 +38,7 @@ class Command(BaseCommand): except os.error as e: raise OperationError(str(e)) code, output = exec_cmd( - "mv %s %s" % (ope.argument, new_mail_home) + "mv %s %s" % (operation.argument, new_mail_home) ) if code: raise OperationError(output) @@ -46,10 +52,30 @@ class Command(BaseCommand): if code: raise OperationError(output) + def check_pidfile(self, path): + """Check if this command is already running + + :param str path: path to the file containing the PID + :return: a boolean, True means we can go further + """ + if os.path.exists(path): + with open(path) as fp: + pid = fp.read().strip() + code, output = exec_cmd( + "grep handle_mailbox_operations /proc/%s/cmdline" % pid + ) + if not code: + return False + with open(path, 'w') as fp: + print >> fp, os.getpid() + return True + def handle(self, *args, **options): AdminConsole().load() if parameters.get_admin("HANDLE_MAILBOXES") == 'no': return + if not self.check_pidfile(options['pidfile']): + return for ope in MailboxOperation.objects.all(): try: f = getattr(self, '%s_mailbox' % ope.type) @@ -58,7 +84,9 @@ class Command(BaseCommand): try: f(ope) except (OperationError, AdminError) as e: - self.logger.critical('%s failed (reason: %s)', ope, e) + self.logger.critical('%s failed (reason: %s)', + ope, str(e).encode('utf-8')) else: self.logger.info('%s succeed', ope) ope.delete() + os.unlink(options['pidfile']) From 524a585444ff8a69bad8e38b275b8968734d648d Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 18:27:57 +0200 Subject: [PATCH 38/61] New SQL map file to use with check_recipient_access. see #482 --- modoboa/core/commands/postfix_maps.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py index f697db4e2..73358fab5 100644 --- a/modoboa/core/commands/postfix_maps.py +++ b/modoboa/core/commands/postfix_maps.py @@ -65,6 +65,12 @@ class CatchallAliasesMap(MapFile): postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled AND al.address='*' AND dom.name='%d' AND dom.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled)" +class MaintainMap(MapFile): + filename = 'sql-maintain.cf' + mysql = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" + postgres = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" + + class TransportMap(MapFile): category = "autoreply" filename = 'sql-transport.cf' From 00fab59c94e0cecc220b853c64e441f30f65b494 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 19:07:21 +0200 Subject: [PATCH 39/61] Documentation updated. fixes #482 --- doc/integration/imap_and_smtp.rst | 49 +++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index bbcab775e..d035c6627 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -45,9 +45,46 @@ Then, edit the ``inbox`` namespace and add the following lines:: It ensures all the special mailboxes will be automaticaly created for new accounts. -.. note:: +Operations on the file system +----------------------------- + +Three operation types are considered: + +#. Mailbox creation +#. Mailbox renaming +#. Mailbox deletion + +The first one is managed by Dovecot. The last two ones may be managed +by Modoboa if it can access the file system where the mailboxes are +stored (see :ref:`admin-params` to activate this feature). + +Those operations are treated asynchronously by a cron script. For +example, when you rename an e-mail address through the web UI, the +associated mailbox on the file system is not modified +directly. Instead of that, a 'rename' order is created for this +mailbox. The mailbox will be considered unavailable until the order is +not executed (see :ref:`Postfix configuration `). + +Edit the crontab of the user who owns the mailboxes on the file system:: + + $ crontab -u -e + +And add the following line inside:: + + * * * * * python /var/www/mail_koalabs_org/manage.py handle_mailbox_operations + +.. warning:: + + The cron script must be executed by the system user owning the mailboxes. + +.. warning:: + + The user running the cron script must have access to the + *settings.py* file of the modoboa instance. + +The result of each order is recorded into Modoboa's log. Go to +*Modoboa > Logs* to consult them. - Modoboa is not responsible for mailboxes creation, Dovecot is. Authentication ============== @@ -369,6 +406,8 @@ directory, run the following command:: ``mapfiles`` is the directory where the files will be stored. Answer the few questions and you're done. +.. _postfix_config: + Configuration ============= @@ -393,6 +432,12 @@ Use the following configuration in the */etc/postfix/main.cf* file mysql:/etc/postfix/sql-email2email.cf, mysql:/etc/postfix/sql-catchall-aliases.cf + smtpd_recipient_restrictions = + ... + check_recipient_access mysql:/etc/postfix/maps/sql-maintain.cf + permit_mynetworks + ... + # Stuff after Then, edit the */etc/postfix/master.cf* file and add the following From db17f923fe36c1cf846c6cd29e131085d7a1b364 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 19:12:08 +0200 Subject: [PATCH 40/61] Release note for 1.0.1 added. --- doc/getting_started/upgrade.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index b9ac53826..fcf7fc452 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -43,6 +43,13 @@ Finally, follow the common procedure:: $ python manage.py syncdb --migrate $ python manage.py collectstatic +1.0.1: operations on mailboxes +============================== + +The way Modoboa handles **rename** and **delete** operations on mailboxes has +been improved. Make sure to consult :ref:`fs_operations`. + + 1.0.0: production ready, at last ================================ From 3e2bb6683a86d9c141796aab10762eb13b7f85f4 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 19:13:10 +0200 Subject: [PATCH 41/61] Label added. --- doc/integration/imap_and_smtp.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index d035c6627..3280b5af2 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -45,6 +45,8 @@ Then, edit the ``inbox`` namespace and add the following lines:: It ensures all the special mailboxes will be automaticaly created for new accounts. +.. _fs_operations: + Operations on the file system ----------------------------- From 0faf4caa733008cfd26758ba01c9e1a69b320fb6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 16 Sep 2013 17:38:02 +0200 Subject: [PATCH 42/61] Don't forget to load the delete handler when the list is reloaded. --- modoboa/extensions/sievefilters/static/sievefilters/js/lib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js b/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js index e38092166..7854c0c13 100644 --- a/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js +++ b/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js @@ -44,6 +44,7 @@ function changefs(evt) { function init_filters_list() { $("a[name=togglestate]").click(toggle_filter_state); $("a[name*=movefilter_]").click(move_filter); + $("a[name=removefilter]").click(removefilter); } /* @@ -63,7 +64,6 @@ function loadfs(response) { $("#fsetmenu").remove(); } $("#menu").after($(response.menu)); - $("a[name=removefilter]").click(removefilter); $("a[name=activatefs]").click(activatefs); $("a[name=savefs]").click(savefs); $("a[name=removefs]").click(removefs); From 5e87fc764c4599d24929b500c242619aa5a2a803 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 17 Sep 2013 08:43:23 +0200 Subject: [PATCH 43/61] Properly encode filenames using RFC6266. see #473 fixes #481 --- modoboa/admin/views.py | 2 +- modoboa/extensions/sievefilters/views.py | 3 ++- modoboa/extensions/webmail/lib.py | 19 ++++++++++++------- modoboa/extensions/webmail/views.py | 7 ++++--- modoboa/lib/emailutils.py | 6 +++--- requirements.txt | 1 + 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index 3582e9b33..fac30eb03 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -804,7 +804,7 @@ def _export(content, filename): resp = HttpResponse(content) resp["Content-Type"] = "text/csv" resp["Content-Length"] = len(content) - resp["Content-Disposition"] = 'attachment; filename="%s"' % filename + resp["Content-Disposition"] = build_header(filename) return resp diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index d730eff2b..9cae88c11 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -1,5 +1,6 @@ # coding: utf-8 from sievelib.managesieve import Error +from rfc6266 import build_header from django.http import HttpResponse from django.template.loader import render_to_string from django.contrib.auth.decorators import login_required @@ -263,7 +264,7 @@ def download_filters_set(request, name): resp = HttpResponse(script) resp["Content-Type"] = "text/plain; charset=utf-8" resp["Content-Length"] = len(script) - resp["Content-Disposition"] = 'attachment; filename="%s.txt"' % name + resp["Content-Disposition"] = build_header('%s.txt' % name) return resp @login_required diff --git a/modoboa/extensions/webmail/lib.py b/modoboa/extensions/webmail/lib.py index 8d39667cb..b8011d852 100644 --- a/modoboa/extensions/webmail/lib.py +++ b/modoboa/extensions/webmail/lib.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import email import lxml import chardet +from rfc6266 import build_header, parse_headers from django.core.files.uploadhandler import FileUploadHandler, SkipFile from django.utils.translation import ugettext as _, ugettext_lazy @@ -277,20 +278,24 @@ class ImapEmail(Email): attname = "part_%s" % att["pnum"] params = None key = None - if att.has_key("params") and att["params"] != "NIL": + if "params" in att and att["params"] != "NIL": params = att["params"] key = "name" - if key is None and \ - att.has_key("disposition") and len(att["disposition"]) > 1: + if key is None and "disposition" in att and len(att["disposition"]) > 1: params = att["disposition"][1] key = "filename" if key and params: for pos, value in enumerate(params): - if value == key: + if not value.startswith(key): + continue + header = "%s; %s=%s" % (att['disposition'][0], value, + u2u_decode.u2u_decode(params[pos + 1]).strip("\r\t\n")) + attname = parse_headers(header).filename_unsafe + if attname is None: attname = u2u_decode.u2u_decode(params[pos + 1]).strip("\r\t\n") - break + break self.attachments[att["pnum"]] = attname def _fetch_inlines(self): @@ -302,7 +307,7 @@ class ImapEmail(Email): params["fname"] = os.path.join(settings.MEDIA_URL, fname) if os.path.exists(path): continue - + pdef, content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"]) fp = open(path, "wb") fp.write(decode_payload(params["encoding"], content)) @@ -613,7 +618,7 @@ def create_mail_attachment(attdef): res.set_payload(fp.read()) fp.close() Encoders.encode_base64(res) - res.add_header("Content-Disposition", 'attachment', filename=attdef["fname"]) + res['Content-Disposition'] = build_header(attdef['fname'].decode('utf-8')) return res diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 113b831da..06234f912 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -43,16 +43,17 @@ def getattachment(request): resp = HttpResponse(decode_payload(partdef["encoding"], payload)) resp["Content-Type"] = partdef["Content-Type"] resp["Content-Transfer-Encoding"] = partdef["encoding"] + print partdef if partdef["disposition"] != 'NIL': disp = partdef["disposition"] # FIXME : ugly hack, see fetch_parser.py for more explanation # :p if type(disp[1][0]) != dict: - cd = '%s; %s="%s"' % (disp[0], disp[1][0], disp[1][1]) + cd = '%s; %s=%s' % (disp[0], disp[1][0], disp[1][1]) else: - cd = '%s; %s="%s"' % (disp[0], disp[1][0]['struct'][0], disp[1][0]['struct'][1]) + cd = '%s; %s=%s' % (disp[0], disp[1][0]['struct'][0], disp[1][0]['struct'][1]) else: - cd = 'attachment; filename="%s"' % request.GET["fname"] + cd = build_header(request.GET["fname"]) resp["Content-Disposition"] = cd resp["Content-Length"] = partdef["size"] return resp diff --git a/modoboa/lib/emailutils.py b/modoboa/lib/emailutils.py index 66fe831c1..a10381ffe 100644 --- a/modoboa/lib/emailutils.py +++ b/modoboa/lib/emailutils.py @@ -79,15 +79,15 @@ class Email(object): must be displayed and not attached. """ if self.dformat == "html" and self.links \ - and msg.has_key("Content-Disposition"): + and "Content-Disposition" in msg: if msg["Content-Disposition"].startswith("inline"): cid = None - if msg.has_key("Content-ID"): + if "Content-ID" in msg: m = re.match("<(.+)>", msg["Content-ID"]) cid = m is not None and m.group(1) or msg["Content-ID"] fname = msg.get_filename() if fname is None: - if msg.has_key("Content-Location"): + if "Content-Location" in msg: fname = msg["Content-Location"] elif cid is not None: fname = cid diff --git a/requirements.txt b/requirements.txt index 195f44718..33a9fecab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ pycrypto sievelib chardet argparse +rfc6266 From 88d755431aa30f74e0c870ae1caea209adaa8780 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 17 Sep 2013 15:03:26 +0200 Subject: [PATCH 44/61] Compress attachments before the HTTP proxy does. fix #480 --- modoboa/extensions/webmail/views.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 06234f912..57eebb635 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -1,12 +1,14 @@ # coding: utf-8 -from django.conf import settings import os +from rfc6266 import build_header +from django.conf import settings from django.http import HttpResponse from django.template import Template, Context from django.utils.translation import ugettext as _, ungettext from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.gzip import gzip_page from modoboa.lib import parameters from modoboa.lib.webutils import _render, _render_to_string, \ ajax_response, ajax_simple_response @@ -20,8 +22,10 @@ from lib import decode_payload, AttachmentUploadHandler, \ ImapEmail, ReplyModifier, ForwardModifier from templatetags import webextras + @login_required @needs_mailbox() +@gzip_page def getattachment(request): """Fetch a message attachment @@ -36,14 +40,11 @@ def getattachment(request): if not mbox or not mailid or not pnum: raise WebmailError(_("Invalid request")) - headers = {"Content-Type": "text/plain", - "Content-Transfer-Encoding": None} imapc = get_imapconnector(request) partdef, payload = imapc.fetchpart(mailid, mbox, pnum) resp = HttpResponse(decode_payload(partdef["encoding"], payload)) resp["Content-Type"] = partdef["Content-Type"] resp["Content-Transfer-Encoding"] = partdef["encoding"] - print partdef if partdef["disposition"] != 'NIL': disp = partdef["disposition"] # FIXME : ugly hack, see fetch_parser.py for more explanation @@ -58,6 +59,7 @@ def getattachment(request): resp["Content-Length"] = partdef["size"] return resp + @login_required @needs_mailbox() def move(request): From 69cf92647c158003cd7d99132521bfd966e88d17 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 17 Sep 2013 15:05:04 +0200 Subject: [PATCH 45/61] Few fixes. --- modoboa/extensions/webmail/static/webmail/js/webmail.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index d60e2cfec..8ddd4c5b8 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -709,14 +709,14 @@ Webmail.prototype = { * is clicked) */ compose_loader: function(e) { - var $link = $(e.target); + var $link = get_target(e, 'a'); e.preventDefault(); this.navobject.reset().setparam("action", $link.attr("href")).update(); }, reply_loader: function(e) { - var $link = $(e.target); + var $link = get_target(e, "a"); e.preventDefault(); this.navobject.reset().updateparams($link.attr("href")).update(); }, From b96706c98d9392b5bac668cd06630552c689b78c Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 17 Sep 2013 19:47:05 +0200 Subject: [PATCH 46/61] Few fixes. --- modoboa/admin/static/admin/css/admin.css | 4 ++++ modoboa/admin/views.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/modoboa/admin/static/admin/css/admin.css b/modoboa/admin/static/admin/css/admin.css index a28919dd9..2768bf843 100644 --- a/modoboa/admin/static/admin/css/admin.css +++ b/modoboa/admin/static/admin/css/admin.css @@ -22,6 +22,10 @@ table .progress { margin-bottom: 0 !important; } +table tbody tr:hover { + background-color: #F9F9F9; +} + .search { width: 85%; -webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px; diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index fac30eb03..1dbcef8e1 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -1,4 +1,7 @@ # coding: utf-8 +import cStringIO +import csv +from rfc6266 import build_header from django.http import HttpResponse, HttpResponseRedirect from django.utils.translation import ugettext as _, ungettext from django.core.urlresolvers import reverse @@ -9,8 +12,6 @@ from django.shortcuts import render from django.template.loader import render_to_string from django.db import transaction, IntegrityError from django.views.decorators.csrf import ensure_csrf_cookie -import cStringIO -import csv from lib import get_sort_order, get_listing_page from forms import * From 4510897020474ba435ef75a1a91c8ebdd6eb6b6b Mon Sep 17 00:00:00 2001 From: Louis-Dominique Dubeau Date: Tue, 17 Sep 2013 18:44:20 -0400 Subject: [PATCH 47/61] Added documentation on uwsgi. --- doc/integration/web_servers.rst | 206 +++++++++++++++++++++++++++++--- 1 file changed, 192 insertions(+), 14 deletions(-) diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index fbe187256..639aba65b 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -10,6 +10,14 @@ Web servers Apache2 ******* +.. note:: The following instructions are meant to help you get your + site up and running quickly. However it is not possible for + the people contributing documentation to Modoboa to test + every single combination of web server, wsgi server, + distribution, etc. So it is possible that **your** + installation of uwsgi or nginx or Apache or what-have-you + works differently. Keep this in mind. + mod_wgsi ======== @@ -37,13 +45,13 @@ following content inside:: WSGIScriptAlias / /wsgi.py -This is just one possible configuration. +This is just one possible configuration. .. note:: *Django* 1.3 users, please consult this `page `_, it contains an example *wsgi.py* file. -.. note:: +.. note:: You will certainly need more configuration in order to launch Apache. @@ -77,9 +85,9 @@ following content inside:: -This is just one possible configuration. +This is just one possible configuration. -.. note:: +.. note:: You will certainly need more configuration in order to launch Apache. @@ -89,17 +97,27 @@ This is just one possible configuration. Nginx ***** +.. note:: The following instructions are meant to help you get your + site up and running quickly. However it is not possible for + the people contributing documentation to Modoboa to test + every single combination of web server, wsgi server, + distribution, etc. So it is possible that **your** + installation of uwsgi or nginx or Apache or what-have-you + works differently. Keep this in mind. + +Green Unicorn +============= + `Nginx `_ is a really fast HTTP server. Associated with `Green Unicorn `_, it gives you one of the -best setup to serve python/Django applications. Modoboa's -performances are really good with this configuration. +best setups to serve python/Django applications. Modoboa's +performance is really good with this configuration. -To use this setup, first download and install those softwares -(`Install gunicorn `_ and `install -nginx `_). - -Then, use the following sample *gunicorn* configuration (create a new -file named *gunicorn.conf.py* inside Modoboa's root dir):: +To use this setup, first download and install `nginx +`__ and `gunicorn +`_. Then, use the following sample +*gunicorn* configuration (create a new file named *gunicorn.conf.py* +inside Modoboa's root dir):: backlog = 2048 bind = "unix:/var/run/gunicorn/modoboa.sock" @@ -123,13 +141,19 @@ following configuration:: } server { - listen 443; + listen 443 ssl; + ssl on; + keepalive_timeout 70; + server_name ; root ; access_log /var/log/nginx/.access.log; error_log /var/log/nginx/.error.log; + ssl_certificate ; + ssl_certificate_key ; + location /sitestatic/ { autoindex on; } @@ -143,10 +167,164 @@ following configuration:: proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Forwarded-Protocol ssl; - proxy_pass http://modoboa; + proxy_pass http://modoboa; } } +If you do not plan to use SSL then change the listen directive to +``listen 80;`` and delete each of the following directives:: + + ssl on; + keepalive_timeout 70; + ssl_certificate ; + ssl_certificate_key ; + proxy_set_header X-Forwarded-Protocol ssl; + +If you do plan to use SSL, you'll have to generate a certificate and a +key. `This article +`__ +contains information about how to do it. + Paste this content to your configuration (replace values between ``<>`` with yours), restart *nginx* and enjoy a really fast application! + +uwsgi +===== + +The following setup is meant to get you started quickly. You should +read the documentation of both nginx and uwsgi to understand how to +optimize their configuration for your site. + +The Django documentation includes the following warning regarding +uwsgi: + +.. warning:: Some distributions, including Debian and Ubuntu, ship an + outdated version of uWSGI that does not conform to the + WSGI specification. Versions prior to 1.2.6 do not call + close on the response object after handling a request. In + those cases the request_finished signal isn’t sent. This + can result in idle connections to database and memcache + servers. + +Use uwsgi 1.2.6 or newer. If you do not, you *will* run into +problems. Modoboa will fail in obscure ways. + +To use this setup, first download and install `nginx +`__ and `uwsgi +`_. + +Here is a sample nginx configuration:: + + server { + listen 443 ssl; + ssl on; + keepalive_timeout 70; + + server_name ; + root ; + + ssl_certificate ; + ssl_certificate_key ; + + access_log /var/log/nginx/.access.log; + error_log /var/log/nginx/.error.log; + + location /sitestatic/ { + autoindex on; + alias ; + } + + # Whether or not Modoboa uses a media directory depends on how + # you configured Modoboa. It does not hurt to have this. + location /media/ { + autoindex on; + alias ; + } + + # This denies access to any file that begins with + # ".ht". Apache's .htaccess and .htpasswd are such files. A + # Modoboa installed from scratch would not contain any such + # files, but you never know what the future holds. + location ~ /\.ht { + deny all; + } + + location / { + include uwsgi_params; + uwsgi_pass ; + uwsgi_param UWSGI_SCRIPT modoboa_server.wsgi:application + uwsgi_param UWSGI_SCHEME https; + } + } + +If you do not plan to use SSL then change the listen directive to +``listen 80;`` and delete each of the following directives:: + + ssl on; + keepalive_timeout 70; + ssl_certificate ; + ssl_certificate_key ; + uwsgi_param UWSGI_SCHEME https; + +If you do plan to use SSL, you'll have to generate a certificate and a +key. `This article +`__ +contains information about how to do it. + +Make sure to replace the ``<...>`` in the sample configuration with +appropriate values. Here are some explanations for the cases that may +not be completely self-explanatory: + +```` + Where Modoboa's :file:`settings.py` resides. This is also where the + :file:`sitestatic` and :file:`media` directories reside. + +```` + This is the URL which will be the root of your Modoboa site at your + domain. For instance, if your Modoboa installation is reachable at + at ``https://foo/modoboa`` then ```` is + ``/modoboa``. In this case you probably also have to set the + ``alias`` directives to point to where Modoboa's sitestatic and + media directories are because otherwise nginx won't be able to find + them. + + If Modoboa is at the root of your domain, then ```` is an empty string and can be deleted from the configuration + above. In this case, you probably do not need the ``alias`` + directives. + +```` + The location where uwsig is listening. It could be a unix domain + socket or an address:port combination. Ubuntu configures uwsgi so + that the port is:: + + unix:/run/uwsgi/app//socket + + where ```` is the name of the application. + +Your uwsgi configuration should be:: + + [uwsgi] + # Not needed when using uwsgi from pip + # plugins = python + chdir = + module = .wsgi:application + master = true + harakiri = 30 + sharedarea = 4 + processes = 4 + vhost = true + no-default-app = true + +The plugins directive should be turned on if you use a uwsgi +installation that requires it. If uwsgi was installed from pip, it +does not require it. In the configuration above: + +```` + The directory where :file:`manage.py` resides. This directory is the + parent of ```` + +```` + The name that you passed to ``modoboa-admin.py deploy`` when you + created your Modoboa instance. From dc00f7d2fd27c8709f0ad07f8fa32d144a34bcbf Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 18 Sep 2013 09:46:28 +0200 Subject: [PATCH 48/61] Properly decode URI components before use them. see #481 --- modoboa/static/js/history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/static/js/history.js b/modoboa/static/js/history.js index 72cd2c069..d582219a1 100644 --- a/modoboa/static/js/history.js +++ b/modoboa/static/js/history.js @@ -179,7 +179,7 @@ History.prototype = { }, getbaseurl: function() { - return this.base.substr(0, this.base.length - 1); + return decodeURIComponent(this.base.substr(0, this.base.length - 1)); }, delparam: function(name) { From 6738cecb4446ed2e306597e785eb7d3d11c59c89 Mon Sep 17 00:00:00 2001 From: ricardopadilha Date: Wed, 18 Sep 2013 16:04:47 +0200 Subject: [PATCH 49/61] Added missing comma in settings.py.tpl A missing comma in settings.py.tpl breaks new deployments. --- modoboa/core/templates/settings.py.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/core/templates/settings.py.tpl b/modoboa/core/templates/settings.py.tpl index 67aa8c691..a80b25769 100644 --- a/modoboa/core/templates/settings.py.tpl +++ b/modoboa/core/templates/settings.py.tpl @@ -212,7 +212,7 @@ LOGGING = { 'class': 'logging.handlers.SysLogHandler', 'facility': SysLogHandler.LOG_CRON, 'formatter': 'syslog' - } + }, 'modoboa': { 'class': 'modoboa.lib.logutils.SQLHandler', } From c207b96a7420785f1e984f08dbfef9ddf01f1e4b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 18 Sep 2013 21:11:06 +0200 Subject: [PATCH 50/61] Useless configuration removed. --- modoboa/core/templates/settings.py.tpl | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/modoboa/core/templates/settings.py.tpl b/modoboa/core/templates/settings.py.tpl index a80b25769..4f926f712 100644 --- a/modoboa/core/templates/settings.py.tpl +++ b/modoboa/core/templates/settings.py.tpl @@ -208,11 +208,6 @@ 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', } @@ -232,11 +227,6 @@ LOGGING = { 'handlers': ['modoboa'], 'level': 'INFO', 'propagate': False - }, - 'modoboa.cron': { - 'handlers': ['syslog-cron'], - 'level': 'INFO', - 'propagate': False } } } From 98f3bbda0fa0ff16a5b39183917c9a5ec65dfc95 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 18 Sep 2013 22:14:36 +0200 Subject: [PATCH 51/61] Consistency. --- doc/additional/migrating.rst | 4 +- doc/additional/virtualmachine.rst | 11 ++--- doc/extending/plugin_api.rst | 6 +-- doc/getting_started/configuration.rst | 16 +++---- doc/getting_started/install.rst | 12 ++--- doc/getting_started/plugins.rst | 28 +++++------ doc/getting_started/upgrade.rst | 58 +++++++++++----------- doc/integration/imap_and_smtp.rst | 69 ++++++++++++++------------- doc/integration/web_servers.rst | 61 ++++++++++++----------- 9 files changed, 134 insertions(+), 131 deletions(-) diff --git a/doc/additional/migrating.rst b/doc/additional/migrating.rst index beaaa66c1..448e7fdd1 100644 --- a/doc/additional/migrating.rst +++ b/doc/additional/migrating.rst @@ -16,7 +16,7 @@ existing `PostfixAdmin (version 2.3.3+) First, you must follow the :ref:`installation` step to create a fresh Modoboa database. -Once done, edit the *setting.py* file. First, add a new database +Once done, edit the :file:`settings.py` file. First, add a new database connection named ``pfxadmin`` into the ``DATABASES`` variable corresponding to your PostfixAdmin setup:: @@ -56,7 +56,7 @@ used by PostfixAdmin, check its configuration file if you're not sure) Click on the *Save* button. -The procedure is over, edit the *settings.py* file and: +The procedure is over, edit the :file:`settings.py` file and: * remove the ``pfxadmin`` database connection from the ``DATABASES`` variable diff --git a/doc/additional/virtualmachine.rst b/doc/additional/virtualmachine.rst index 942da5a10..471c2c418 100644 --- a/doc/additional/virtualmachine.rst +++ b/doc/additional/virtualmachine.rst @@ -4,9 +4,9 @@ Using the virtual machine Introduction ============ -A virtual machine with a ready-to-use *Modoboa* setup is available -`here `_. It is composed -of the following components: +A virtual machine with a ready-to-use Modoboa setup is available `here +`_. It is composed of +the following components: * Debian 6.0 (squeeze) * Modoboa and its prerequisites @@ -32,7 +32,7 @@ If you can't use the vmdk format, you can use `qemu Then, just use your prefered virtualization software (qemu, kvm, virtualbox, etc.) to start the machine. You'll need to configure at least one bridged network interface if you want to be able to play -with *Modoboa*, ie. your machine must be visible from your network. +with Modoboa, ie. your machine must be visible from your network. The default network interface of the machine (``eth0``) is configured to use the DHCP protocol. @@ -49,7 +49,7 @@ root demo the root user demo demo an unpriviliged user ===== ======== ==================== -To connect to *Modoboa*, first connect to the system and retrieve its +To connect to Modoboa, first connect to the system and retrieve its current network address like this:: $ /sbin/ifconfig eth0 @@ -72,4 +72,3 @@ admin@demo.local admin Administrator of the domain *demo.local*. user@demo.local user Simple user. Can access to applications. ================ ======== ============================================ - diff --git a/doc/extending/plugin_api.rst b/doc/extending/plugin_api.rst index b6d2e5f27..a96381405 100644 --- a/doc/extending/plugin_api.rst +++ b/doc/extending/plugin_api.rst @@ -33,12 +33,12 @@ The required glue ***************** To create a new plugin, just start a new django application like -this (into modoboa's directory):: +this (into Modoboa's directory):: $ python manage.py startapp Then, you need to register this application using the provided -API. Just copy/paste the following example into the *__init__.py* file +API. Just copy/paste the following example into the :file:`__init__.py` file of the future extension:: from modoboa.extensions import ModoExtension, exts_pool @@ -72,7 +72,7 @@ of the future extension:: exts_pool.register_extension(MyExtension) Once done, simply add your plugin's module name to the -``INSTALLED_APPS`` variable located inside *settings.py*. Optionaly, +``INSTALLED_APPS`` variable located inside :file:`settings.py`. Optionaly, run ``python manage.py syncdb`` if your plugin provides custom tables and ``python manage.py collectstatic`` to update static files. diff --git a/doc/getting_started/configuration.rst b/doc/getting_started/configuration.rst index cad72e254..efcf39658 100644 --- a/doc/getting_started/configuration.rst +++ b/doc/getting_started/configuration.rst @@ -86,7 +86,7 @@ Custom logo You have the possibility to use a custom logo instead of the default one on the login page. -To do so, open the *settings.py* file and add a +To do so, open the :file:`settings.py` file and add a ``MODOBOA_CUSTOM_LOGO`` variable. This variable must contain the relative URL of your logo under ``MEDIA_URL``. For example:: @@ -107,7 +107,7 @@ Host configuration To manipulate mailboxes on the filesystem, you must allow the user who runs Modoboa to execute commands as the user who owns mailboxes. -To do so, edit the */etc/sudoers* file and add the following inside:: +To do so, edit the :file:`/etc/sudoers` file and add the following inside:: ALL=() NOPASSWD: ALL @@ -121,7 +121,7 @@ Time zone and language Modoboa is available in many languages. -To specify the default language to use, edit the *settings.py* file +To specify the default language to use, edit the :file:`settings.py` file and modify the ``LANGUAGE_CODE`` variable:: LANGUAGE_CODE = 'fr' # or 'en' for english, etc. @@ -143,7 +143,7 @@ Modoboa uses `Django's session framework `_ to store per-user information. -Few parameters need to be set in the *settings.py* configuration +Few parameters need to be set in the :file:`settings.py` configuration file to make Modoboa behave as expected:: SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Default value @@ -151,7 +151,7 @@ file to make Modoboa behave as expected:: This parameter is optional but you must ensure it is set to ``False`` (the default value). -The default configuration file provided by the *modoboa-admin.py* +The default configuration file provided by the ``modoboa-admin.py`` command is properly configured. **** @@ -163,7 +163,7 @@ LDAP Authentication ============== -*Modoboa* supports external LDAP authentication using the following extra components: +Modoboa supports external LDAP authentication using the following extra components: * `Python LDAP client `_ * `Django LDAP authentication backend `_ @@ -172,7 +172,7 @@ If you want to use this feature, you must first install those components:: $ pip install python-ldap django-auth-ldap -Then, all you have to do is to modify the *settings.py* file. Add a +Then, all you have to do is to modify the :file:`settings.py` file. Add a new authentication backend to the `AUTHENTICATION_BACKENDS` variable, like this:: @@ -247,7 +247,7 @@ If you need additional parameters, you will find a detailled documentation `here `_. Once the authentication is properly configured, the users defined in -your LDAP directory will be able to connect to *Modoboa*, the associated +your LDAP directory will be able to connect to Modoboa, the associated domain and mailboxes will be automatically created if needed. .. note:: diff --git a/doc/getting_started/install.rst b/doc/getting_started/install.rst index 426f6b89d..1c4f21d6e 100644 --- a/doc/getting_started/install.rst +++ b/doc/getting_started/install.rst @@ -30,8 +30,8 @@ You can choose between two options: * Use the Python package available on the `PyPI `_ * Download the sources tarball -The easiest one is to install it from the *PyPI*. Just run the -following command and you're done:: +The easiest one is to install it from PyPI. Just run the following +command and you're done:: $ pip install modoboa @@ -56,8 +56,8 @@ package. On Debian/Ubuntu:: `virtualenv `_ users ========================================================== -When you deploy an application using *virtualenv*, you may have to -compile some dependencies. For example, *modoboa* relies on *lxml*, +When you deploy an application using virtualenv, you may have to +compile some dependencies. For example, modoboa relies on lxml, which is a C python module. In order to install it, you will need to install the following requirements: @@ -65,7 +65,7 @@ install the following requirements: * libxslt development files * libxml2 development files -On a *Debian* like system, just run the following command:: +On a Debian like system, just run the following command:: $ apt-get install python-dev libxml2-dev libxslt-dev @@ -75,7 +75,7 @@ On a *Debian* like system, just run the following command:: Database ******** -Thanks to *django*, Modoboa supports several databases. Depending on +Thanks to Django, Modoboa supports several databases. Depending on the one you will use, you must install the appropriate python package: * `mysqldb `_ for `MySQL `_ diff --git a/doc/getting_started/plugins.rst b/doc/getting_started/plugins.rst index 5fcfcffb6..cb47c879f 100644 --- a/doc/getting_started/plugins.rst +++ b/doc/getting_started/plugins.rst @@ -50,11 +50,11 @@ This plugin provides a simple management frontend for `amavisd-new The per-domain policies feature only works for new installations. Currently, you can't use modoboa with an existing - database (ie. with data in *users* and *policies* tables). + database (ie. with data in ``users`` and ``policies`` tables). .. note:: - This plugin requires *amavisd-new* version **2.7.0** or higher. If + This plugin requires amavisd-new version **2.7.0** or higher. If you're planning to use the :ref:`selfservice`, you'll need version **2.8.0**. @@ -62,7 +62,7 @@ Database ======== You must tell to Modoboa where it can find the amavis -database. Inside *settings.py*, add a new connection to the +database. Inside :file:`settings.py`, add a new connection to the ``DATABASES`` variable like this:: DATABASES = { @@ -81,7 +81,7 @@ Replace values between ``<>`` with yours. .. note:: - Modoboa doesn't create *amavis* tables. You need to install them + Modoboa doesn't create amavis tables. You need to install them following the `official documentation `_. @@ -106,7 +106,7 @@ Release messages To release messages, first take a look at `this page `_. It -explains how to configure *amavisd-new* to listen somewhere for the +explains how to configure amavisd-new to listen somewhere for the AM.PDP protocol. This protocol is used to send requests. Below is an example of a working configuration:: @@ -126,7 +126,7 @@ Below is an example of a working configuration:: Don't forget to update the ``inet_acl`` list if you plan to release from the network. -Once *amavisd-new* is configured, just tell Modoboa where it can find +Once amavisd-new is configured, just tell Modoboa where it can find the *release server* by modifying the following parameters in the online panel: @@ -212,7 +212,7 @@ the required identifiers. To activate this feature, go the administration panel and set the **Enable self-service mode** parameter to yes. -The last step is to customize the notification messages *amavis* +The last step is to customize the notification messages amavis sends. The most important is to embark a direct link. Take a look at the `README.customize `_ file to learn what you're allowed to do. @@ -229,7 +229,7 @@ Graphical statistics This plugin collects various statistics about emails traffic on your server. It parses a log file to collect information, store it into RRD -files (see `rrdtool `_)and then +files (see `rrdtool `_) and then generates graphics in PNG format. To use it, go to the online parameters panel and adapt the following @@ -273,15 +273,15 @@ Postifx auto-reply messages *************************** This plugin let users define an auto-reply message (*vacation*). It is -based on *postfix* capabilities. +based on Postfix capabilities. The user that executes the autoreply script needs to access -*settings.py*. You must apply proper permissions on this file. For -example, if *settings.py* belongs to *www-data:www-data*, you can add -the *vmail* user to the *www-data* group and set the read permission +:file:`settings.py`. You must apply proper permissions on this file. For +example, if :file:`settings.py` belongs to ``www-data:www-data``, you can add +the ``vmail`` user to the ``www-data`` group and set the read permission for the group. -To make *postfix* use this feature, you need to update your +To make Postfix use this feature, you need to update your configuration files as follows: ``/etc/postfix/main.cf``:: @@ -334,7 +334,7 @@ Two working modes are available: To use this plugin, your hosting setup must include a *ManageSieve* server and your local delivery agent must understand the *Sieve* -language. Don't panic, *Dovecot* supports both :-) (refer to +language. Don't panic, Dovecot supports both :-) (refer to :ref:`dovecot` to know how to enable those features). .. note:: diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index fcf7fc452..9015d9e0a 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -56,7 +56,7 @@ been improved. Make sure to consult :ref:`fs_operations`. Configuration file update ------------------------- -Several modifications need to be done into *settings.py*. +Several modifications need to be done into :file:`settings.py`. #. Add the following import statement:: @@ -190,7 +190,7 @@ It is necessary to update the queries used to retrieve users and mailboxes: #. Run ``modoboa-admin.py postfix_maps --dbtype `` and compare the files with those that postfix currently use. Make necessary updates in light of the differences -#. Into *dovecot-sql.conf*, update the ``user_query`` query, refer to +#. Into :file:`dovecot-sql.conf`, update the ``user_query`` query, refer to :ref:`dovecot_mysql_queries` or :ref:`dovecot_pg_queries` #. Update dovecot's configuration to activate the new :ref:`quota related features ` @@ -207,7 +207,7 @@ may encounter the following issues: #. South fails to migrate ``reversion`` - Due to the admin user model change, the script *0001_initial.py* + Due to the admin user model change, the script :file:`0001_initial.py` may fail. Just deactivate ``reversion`` from ``INSTALLED_APPS`` and run the command again. Once done, reactivate ``reversion`` and run the command one last time. @@ -216,7 +216,7 @@ may encounter the following issues: 0.9.4: administrative panel performance improved ================================================ -#. Edit the *settings.py* file and remove +#. Edit the :file:`settings.py` file and remove ``'django.contrib.auth.backends.ModelBackend'`` from the ``AUTHENTICATION_BACKENDS`` variable @@ -229,10 +229,10 @@ For this version, we recommend to install a new instance (see Then, copy the following content from the old installation to the new one: -* The ``media`` directory +* The :file:`media` directory * The directory containing RRD files if you use the :ref:`stats` plugin -Don't copy the old *settings.py* file, just keep the new one and +Don't copy the old :file:`settings.py` file, just keep the new one and modify it (see :ref:`database` and :ref:`timezone_lang`). Migrate your database (see :ref:`latestversion`). @@ -260,16 +260,16 @@ requires a migration. .. note:: Many files have been renamed/removed for this version. I recommend that you backup important files (*settings.py*, etc.) elsewhere - (ie. */tmp* for example). Then, remove the *modoboa* directory, + (ie. :file:`/tmp` for example). Then, remove the :file:`modoboa` directory, extract the new tarball at the same place, rename the new directory - to *modoboa* and copy the files you've just backup into it. + to :file:`modoboa` and copy the files you've just backup into it. .. note:: If the first super administrator you created is named ``admin``, its password will be changed to ``password`` at the end of this upgrade. Don't forget to modify it! -#. Edit the *settings.py* file and update the following variables +#. Edit the :file:`settings.py` file and update the following variables (just copy/paste their new content):: MIDDLEWARE_CLASSES = ( @@ -326,10 +326,10 @@ requires a migration. $ ./manage.py collectstatic #. If you are using the *stats* extension, please rename the - */static/stats* directory to */media/stats* + :file:`/static/stats` directory to :file:`/media/stats` and change the value of the ``IMG_ROOTDIR`` parameter (go to the adminstration panel) -#. Restart the python instance(s) that serve *Modoboa* +#. Restart the python instance(s) that serve Modoboa #. Log into Modoboa, go to *Modoboa > Extensions*, uncheck all extensions, save. Then, check the extensions you want to use and @@ -339,7 +339,7 @@ requires a migration. (see :ref:`webservers`) #. **For Dovecot users only**, you need to modify the - ``password_query`` (file */etc/dovecot/dovecot-sql.conf* by default + ``password_query`` (file :file:`/etc/dovecot/dovecot-sql.conf` by default on a Debian system) like this:: password_query = SELECT email AS user, password FROM auth_user WHERE email='%u' @@ -347,7 +347,7 @@ requires a migration. 0.8.8: CSV import feature and minor fixes ========================================= -#. Edit the *settings.py* file and add +#. Edit the :file:`settings.py` file and add ``'modoboa.lib.middleware.AjaxLoginRedirect'`` to the ``MIDDLEWARE_CLASSES`` variable like this:: @@ -362,7 +362,7 @@ requires a migration. 'modoboa.extensions.webmail.middleware.WebmailErrorMiddleware', ) -#. Still inside *settings.py*, modify the ``DATABASE_ROUTERS`` +#. Still inside :file:`settings.py`, modify the ``DATABASE_ROUTERS`` variable like this:: DATABASE_ROUTERS = ["modoboa.extensions.amavis_quarantine.dbrouter.AmavisRouter"] @@ -371,7 +371,7 @@ requires a migration. 0.8.7: per-user language selection ================================== -#. Edit the *settings.py* file and add the +#. Edit the :file:`settings.py` file and add the ``'django.middleware.locale.LocaleMiddleware'`` middleware to the ``MIDDLEWARE_CLASSES`` variable like this:: @@ -404,7 +404,7 @@ requires a migration. ========================================================= #. Just update your configuration if you are using the quarantine - plugin. Open *settings.py*, move the database configuration from + plugin. Open :file:`settings.py`, move the database configuration from the ``DB_CONNECTIONS`` variable to the ``DATABASES`` variable, like this:: @@ -426,7 +426,7 @@ requires a migration. DATABASE_ROUTERS = ["modoboa.extensions.amavis_quarantine.dbrouter.AmavisRouter"] -#. Remove the deprecated ``DB_CONNECTIONS`` variable from *settings.py*. +#. Remove the deprecated ``DB_CONNECTIONS`` variable from :file:`settings.py`. 0.8.5: new "Sieve filters" plugin, improved admin app ===================================================== @@ -437,7 +437,7 @@ requires a migration. $ python manage.py migrate admin #. Add ``modoboa.auth`` and ``modoboa.extensions.sievefilters`` to the - ``INSTALLED_APPS`` variable in *settings.py*. + ``INSTALLED_APPS`` variable in :file:`settings.py`. #. Go the *Settings/Extensions* panel, deactivate and activate your extensions, it will update all the symbolic links. @@ -445,7 +445,7 @@ requires a migration. 0.8.4: folders manipulation support (webmail) and bugfixes ========================================================== -#. Update the ``MIDDLEWARE_CLASSES`` variable in *settings.py*:: +#. Update the ``MIDDLEWARE_CLASSES`` variable in :file:`settings.py`:: MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', @@ -460,7 +460,7 @@ requires a migration. extensions, it will update all the symbolic links to the new format. #. Optional: update the ``DATABASES`` and ``TEMPLATE_LOADERS`` - variables in *settings.py* to remove warning messages (appearing with + variables in :file:`settings.py` to remove warning messages (appearing with Django 1.3):: DATABASES = { @@ -482,14 +482,14 @@ requires a migration. 0.8.3: admin application refactoring and more ============================================= -#. Migrate the *admin* application:: +#. Migrate the ``admin`` application:: $ python manage.py migrate admin #. Update SQL queries used in your environnement (see :ref:`postfix` or :ref:`dovecot`). -#. Update *postfix* configuration so that it can handle domain aliases +#. Update Postfix configuration so that it can handle domain aliases (see :ref:`postfix`). @@ -502,9 +502,9 @@ requires a migration. #. Update your config file and add all extensions to ``INSTALLED_APPS`` (even those you are not going to use). -#. Inside the */templates/* directory, remove all symbolic links. -#. Download the latest release of ckeditor and extract it into */static/js/*. It should create a new directory named *ckeditor*. -#. Update the following variables inside *settings.py*:: +#. Inside the :file:`/templates/` directory, remove all symbolic links. +#. Download the latest release of ckeditor and extract it into :file:`/static/js/`. It should create a new directory named ``ckeditor``. +#. Update the following variables inside :file:`settings.py`:: MEDIA_ROOT = os.path.join(MODOBOA_DIR, 'static') MEDIA_URL = '/static/' @@ -518,9 +518,9 @@ requires a migration. 0.8.1 : project renamed ======================= -#. First, rename the *mailng* directory to *modoboa* and copy all the - content from *modoboa-0.8.1* to *modoboa*. -#. Edit *settings.py* and replace all occurences of mailng by +#. First, rename the ``mailng`` directory to ``modoboa`` and copy all the + content from ``modoboa-0.8.1`` to ``modoboa``. +#. Edit :file:`settings.py` and replace all occurences of mailng by modoboa. Make sure you don't modify the ``DATABASE`` section as you're not going to rename your database. #. Rename the ``MAILNG_DIR`` variable to ``MODOBOA_DIR``. @@ -554,7 +554,7 @@ Before you start the migration, make sure you have updated your 'mailng.userprefs', ) -Starting with 0.8, mailng.main doesn't exist anymore. You must remove +Starting with 0.8, ``mailng.main`` doesn't exist anymore. You must remove it from your ``INSTALLED_APPS``. Finally, run the following commands:: diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index 3280b5af2..50d396112 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -12,12 +12,12 @@ Modoboa works better with Dovecot 2.0 so the following documentation is suitable for this combination. In this section, we assume dovecot's configuration resides in -*/etc/dovecot*, all pathes will be relative to this directory. +:file:`/etc/dovecot`, all pathes will be relative to this directory. Mailboxes ========= -First, edit the *conf.d/10-mail.conf* and set the ``mail_location`` +First, edit the :file:`conf.d/10-mail.conf` and set the ``mail_location`` variable:: # maildir @@ -63,7 +63,7 @@ stored (see :ref:`admin-params` to activate this feature). Those operations are treated asynchronously by a cron script. For example, when you rename an e-mail address through the web UI, the associated mailbox on the file system is not modified -directly. Instead of that, a 'rename' order is created for this +directly. Instead of that, a *rename* order is created for this mailbox. The mailbox will be considered unavailable until the order is not executed (see :ref:`Postfix configuration `). @@ -82,7 +82,7 @@ And add the following line inside:: .. warning:: The user running the cron script must have access to the - *settings.py* file of the modoboa instance. + :file:`settings.py` file of the modoboa instance. The result of each order is recorded into Modoboa's log. Go to *Modoboa > Logs* to consult them. @@ -91,7 +91,7 @@ The result of each order is recorded into Modoboa's log. Go to Authentication ============== -To make the authentication work, edit the *conf.d/10-auth.conf* and +To make the authentication work, edit the :file:`conf.d/10-auth.conf` and uncomment the following line at the end:: #!include auth-system.conf.ext @@ -103,7 +103,7 @@ uncomment the following line at the end:: #!include auth-static.conf.ext -Then, edit the *conf.d/auth-sql.conf.ext* file and add the following +Then, edit the :file:`conf.d/auth-sql.conf.ext` file and add the following content inside:: passdb sql { @@ -120,13 +120,13 @@ content inside:: Make sure to activate only one backend (per type) inside your configuration (just comment the other ones). -Edit the *dovecot-sql.conf.ext* and modify the configuration according +Edit the :file:`dovecot-sql.conf.ext` and modify the configuration according to your database engine. .. _dovecot_mysql_queries: -*MySQL* users -------------- +MySQL users +----------- :: @@ -144,8 +144,8 @@ to your database engine. .. _dovecot_pg_queries: -*PostgreSQL* users ------------------- +PostgreSQL users +---------------- :: @@ -169,7 +169,7 @@ LDA === The LDA is activated by default but you must define a *postmaster* -address. Open the *conf.d/15-lda.conf* file modify the following line:: +address. Open the :file:`conf.d/15-lda.conf` file modify the following line:: postmaster_address = postmaster@ @@ -182,12 +182,12 @@ Modoboa lets adminstrators define per-domain and/or per-account limits (quota). It also lists the current quota usage of each account. Those features require Dovecot to be configured in a specific way. -Inside *conf.d/10-mail.conf*, add the ``quota`` plugin to the default +Inside :file:`conf.d/10-mail.conf`, add the ``quota`` plugin to the default activated ones:: mail_plugins = quota -Inside *conf.d/10-master.conf*, update the *dict* service to set +Inside :file:`conf.d/10-master.conf`, update the ``dict`` service to set proper permissions:: service dict { @@ -200,7 +200,7 @@ proper permissions:: } } -Inside *conf.d/20-imap.conf*, activate the ``imap_quota`` plugin:: +Inside :file:`conf.d/20-imap.conf`, activate the ``imap_quota`` plugin:: protocol imap { # ... @@ -210,13 +210,13 @@ Inside *conf.d/20-imap.conf*, activate the ``imap_quota`` plugin:: # ... } -Inside *dovecot.conf*, activate the quota SQL dictionary backend:: +Inside :file:`dovecot.conf`, activate the quota SQL dictionary backend:: dict { quota = :/etc/dovecot/dovecot-dict-sql.conf.ext } -Inside *conf.d/90-quota.conf*, activate the *quota dictionary* backend:: +Inside :file:`conf.d/90-quota.conf`, activate the *quota dictionary* backend:: plugin { quota = dict:User quota::proxy::quota @@ -224,7 +224,8 @@ Inside *conf.d/90-quota.conf*, activate the *quota dictionary* backend:: It will tell Dovecot to keep quota usage in the SQL dictionary. -Finally, edit the *dovecot-dict-sql.conf.ext* file and put the following content inside:: +Finally, edit the :file:`dovecot-dict-sql.conf.ext` file and put the +following content inside:: connect = host= dbname= user= password= @@ -247,9 +248,9 @@ Finally, edit the *dovecot-dict-sql.conf.ext* file and put the following content Database schema update ^^^^^^^^^^^^^^^^^^^^^^ -The `admin_quota` table is created by *Django* but unfortunately it -doesn't support `DEFAULT` constraints (it only simulates them when the -ORM is used). As *PostgreSQL* is a bit strict about constraint +The ``admin_quota`` table is created by Django but unfortunately it +doesn't support ``DEFAULT`` constraints (it only simulates them when the +ORM is used). As PostgreSQL is a bit strict about constraint violations, you must execute the following query manually:: db=> ALTER TABLE admin_quota ALTER COLUMN bytes SET DEFAULT 0; @@ -318,8 +319,8 @@ work for Modoboa. You should use the following one instead: CREATE TRIGGER setmboxid AFTER INSERT ON admin_quota FOR EACH ROW EXECUTE PROCEDURE set_mboxid(); -Copy this example into a file (for example: *quota-trigger.sql*) on -server running *postgres* and execute the following commands:: +Copy this example into a file (for example: :file:`quota-trigger.sql`) on +server running postgres and execute the following commands:: $ su - postgres $ psql < /path/to/quota-trigger.sql @@ -332,7 +333,7 @@ For existing installations, *Dovecot* (> 2) offers a command to recalculate the current quota usages. For example, if you want to update all usages, run the following command:: - $ doveadmin quota recalc -A + $ doveadm quota recalc -A Be carefull, it can take a while to execute. @@ -342,7 +343,7 @@ ManageSieve/Sieve Modoboa lets users define filtering rules from the web interface. To do so, it requires *ManageSieve* to be activated on your server. -Inside *conf.d/20-managesieve.conf*, make sure the following lines are +Inside :file:`conf.d/20-managesieve.conf`, make sure the following lines are uncommented:: protocols = $protocols sieve @@ -361,7 +362,7 @@ uncommented:: Messages filtering using Sieve is done by the LDA. -Inside *conf.d/15-lda.conf*, activate the ``sieve`` plugin like this:: +Inside :file:`conf.d/15-lda.conf`, activate the ``sieve`` plugin like this:: protocol lda { # Space separated list of plugins to load (default is global mail_plugins). @@ -369,7 +370,7 @@ Inside *conf.d/15-lda.conf*, activate the ``sieve`` plugin like this:: } Finally, configure the ``sieve`` plugin by editing the -*conf.d/90-sieve.conf* file. Put the follwing content inside:: +:file:`conf.d/90-sieve.conf` file. Put the follwing caontent inside:: plugin { # Location of the active script. When ManageSieve is used this is actually @@ -382,7 +383,7 @@ Finally, configure the ``sieve`` plugin by editing the sieve_dir = ~/sieve } -Restart *Dovecot*. +Restart Dovecot. .. _postfix: @@ -398,22 +399,22 @@ Map files ========= You first need to create configuration files (or map files) that will -be used by *postfix* to lookup into Modoboa tables. +be used by Postfix to lookup into Modoboa tables. To automaticaly generate the requested map files and store them in a directory, run the following command:: $ modoboa-admin.py postfix_maps --dbtype mapfiles -``mapfiles`` is the directory where the files will be stored. Answer the -few questions and you're done. +:file:`mapfiles` is the directory where the files will be +stored. Answer the few questions and you're done. .. _postfix_config: Configuration ============= -Use the following configuration in the */etc/postfix/main.cf* file +Use the following configuration in the :file:`/etc/postfix/main.cf` file (this is just one possible configuration):: # Stuff before @@ -442,10 +443,10 @@ Use the following configuration in the */etc/postfix/main.cf* file # Stuff after -Then, edit the */etc/postfix/master.cf* file and add the following +Then, edit the :file:`/etc/postfix/master.cf` file and add the following definition at the end:: dovecot unix - n n - - pipe flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${recipient} -Restart *Postfix*. +Restart Postfix. diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index 639aba65b..2c2d11185 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -10,18 +10,19 @@ Web servers Apache2 ******* -.. note:: The following instructions are meant to help you get your - site up and running quickly. However it is not possible for - the people contributing documentation to Modoboa to test - every single combination of web server, wsgi server, - distribution, etc. So it is possible that **your** - installation of uwsgi or nginx or Apache or what-have-you - works differently. Keep this in mind. +.. note:: + + The following instructions are meant to help you get your site up + and running quickly. However it is not possible for the people + contributing documentation to Modoboa to test every single + combination of web server, wsgi server, distribution, etc. So it is + possible that **your** installation of uwsgi or nginx or Apache or + what-have-you works differently. Keep this in mind. mod_wgsi ======== -First, make sure that *mod_wsgi* is installed on your server. +First, make sure that mod_wsgi is installed on your server. Create a new virtualhost in your Apache configuration and put the following content inside:: @@ -48,8 +49,8 @@ following content inside:: This is just one possible configuration. .. note:: - *Django* 1.3 users, please consult this `page `_, - it contains an example *wsgi.py* file. + Django 1.3 users, please consult this `page `_, + it contains an example :file:`wsgi.py` file. .. note:: You will certainly need more configuration in order to launch @@ -58,7 +59,7 @@ This is just one possible configuration. mod_python ========== -First, make sure that *mod_python* is installed on your server. +First, make sure that mod_python is installed on your server. Create a new virtualhost in your Apache configuration and put the following content inside:: @@ -97,13 +98,14 @@ This is just one possible configuration. Nginx ***** -.. note:: The following instructions are meant to help you get your - site up and running quickly. However it is not possible for - the people contributing documentation to Modoboa to test - every single combination of web server, wsgi server, - distribution, etc. So it is possible that **your** - installation of uwsgi or nginx or Apache or what-have-you - works differently. Keep this in mind. +.. note:: + + The following instructions are meant to help you get your site up + and running quickly. However it is not possible for the people + contributing documentation to Modoboa to test every single + combination of web server, wsgi server, distribution, etc. So it is + possible that **your** installation of uwsgi or nginx or Apache or + what-have-you works differently. Keep this in mind. Green Unicorn ============= @@ -116,7 +118,7 @@ performance is really good with this configuration. To use this setup, first download and install `nginx `__ and `gunicorn `_. Then, use the following sample -*gunicorn* configuration (create a new file named *gunicorn.conf.py* +gunicorn configuration (create a new file named :file:`gunicorn.conf.py` inside Modoboa's root dir):: backlog = 2048 @@ -133,7 +135,7 @@ To start gunicorn, execute the following commands:: $ cd $ gunicorn -c gunicorn.conf.py .wsgi:application -Now the *nginx* part. Just create a new virtual host and use the +Now the nginx part. Just create a new virtual host and use the following configuration:: upstream modoboa { @@ -167,7 +169,7 @@ following configuration:: proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Forwarded-Protocol ssl; - proxy_pass http://modoboa; + proxy_pass http://modoboa; } } @@ -186,7 +188,7 @@ key. `This article contains information about how to do it. Paste this content to your configuration (replace values between -``<>`` with yours), restart *nginx* and enjoy a really fast +``<>`` with yours), restart nginx and enjoy a really fast application! uwsgi @@ -199,13 +201,14 @@ optimize their configuration for your site. The Django documentation includes the following warning regarding uwsgi: -.. warning:: Some distributions, including Debian and Ubuntu, ship an - outdated version of uWSGI that does not conform to the - WSGI specification. Versions prior to 1.2.6 do not call - close on the response object after handling a request. In - those cases the request_finished signal isn’t sent. This - can result in idle connections to database and memcache - servers. +.. warning:: + + Some distributions, including Debian and Ubuntu, ship an outdated + version of uWSGI that does not conform to the WSGI + specification. Versions prior to 1.2.6 do not call close on the + response object after handling a request. In those cases the + request_finished signal isn’t sent. This can result in idle + connections to database and memcache servers. Use uwsgi 1.2.6 or newer. If you do not, you *will* run into problems. Modoboa will fail in obscure ways. From f5626ecaa6fd4a5d2c46744be30a1fb9a995b217 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 19 Sep 2013 21:12:50 +0200 Subject: [PATCH 52/61] Display the name of the object about to be deleted. fixes #483 --- modoboa/admin/static/admin/js/admin.js | 16 ++-------- modoboa/admin/templatetags/admin_extras.py | 31 +++++-------------- modoboa/admin/tests.py | 4 +-- modoboa/admin/urls.py | 3 -- modoboa/admin/views.py | 36 ++++++++-------------- modoboa/static/js/confirmation.js | 4 ++- 6 files changed, 29 insertions(+), 65 deletions(-) diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/admin/static/admin/js/admin.js index 825d645aa..835ea339d 100644 --- a/modoboa/admin/static/admin/js/admin.js +++ b/modoboa/admin/static/admin/js/admin.js @@ -114,7 +114,7 @@ Domains.prototype = { : gettext("This operation will remove all data associated to this domain, excepting accounts."); $("a[name=deldomain]").confirm({ - question: gettext("Delete this domain?"), + question: function() { return this.$element.attr('title'); }, method: "POST", warning: warnmsg, checkboxes: deloptions, @@ -240,23 +240,13 @@ Identities.prototype = { $("a.filter").click($.proxy(this.filter_by_tag, this)); $("a[name=delaccount]").confirm({ - question: gettext("Delete this account?"), + question: function() { return this.$element.attr('title'); }, method: "POST", checkboxes: deloptions, success_cb: $.proxy(this.reload_listing, this) }); - $("a[name=deldlist]").confirm({ - question: gettext("Delete this list?"), - method: "DELETE", - success_cb: $.proxy(this.reload_listing, this) - }); - $("a[name=delforward]").confirm({ - question: gettext("Delete this forward?"), - method: "DELETE", - success_cb: $.proxy(this.reload_listing, this) - }); $("a[name=delalias]").confirm({ - question: gettext("Delete this alias?"), + question: function() { return this.$element.attr('title'); }, method: "DELETE", success_cb: $.proxy(this.reload_listing, this) }); diff --git a/modoboa/admin/templatetags/admin_extras.py b/modoboa/admin/templatetags/admin_extras.py index 5cdf8b245..504a75295 100644 --- a/modoboa/admin/templatetags/admin_extras.py +++ b/modoboa/admin/templatetags/admin_extras.py @@ -175,7 +175,7 @@ def domain_actions(user, domid): actions.append({ "name": "deldomain", "url": reverse("modoboa.admin.views.deldomain", args=[domid]), - "title": _("Delete the domain"), + "title": _("Delete %s?" % domain.name), "img": "icon-trash" }) return render_actions(actions) @@ -191,30 +191,15 @@ def identity_actions(user, ident): {"name" : "delaccount", "url" : reverse("modoboa.admin.views.delaccount", args=[objid]), "img" : "icon-trash", - "title" : _("Delete this account")}, + "title" : _("Delete %s?" % ident.username)}, ] else: - if ident.get_recipients_count() >= 2: - actions = [ - {"name" : "deldlist", - "url" : reverse("modoboa.admin.views.deldlist") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this distribution list")}, - ] - elif ident.extmboxes != "": - actions = [ - {"name" : "delforward", - "url" : reverse("modoboa.admin.views.delforward") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this forward")}, - ] - else: - actions = [ - {"name" : "delalias", - "url" : reverse("modoboa.admin.views.delalias") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this alias")}, - ] + actions = [ + {"name" : "delalias", + "url" : reverse("modoboa.admin.views.delalias") + "?selection=%s" % objid, + "img" : "icon-trash", + "title" : _("Delete %s?" % ident.full_address)}, + ] return render_actions(actions) @register.simple_tag diff --git a/modoboa/admin/tests.py b/modoboa/admin/tests.py index 60d380a20..18e5a7f4e 100644 --- a/modoboa/admin/tests.py +++ b/modoboa/admin/tests.py @@ -208,7 +208,7 @@ class AliasTestCase(ModoTestCase): values) self.assertEqual(dlist.get_recipients_count(), 2) - self.check_ajax_get(reverse("modoboa.admin.views.deldlist") + "?selection=%d" \ + self.check_ajax_get(reverse("modoboa.admin.views.delalias") + "?selection=%d" \ % dlist.id, {}) self.assertRaises(Alias.DoesNotExist, Alias.objects.get, address="all", domain__name="test.com") @@ -224,7 +224,7 @@ class AliasTestCase(ModoTestCase): values) self.assertEqual(fwd.get_recipients_count(), 1) - self.check_ajax_get(reverse("modoboa.admin.views.delforward") + "?selection=%d" \ + self.check_ajax_get(reverse("modoboa.admin.views.delalias") + "?selection=%d" \ % fwd.id, {}) self.assertRaises(Alias.DoesNotExist, Alias.objects.get, address="forward", domain__name="test.com") diff --git a/modoboa/admin/urls.py b/modoboa/admin/urls.py index 048d1b859..edf49f22c 100644 --- a/modoboa/admin/urls.py +++ b/modoboa/admin/urls.py @@ -35,10 +35,7 @@ urlpatterns = patterns( (r'^accounts/delete/(?P\d+)/$', 'delaccount'), (r'^distriblists/new/$', 'newdlist'), - (r'^distriblists/delete/$', 'deldlist'), - (r'^forwards/new/$', 'newforward'), - (r'^forwards/delete/$', 'delforward'), (r'^aliases/new/$', 'newalias'), (r'^aliases/edit/(?P\d+)/$', 'editalias'), diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index 1dbcef8e1..7d142c41c 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -300,40 +300,30 @@ def editalias(request, alid, tplname="common/generic_modal_form.html"): return render(request, tplname, ctx) -def _del_alias(request, msg, msgs): +@login_required +@permission_required("admin.delete_alias") +@transaction.commit_on_success +def delalias(request): selection = request.GET["selection"].split(",") for alid in selection: alias = Alias.objects.get(pk=alid) if not request.user.can_access(alias): raise PermDeniedException + if alias.type == 'dlist': + msg = "Distribution list deleted" + msgs = "Distribution lists deleted" + elif alias.type == 'forward': + msg = "Forward deleted" + msgs = "Forwards deleted" + else: + msg = "Alias deleted" + msgs = "Aliases deleted" alias.delete() msg = ungettext(msg, msgs, len(selection)) return ajax_simple_response({"status": "ok", "respmsg": msg}) -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delalias(request): - return _del_alias(request, "Alias deleted", "Aliases deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def deldlist(request): - return _del_alias(request, "Distribution list deleted", - "Distribution lists deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delforward(request): - return _del_alias(request, "Forward deleted", "Forwards deleted") - - @login_required @user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) def _identities(request): diff --git a/modoboa/static/js/confirmation.js b/modoboa/static/js/confirmation.js index e360eba04..59bd9314d 100644 --- a/modoboa/static/js/confirmation.js +++ b/modoboa/static/js/confirmation.js @@ -36,9 +36,11 @@ id: "confirmbox", 'class': "modal" }); + var question = (typeof this.options.question == 'function') + ? this.options.question.apply(this) : this.options.question; box.append($('