diff --git a/doc/conf.py b/doc/conf.py index 39fa7e833..3a248f79b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -50,7 +50,7 @@ copyright = u'2014, Antoine Nguyen' # The short X.Y version. version = '1.1' # The full version, including alpha/beta/rc tags. -release = '1.1.0' +release = '1.1.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/extending/plugin_api.rst b/doc/extending/plugin_api.rst index a96381405..46ee7fe12 100644 --- a/doc/extending/plugin_api.rst +++ b/doc/extending/plugin_api.rst @@ -41,7 +41,7 @@ Then, you need to register this application using the provided API. Just copy/paste the following example into the :file:`__init__.py` file of the future extension:: - from modoboa.extensions import ModoExtension, exts_pool + from modoboa.core.extensions import ModoExtension, exts_pool class MyExtension(ModoExtension): name = "myext" diff --git a/doc/getting_started/plugins.rst b/doc/getting_started/plugins.rst index 0cfd93378..61556029e 100644 --- a/doc/getting_started/plugins.rst +++ b/doc/getting_started/plugins.rst @@ -58,16 +58,16 @@ Replace values between ``<>`` by yours. Edit the :file:`/etc/postfix/main.cf` file and copy the following lines inside:: - relay_domains = :/etc/postfix/maps/sql-relaydomains.cf + relay_domains = :/etc/postfix/sql-relaydomains.cf transport_maps = - :/etc/postfix/maps/sql-relaydomains-transport.cf - :/etc/postfix/maps/sql-relaydomain-aliases-transport.cf + :/etc/postfix/sql-relaydomains-transport.cf + :/etc/postfix/sql-relaydomain-aliases-transport.cf smtpd_recipient_restrictions = permit_mynetworks reject_unauth_destination check_recipient_access - :/etc/postfix/maps/sql-relay-recipient-verification.cf + :/etc/postfix/sql-relay-recipient-verification.cf Replace ```` by the name of the database you use. @@ -126,6 +126,11 @@ Replace values between ``<>`` with yours. following the `official documentation `_. +.. note:: + + Amavis configuration allows for separate lookup and storage + databases but Modoboa doesn't support it yet. + Cleanup ------- @@ -327,11 +332,11 @@ configuration files as follows: ``/etc/postfix/main.cf``:: - transport_maps = mysql:/etc/postfix/maps/sql-autoreplies-transport.cf - virtual_alias_maps = :/etc/postfix/maps/sql-aliases.cf - :/etc/postfix/mapfiles/sql-domain-aliases-mailboxes.cf, - :/etc/postfix/maps/sql-autoreplies.cf, - :/etc/postfix/mapfiles/sql-catchall-aliases.cf + transport_maps = :/etc/postfix/sql-autoreplies-transport.cf + virtual_alias_maps = :/etc/postfix/sql-aliases.cf + :/etc/postfix/sql-domain-aliases-mailboxes.cf, + :/etc/postfix/sql-autoreplies.cf, + :/etc/postfix/sql-catchall-aliases.cf .. note:: @@ -340,7 +345,7 @@ configuration files as follows: ``/etc/postfix/master.cf``:: autoreply unix - n n - - pipe - flags= user=vmail: argv=/manage.py autoreply $sender $mailbox + flags= user=vmail: argv=python /manage.py autoreply $sender $mailbox Replace ```` by the name of the database you use. ```` is the path of your Modoboa instance. diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index 9aa488ed9..fde360a62 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -48,6 +48,27 @@ Finally, follow the common procedure:: $ python manage.py syncdb --migrate $ python manage.py collectstatic +1.1.2: Audit trail issues +========================= + +Update the :file:`settings.py` file as follows: + +#. Remove the ``'reversion.middleware.RevisionMiddleware'`` + middleware from the ``MIDDLEWARE_CLASSES`` variable + +#. Add the new ``'modoboa.lib.middleware.RequestCatcherMiddleware'`` + middleware at the end of the ``MIDDLEWARE_CLASSES`` variable + +1.1.1: Few bugfixes +=================== + +For those who installed Dovecot in a non-standard location, it is now +possible to tell Modoboa where to find it. Just define a variable +named ``DOVECOT_LOOKUP_PATH`` in the :file:`settings.py` file and +include the appropriate lookup path inside:: + + DOVECOT_LOOKUP_PATH = ("/usr/sbin/dovecot", "/usr/local/sbin/dovecot") + .. _1.1.0: 1.1.0: relay domains and better passwords encryption diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index ded944f66..b0f0df61c 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -28,19 +28,19 @@ Then, edit the ``inbox`` namespace and add the following lines:: inbox = yes mailbox Drafts { - auto = create + auto = subscribe special_use = \Drafts } mailbox Junk { - auto = create + auto = subscribe special_use = \Junk } mailbox Sent { - auto = create + auto = subscribe special_use = \Sent } mailbox Trash { - auto = create + auto = subscribe special_use = \Trash } @@ -357,8 +357,10 @@ 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 + $ psql [modoboa database] < /path/to/quota-trigger.sql $ exit + +Replace ``[modoboa database]`` by the appropriate value. Forcing recalculation --------------------- diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 87b8caa88..c0df35122 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -113,11 +113,11 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.locale.LocaleMiddleware', # Uncomment the next line for simple clickjacking protection: # 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'reversion.middleware.RevisionMiddleware', 'modoboa.lib.middleware.AjaxLoginRedirect', 'modoboa.lib.middleware.CommonExceptionCatcher', 'modoboa.lib.middleware.ExtControlMiddleware', + 'modoboa.lib.middleware.RequestCatcherMiddleware', ) ROOT_URLCONF = '{{ name }}.urls' diff --git a/modoboa/core/models.py b/modoboa/core/models.py index db3d05f22..56bdcbe35 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -472,10 +472,20 @@ class Log(models.Model): @receiver(reversion.post_revision_commit) def post_revision_commit(sender, **kwargs): - if kwargs["revision"].user is None: - return + """Custom post-revision hook. + + We want to track all creations and modifications of admin. objects + (alias, mailbox, user, domain, domain alias, etc.) so we use + django-reversion for that. + + """ + from modoboa.lib.signals import get_request + + current_user = get_request().user.username logger = logging.getLogger("modoboa.admin") for version in kwargs["versions"]: + if version.object is None: + continue prev_revisions = reversion.get_for_object(version.object) if prev_revisions.count() == 1: action = _("added") @@ -486,18 +496,19 @@ def post_revision_commit(sender, **kwargs): message = _("%(object)s '%(name)s' %(action)s by user %(user)s") % { "object": unicode(version.content_type).capitalize(), "name": version.object_repr, "action": action, - "user": kwargs["revision"].user.username + "user": current_user } getattr(logger, level)(message) @receiver(post_delete) -def post_delete_hook(sender, instance, **kwargs): +def log_object_removal(sender, instance, **kwargs): """Custom post-delete hook. We want to know who was responsible for an object deletion. """ from reversion.models import Version + from modoboa.lib.signals import get_request if not reversion.is_registered(sender): return @@ -510,6 +521,6 @@ def post_delete_hook(sender, instance, **kwargs): msg = _("%(object)s '%(name)s' %(action)s by user %(user)s") % { "object": unicode(version.content_type).capitalize(), "name": version.object_repr, "action": _("deleted"), - "user": version.revision.user.username + "user": get_request().user.username } logger.critical(msg) diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index 38ca5504c..a0f29e83b 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy from django.core.urlresolvers import reverse from modoboa.core.extensions import ModoExtension, exts_pool from modoboa.lib import parameters, events -from modoboa.lib.exceptions import PermDeniedException, BadRequest +from modoboa.lib.exceptions import PermDeniedException, BadRequest, Conflict from modoboa.lib.emailutils import split_mailbox from modoboa.extensions.admin.models import ( Domain, DomainAlias, Mailbox, Alias @@ -164,6 +164,12 @@ def import_account_mailbox(user, account, row): ) if not user.can_access(domain): raise PermDeniedException + try: + mb = Mailbox.objects.get(address=mailbox, domain=domain) + except Mailbox.DoesNotExist: + pass + else: + raise Conflict(_("Mailbox %s already exists" % account.email)) quota = int(row[1].strip()) use_domain_quota = True if not quota else False mb = Mailbox(address=mailbox, domain=domain, diff --git a/modoboa/extensions/admin/views/alias.py b/modoboa/extensions/admin/views/alias.py index c8410615f..dc4e8a04e 100644 --- a/modoboa/extensions/admin/views/alias.py +++ b/modoboa/extensions/admin/views/alias.py @@ -1,3 +1,4 @@ +import reversion from django.db import transaction, IntegrityError from django.utils.translation import ugettext as _, ungettext from django.shortcuts import render @@ -56,6 +57,7 @@ def _new_alias(request, title, action, successmsg, @login_required @permission_required("admin.add_alias") @transaction.commit_on_success +@reversion.create_revision() def newdlist(request): return _new_alias( request, _("New distribution list"), reverse(newdlist), @@ -66,6 +68,7 @@ def newdlist(request): @login_required @permission_required("admin.add_alias") @transaction.commit_on_success +@reversion.create_revision() def newalias(request): return _new_alias( request, _("New alias"), reverse(newalias), @@ -76,6 +79,7 @@ def newalias(request): @login_required @permission_required("admin.add_alias") @transaction.commit_on_success +@reversion.create_revision() def newforward(request): return _new_alias( request, _("New forward"), reverse(newforward), @@ -86,6 +90,7 @@ def newforward(request): @login_required @permission_required("admin.change_alias") @transaction.commit_on_success +@reversion.create_revision() def editalias(request, alid, tplname="admin/aliasform.html"): alias = Alias.objects.get(pk=alid) if not request.user.can_access(alias): diff --git a/modoboa/extensions/admin/views/domain.py b/modoboa/extensions/admin/views/domain.py index 2dd635c65..cb94bc3a5 100644 --- a/modoboa/extensions/admin/views/domain.py +++ b/modoboa/extensions/admin/views/domain.py @@ -1,3 +1,4 @@ +import reversion from django.http import HttpResponseRedirect from django.utils.translation import ugettext as _, ungettext from django.core.urlresolvers import reverse @@ -88,6 +89,7 @@ def domains_list(request): @login_required @permission_required("admin.add_domain") @transaction.commit_on_success +@reversion.create_revision() def newdomain(request, tplname="common/wizard_forms.html"): events.raiseEvent("CanCreate", request.user, "domains") @@ -139,6 +141,7 @@ def newdomain(request, tplname="common/wizard_forms.html"): @login_required @permission_required("admin.view_domains") @transaction.commit_on_success +@reversion.create_revision() def editdomain(request, dom_id, tplname="admin/editdomainform.html"): domain = Domain.objects.get(pk=dom_id) if not request.user.can_access(domain): diff --git a/modoboa/extensions/admin/views/identity.py b/modoboa/extensions/admin/views/identity.py index 8e8fea69a..a5af940ce 100644 --- a/modoboa/extensions/admin/views/identity.py +++ b/modoboa/extensions/admin/views/identity.py @@ -1,3 +1,4 @@ +import reversion from django.shortcuts import render from django.db import transaction from django.utils.translation import ugettext as _, ungettext @@ -8,7 +9,7 @@ from django.contrib.auth.decorators import ( from django.views.decorators.csrf import ensure_csrf_cookie from modoboa.lib import parameters, events from modoboa.lib.exceptions import ( - ModoboaException, PermDeniedException, BadRequest + PermDeniedException, BadRequest ) from modoboa.lib.webutils import ( _render_to_string, render_to_json_response @@ -114,6 +115,7 @@ def list_quotas(request, tplname="admin/quotas.html"): @login_required @permission_required("core.add_user") @transaction.commit_on_success +@reversion.create_revision() def newaccount(request, tplname='common/wizard_forms.html'): """Create a new account. @@ -166,6 +168,7 @@ def newaccount(request, tplname='common/wizard_forms.html'): @login_required @permission_required("core.change_user") @transaction.commit_on_success +@reversion.create_revision() def editaccount(request, accountid, tplname="common/tabforms.html"): account = User.objects.get(pk=accountid) if not request.user.can_access(account): diff --git a/modoboa/extensions/admin/views/import.py b/modoboa/extensions/admin/views/import.py index be306e048..2251d23c1 100644 --- a/modoboa/extensions/admin/views/import.py +++ b/modoboa/extensions/admin/views/import.py @@ -1,8 +1,9 @@ import csv +import reversion from django.utils.translation import ugettext as _ from django.shortcuts import render from django.core.urlresolvers import reverse -from django.db import transaction, IntegrityError +from django.db import transaction from django.contrib.auth.decorators import ( login_required, permission_required, user_passes_test ) @@ -86,6 +87,7 @@ def import_dlist(user, row, formopts): _import_alias(user, row) +@reversion.create_revision() def importdata(request, formclass=ImportDataForm): """Generic import function diff --git a/modoboa/extensions/admin/views/user.py b/modoboa/extensions/admin/views/user.py index f836968b1..15e0a9b6c 100644 --- a/modoboa/extensions/admin/views/user.py +++ b/modoboa/extensions/admin/views/user.py @@ -1,6 +1,6 @@ +import reversion from django.contrib.auth.decorators import login_required from django.utils.translation import ugettext as _ -from modoboa.lib.exceptions import ModoboaException from modoboa.lib.webutils import ( render_to_json_response, _render_to_string ) @@ -11,6 +11,7 @@ from modoboa.extensions.admin.forms import ForwardForm @login_required @needs_mailbox() +@reversion.create_revision() def forward(request, tplname='admin/forward.html'): mb = request.user.mailbox_set.all()[0] try: diff --git a/modoboa/extensions/amavis/management/commands/qcleanup.py b/modoboa/extensions/amavis/management/commands/qcleanup.py index c57aee2ef..c31d348f5 100644 --- a/modoboa/extensions/amavis/management/commands/qcleanup.py +++ b/modoboa/extensions/amavis/management/commands/qcleanup.py @@ -50,7 +50,7 @@ class Command(BaseCommand): flags += ['R'] self.__vprint("Deleting marked messages...") - ids = Msgrcpt.objects.filter(rs__in=flags).values("mail_id") + ids = Msgrcpt.objects.filter(rs__in=flags).values("mail_id").distinct() for msg in Msgs.objects.filter(mail_id__in=ids): if not msg.msgrcpt_set.exclude(rs__in=flags).count(): msg.delete() diff --git a/modoboa/extensions/amavis/models.py b/modoboa/extensions/amavis/models.py index 50f29305b..7a711fdd8 100644 --- a/modoboa/extensions/amavis/models.py +++ b/modoboa/extensions/amavis/models.py @@ -65,7 +65,7 @@ class Msgs(models.Model): class Msgrcpt(models.Model): partition_tag = models.IntegerField(null=True, blank=True) mail = models.ForeignKey(Msgs, primary_key=True) - rid = models.ForeignKey(Maddr, db_column='rid', primary_key=True) + rid = models.ForeignKey(Maddr, db_column='rid') rseqnum = models.IntegerField(default=0) is_local = models.CharField(max_length=3) content = models.CharField(max_length=3) diff --git a/modoboa/extensions/amavis/sql_listing.py b/modoboa/extensions/amavis/sql_listing.py index 71c2cd5aa..b7842eddb 100644 --- a/modoboa/extensions/amavis/sql_listing.py +++ b/modoboa/extensions/amavis/sql_listing.py @@ -9,7 +9,7 @@ from modoboa.lib.webutils import static_url from modoboa.lib.email_listing import MBconnector, EmailListing from modoboa.lib.emailutils import Email from modoboa.lib.dbutils import db_type -from modoboa.extensions.admin.models import Domain, Alias +from modoboa.extensions.admin.models import Domain from .models import Quarantine, Msgrcpt @@ -46,9 +46,10 @@ class Qtable(tables.Table): "date", label=ugettext_lazy("Date"), sort_order="date", cssclass="openable" ) - + cols_order = [ - 'selection', 'type', 'rstatus', 'score', 'to', 'from_', 'subject', 'time' + 'selection', 'type', 'rstatus', 'score', 'to', 'from_', 'subject', + 'time' ] def parse_type(self, value): @@ -101,7 +102,7 @@ class SQLconnector(MBconnector): order = self.order_translation_table[order] self.messages = self.messages.order_by(sign + order) - self.count = len(self.messages) + self.count = self.messages.count() return self.count def fetch(self, start=None, stop=None, **kwargs): @@ -115,7 +116,9 @@ class SQLconnector(MBconnector): "type": qm["mail__msgrcpt__content"], "score": qm["mail__msgrcpt__bspam_level"]} rs = qm["mail__msgrcpt__rs"] - if rs == '': + if rs == 'D': + continue + elif rs == '': m["class"] = "unseen" elif rs == 'R': m["img_rstatus"] = static_url("pics/release.png") @@ -138,6 +141,18 @@ class SQLWrapper(object): See ``PgWrapper`` for the real mess... """ + def _exec(self, query, args): + """Execute a raw SQL query. + + :param string query: query to execute + :param list args: a list of arguments to replace in :kw:`query` + """ + from django.db import connections, transaction + + cursor = connections['amavis'].cursor() + cursor.execute(query, args) + transaction.commit_unless_managed(using='amavis') + def get_mails(self, request, rcptfilter=None): """Retrieve all messages visible by a user. @@ -163,6 +178,19 @@ class SQLWrapper(object): def get_recipient_message(self, address, mailid): return Msgrcpt.objects.get(mail=mailid, rid__email=address) + def set_msgrcpt_status(self, address, mailid, status): + """Change the status (rs field) of a message recipient. + + :param string status: status + """ + from modoboa.extensions.amavis.models import Maddr + + addr = Maddr.objects.get(email=address) + self._exec( + "UPDATE msgrcpt SET rs=%s WHERE mail_id=%s AND rid=%s", + [status, mailid, addr.id] + ) + def get_recipient_messages(self, address, mailids): return Msgrcpt.objects.filter(mail__in=mailids, rid__email=address) diff --git a/modoboa/extensions/amavis/views.py b/modoboa/extensions/amavis/views.py index 91b3d52fb..c4d7f51d1 100644 --- a/modoboa/extensions/amavis/views.py +++ b/modoboa/extensions/amavis/views.py @@ -57,7 +57,6 @@ def _listing(request): else: raise BadRequest("unsupported search criteria %s" % c) flt = nfilter if flt is None else flt | nfilter - msgtype = navparams.get('msgtype', None) if msgtype is not None: nfilter = Q(mail__msgrcpt__content=msgtype) @@ -140,9 +139,7 @@ def viewmail(request, mail_id): if request.user.mailbox_set.count(): mb = Mailbox.objects.get(user=request.user) if rcpt in mb.alias_addresses: - msgrcpt = get_wrapper().get_recipient_message(rcpt, mail_id) - msgrcpt.rs = 'V' - msgrcpt.save() + get_wrapper().set_msgrcpt_status(rcpt, mail_id, 'V') content = Template(""" @@ -178,9 +175,7 @@ def delete_selfservice(request, mail_id): if rcpt is None: raise BadRequest(_("Invalid request")) try: - msgrcpt = get_wrapper().get_recipient_message(rcpt, mail_id) - msgrcpt.rs = 'D' - msgrcpt.save() + get_wrapper().set_msgrcpt_status(rcpt, mail_id, 'D') except Msgrcpt.DoesNotExist: raise BadRequest(_("Invalid request")) return render_to_json_response(_("Message deleted")) @@ -201,9 +196,7 @@ def delete(request, mail_id): if mb is not None and r != mb.full_address \ and not r in mb.alias_addresses: continue - msgrcpt = wrapper.get_recipient_message(r, i) - msgrcpt.rs = 'D' - msgrcpt.save() + wrapper.set_msgrcpt_status(r, i, 'D') message = ungettext("%(count)d message deleted successfully", "%(count)d messages deleted successfully", len(mail_id)) % {"count": len(mail_id)} @@ -218,24 +211,24 @@ def release_selfservice(request, mail_id): secret_id = request.GET.get("secret_id", None) if rcpt is None or secret_id is None: raise BadRequest(_("Invalid request")) + wrapper = get_wrapper() try: - msgrcpt = get_wrapper().get_recipient_message(rcpt, mail_id) + msgrcpt = wrapper.get_recipient_message(rcpt, mail_id) except Msgrcpt.DoesNotExist: raise BadRequest(_("Invalid request")) if secret_id != msgrcpt.mail.secret_id: raise BadRequest(_("Invalid request")) if parameters.get_admin("USER_CAN_RELEASE") == "no": - msgrcpt.rs = 'p' + wrapper.set_msgrcpt_status(rcpt, mail_id, 'p') msg = _("Request sent") else: amr = AMrelease() result = amr.sendreq(mail_id, secret_id, rcpt) if result: - rcpt.rs = 'R' + wrapper.set_msgrcpt_status(rcpt, mail_id, 'R') msg = _("Message released") else: raise BadRequest(result) - msgrcpt.save() return render_to_json_response(msg) @@ -257,14 +250,10 @@ def release(request, mail_id): continue msgrcpts += [wrapper.get_recipient_message(r, i)] if mb is not None and parameters.get_admin("USER_CAN_RELEASE") == "no": - # FIXME : can't use this syntax because extra SQL (using - # .extra() for postgres) is not propagated (the 'tables' - # parameter is lost somewhere...) - # - # msgrcpts.update(rs='p') for msgrcpt in msgrcpts: - msgrcpt.rs = 'p' - msgrcpt.save() + wrapper.set_msgrcpt_status( + msgrcpt.rid.email, msgrcpt.mail.mail_id, 'p' + ) message = ungettext("%(count)d request sent", "%(count)d requests sent", len(mail_id)) % {"count": len(mail_id)} @@ -276,10 +265,11 @@ def release(request, mail_id): amr = AMrelease() error = None for rcpt in msgrcpts: - result = amr.sendreq(rcpt.mail.mail_id, rcpt.mail.secret_id, rcpt.rid.email) + result = amr.sendreq( + rcpt.mail.mail_id, rcpt.mail.secret_id, rcpt.rid.email + ) if result: - rcpt.rs = 'R' - rcpt.save() + wrapper.set_msgrcpt_status(rcpt.rid.email, rcpt.mail.mail_id, 'R') else: error = result break diff --git a/modoboa/extensions/postfix_relay_domains/views.py b/modoboa/extensions/postfix_relay_domains/views.py index e6df53621..a1af875f6 100644 --- a/modoboa/extensions/postfix_relay_domains/views.py +++ b/modoboa/extensions/postfix_relay_domains/views.py @@ -1,3 +1,4 @@ +import reversion from django.shortcuts import render from django.db import transaction from django.core.urlresolvers import reverse @@ -16,6 +17,7 @@ from .forms import RelayDomainForm, RelayDomainFormGeneral @login_required @permission_required("postfix_relay_domains.add_relaydomain") @transaction.commit_on_success +@reversion.create_revision() def create(request, tplname="postfix_relay_domains/new_relaydomain_form.html"): events.raiseEvent("CanCreate", request.user, "relay_domains") if request.method == 'POST': @@ -39,6 +41,7 @@ def create(request, tplname="postfix_relay_domains/new_relaydomain_form.html"): @login_required @permission_required("postfix_relay_domains.change_relaydomain") +@reversion.create_revision() def edit(request, rdom_id, tplname='common/tabforms.html'): rdom = RelayDomain.objects.get(pk=rdom_id) if not request.user.can_access(rdom): diff --git a/modoboa/extensions/stats/app_settings.py b/modoboa/extensions/stats/app_settings.py index 8020e7299..86e0a749a 100644 --- a/modoboa/extensions/stats/app_settings.py +++ b/modoboa/extensions/stats/app_settings.py @@ -21,9 +21,3 @@ class ParametersForm(AdminParametersForm): initial="/tmp/modoboa", help_text=ugettext_lazy("Path to directory where RRD files are stored") ) - - img_rootdir = forms.CharField( - label=ugettext_lazy("Directory to store PNG files"), - initial="/tmp/modoboa", - help_text=ugettext_lazy("Path to directory where PNG files are stored") - ) diff --git a/modoboa/extensions/webmail/imaputils.py b/modoboa/extensions/webmail/imaputils.py index 46cdd73f2..67065363a 100644 --- a/modoboa/extensions/webmail/imaputils.py +++ b/modoboa/extensions/webmail/imaputils.py @@ -415,6 +415,9 @@ class IMAPconnector(object): @capability('LIST-EXTENDED', '_listmboxes_simple') def _listmboxes(self, topmailbox, mailboxes, until_mailbox=None): + """Retrieve mailboxes list. + + """ pattern = "%s%s%%" % (topmailbox.encode("imap4-utf-7"), self.hdelimiter) \ if len(topmailbox) else "%" resp = self._cmd("LIST", "", pattern, "RETURN", "(CHILDREN)") @@ -443,6 +446,8 @@ class IMAPconnector(object): if r'\Marked' in flags or not r'\UnMarked' in flags: descr["send_status"] = True if r'\HasChildren' in flags: + if r'\NonExistent' in flags: + descr["removed"] = True descr["path"] = name descr["sub"] = [] if until_mailbox and until_mailbox.startswith(name): @@ -489,7 +494,8 @@ class IMAPconnector(object): continue del mb["send_status"] key = "path" if "path" in mb else "name" - count = self.unseen_messages(mb[key]) + if not mb.get("removed", False): + count = self.unseen_messages(mb[key]) if count == 0: continue mb["unseen"] = count diff --git a/modoboa/extensions/webmail/lib.py b/modoboa/extensions/webmail/lib.py index 0b099f8ef..5770086d0 100644 --- a/modoboa/extensions/webmail/lib.py +++ b/modoboa/extensions/webmail/lib.py @@ -3,7 +3,7 @@ import os import re from datetime import datetime, timedelta import email -import lxml +import lxml.html import chardet from rfc6266 import build_header, parse_headers from django.core.files.uploadhandler import FileUploadHandler, SkipFile diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index 110b8c6ef..14ea120c7 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -693,8 +693,14 @@ Webmail.prototype = { }, listmailbox_loader: function(event) { + var $target = get_target(event, "a"); + + if ($target.parent().hasClass("disabled")) { + event.preventDefault(); + return; + } this.select_mailbox(event.target); - this._listmailbox_loader(event, get_target(event, "a")); + this._listmailbox_loader(event, $target); }, /* diff --git a/modoboa/extensions/webmail/templatetags/webmail_tags.py b/modoboa/extensions/webmail/templatetags/webmail_tags.py index 3ac199460..49a907af6 100644 --- a/modoboa/extensions/webmail/templatetags/webmail_tags.py +++ b/modoboa/extensions/webmail/templatetags/webmail_tags.py @@ -132,7 +132,9 @@ def print_mailboxes(tree, selected=None, withunseen=False, selectonly=False, hde cssclass = "" name = mbox["path"] if "sub" in mbox else mbox["name"] label = separate_mailbox(mbox["name"], hdelimiter)[0] - if selected == name: + if mbox.get("removed", False): + cssclass = "disabled" + elif selected == name: cssclass = "active" result += "
  • \n" % (name, cssclass) if "sub" in mbox: diff --git a/modoboa/lib/fixtures/demo.json b/modoboa/lib/fixtures/demo.json index cdd99ecb9..737dff0b0 100644 --- a/modoboa/lib/fixtures/demo.json +++ b/modoboa/lib/fixtures/demo.json @@ -39,14 +39,6 @@ "value": "vmail" } }, - { - "pk": 12, - "model": "lib.parameter", - "fields": { - "name": "stats.IMG_ROOTDIR", - "value": "/srv/www/modoboa/media/stats" - } - }, { "pk": 13, "model": "lib.parameter", diff --git a/modoboa/lib/middleware.py b/modoboa/lib/middleware.py index 44f08dd07..d32e3037a 100644 --- a/modoboa/lib/middleware.py +++ b/modoboa/lib/middleware.py @@ -1,4 +1,7 @@ # coding: utf-8 +""" +Custom middlewares. +""" import re from django.http import Http404, HttpResponseRedirect from modoboa.core.models import Extension @@ -7,6 +10,7 @@ from modoboa.lib.webutils import ( _render_error, ajax_response, render_to_json_response ) from modoboa.lib.exceptions import ModoboaException +from modoboa.lib.signals import request_accessor class ExtControlMiddleware(object): @@ -50,3 +54,19 @@ class CommonExceptionCatcher(object): return render_to_json_response( unicode(exception), status=exception.http_code ) + + +class RequestCatcherMiddleware(object): + """ + Simple middleware to store the current request. + """ + + def __init__(self): + self._request = None + request_accessor.connect(self) + + def process_request(self, request): + self._request = request + + def __call__(self, **kwargs): + return self._request diff --git a/modoboa/lib/signals.py b/modoboa/lib/signals.py new file mode 100644 index 000000000..fbd5baad8 --- /dev/null +++ b/modoboa/lib/signals.py @@ -0,0 +1,11 @@ +""" +Custom signals. +""" +from django.dispatch import Signal + +request_accessor = Signal() + +def get_request(): + """Get the current request from anywhere. + """ + return request_accessor.send(None)[0][1] diff --git a/setup.py b/setup.py index bb2669e0c..d9c68ba97 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def read(fname): setup( name="modoboa", - version='1.1.0', + version='1.1.1', url='http://modoboa.org/', license='MIT', description="Mail hosting made simple", diff --git a/tests/install_from_scratch.py b/tests/install_from_scratch.py index 17f497ff7..3ced6cbab 100644 --- a/tests/install_from_scratch.py +++ b/tests/install_from_scratch.py @@ -21,7 +21,6 @@ class DeployTest(unittest.TestCase): def tearDown(self): path = os.path.join(self.workdir, self.projname) code, output = exec_cmd("python manage.py test core lib admin limits postfix_relay_domains", cwd=path) - print output self.assertEqual(code, 0) def test_standard(self):