diff --git a/INSTALL b/INSTALL index 0885d707f..4fb35c5fd 100644 --- a/INSTALL +++ b/INSTALL @@ -1,3 +1,4 @@ +:q Installation ============ @@ -31,6 +32,17 @@ Run the initialisation script. It will insert needed data into the database : export PYTHONPATH=$PWD/.. DJANGO_SETTINGS_MODULE=mailng.settings ./scripts/populate.py +How to upgrade +-------------- + +0.5 -> 0.6 : No modifications have been made to the core SQL schema so you +just have to decompress the new tarball at the same location than your +current installation. Once done, go to MailNG repository and run:: + + python manage.py syncdb + +You must also modify 'settings.py' in order to activate new components. + Host configuration ------------------ @@ -109,20 +121,71 @@ Then, use the following configuration in your /etc/postfix/main.cf file virtual_alias_maps = mysql:/etc/postfix/sql-aliases.cf # Stuff after -NB: Mailng only supports maildir format for now. Others will surely be -added soon. +NB: Mailng supports both maildir and mbox formats. You can specify +which format to use by modifying the MAILBOX_TYPE variable located +inside 'settings.py'. + +Tips to activate auto-reply messages in Postifx +----------------------------------------------- + +Since version 0.6, mail users can define an auto-reply message with MailNG. +To make Postfix understand this feature, you need to update your configuration +files as follow:: + + # /etc/postfix/main.cf + transport_maps = mysql:/etc/postfix/maps/sql-transport.cf + virtual_alias_maps = mysql:/etc/postfix/maps/sql-aliases.cf + mysql:/etc/postfix/maps/sql-autoreplies.cf + + # /etc/postfix/master.cf + autoreply unix - n n - - pipe + flags= user=nobody argv=/scripts/autoreply $sender $mailbox + +Then, create new map files with the following content:: + + # /etc/postfix/maps/sql-transport.cf + user = + password = + dbname = + hosts = 127.0.0.1 + query = SELECT method FROM postfix_autoreply_transport WHERE domain='%s' + + # /etc/postfix/maps/sql-autoreplies.cf + user = + password = + dbname = + hosts = 127.0.0.1 + query = SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s' + +NB: auto-reply messages are just sent one time per sender for a +pre-defined time period. By default, this period is equal to 1 day +(86400s), you can adjust this value by modifying the AUTOREPLY_TIMEOUT +variable located inside 'settings.py'. + +NB: Be sure that both 'autoreply' and 'autoreply.py' scripts (located +inside the 'scripts' directory) have good permissions (ie. 0755). Tips to link Mailng with dovecot -------------------------------- -Edit your main Dovecot config file (/etc/dovecot/dovecot.conf) and put the +If you are using 'maildir' format to store mailboxes, add the +following line inside your main Dovecot config file +(/etc/dovecot/dovecot.conf):: + + mail_location = maildir:/%d/%n/.maildir + +If you are using 'mbox' format, put the following:: + + mail_location = mbox:/%d/%n/:INBOX=/%d/%n/Inbox + +To make the authentication work, edit 'dovecot.conf' and add the following inside:: auth default { # ... stuff before passdb sql { - # Path for SQL configuration file, see /etc/dovecot/dovecot-sql.conf for + # Path for SQL configuration file, see /etc/dovecot/dovecot-sql.conf for # example args = /etc/dovecot/dovecot-sql.conf } @@ -155,23 +218,28 @@ Put the following lines inside your dovecot.conf file:: mail_plugins = quota quota_imap } +Before continuing, you need to know which quota backend must be user +(function of mailboxes format):: + * 'mbox' : backend=dirsize, + * 'maildir' : backend=maildir. + If you use version prior to 1.1, add also the following configuration:: plugin { # 10 MB default quota limit - quota = maildir:storage=10240 + quota = :storage=10240 } Then modify the above user_query inside dovecot-sql.conf file like that to activate per user quotas:: - user_query = SELECT path AS home, uid, gid, concat('maildir:storage=', quota / 1024) AS quota FROM admin_mailbox WHERE full_address = '%u' + user_query = SELECT path AS home, uid, gid, concat(':storage=', quota / 1024) AS quota FROM admin_mailbox WHERE full_address = '%u' For version >= 1.1, put the following configuration inside dovecot.conf file:: plugin { # Default 10M storage limit with an extra 5M limit when saving to Trash. - quota = maildir:User quota + quota = :User quota quota_rule = *:storage=10M quota_rule2 = Trash:storage=5M } diff --git a/admin/models.py b/admin/models.py index bb530ea24..83ef556d6 100644 --- a/admin/models.py +++ b/admin/models.py @@ -18,9 +18,7 @@ class Domain(models.Model): def create_dir(self): path = "%s/%s" % (settings.STORAGE_PATH, self.name) - if exec_as_vuser("mkdir %s" % path): - return False - return True + return exec_as_vuser("mkdir %s" % path) def rename_dir(self, newname): code, output = exec_pipe("sudo -u %s mv %s/%s %s/%s" \ @@ -74,14 +72,16 @@ class Mailbox(models.Model): except AttributeError: mbtype = "maildir" if mbtype == "mbox": + template = ["Inbox", "Drafts", "Sent", "Trash", "Junk"] + for dir in template: + exec_as_vuser("touch %s/%s" % (path, dir)) + + self.path = "%s/%s/Inbox" % (domain.name, self.address) + else: template = [".Drafts/", ".Sent/", ".Trash/", ".Junk/"] for dir in template: for sdir in ["cur", "new", "tmp"]: - exec_as_vuser("mkdir -p %s/%s/%s" % (path, dir, sdir)) - - self.path = "%s/%s/" % (domain.name, self.address) - else: - exec_as_vuser("mkdir %s/.maildir/" % path) + exec_as_vuser("mkdir -p %s/.maildir/%s/%s" % (path, dir, sdir)) self.path = "%s/%s/.maildir/" % (domain.name, self.address) return True diff --git a/admin/views.py b/admin/views.py index 146a3e0f7..bedf5ba95 100644 --- a/admin/views.py +++ b/admin/views.py @@ -14,6 +14,7 @@ from mailng.admin.models import Domain, Mailbox, Alias from forms import MailboxForm, DomainForm, AliasForm, PermissionForm from mailng.lib.authbackends import crypt_password from mailng.lib import _render, _ctx_ok, _ctx_ko +from mailng.lib import events def is_superuser(user): if not user.is_superuser: @@ -50,6 +51,7 @@ def newdomain(request): domain = form.save(commit=False) if domain.create_dir(): form.save() + events.raiseEvent("CreateDomain", dom=domain) ctx = _ctx_ok(reverse(admin.views.domains)) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @@ -98,6 +100,7 @@ def editdomain(request, dom_id): @permission_required("admin.delete_domain") def deldomain(request, dom_id): domain = Domain.objects.get(pk=dom_id) + events.raiseEvent("DeleteDomain", dom=domain) domain.delete_dir() domain.delete() return HttpResponseRedirect('/mailng/admin/') @@ -157,6 +160,9 @@ def newmailbox(request, dom_id=None): mb.quota = domain.quota mb.full_address = "%s@%s" % (mb.address, domain.name) mb.save() + + events.raiseEvent("CreateMailbox", mbox=mb) + ctx = _ctx_ok(reverse(admin.views.mailboxes, args=[domain.id])) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @@ -221,6 +227,7 @@ def editmailbox(request, dom_id, mbox_id=None): @permission_required("admin.delete_mailbox") def delmailbox(request, dom_id, mbox_id=None): mb = Mailbox.objects.get(pk=mbox_id) + events.raiseEvent("DeleteMailbox", mbox=mb) mb.delete_dir() mb.user.delete() mb.delete() diff --git a/extensions/__init__.py b/extensions/__init__.py new file mode 100644 index 000000000..2b9599676 --- /dev/null +++ b/extensions/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import os +import re + +def loadextensions(): + from django.conf import settings + + expr = re.compile("mailng\.extensions\.(.+)") + for app in settings.INSTALLED_APPS: + m = expr.match(app) + if not m: + continue + module = __import__(m.group(1), globals(), locals(), ['main']) + module.main.init() diff --git a/extensions/postfix_autoreply/__init__.py b/extensions/postfix_autoreply/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/postfix_autoreply/main.py b/extensions/postfix_autoreply/main.py new file mode 100644 index 000000000..80ec2a156 --- /dev/null +++ b/extensions/postfix_autoreply/main.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +""" +Postfix auto-replies plugin. + +This module provides a way to integrate MailNG auto-reply +functionality into Postfix. + +""" +from mailng.lib import events +from models import Transport, Alias + +def init(): + events.register("CreateDomain", onCreateDomain) + events.register("DeleteDomain", onDeleteDomain) + events.register("CreateMailbox", onCreateMailbox) + events.register("DeleteMailbox", onDeleteMailbox) + +def onCreateDomain(**kwargs): + dom = kwargs["dom"] + transport = Transport() + transport.domain = "autoreply.%s" % dom.name + transport.method = "autoreply:" + transport.save() + +def onDeleteDomain(**kwargs): + dom = kwargs["dom"] + trans = Transport.objects.get(domain="autoreply.%s" % dom.name) + trabs.delete() + +def onCreateMailbox(**kwargs): + mbox = kwargs["mbox"] + alias = Alias() + alias.full_address = mbox.full_address + alias.autoreply_address = \ + "%s@autoreply.%s" % (mbox.full_address, mbox.domain.name) + alias.save() + +def onDeleteMailbox(**kwargs): + mbox = kwargs["mbox"] + alias = Alias.objects.get(full_address=mbox.full_address) + alias.delete() diff --git a/extensions/postfix_autoreply/models.py b/extensions/postfix_autoreply/models.py new file mode 100644 index 000000000..a2dab5e36 --- /dev/null +++ b/extensions/postfix_autoreply/models.py @@ -0,0 +1,10 @@ +from django.db import models +from mailng.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) diff --git a/lib/events.py b/lib/events.py new file mode 100644 index 000000000..da77f6c94 --- /dev/null +++ b/lib/events.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +""" +This module provides a simple way of managing events between MailNG +core application and additional components. + +""" + +events = ["CreateDomain", + "DeleteDomain", + "CreateMailbox", + "DeleteMailbox"] + +callbacks = {} + +def register(event, callback): + if not event in events: + return 0 + if not event in callbacks.keys(): + callbacks[event] = [] + callbacks[event].append(callback) + return 1 + +def raiseEvent(event, **kwargs): + if not event in events or not event in callbacks.keys(): + return 0 + for callback in callbacks[event]: + callback(**kwargs) + return 1 diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index fcce0bcba..cffd2ff3e 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-06-17 10:13+0200\n" +"POT-Creation-Date: 2009-06-19 10:29+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Antoine Nguyen \n" "Language-Team: LANGUAGE \n" @@ -25,11 +25,11 @@ msgstr "" msgid "Password" msgstr "Mot de passe" -#: admin/forms.py:18 +#: admin/forms.py:18 main/forms.py:21 msgid "Confirmation" msgstr "" -#: admin/forms.py:32 +#: admin/forms.py:32 main/forms.py:36 msgid "Passwords mismatch" msgstr "Les mots de passe diffèrent" @@ -54,43 +54,43 @@ msgstr "Super administrateur" msgid "Domain administrator" msgstr "Admin. de domaine" -#: admin/models.py:9 admin/models.py:48 +#: admin/models.py:9 admin/models.py:46 msgid "name" msgstr "Nom" -#: admin/models.py:11 admin/models.py:112 main/models.py:9 +#: admin/models.py:11 admin/models.py:110 main/models.py:9 msgid "enabled" msgstr "Activé" -#: admin/models.py:49 admin/models.py:109 +#: admin/models.py:47 admin/models.py:107 msgid "address" msgstr "Adresse" -#: admin/models.py:51 +#: admin/models.py:49 msgid "password" msgstr "Mot de passe" -#: admin/models.py:111 +#: admin/models.py:109 msgid "mailbox" msgstr "Boite aux lettres" -#: admin/views.py:26 +#: admin/views.py:21 msgid "Permission denied." msgstr "Permission refusée." -#: admin/views.py:54 admin/views.py:85 +#: admin/views.py:49 admin/views.py:81 msgid "Domain with this name already defined" msgstr "Un domaine avec ce nom existe déjà" -#: admin/views.py:62 +#: admin/views.py:58 msgid "Failed to initialise domain, check permissions" msgstr "Impossible d'initialiser le domaine, vérifier les permissions" -#: admin/views.py:88 +#: admin/views.py:84 msgid "Failed to rename domain, check permissions" msgstr "Impossible de renommer le domaine, vérifier les permissions" -#: admin/views.py:138 admin/views.py:191 +#: admin/views.py:135 admin/views.py:191 msgid "Mailbox with this address already exists" msgstr "Une boite aux lettres pour cette adresse existe déjà" @@ -103,7 +103,7 @@ msgstr "" msgid "Failed to rename mailbox, check permissions" msgstr "Impossible de renommer la boite aux lettres, vérifier les permissions." -#: admin/views.py:261 admin/views.py:290 +#: admin/views.py:262 admin/views.py:291 msgid "Alias with this address already exists" msgstr "Un alias avec cette adresse existe déjà" @@ -115,6 +115,18 @@ msgstr "Activé" msgid "enabled_f" msgstr "Activée" +#: main/forms.py:17 +msgid "Old password" +msgstr "Ancien mot de passe" + +#: main/forms.py:19 +msgid "New password" +msgstr "Nouveau mot de passe" + +#: main/forms.py:31 +msgid "Old password mismatchs" +msgstr "L'ancien mot de passe est incorrect" + #: main/models.py:7 msgid "subject" msgstr "sujet" @@ -123,7 +135,7 @@ msgstr "sujet" msgid "content" msgstr "contenu" -#: main/views.py:34 +#: main/views.py:45 msgid "Auto reply message updated successfully." msgstr "Message de réponse auto. mis à jour." @@ -237,7 +249,8 @@ msgid "yes,no" msgstr "oui,non" #: templates/admin/editalias.html:14 templates/admin/editdomain.html:15 -#: templates/admin/editmailbox.html:16 templates/main/autoreply.html:16 +#: templates/admin/editmailbox.html:16 templates/main/autoreply.html:17 +#: templates/main/chpassword.html:15 msgid "Update" msgstr "Mettre à jour" @@ -297,15 +310,23 @@ msgstr "Aucun admin de domaine défini." msgid "Permissions" msgstr "" -#: templates/main/header.html:5 -msgid "General" -msgstr "Général" - -#: templates/main/menu.html:5 +#: templates/main/chpassword.html:7 templates/main/menu.html:13 msgid "Change password" msgstr "Changer de mot de passe" -#: templates/main/menu.html:10 +#: templates/main/header.html:8 +msgid "General" +msgstr "Général" + +#: templates/main/index.html:11 +msgid "Your auto-reply message is activated." +msgstr "Votre réponse automatique est active." + +#: templates/main/index.html:13 +msgid "Your auto-reply message is not activated." +msgstr "Votre réponse automatique n'est pas active." + +#: templates/main/menu.html:6 msgid "Auto-reply message" msgstr "Message de réponse automatique" diff --git a/main/views.py b/main/views.py index 617294aa7..4053dd329 100644 --- a/main/views.py +++ b/main/views.py @@ -13,11 +13,16 @@ from mailng.lib import _render, _ctx_ok, _ctx_ko from mailng.lib.authbackends import crypt_password from forms import ARmessageForm, ChangePasswordForm from models import ARmessage -from admin.models import Mailbox +from mailng.admin.models import Mailbox @login_required def index(request): - return _render(request, "main/index.html", {}) + mb = Mailbox.objects.get(user=request.user.id) + try: + arm = ARmessage.objects.filter(mbox=mb.id, enabled=True) + except ARmessage.DoesNotExist: + arm = None + return _render(request, "main/index.html", {"arm" : arm}) @login_required def autoreply(request): diff --git a/scripts/autoreply b/scripts/autoreply new file mode 100755 index 000000000..7975c43d7 --- /dev/null +++ b/scripts/autoreply @@ -0,0 +1,7 @@ +#!/bin/sh + +BASEDIR=/home/tonio +export PYTHONPATH=$BASEDIR +export DJANGO_SETTINGS_MODULE=mailng.settings + +$BASEDIR/mailng/scripts/autoreply.py $1 $2 diff --git a/scripts/autoreply.py b/scripts/autoreply.py index a9d261172..c36b348fd 100755 --- a/scripts/autoreply.py +++ b/scripts/autoreply.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import sys +import smtplib from email.mime.text import MIMEText import datetime from django.conf import settings @@ -38,7 +39,12 @@ if __name__ == "__main__": msg['Subject'] = armessage.subject msg['From'] = mailbox msg['To'] = sender - print msg + + s = smtplib.SMTP() + s.connect() + s.sendmail(mailbox, sender, msg.as_string()) + s.quit() + lastar.last_sent = datetime.datetime.now() lastar.save() sys.exit(0) diff --git a/settings_sample.py b/settings_sample.py index dd5c35d4a..8800b82d6 100644 --- a/settings_sample.py +++ b/settings_sample.py @@ -80,6 +80,9 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'mailng.admin', 'mailng.main' + + # MailNG extensions here. + # 'mailng.extensions.postfix_autoreply' ) AUTHENTICATION_BACKENDS = ( diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 000000000..c89c7f9ba --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,5 @@ +window.addEvent('domready', function() { + SqueezeBox.assign($$('a.boxed'), { + parse: 'rel' + }); +}); diff --git a/static/pics/info.png b/static/pics/info.png new file mode 100644 index 000000000..8851b99ba Binary files /dev/null and b/static/pics/info.png differ diff --git a/static/stylesheets/basic.css b/static/stylesheets/basic.css index d4847cd8b..53403fb9e 100644 --- a/static/stylesheets/basic.css +++ b/static/stylesheets/basic.css @@ -297,4 +297,9 @@ body { color: green; font-weight: bold; margin-right: 10px; +} + +.info { + float: left; + font-style: italic; } \ No newline at end of file diff --git a/templates/main/index.html b/templates/main/index.html index d02849226..f5d1a9336 100644 --- a/templates/main/index.html +++ b/templates/main/index.html @@ -5,5 +5,13 @@ {% menu "" perms %} {% endblock %} {% block subcontent %} - +
+ + {% if arm %} + {% trans "Your auto-reply message is activated." %} + {% else %} + {% trans "Your auto-reply message is not activated." %} + {% endif %} + +
{% endblock %} \ No newline at end of file diff --git a/templates/main/menu.html b/templates/main/menu.html index ff15fad03..5e5c5c050 100644 --- a/templates/main/menu.html +++ b/templates/main/menu.html @@ -1,15 +1,15 @@ {% load i18n %} \ No newline at end of file diff --git a/urls.py b/urls.py index 6915caef2..eb6b08d79 100644 --- a/urls.py +++ b/urls.py @@ -5,6 +5,9 @@ from django.conf import settings # from django.contrib import admin # admin.autodiscover() +from mailng.extensions import loadextensions +loadextensions() + urlpatterns = patterns('', # Example: # (r'^mailng/', include('mailng.foo.urls')),