diff --git a/COPYING b/COPYING index 8a44bb1df..39eaba158 100644 --- a/COPYING +++ b/COPYING @@ -1,4 +1,4 @@ -Copyright (c) 2009-2011, Antoine Nguyen +Copyright (c) 2009-2012, Antoine Nguyen Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/INSTALL b/INSTALL index 71fc3d0ef..8c756eadb 100644 --- a/INSTALL +++ b/INSTALL @@ -186,8 +186,10 @@ Then, all you have to do is to modify the `settings.py` file: AUTH_LDAP_BIND_DN = "" AUTH_LDAP_BIND_PASSWORD = "" - AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=example,dc=com", - ldap.SCOPE_SUBTREE, "(uid=%(user)s)") + LDAP_USER_BASE = "ou=users,dc=example,dc=com" + LDAP_USER_FILTER = "(mail=%(user)s)" + AUTH_LDAP_USER_SEARCH = LDAPSearch(LDAP_USER_BASE, + ldap.SCOPE_SUBTREE, LDAP_USER_FILTER) You will find a detailled documentation `here `_. @@ -196,6 +198,28 @@ Once the authentication is properly configured, the users defined in your LDAP directory will be able to connect to *Modoboa*, the associated domain and mailboxes will be automatically created if needed. +Users will also be able to update their LDAP password directly from +Modoboa. + +.. note:: + + Modoboa doesn't provide any synchronization mechanism once a user + is registered into the database. Any modification done from the + directory to a user account will not be reported to Modoboa (an + email address change for example). Currently, the only solution is + to manually delete the Modoboa record, it will be recreated on the + next user login. + +Available settings +~~~~~~~~~~~~~~~~~~ + +* ``LDAP_USER_BASE`` : the distinguish name of the search base +* ``LDAP_USER_FILTER`` : the filter used to retrieve users distinguish name +* ``LDAP_PASSWORD_ATTR`` : the attribute used to store the password + (default: ``userPassword``) +* ``LDAP_ACTIVE_DIRECTORY`` : used to indicate if your directory is an + Active Directory one (default: ``False``) + How to upgrade ============== @@ -208,6 +232,30 @@ requires a migration. Before executing a schema/data migration, I recommend that you make a copy of your existing database. +0.8.8: CSV import feature and minor fixes +----------------------------------------- + +#. Edit the *settings.py* file and + ``'modoboa.lib.middleware.AjaxLoginRedirect'`` to the + ``MIDDLEWARE_CLASSES`` variable like this:: + + MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'modoboa.lib.middleware.AjaxLoginRedirect', + 'modoboa.lib.middleware.ExtControlMiddleware', + 'modoboa.extensions.webmail.middleware.WebmailErrorMiddleware', + ) + +#. Still inside *settings.py*, modify the ``DATABASE_ROUTERS`` + variable like this:: + + DATABASE_ROUTERS = ["modoboa.extensions.amavis_quarantine.dbrouter.AmavisRouter"] + + 0.8.7: per-user language selection ---------------------------------- diff --git a/admin/__init__.py b/admin/__init__.py index c886038f4..2d8a8ff17 100644 --- a/admin/__init__.py +++ b/admin/__init__.py @@ -5,6 +5,10 @@ from django.utils.translation import ugettext as _ from django.db.utils import DatabaseError from models import Extension +parameters.register_admin("AUTHENTICATION_TYPE", type="list", deflt="local", + values=[('local', _("Local")), + ('ldap', "LDAP")], + help=_("The backend used for authentication")) parameters.register_admin("CREATE_DIRECTORIES", type="list_yesno", deflt="yes", help=_("Modoboa will handle mailbox creation on filesystem")) parameters.register_admin("STORAGE_PATH", type="string", deflt="/var/vmail", diff --git a/admin/models.py b/admin/models.py index f0895b644..46f979051 100644 --- a/admin/models.py +++ b/admin/models.py @@ -6,6 +6,7 @@ from django.conf import settings from modoboa.lib import parameters, events from modoboa.lib.sysutils import exec_cmd, exec_as_vuser from modoboa.lib.emailutils import split_mailbox +from modoboa.lib.ldaputils import * from modoboa.auth.lib import crypt_password import os import pwd @@ -291,6 +292,26 @@ class Mailbox(DatesAware): pass super(Mailbox, self).save(*args, **kwargs) + def set_password(self, curvalue, newvalue): + """Password update + + Update the current mailbox's password with the given clear + value. This value is encrypted according to the defined method + before it is saved. + + :param value: the new password's value + """ + if parameters.get_admin("AUTHENTICATION_TYPE") == "local": + self.password = crypt_password(newvalue) + self.save() + return + + ab = LDAPAuthBackend() + try: + ab.update_user_password(self.user.username, curvalue, newvalue) + except LDAPException, e: + raise AdminError(_("Failed to update password: %s" % str(e))) + def save_from_user(self, localpart, domain, user): """Simple save method called for automatic creations @@ -307,16 +328,16 @@ class Mailbox(DatesAware): # If we ever had numerical uid, don't resolve it v_uid = parameters.get_admin("VIRTUAL_UID"); if v_uid.isdigit(): - self.uid = v_uid + self.uid = v_uid else: - self.uid = pwd.getpwnam(parameters.get_admin("VIRTUAL_UID")).pw_uid + self.uid = pwd.getpwnam(parameters.get_admin("VIRTUAL_UID")).pw_uid # Just the same for gid v_gid = parameters.get_admin("VIRTUAL_GID") - if v_gid.isdigit: - self.gid = v_gid + if v_gid.isdigit(): + self.gid = v_gid else: - self.gid = pwd.getpwnam(parameters.get_admin("VIRTUAL_GID")).pw_gid - self.quota = self.domain.quota + self.gid = pwd.getpwnam(parameters.get_admin("VIRTUAL_GID")).pw_gid + self.quota = self.domain.quota super(Mailbox, self).save() def create_from_csv(self, line): @@ -328,9 +349,9 @@ class Mailbox(DatesAware): :param line: a list containing the expected information """ - if len(line) < 5: + if len(line) < 4: raise AdminError(_("Invalid line")) - mailbox, domname = split_mailbox(line[4]) + mailbox, domname = split_mailbox(line[0]) self.address = mailbox try: self.domain = Domain.objects.get(name=domname) @@ -489,31 +510,39 @@ class Extension(models.Model): events.raiseEvent("ExtDisabled", ext=self) -# -# Optional callback to execute if django-auth-ldap is enabled. -# +def populate_callback(sender, user=None, **kwargs): + """Populate signal callback + + If the LDAP authentication backend is in use, this callback will + be called each time a new user authenticates succesfuly to + Modoboa. This function is in charge of creating the mailbox + associated to the provided ``User`` object. + + :param sender: ?? + :param user: a ``User`` instance + """ + if parameters.get_admin("AUTHENTICATION_TYPE") != "ldap": + return + if user is None: + return + localpart, domname = split_mailbox(user.username) + try: + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + domain = Domain() + domain.name = domname + domain.enabled = True + domain.quota = 0 + domain.save() + try: + mb = Mailbox.objects.get(domain=domain, address=localpart) + except Mailbox.DoesNotExist: + mb = Mailbox() + mb.save_from_user(localpart, domain, user) + try: from django_auth_ldap.backend import populate_user - - def populate_callback(sender, user=None, **kwargs): - if user is None: - return - localpart, domname = split_mailbox(user.username) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - domain = Domain() - domain.name = domname - domain.enabled = True - domain.quota = 0 - domain.save() - try: - mb = Mailbox.objects.get(domain=domain, address=localpart) - except Mailbox.DoesNotExist: - mb = Mailbox() - mb.save_from_user(localpart, domain, user) - - populate_user.connect(populate_callback, dispatch_uid="myuid") - except ImportError, inst: pass +else: + populate_user.connect(populate_callback, dispatch_uid="myuid") diff --git a/admin/tests.py b/admin/tests.py index c8934b8ab..45ba6f0ed 100644 --- a/admin/tests.py +++ b/admin/tests.py @@ -79,7 +79,7 @@ class MailboxTestCase(TestCase): Two cases: * The default admin changes his password (no associated Mailbox) - * A normal user changes hist password + * A normal user changes his password """ clt = Client() self.assertEqual(clt.login(username="admin", password="toto"), True) diff --git a/auth/lib.py b/auth/lib.py index 8592cad86..33b541601 100644 --- a/auth/lib.py +++ b/auth/lib.py @@ -8,10 +8,11 @@ import base64 from modoboa.lib import parameters def encrypt(clear): - obj = AES.new(parameters.get_admin("SECRET_KEY"), - AES.MODE_ECB) + key = parameters.get_admin("SECRET_KEY") + obj = AES.new(key, AES.MODE_ECB) + clear = str(clear) if len(clear) % AES.block_size: - clear += "\0" * (AES.block_size - len(clear) % AES.block_size) + clear += " " * (AES.block_size - len(clear) % AES.block_size) ciph = obj.encrypt(clear) ciph = base64.b64encode(ciph) return ciph @@ -20,7 +21,7 @@ def decrypt(ciph): obj = AES.new(parameters.get_admin("SECRET_KEY"), AES.MODE_ECB) ciph = base64.b64decode(ciph) clear = obj.decrypt(ciph) - return clear.rstrip('\0') + return clear.rstrip(' ') def get_password(request): return decrypt(request.session["password"]) @@ -33,11 +34,12 @@ def _check_password(password, crypted): return hashlib.md5(password).hexdigest() == crypted return password -def crypt_password(password): +def crypt_password(password, hexdigest=True): scheme = parameters.get_admin("PASSWORD_SCHEME", app="admin") if scheme == "crypt": salt = ''.join(Random().sample(string.letters + string.digits, 2)) return crypt.crypt(password, salt) if scheme == "md5": - return hashlib.md5(password).hexdigest() + obj = hashlib.md5(password) + return obj.hexdigest() if hexdigest else obj.digest() return password diff --git a/auth/views.py b/auth/views.py index 6a08f5d7d..07c99ab03 100755 --- a/auth/views.py +++ b/auth/views.py @@ -6,7 +6,7 @@ from django.shortcuts import render_to_response from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from modoboa.lib import events -import modoboa.admin +from modoboa.admin import views as admviews from forms import LoginForm from modoboa.auth.lib import * @@ -32,7 +32,7 @@ def dologin(request): next = request.POST["next"] if next.strip() in ["", "None"]: - next = reverse(modoboa.admin.views.domains) + next = reverse(admviews.domains) return HttpResponseRedirect(next) else: error = _("Account is disabled.") diff --git a/doc/conf.py b/doc/conf.py index 7c98365dd..ba9ba8d6c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -41,16 +41,16 @@ master_doc = 'index' # General information about the project. project = u'Modoboa' -copyright = u'2011, Antoine Nguyen' +copyright = u'2012, Antoine Nguyen' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.8.7' +version = '0.8.8' # The full version, including alpha/beta/rc tags. -release = '0.8.7' +release = '0.8.8' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/extensions/amavis_quarantine/static/js/quarantine.js b/extensions/amavis_quarantine/static/js/quarantine.js index 8e34dd589..5a6347947 100644 --- a/extensions/amavis_quarantine/static/js/quarantine.js +++ b/extensions/amavis_quarantine/static/js/quarantine.js @@ -30,7 +30,7 @@ function actioncallback(resp) { } function actionclick(evt) { - new Request.JSON({ + new Request.JSON.mdb({ url: evt.target.get("href"), onSuccess: actioncallback }).get(); diff --git a/extensions/sievefilters/static/js/lib.js b/extensions/sievefilters/static/js/lib.js index d2c19da31..39add7588 100644 --- a/extensions/sievefilters/static/js/lib.js +++ b/extensions/sievefilters/static/js/lib.js @@ -14,7 +14,7 @@ function removefilter(evt) { if (!confirm(gettext("Remove this filter?"))) { return; } - new Request.JSON({ + new Request.JSON.mdb({ url: this.get("href"), onSuccess: function(response) { if (response.status == "ok") { @@ -72,7 +72,7 @@ function loadfs(response) { * (remove, activate) */ function send_command(cmd, extracb) { - new Request.JSON({ + new Request.JSON.mdb({ url: cmd, onSuccess: function(response) { if (response.status == "ko") { @@ -116,7 +116,7 @@ function savefs(evt) { if (!check_prereqs(evt, gettext("Save changes?"))) { return; } - new Request.JSON({ + new Request.JSON.mdb({ url: $("feditor").get("action"), method: $("feditor").get("method"), data: $("feditor").toQueryString(), @@ -177,7 +177,7 @@ function activatefs(evt) { function toggle_filter_state(event) { event.stop(); - new Request.JSON({ + new Request.JSON.mdb({ url: this.get("href"), onSuccess: function(response) { if (response.status == "ok") { @@ -193,7 +193,7 @@ function toggle_filter_state(event) { function move_filter(event) { event.stop(); - new Request.JSON({ + new Request.JSON.mdb({ url: this.get("href"), onSuccess: function(response) { $("filters_list").set("html", response.content); diff --git a/extensions/stats/templates/stats/index.html b/extensions/stats/templates/stats/index.html index 6e33f0f4f..bd041abbd 100644 --- a/extensions/stats/templates/stats/index.html +++ b/extensions/stats/templates/stats/index.html @@ -68,8 +68,8 @@ window.addEvent("domready", function() { if (p == "") { return; } - new Request.JSON({url : url + "?" + p, noCache: true, - onSuccess : getgraph_callback}).get(); + new Request.JSON.mdb({url : url + "?" + p, noCache: true, + onSuccess : getgraph_callback}).get(); }; {% if user.is_superuser %} diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index 09317b315..3cd6d3bfa 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -409,6 +409,9 @@ def html2plaintext(content): return plaintext def get_current_url(request): + if not request.session.has_key("folder"): + return "" + res = "%s?page=%s" % (request.session["folder"], request.session["page"]) for p in ["criteria", "pattern", "order"]: if p in request.session.keys(): diff --git a/extensions/webmail/static/js/fdmenu.js b/extensions/webmail/static/js/fdmenu.js index 721d69ecd..570d6e80d 100644 --- a/extensions/webmail/static/js/fdmenu.js +++ b/extensions/webmail/static/js/fdmenu.js @@ -46,7 +46,7 @@ var FdMenu = new Class({ if (!confirm(gettext("Delete folder?"))) { return; } - new Request.JSON({ + new Request.JSON.mdb({ url: evt.target.get("href"), method: "GET", onSuccess: function(resp) { diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index 16e520fc9..dc3b3ee0e 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -876,13 +876,17 @@ function refreshFolder() { profile : "gray", spinner : true }); - new Request.JSON({url: query, noCache : true, onSuccess: function(resp) { - var callback = ($defined(resp.callback)) ? resp.callback : "default"; + new Request.JSON.mdb({ + url: query, + noCache : true, + onSuccess: function(resp) { + var callback = ($defined(resp.callback)) ? resp.callback : "default"; - current_anchor.get_callback(callback)(resp); - infobox.info(gettext("Done")); - infobox.hide(1); - }}).get(); + current_anchor.get_callback(callback)(resp); + infobox.info(gettext("Done")); + infobox.hide(1); + } + }).get(); } /* @@ -897,7 +901,7 @@ function simple_request(url, params) { profile: "gray", spinner: true }); - new Request.JSON({ + new Request.JSON.mdb({ url : url, onSuccess : function(response) { if (response.status == "ok") { diff --git a/extensions/webmail/templates/webmail/attachments.html b/extensions/webmail/templates/webmail/attachments.html index d1660e605..bf4f2e591 100644 --- a/extensions/webmail/templates/webmail/attachments.html +++ b/extensions/webmail/templates/webmail/attachments.html @@ -6,7 +6,7 @@ + {% url django.views.i18n.javascript_catalog as catalog_url %} {% block html_head %}{% endblock %} diff --git a/templates/nlayout.html b/templates/nlayout.html index 4ccd9a0e8..2bdb541d4 100644 --- a/templates/nlayout.html +++ b/templates/nlayout.html @@ -15,6 +15,7 @@ + {% url django.views.i18n.javascript_catalog as catalog_url %}