From 6c7a64e94792b9c9413d0afe78d57c9e9bbc463e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 15 Jan 2010 12:15:15 +0100 Subject: [PATCH] !! VERY UNSTABLE CHANGESET !! Many updates in this commit: * first work on the webmail plugin, for now only emails listing and folders browsing is supported... * New login form, * New graphical layout (still needing improvments) * code factoring between webmail and quarantine plugins * etc. --- admin/templatetags/admin_extras.py | 14 + admin/views.py | 2 +- auth/__init__.py | 0 auth/forms.py | 7 + auth/models.py | 3 + auth/tests.py | 23 ++ auth/views.py | 32 ++ extensions/amavis_quarantine/main.py | 97 ++--- extensions/amavis_quarantine/sql_listing.py | 85 ++++ .../amavis_quarantine/templates/index.html | 209 +++------- .../amavis_quarantine/templates/listing.html | 51 --- .../amavis_quarantine/templates/viewmail.html | 19 +- extensions/amavis_quarantine/urls.py | 4 +- extensions/webmail/__init__.py | 0 extensions/webmail/imap_listing.py | 124 ++++++ extensions/webmail/main.py | 57 +++ extensions/webmail/templates/index.html | 85 ++++ extensions/webmail/urls.py | 6 + lib/__init__.py | 2 +- lib/email_listing.py | 95 +++++ lib/imap_utf7.py | 210 ++++++++++ lib/parameters.py | 6 +- lib/tables.py | 56 +++ main/forms.py | 2 +- main/models.py | 1 + static/js/common.js | 52 ++- static/js/domains.js | 4 +- static/js/main.js | 5 + static/js/mootools-more.js | 391 +++++++++++------- static/pics/folder.png | Bin 0 -> 498 bytes static/pics/overview.png | Bin 0 -> 528 bytes static/pics/webmail.png | Bin 0 -> 550 bytes static/stylesheets/basic.css | 11 + static/stylesheets/default.css | 254 ++++++++++++ static/stylesheets/login.css | 70 ++++ static/stylesheets/mailng.png | Bin 0 -> 13264 bytes templates/admin/addpermission.html | 2 +- templates/admin/domain_header.html | 2 +- templates/admin/domains.html | 18 +- templates/admin/editalias.html | 2 +- templates/admin/editdomain.html | 2 +- templates/admin/editmailbox.html | 2 +- templates/admin/newalias.html | 2 +- templates/admin/newdomain.html | 2 +- templates/admin/newmailbox.html | 2 +- templates/admin/parameters.html | 6 +- templates/admin/settings_header.html | 2 +- templates/common/email_listing.html | 57 +++ templates/{admin => common}/genericform.html | 3 +- templates/common/navbar.html | 19 + templates/common/tables.html | 18 + templates/genericform.html | 5 - templates/main/autoreply.html | 10 +- templates/main/chpassword.html | 4 +- templates/main/header.html | 2 + templates/main/menu.html | 4 +- templates/nlayout.html | 92 +++++ templates/registration/login.html | 62 +-- urls.py | 2 +- 59 files changed, 1813 insertions(+), 484 deletions(-) create mode 100755 auth/__init__.py create mode 100644 auth/forms.py create mode 100755 auth/models.py create mode 100755 auth/tests.py create mode 100755 auth/views.py create mode 100644 extensions/amavis_quarantine/sql_listing.py delete mode 100644 extensions/amavis_quarantine/templates/listing.html create mode 100644 extensions/webmail/__init__.py create mode 100644 extensions/webmail/imap_listing.py create mode 100644 extensions/webmail/main.py create mode 100644 extensions/webmail/templates/index.html create mode 100644 extensions/webmail/urls.py create mode 100644 lib/email_listing.py create mode 100644 lib/imap_utf7.py create mode 100644 lib/tables.py create mode 100644 static/pics/folder.png create mode 100644 static/pics/overview.png create mode 100644 static/pics/webmail.png create mode 100644 static/stylesheets/default.css create mode 100644 static/stylesheets/login.css create mode 100644 static/stylesheets/mailng.png create mode 100644 templates/common/email_listing.html rename templates/{admin => common}/genericform.html (87%) create mode 100644 templates/common/navbar.html create mode 100644 templates/common/tables.html delete mode 100644 templates/genericform.html create mode 100644 templates/nlayout.html diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index b377b3a2c..cdc0a1dec 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -67,6 +67,20 @@ def settings_menu(selection, perms): {"selection" : selection, "entries" : entries, "perms" : perms}) +@register.simple_tag +def domains_menu(selection, perms): + entries = [ + {"name" : "domains", + "url" : reverse(admin.views.domains), + "label" : _("Domains"), + "img" : "/static/pics/domains.png"} + ] + entries += events.raiseQueryEvent("AdminMenuDisplay", target="admin_menu_box", + perms=perms) + return render_to_string('main/menulist.html', + {"menu" : entries, "selection" : selection, + "perms" : perms}) + @register.simple_tag def loadadminextmenu(perms): menu = events.raiseQueryEvent("AdminMenuDisplay", target="admin_menu_box", diff --git a/admin/views.py b/admin/views.py index 7b917765f..5127f1c74 100644 --- a/admin/views.py +++ b/admin/views.py @@ -49,7 +49,7 @@ def domains(request): return mailboxes(request, dom_id=mb.domain.id) return HttpResponseRedirect(reverse(main.views.index)) - + domains = Domain.objects.all() counters = {} for dom in domains: diff --git a/auth/__init__.py b/auth/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/auth/forms.py b/auth/forms.py new file mode 100644 index 000000000..909d7d3a6 --- /dev/null +++ b/auth/forms.py @@ -0,0 +1,7 @@ +from django import forms +from django.utils.translation import ugettext as _ + +class LoginForm(forms.Form): + username = forms.CharField(label=_("Username")) + password = forms.CharField(label=_("Password"), widget=forms.PasswordInput, + help_text=_("Password used to authenticate")) diff --git a/auth/models.py b/auth/models.py new file mode 100755 index 000000000..71a836239 --- /dev/null +++ b/auth/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/auth/tests.py b/auth/tests.py new file mode 100755 index 000000000..2247054b3 --- /dev/null +++ b/auth/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/auth/views.py b/auth/views.py new file mode 100755 index 000000000..7cd567b80 --- /dev/null +++ b/auth/views.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth import authenticate, login +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render_to_response +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +import mailng.admin +from forms import LoginForm + +def dologin(request): + error = None + if request.method == "POST": + form = LoginForm(request.POST) + user = authenticate(username=request.POST["username"], + password=request.POST["password"]) + if user: + if user.is_active: + login(request, user) + request.session["password"] = request.POST["password"] + next = request.POST["next"] + if next == "None": + next = reverse(mailng.admin.views.domains) + return HttpResponseRedirect(next) + else: + error = _("Account is disabled.") + error = _("Your username and password didn't match. Please try again.") + else: + form = LoginForm() + next = request.GET.has_key("next") and request.GET["next"] or None + return render_to_response("registration/login.html", { + "form" : form, "error" : error, "next" : next + }) diff --git a/extensions/amavis_quarantine/main.py b/extensions/amavis_quarantine/main.py index 3ffd5b9e6..c6f319ce2 100644 --- a/extensions/amavis_quarantine/main.py +++ b/extensions/amavis_quarantine/main.py @@ -2,15 +2,19 @@ """ """ +from datetime import datetime +import time +import email +import re +import os +import lxml.html +from lxml.html.clean import Cleaner from django.http import HttpResponseRedirect, HttpResponse -from django.shortcuts import get_object_or_404, render_to_response -from django.template.loader import render_to_string from django.template import Template from django.utils import simplejson from django.conf.urls.defaults import * from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from django.core.paginator import Paginator from django.contrib.auth.decorators \ import login_required from mailng.lib import events, parameters @@ -18,12 +22,7 @@ from mailng.lib import _render, _ctx_ok, _ctx_ko, decode from mailng.lib import db from mailng.admin.models import Mailbox from lib import AMrelease -from datetime import datetime -import email -import re -import os -import lxml.html -from lxml.html.clean import Cleaner +from sql_listing import * attached_map = {} @@ -53,62 +52,22 @@ def menu(**kwargs): ] return [] -def lookup(*args): - filter = "" - for a in args: - if not a: - continue - if a[0] == "&": - filter += " AND " - else: - filter += " OR " - filter += a[1:] - conn = db.getconnection("amavis_quarantine") - status, cursor = db.execute(conn, """ -SELECT msgs.from_addr, maddr.email, msgs.subject, msgs.content, quarantine.mail_id, - msgs.time_num, msgs.content -FROM quarantine, maddr, msgrcpt, msgs -WHERE quarantine.mail_id=msgrcpt.mail_id -AND msgrcpt.rid=maddr.id -AND msgrcpt.mail_id=msgs.mail_id -AND quarantine.chunk_ind=1 -%s -ORDER BY msgs.time_num DESC -""" % filter) - return cursor - -def render_listing(request, cursor): - emails = [] - rows = cursor.fetchall() - for row in rows: - emails.append({"from" : row[0], "to" : row[1], - "subject" : row[2], "content" : row[3], - "mailid" : row[4], "time" : datetime.fromtimestamp(row[5]), - "type" : row[6]}) - paginator = Paginator(emails, 10) - try: - pagenum = int(request.GET.get('page', '1')) - except ValueError: - pagenum = 1 - page = paginator.page(pagenum) - return render_to_string("amavis_quarantine/listing.html", { - "count" : len(rows), - "emails" : page, "current_page" : pagenum, - "last_page" : paginator._get_num_pages() - }) - @login_required def index(request, message=None): + start = time.time() if not request.user.is_superuser: mb = Mailbox.objects.get(user=request.user.id) - filter = "&maddr.email='%s'" % mb.full_address + filter = ["&maddr.email='%s'" % mb.full_address] else: filter = None - cursor = lookup(filter) - - return _render(request, 'amavis_quarantine/index.html', { - "content" : render_listing(request, cursor) - }) + try: + pageid = request.GET["page"] + if pageid == "": + pageid = 1 + except KeyError: + pageid = 1 + lst = SQLlisting(filter) + return lst.render(request, pageid=int(pageid), start=start) @login_required def viewmail_plain(request, content): @@ -311,7 +270,19 @@ def search(request): filter += " OR " filter += "msgs.%s LIKE '%%%s%%'" % (c, pattern) filter = "&(%s)" % filter - cursor = lookup(filter) - ctx = {"status" : "ok", "content" : render_listing(request, cursor)} - return HttpResponse(simplejson.dumps(ctx), - mimetype="application/json") + lst = SQLlisting([filter]) + try: + pageid = request.GET["page"] + if pageid == "": + pageid = 1 + except KeyError: + pageid = 1 + page = lst.paginator.getpage(pageid) + if page: + content = lst.fetch(page.id_start, page.id_stop).render(request) + navbar = lst.render_navbar(page) + else: + content = "Empty quarantine" + navbar = "" + ctx = {"status" : "ok", "listing" : content, "navbar" : navbar} + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") diff --git a/extensions/amavis_quarantine/sql_listing.py b/extensions/amavis_quarantine/sql_listing.py new file mode 100644 index 000000000..f4744adef --- /dev/null +++ b/extensions/amavis_quarantine/sql_listing.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime +from django.utils.translation import ugettext as _ +from mailng.lib import tables, db +from mailng.lib.email_listing import MBconnector, EmailListing + +class Qtable(tables.Table): + idkey = "mailid" + selection = tables.SelectionColumn("selection", first=True) + type = tables.Column("type", label=_("Type")) + from_ = tables.Column("from", label=_("From")) + subject = tables.Column("subject", label=_("Subject")) + time = tables.Column("date", label=_("Date")) + + def parse_date(self, value): + return datetime.fromtimestamp(value) + +class SQLconnector(MBconnector): + def __init__(self, filter=None): + self.conn = db.getconnection("amavis_quarantine") + self.filter = "" + if filter: + for a in filter: + if not a: + continue + if a[0] == "&": + self.filter += " AND " + else: + self.filter += " OR " + self.filter += a[1:] + query = self._get_query() + status, cursor = db.execute(self.conn, """ +SELECT count(quarantine.mail_id) AS total +%s +""" % query) + if not status: + print cursor + self.count = 0 + else: + self.count = int(cursor.fetchone()[0]) + + def _get_query(self): + return """ +FROM quarantine, maddr, msgrcpt, msgs +WHERE quarantine.mail_id=msgrcpt.mail_id +AND msgrcpt.rid=maddr.id +AND msgrcpt.mail_id=msgs.mail_id +AND quarantine.chunk_ind=1 +%s +ORDER BY msgs.time_num DESC +""" % self.filter + + def messages_count(self, folder=None): + return self.count + + def fetch(self, start=None, stop=None, **kwargs): + query = self._get_query() + status, cursor = db.execute(self.conn, """ +SELECT msgs.from_addr, maddr.email, msgs.subject, msgs.content, quarantine.mail_id, + msgs.time_num, msgs.content +%s +LIMIT %d,%d +""" % (query, start - 1, stop - 1)) + if not status: + print cursor + return [] + emails = [] + rows = cursor.fetchall() + for row in rows: + emails.append({"from" : row[0], "to" : row[1], + "subject" : row[2], "content" : row[3], + "mailid" : row[4], "date" : row[5], + "type" : row[6]}) + return emails + +class SQLlisting(EmailListing): + tpl = "amavis_quarantine/index.html" + + def __init__(self, filter, **kwargs): + self.mbc = SQLconnector(filter) + EmailListing.__init__(self, **kwargs) + + def fetch(self, id_start, id_stop): + return Qtable(self.mbc.fetch(start=id_start, stop=id_stop)) diff --git a/extensions/amavis_quarantine/templates/index.html b/extensions/amavis_quarantine/templates/index.html index c7da1ca4c..3d780c5d7 100644 --- a/extensions/amavis_quarantine/templates/index.html +++ b/extensions/amavis_quarantine/templates/index.html @@ -1,49 +1,11 @@ -{% extends "layout.html" %} +{% extends "common/email_listing.html" %} {% load i18n %} {% load amextras %} -{% block html_head %} - - - - +$$("a[name=release]").addEvent("click", function(event) { + if (!checkSelection("selection", "{% trans "Release this selection?" %}")) { + event.stop(); + return; + } + mysubmit(event, "release"); +}); + + +var myFx = new Fx.Slide($("searchparams")); +myFx.hide(); +$("searchopts").addEvent("click", function(event) { + myFx.cancel(); + myFx.toggle(); +}); +$$("input[name=scriteria]").addEvent("click", function(event) { + myFx.cancel(); + myFx.toggle(); +}); +$("searchfield").addEvent("change", function(event) { + var criteria = ""; + $$("input[name=scriteria]:checked").each(function(obj) { + criteria = obj.value; + }); + {% url extensions.amavis_quarantine.main.search as s_url %} + new Request.JSON({url: "{{ s_url }}", onSuccess: function(resp) { + $("listing").set("html", resp.listing); + $("navbar").set("html", resp.navbar); + {% url extensions.amavis_quarantine.main.viewmail as view_url %} + bindRows("{{ view_url }}", "{{ current_page }}"); + }}).get({"pattern" : $("searchfield").value, "criteria" : criteria}); +}); +$("searchfield").addEvent("click", function(event) { + $("searchfield").select(); +}); + +$("searchfield").addEvent("blur", function(event) { + if ($("searchfield").value == "") { + $("searchfield").set("value", "{% trans "Search..." %}"); + } +}); +{% url extensions.amavis_quarantine.main.viewmail as view_url %} +bindRows("{{ view_url }}", "{{ current_page }}"); {% endblock %} {% block middletitle %}{% trans "Quarantine" %}{% endblock %} {% block menu %} @@ -162,15 +101,3 @@ window.addEvent("domready", function() { {% endblock %} -{% block content %} -
-{% url extensions.amavis_quarantine.main.process as actionurl %} -
- - -
- {{ content }} -
-
-
-{% endblock %} diff --git a/extensions/amavis_quarantine/templates/listing.html b/extensions/amavis_quarantine/templates/listing.html deleted file mode 100644 index 9ce78ec9c..000000000 --- a/extensions/amavis_quarantine/templates/listing.html +++ /dev/null @@ -1,51 +0,0 @@ -{% load i18n %} - -{% if not count %} -
{% trans "No messages in quarantine" %}
-{% else %} -
-
- Messages {{ emails.start_index }} à {{ emails.end_index }} sur {{ count }} -
- -
- - - - - - - - - - {% for m in emails.object_list %} - - - - - {% url extensions.amavis_quarantine.main.viewmail m.mailid as view_url %} - - - - {% endfor %} -
{% trans "Type" %}{% trans "From" %}{% trans "Subject" %}{% trans "Date" %}
{{ m.type }}{{ m.from }}{{ m.subject }}{{ m.time }}
- -{% endif %} diff --git a/extensions/amavis_quarantine/templates/viewmail.html b/extensions/amavis_quarantine/templates/viewmail.html index b0f59790e..d1f9c0539 100644 --- a/extensions/amavis_quarantine/templates/viewmail.html +++ b/extensions/amavis_quarantine/templates/viewmail.html @@ -1,11 +1,11 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% load i18n %} {% block html_head %} - + + + {% endblock %} {% block middletitle %}{% trans "Mail content" %}{% endblock %} {% block menu %} diff --git a/extensions/amavis_quarantine/urls.py b/extensions/amavis_quarantine/urls.py index 515bd274a..06806aa1f 100644 --- a/extensions/amavis_quarantine/urls.py +++ b/extensions/amavis_quarantine/urls.py @@ -2,7 +2,9 @@ from django.conf.urls.defaults import * urlpatterns = patterns('mailng.extensions.amavis_quarantine.main', (r'^$', 'index'), - (r'^viewmail/(?P[\w\-\+]+)/$', 'viewmail'), + url(r'^viewmail/(?P[\w\-\+]+)/$', 'viewmail', + name="viewmail_std"), + url(r'^viewmail/$', 'viewmail', name="viewmail_null"), (r'^getmailcontent/(?P[\w\-\+]+)/$', 'getmailcontent'), (r'^viewheaders/(?P[\w\-\+]+)/$', 'viewheaders'), (r'^process/$', 'process'), diff --git a/extensions/webmail/__init__.py b/extensions/webmail/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/webmail/imap_listing.py b/extensions/webmail/imap_listing.py new file mode 100644 index 000000000..b6ff4ad19 --- /dev/null +++ b/extensions/webmail/imap_listing.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +import re +import time +from datetime import datetime, timedelta +from email.header import decode_header +import email.utils +from django.utils.translation import ugettext as _ +from mailng.lib import decode, tables, imap_utf7 +from mailng.lib.email_listing import MBconnector, EmailListing +from mailng.lib import tables, imap_utf7, parameters + +class WMtable(tables.Table): + idkey = "imapid" + selection = tables.SelectionColumn("selection", first=True) + subject = tables.Column("subject", label=_("Subject")) + from_ = tables.Column("from", label=_("From")) + date = tables.Column("date", label=_("Date")) + from_exp = re.compile("([^<]+)<([^>]+)>") + name_exp = re.compile("\"(.+)\"") + + def parse_from(self, value): + m = self.from_exp.match(value) + if m: + name = m.group(1) + name = name.strip() + m2 = self.name_exp.match(name) + if m2: + return m2.group(1) + return m.group(1) + return value + + def parse_date(self, value): + ndate = datetime(*(email.utils.parsedate_tz(value))[:7]) + now = datetime.now() + if now - ndate > timedelta(7): + return ndate.strftime("%d.%m.%Y %H:%M") + return ndate.strftime("%a %H:%M") + + def parse_subject(self, value): + res = "" + dcd = decode_header(value) + for part in dcd: + if res != "": + res += " " + res += part[0].strip(" ") + return decode(res) + + +class IMAPconnector(MBconnector): + def login(self, user, passwd): + import imaplib + + try: + self.m = imaplib.IMAP4_SSL(self.address) + self.m.login(user, passwd) + except imaplib.IMAP4.error, test: + return False, test + return True, None + + def _encodefolder(self, folder): + if not folder: + return "INBOX" + return folder.encode("imap4-utf-7") + + def messages_count(self, folder=None): + (status, data) = self.m.select(self._encodefolder(folder)) + return int(data[0]) + + def listfolders(self, topfolder='INBOX', md_folders=[]): + (status, data) = self.m.list() + result = [] + for mb in data: + attrs, truc, name = mb.split() + name = name.strip('"').decode("imap4-utf-7") + present = False + for mdf in md_folders: + if mdf["name"] == name: + present = True + break + if not present: + result += [name] + return sorted(result) + + def fetch(self, start=None, stop=None, folder=None): + if not start or not stop: + return [] + result = [] + self.m.select(self._encodefolder(folder)) + range = "%d:%d" % (start, stop) + typ, data = self.m.fetch(range, '(BODY[HEADER.FIELDS (DATE FROM SUBJECT)])') + imapid = start + for response_part in data: + if isinstance(response_part, tuple): + msg = email.message_from_string(response_part[1]) + msg["imapid"] = imapid + result += [msg] + imapid += 1 + return result + +class ImapListing(EmailListing): + tpl = "webmail/index.html" + + def __init__(self, user, password, **kwargs): + self.mbc = IMAPconnector(parameters.get("webmail", "SERVER_ADDRESS"), 143) + status, text = self.mbc.login(user, password) + if not status: + print "Login error: %s" % text + EmailListing.__init__(self, **kwargs) + + def getfolders(self): + md_folders = [{"name" : "INBOX", "icon" : "overview.png"}, + {"name" : 'Drafts'}, + {"name" : 'Junk'}, + {"name" : 'Sent'}, + {"name" : 'Trash', "icon" : "trash.png"}] + folders = self.mbc.listfolders(md_folders=md_folders) + for fd in folders: + md_folders += [{"name" : fd}] + return md_folders + + def fetch(self, id_start, id_stop): + return WMtable(self.mbc.fetch(start=id_start, stop=id_stop, + folder=self.folder)) diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py new file mode 100644 index 000000000..684587404 --- /dev/null +++ b/extensions/webmail/main.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +import time +from django.http import HttpResponse +from django.utils import simplejson +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from django.conf.urls.defaults import include +from django.contrib.auth.decorators import login_required +from mailng.admin.models import Mailbox +from mailng.lib import events, parameters +from mailng.lib import _render +from imap_listing import * + +def init(): + events.register("UserMenuDisplay", menu) + parameters.register("webmail", "SERVER_ADDRESS", "string", "127.0.0.1", + help=_("Mailboxes server address")) + +def urls(): + return (r'^mailng/webmail/', + include('mailng.extensions.webmail.urls')) + +def menu(**kwargs): + if kwargs["target"] != "user_menu_box": + return [] + return [ + {"name" : _("Webmail"), + "url" : reverse(index), + "img" : "/static/pics/webmail.png"} + ] + +@login_required +def index(request): + start = time.time() + lst = ImapListing(request.user.username, request.session["password"]) + try: + pageid = request.GET["page"] + except KeyError: + pageid = 1 + return lst.render(request, pageid=int(pageid), start=start) + +@login_required +def folder(request, folder): + lst = ImapListing(folder=folder) + try: + pageid = request.GET["page"] + except KeyError: + pageid = 1 + page = lst.paginator.getpage(pageid) + if page: + content = lst.fetch(page.id_start, page.id_stop).render(request) + navbar = lst.render_navbar(page) + else: + content = "Empty folder" + navbar = "" + ctx = {"status" : "ok", "listing" : content, "navbar" : navbar} + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") diff --git a/extensions/webmail/templates/index.html b/extensions/webmail/templates/index.html new file mode 100644 index 000000000..af4971081 --- /dev/null +++ b/extensions/webmail/templates/index.html @@ -0,0 +1,85 @@ +{% extends "common/email_listing.html" %} +{% load i18n %} +{% block css %} +#leftcol { + float: left; + width: 150px; +} + +.box h3 { + font-size: small; + margin: 2px 0px 2px 0px; + text-align: center; +} + +.box ul { + list-style: none; + padding: 5px 5px; + margin: 0; +} + +.box li { + clear: both; + margin-bottom: 4px; +} + +.box li.selected { + background: #ffffcc; +} + +.box a:link, .box a:visited { + background: transparent; + color: black; + font-weight: bold; + text-decoration: none; +} + +.box a:hover { + color: #6AB0C5; + text-decoration: none; +} + +.box img { + float: left; + margin-right: 2px; + border: 0; +} +{% endblock %} +{% block js %} + $$("a[name=loadfolder]").addEvent("click", function(event) { + event.stop(); + $$("a[name=loadfolder]").each(function(item) { + item.setStyle("background", "white"); + }); + event.target.setStyle("background", "#FFFFCC"); + new Request.JSON({url: event.target.get("href"), onSuccess: function(resp) { + $("listing").set("html", resp.listing); + $("navbar").set("html", resp.navbar); + }}).get(); + }); +{% endblock %} +{% block middletitle %}{% trans "Webmail" %}{% endblock %} +{% block menu %} + +{% endblock %} +{% block leftcol %} +
+

{% trans "Folders" %}

+ +
+{% endblock %} diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py new file mode 100644 index 000000000..c61787ee9 --- /dev/null +++ b/extensions/webmail/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('mailng.extensions.webmail.main', + (r'^$', "index"), + (r'^(?P.+)/$', "folder"), + ) diff --git a/lib/__init__.py b/lib/__init__.py index ff9fab9b6..25b629862 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -63,7 +63,7 @@ def _ctx_ok(url): def _ctx_ko(tpl, ctx): return {"status" : "ko", "content" : render_to_string(tpl, ctx)} -def decode(s, encodings=('ascii', 'utf8', 'latin1')): +def decode(s, encodings=('utf8', 'latin1', 'windows-1252', 'ascii')): for encoding in encodings: try: return s.decode(encoding) diff --git a/lib/email_listing.py b/lib/email_listing.py new file mode 100644 index 000000000..e56867631 --- /dev/null +++ b/lib/email_listing.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +import time +from django.template.loader import render_to_string +from mailng.lib import _render + +class MBconnector(object): + def __init__(self, address, port): + self.address = address + self.port = port + +class Page(object): + def __init__(self, pageid, id_start, id_stop, items, + items_per_page, has_previous, has_next): + self.pageid = pageid + self.id_start = id_start + self.id_stop = id_stop + self.items = items + self.items_per_page = items_per_page + self.has_previous = has_previous + self.has_next = has_next + + def previous_page(self): + if not self.has_previous: + return False + return self.pageid - 1 + + def next_page(self): + if not self.has_next: + return False + return self.pageid + 1 + + def last_page(self): + lid = self.items / self.items_per_page + if not lid: + return 1 + if self.items % self.items_per_page: + lid += 1 + return lid + +class Paginator(object): + def __init__(self, total, elems_per_page): + self.total = total + self.elems_per_page = elems_per_page + + def _indexes(self, page): + id_start = self.elems_per_page * page + 1 + id_stop = id_start + self.elems_per_page - 1 + return (id_start, id_stop) + + def getpage(self, page): + if page < 1: + return None + id_start, id_stop = self._indexes(page - 1) + has_previous = has_next = False + if id_start < self.total: + if page > 1: + has_previous = True + if id_stop < self.total: + has_next = True + else: + id_stop = self.total + else: + return None + return Page(page, id_start, id_stop, self.total, + self.elems_per_page, has_previous, has_next) + +class EmailListing: + def __init__(self, folder=None, elems_per_page=40, **kwargs): + self.folder = folder + self.elems_per_page = elems_per_page + self.paginator = Paginator(self.mbc.messages_count(self.folder), + elems_per_page) + + def render_navbar(self, page): + return render_to_string("common/navbar.html", { + "page" : page + }) + + def getfolders(self): + return None + + def render(self, request, pageid=1, **kwargs): + page = self.paginator.getpage(pageid) + if not page: + listing = "Empty folder" + else: + listing = self.fetch(page.id_start, page.id_stop).render(request) + elapsed = kwargs.has_key("start") and time.time() - kwargs["start"] or 0 + return _render(request, self.tpl, { + "listing" : listing, "elapsed" : elapsed, + "navbar" : self.render_navbar(page), + "folders" : self.getfolders(), + "selection" : self.folder, + "current_page" : pageid + }) diff --git a/lib/imap_utf7.py b/lib/imap_utf7.py new file mode 100644 index 000000000..516a45d7f --- /dev/null +++ b/lib/imap_utf7.py @@ -0,0 +1,210 @@ +# -*- coding: latin-1 -*- + +""" +Imap folder names are encoded using a special version of utf-7 as defined in RFC +2060 section 5.1.3. + +5.1.3. Mailbox International Naming Convention + + By convention, international mailbox names are specified using a + modified version of the UTF-7 encoding described in [UTF-7]. The + purpose of these modifications is to correct the following problems + with UTF-7: + + 1) UTF-7 uses the "+" character for shifting; this conflicts with + the common use of "+" in mailbox names, in particular USENET + newsgroup names. + + 2) UTF-7's encoding is BASE64 which uses the "/" character; this + conflicts with the use of "/" as a popular hierarchy delimiter. + + 3) UTF-7 prohibits the unencoded usage of "\"; this conflicts with + the use of "\" as a popular hierarchy delimiter. + + 4) UTF-7 prohibits the unencoded usage of "~"; this conflicts with + the use of "~" in some servers as a home directory indicator. + + 5) UTF-7 permits multiple alternate forms to represent the same + string; in particular, printable US-ASCII chararacters can be + represented in encoded form. + + In modified UTF-7, printable US-ASCII characters except for "&" + represent themselves; that is, characters with octet values 0x20-0x25 + and 0x27-0x7e. The character "&" (0x26) is represented by the two- + octet sequence "&-". + + All other characters (octet values 0x00-0x1f, 0x7f-0xff, and all + Unicode 16-bit octets) are represented in modified BASE64, with a + further modification from [UTF-7] that "," is used instead of "/". + Modified BASE64 MUST NOT be used to represent any printing US-ASCII + character which can represent itself. + + "&" is used to shift to modified BASE64 and "-" to shift back to US- + ASCII. All names start in US-ASCII, and MUST end in US-ASCII (that + is, a name that ends with a Unicode 16-bit octet MUST end with a "- + "). + + For example, here is a mailbox name which mixes English, Japanese, + and Chinese text: ~peter/mail/&ZeVnLIqe-/&U,BTFw- + +Found here: +http://svn.plone.org/svn/collective/mxmImapClient/trunk/imapUTF7.py + +""" +import binascii +import codecs + +# encoding + +def modified_base64(s): + s = s.encode('utf-16be') + return binascii.b2a_base64(s).rstrip('\n=').replace('/', ',') + +def doB64(_in, r): + if _in: + r.append('&%s-' % modified_base64(''.join(_in))) + del _in[:] + +def encoder(s): + r = [] + _in = [] + for c in s: + ordC = ord(c) + if 0x20 <= ordC <= 0x25 or 0x27 <= ordC <= 0x7e: + doB64(_in, r) + r.append(c) + elif c == '&': + doB64(_in, r) + r.append('&-') + else: + _in.append(c) + doB64(_in, r) + return (str(''.join(r)), len(s)) + + +# decoding + +def modified_unbase64(s): + b = binascii.a2b_base64(s.replace(',', '/') + '===') + return unicode(b, 'utf-16be') + + +def decoder(s): + r = [] + decode = [] + for c in s: + if c == '&' and not decode: + decode.append('&') + elif c == '-' and decode: + if len(decode) == 1: + r.append('&') + else: + r.append(modified_unbase64(''.join(decode[1:]))) + decode = [] + elif decode: + decode.append(c) + else: + r.append(c) + if decode: + r.append(modified_unbase64(''.join(decode[1:]))) + bin_str = ''.join(r) + return (bin_str, len(s)) + + +class StreamReader(codecs.StreamReader): + def decode(self, s, errors='strict'): + return decoder(s) + + +class StreamWriter(codecs.StreamWriter): + def decode(self, s, errors='strict'): + return encoder(s) + + +def imap4_utf_7(name): + if name == 'imap4-utf-7': + return (encoder, decoder, StreamReader, StreamWriter) +codecs.register(imap4_utf_7) + + +## testing methods + +def imapUTF7Encode(ust): + "Returns imap utf-7 encoded version of string" + return ust.encode('imap4-utf-7') + +def imapUTF7EncodeSequence(seq): + "Returns imap utf-7 encoded version of strings in sequence" + return [imapUTF7Encode(itm) for itm in seq] + + +def imapUTF7Decode(st): + "Returns utf7 encoded version of imap utf-7 string" + return st.decode('imap4-utf-7') + +def imapUTF7DecodeSequence(seq): + "Returns utf7 encoded version of imap utf-7 strings in sequence" + return [imapUTF7Decode(itm) for itm in seq] + + +def utf8Decode(st): + "Returns utf7 encoded version of imap utf-7 string" + return st.decode('utf-8') + + +def utf7SequenceToUTF8(seq): + "Returns utf7 encoded version of imap utf-7 strings in sequence" + return [itm.decode('imap4-utf-7').encode('utf-8') for itm in seq] + + +__all__ = ['imapUTF7Encode', 'imapUTF7Decode', ] + +if __name__=='__main__': + +## print u'bøx'.encode('imap4-utf-7') +## print 'expected b&APg-x' +## +## print u'båx'.encode('imap4-utf-7') +## print 'expected b&AOU-x' +## + print '#######' + print 'bøx' + e = imapUTF7Encode(u'bøx') + print e + print imapUTF7Decode(e).encode('latin-1') + + print '#######' + print 'båx' + e = imapUTF7Encode(u'båx') + print e + print imapUTF7Decode(e).encode('latin-1') + + print '#######' + print '~/bågø' + e = imapUTF7Encode(u'~/bågø') + print e + print imapUTF7Decode(e).encode('latin-1') + + print '#######' + print 'Ting & Såger' + e = imapUTF7Encode(u'Ting & Såger') + print e + print imapUTF7Decode(e).encode('latin-1') + +## e = imapUTF7Decode('b&AOU-x') +## print e.encode('latin-1') +## +## e = imapUTF7Decode('b&APg-x') +## print e.encode('latin-1') +## + print '#######' + print '~/Følder/mailbåx & stuff + more' + n = u'~/Følder/mailbåx & stuff + more' + e = imapUTF7Encode(n) + print e + print imapUTF7Decode(e).encode('latin-1') + + print '#######' + print '~peter/mail/&ZeVnLIqe-/&U,BTFw-' + print imapUTF7Decode('~peter/mail/&ZeVnLIqe-/&U,BTFw-').encode('utf-8') + diff --git a/lib/parameters.py b/lib/parameters.py index dfaf50f27..e7b1e484f 100644 --- a/lib/parameters.py +++ b/lib/parameters.py @@ -24,14 +24,14 @@ class NotDefined(Exception): return "Application '%s' and/or parameter '%s' not defined" % (self.app, self.name) -def register(app, name, type="string", default=None, help=None): +def register(app, name, type="string", deflt=None, help=None): """Register a new parameter. app corresponds to a core component (admin, main) or an extension. """ if not app in _params.keys(): _params[app] = {} - _params[app][name] = {"type" : type, "default" : default, "help" : help} + _params[app][name] = {"type" : type, "default" : deflt, "help" : help} def save(app, name, value): if not app in _params.keys() or not name in _params[app].keys(): @@ -53,5 +53,5 @@ def get(app, name): try: p = Parameter.objects.get(name="%s.%s" % (app, name)) except Parameter.DoesNotExist: - return _params[app][name]["defaut"] + return _params[app][name]["default"] return p.value diff --git a/lib/tables.py b/lib/tables.py new file mode 100644 index 000000000..a83b806a3 --- /dev/null +++ b/lib/tables.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +import inspect +import lxml +from django.template.loader import render_to_string + +class Column: + def __init__(self, name, **kwargs): + self.name = name + for k, v in kwargs.iteritems(): + setattr(self, k, v) + + def __str__(self): + try: + label = getattr(self, "label") + except KeyError: + label = getattr(self, "name") + return label + +class SelectionColumn(Column): + def __str__(self): + return "" + + def render(self, value): + return "" \ + % (self.name, value) + +class Table(object): + def __init__(self, rows={}): + self.columns = [] + for m in inspect.getmembers(self): + if isinstance(m[1], Column): + try: + if getattr(m[1], "first"): + self.columns = [m[1]] + self.columns + except AttributeError: + self.columns += [m[1]] + self.rows = [] + for row in rows: + nrow = {"id" : row[self.idkey], "cols" : []} + for c in self.columns: + if isinstance(c, SelectionColumn): + nrow["cols"] += [{"value" : c.render(row[self.idkey]), "safe" : True}] + continue + if row.has_key(c.name): + try: + nrow["cols"] += [{"value" : getattr(self, "parse_%s" % c.name)(row[c.name])}] + except AttributeError: + nrow["cols"] += [{"value" : row[c.name]}] + self.rows += [nrow] + + def render(self, request): + return render_to_string("common/tables.html", { + "table" : self + }) + + diff --git a/main/forms.py b/main/forms.py index f267eb72e..e038c46e6 100644 --- a/main/forms.py +++ b/main/forms.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext as _ class ARmessageForm(forms.ModelForm): class Meta: model = ARmessage - fields = ('subject', 'content', 'enabled') + fields = ('subject', 'content', 'enabled', 'untildate') def __init__(self, *args, **kwargs): super(ARmessageForm, self).__init__(*args, **kwargs) diff --git a/main/models.py b/main/models.py index 4f4348057..a3e5329f0 100644 --- a/main/models.py +++ b/main/models.py @@ -7,6 +7,7 @@ class ARmessage(models.Model): subject = models.CharField(_('subject'), max_length=255) content = models.TextField(_('content')) enabled = models.BooleanField(_('enabled')) +# untildate = models.DateTimeField(_('until')) class ARhistoric(models.Model): armessage = models.ForeignKey(ARmessage) diff --git a/static/js/common.js b/static/js/common.js index 4f1e387e4..17bcf706a 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -1,6 +1,14 @@ window.addEvent('domready', function(){ - if ($('menubar')) { - $('menubar').getElements('li.dropdown').each( function( elem ){ + parse_menubar('topmenubar'); + parse_menubar('menubar'); + SqueezeBox.assign($$('a.boxed'), { + parse: 'rel', + }); +}); + +parse_menubar = function(id) { + if ($(id)) { + $(id).getElements('li.dropdown').each( function( elem ){ var list = elem.getElement('ul.links'); var myFx = new Fx.Slide(list).hide(); elem.addEvents({ @@ -15,10 +23,7 @@ window.addEvent('domready', function(){ }); }); } - SqueezeBox.assign($$('a.boxed'), { - parse: 'rel', - }); -}); +} get_iframe_body = function(id) { var saf = navigator.userAgent.match(/Safari/i); @@ -65,3 +70,38 @@ confirmation = function(question, action, callback) { }); SqueezeBox.open("/mailng/main/confirm/?question=" + question); } + +setContentSize = function(cellsize) { + var contentsize = $(document.body).getSize().y - $("topmenubar").getSize().y + - $("header").getSize().y; + + contentsize -= $("navbar").getSize().y * 2; + contentsize -= contentsize % cellsize; + $("listing").setStyle("height", contentsize + "px"); +} + +bindRows = function(rooturl, page) { + $$("tr").addEvent("click", function(event) { + if (event.target.parentNode.id) { + var url = rooturl + event.target.parentNode.id + "?page=" + page; + event.target.parentNode.setStyle("background", "#ffffcc"); + location.href = url; + } + }); +} + +toggleSelection = function(id, value) { + $$("input[name=" + id + "]").each(function(obj) { + obj.checked = value; + }); +} + +checkSelection = function(id, text) { + var getout = $$("input[name=" + id + "]").every(function(item, index) { + return item.checked == false; + }); + if (getout) { + return false; + } + return confirm(text); +} \ No newline at end of file diff --git a/static/js/domains.js b/static/js/domains.js index e59eafc99..286515bac 100644 --- a/static/js/domains.js +++ b/static/js/domains.js @@ -40,7 +40,7 @@ window.addEvent('domready', function() { }); - $('body').addEvent('click', function(event) { + $(document.body).addEvent('click', function(event) { target = event.target; if (target.get('tag') == 'a') { name = target.get('name'); @@ -54,7 +54,7 @@ window.addEvent('domready', function() { } }); - $('body').addEvent('submit', function(event) { + $(document.body).addEvent('submit', function(event) { target = event.target; if (target.get('tag') == 'form') { name = target.get('name'); diff --git a/static/js/main.js b/static/js/main.js index c89c7f9ba..97c3c0340 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -2,4 +2,9 @@ window.addEvent('domready', function() { SqueezeBox.assign($$('a.boxed'), { parse: 'rel' }); + + new DatePicker("#untildate", { + pickerClass: "datepicker_vista", + allowEmpty: true + }); }); diff --git a/static/js/mootools-more.js b/static/js/mootools-more.js index 2337e8bba..6dce4406c 100644 --- a/static/js/mootools-more.js +++ b/static/js/mootools-more.js @@ -182,6 +182,101 @@ Element.implement({ }); +/* +--- + +script: Scroller.js + +description: Class which scrolls the contents of any Element (including the window) when the mouse reaches the Element's boundaries. + +license: MIT-style license + +authors: +- Valerio Proietti + +requires: +- core:1.2.4/Events +- core:1.2.4/Options +- core:1.2.4/Element.Event +- core:1.2.4/Element.Dimensions + +provides: [Scroller] + +... +*/ + +var Scroller = new Class({ + + Implements: [Events, Options], + + options: { + area: 20, + velocity: 1, + onChange: function(x, y){ + this.element.scrollTo(x, y); + }, + fps: 50 + }, + + initialize: function(element, options){ + this.setOptions(options); + this.element = document.id(element); + this.listener = ($type(this.element) != 'element') ? document.id(this.element.getDocument().body) : this.element; + this.timer = null; + this.bound = { + attach: this.attach.bind(this), + detach: this.detach.bind(this), + getCoords: this.getCoords.bind(this) + }; + }, + + start: function(){ + this.listener.addEvents({ + mouseover: this.bound.attach, + mouseout: this.bound.detach + }); + }, + + stop: function(){ + this.listener.removeEvents({ + mouseover: this.bound.attach, + mouseout: this.bound.detach + }); + this.detach(); + this.timer = $clear(this.timer); + }, + + attach: function(){ + this.listener.addEvent('mousemove', this.bound.getCoords); + }, + + detach: function(){ + this.listener.removeEvent('mousemove', this.bound.getCoords); + this.timer = $clear(this.timer); + }, + + getCoords: function(event){ + this.page = (this.listener.get('tag') == 'body') ? event.client : event.page; + if (!this.timer) this.timer = this.scroll.periodical(Math.round(1000 / this.options.fps), this); + }, + + scroll: function(){ + var size = this.element.getSize(), + scroll = this.element.getScroll(), + pos = this.element.getOffsets(), + scrollSize = this.element.getScrollSize(), + change = {x: 0, y: 0}; + for (var z in this.page){ + if (this.page[z] < (this.options.area + pos[z]) && scroll[z] != 0) + change[z] = (this.page[z] - this.options.area - pos[z]) * this.options.velocity; + else if (this.page[z] + this.options.area > (size[z] + pos[z]) && scroll[z] + size[z] != scrollSize[z]) + change[z] = (this.page[z] - size[z] + this.options.area - pos[z]) * this.options.velocity; + } + if (change.y || change.x) this.fireEvent('change', [scroll.x + change.x, scroll.y + change.y]); + } + +}); + /* --- @@ -211,167 +306,159 @@ provides: [Tips] (function(){ var read = function(option, element){ - return (option) ? ($type(option) == 'function' ? option(element) : element.get(option)) : ''; + return (option) ? ($type(option) == 'function' ? option(element) : element.get(option)) : ''; }; this.Tips = new Class({ - Implements: [Events, Options], + Implements: [Events, Options], - options: { - /* - onAttach: $empty(element), - onDetach: $empty(element), - */ - onShow: function(){ - this.tip.setStyle('display', 'block'); - }, - onHide: function(){ - this.tip.setStyle('display', 'none'); - }, - title: 'title', - text: function(element){ - return element.get('rel') || element.get('href'); - }, - showDelay: 100, - hideDelay: 100, - className: 'tip-wrap', - offset: {x: 16, y: 16}, - fixed: false - }, + options: { + /* + onAttach: $empty(element), + onDetach: $empty(element), + */ + onShow: function(){ + this.tip.setStyle('display', 'block'); + }, + onHide: function(){ + this.tip.setStyle('display', 'none'); + }, + title: 'title', + text: function(element){ + return element.get('rel') || element.get('href'); + }, + showDelay: 100, + hideDelay: 100, + className: 'tip-wrap', + offset: {x: 16, y: 16}, + fixed: false + }, - initialize: function(){ - var params = Array.link(arguments, {options: Object.type, elements: $defined}); - this.setOptions(params.options); - document.id(this); - - if (params.elements) this.attach(params.elements); - }, + initialize: function(){ + var params = Array.link(arguments, {options: Object.type, elements: $defined}); + this.setOptions(params.options); + document.id(this); + + if (params.elements) this.attach(params.elements); + }, - toElement: function(){ - if (this.tip) return this.tip; - - this.container = new Element('div', {'class': 'tip'}); - return this.tip = new Element('div', { - 'class': this.options.className, - styles: { - position: 'absolute', - top: 0, - left: 0, - visibility: 'hidden', - display: 'none' - } - }).adopt( - new Element('div', {'class': 'tip-top'}), - this.container, - new Element('div', {'class': 'tip-bottom'}) - ).inject(document.body); - }, - - attach: function(elements){ - $$(elements).each(function(element){ - var title = read(this.options.title, element), - text = read(this.options.text, element); - - element.erase('title').store('tip:native', title).retrieve('tip:title', title); - element.retrieve('tip:text', text); - this.fireEvent('attach', [element]); - - var events = ['enter', 'leave']; - if (!this.options.fixed) events.push('move'); + toElement: function(){ + if (this.tip) return this.tip; + + this.container = new Element('div', {'class': 'tip'}); + return this.tip = new Element('div', { + 'class': this.options.className, + styles: { + position: 'absolute', + top: 0, + left: 0 + } + }).adopt( + new Element('div', {'class': 'tip-top'}), + this.container, + new Element('div', {'class': 'tip-bottom'}) + ).inject(document.body); + }, - events.each(function(value){ - var event = element.retrieve('tip:' + value); - if (!event) event = this['element' + value.capitalize()].bindWithEvent(this, element); - element.store('tip:' + value, event).addEvent('mouse' + value, event); - }, this); - - element.addEvent('mouseout', this.hide.bind(this, element)); // firefox doesn't handle mouseleave correct - - }, this); - - return this; - }, - - detach: function(elements){ - $$(elements).each(function(element){ - ['enter', 'leave', 'move'].each(function(value){ - element.removeEvent('mouse' + value, element.retrieve('tip:' + value)).eliminate('tip:' + value); - }); - - this.fireEvent('detach', [element]); - - if (this.options.title == 'title'){ // This is necessary to check if we can revert the title - var original = element.retrieve('tip:native'); - if (original) element.set('title', original); - } - }, this); - - return this; - }, - - elementEnter: function(event, element){ - this.container.empty(); - - ['title', 'text'].each(function(value){ - var content = element.retrieve('tip:' + value); - if (content) this.fill(new Element('div', {'class': 'tip-' + value}).inject(this.container), content); - }, this); - - $clear(this.timer); - this.timer = this.show.delay(this.options.showDelay, this, [element, event]); - }, - - elementLeave: function(event, element){ - this.tip.setStyle('visibility', 'hidden'); - $clear(this.timer); - this.timer = this.hide.delay(this.options.hideDelay, this, element); - this.fireForParent(event, element); - }, - - fireForParent: function(event, element){ - var element = $(element); - if (!element) return; - parentNode = element.getParent(); - if (parentNode == document.body) return; - if (parentNode.retrieve('tip:enter')) parentNode.fireEvent('mouseenter', event); - else this.fireForParent(event, parentNode); - }, - - elementMove: function(event, element) { - this.position(event); - }, + attach: function(elements){ + $$(elements).each(function(element){ + var title = read(this.options.title, element), + text = read(this.options.text, element); + + element.erase('title').store('tip:native', title).retrieve('tip:title', title); + element.retrieve('tip:text', text); + this.fireEvent('attach', [element]); + + var events = ['enter', 'leave']; + if (!this.options.fixed) events.push('move'); + + events.each(function(value){ + var event = element.retrieve('tip:' + value); + if (!event) event = this['element' + value.capitalize()].bindWithEvent(this, element); + + element.store('tip:' + value, event).addEvent('mouse' + value, event); + }, this); + }, this); + + return this; + }, + + detach: function(elements){ + $$(elements).each(function(element){ + ['enter', 'leave', 'move'].each(function(value){ + element.removeEvent('mouse' + value, element.retrieve('tip:' + value)).eliminate('tip:' + value); + }); + + this.fireEvent('detach', [element]); + + if (this.options.title == 'title'){ // This is necessary to check if we can revert the title + var original = element.retrieve('tip:native'); + if (original) element.set('title', original); + } + }, this); + + return this; + }, + + elementEnter: function(event, element){ + this.container.empty(); + + ['title', 'text'].each(function(value){ + var content = element.retrieve('tip:' + value); + if (content) this.fill(new Element('div', {'class': 'tip-' + value}).inject(this.container), content); + }, this); + + $clear(this.timer); + this.timer = this.show.delay(this.options.showDelay, this, element); + this.position((this.options.fixed) ? {page: element.getPosition()} : event); + }, + + elementLeave: function(event, element){ + $clear(this.timer); + this.timer = this.hide.delay(this.options.hideDelay, this, element); + this.fireForParent(event, element); + }, + + fireForParent: function(event, element){ + if (!element) return; + parentNode = element.getParent(); + if (parentNode == document.body) return; + if (parentNode.retrieve('tip:enter')) parentNode.fireEvent('mouseenter', event); + else this.fireForParent(parentNode, event); + }, + + elementMove: function(event, element){ + this.position(event); + }, + + position: function(event){ + var size = window.getSize(), scroll = window.getScroll(), + tip = {x: this.tip.offsetWidth, y: this.tip.offsetHeight}, + props = {x: 'left', y: 'top'}, + obj = {}; + + for (var z in props){ + obj[props[z]] = event.page[z] + this.options.offset[z]; + if ((obj[props[z]] + tip[z] - scroll[z]) > size[z]) obj[props[z]] = event.page[z] - this.options.offset[z] - tip[z]; + } + + this.tip.setStyles(obj); + }, + + fill: function(element, contents){ + if(typeof contents == 'string') element.set('html', contents); + else element.adopt(contents); + }, + + show: function(element){ + this.fireEvent('show', [this.tip, element]); + }, + + hide: function(element){ + this.fireEvent('hide', [this.tip, element]); + } - position: function(event){ - if (this.tip.getStyle('display') != 'block') return; - var size = window.getSize(), scroll = window.getScroll(), - tip = {x: this.tip.offsetWidth, y: this.tip.offsetHeight}, - props = {x: 'left', y: 'top'}, - obj = {}; - - for (var z in props){ - obj[props[z]] = event.page[z] + this.options.offset[z]; - if ((obj[props[z]] + tip[z] - scroll[z]) > size[z]) obj[props[z]] = event.page[z] - this.options.offset[z] - tip[z]; - } - - this.tip.setStyles(obj); - this.tip.setStyle('visibility', 'visible'); - }, - - fill: function(element, contents){ - if(typeof contents == 'string') element.set('html', contents); - else element.adopt(contents); - }, - - show: function(element, event) { - this.fireEvent('show', [this.tip, element]); - this.position((this.options.fixed) ? {page: element.getPosition()} : event); - }, - - hide: function(element){ - this.fireEvent('hide', [this.tip, element]); - } - }); })(); \ No newline at end of file diff --git a/static/pics/folder.png b/static/pics/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..901edc9624a23d92a9fcbc0c9c450bf37409da18 GIT binary patch literal 498 zcmVT` z=fK?B+ueUcgw&mTx8?fP8#5oZ_IS4UlwPlQrCO;yj|7yQi?lwqqD*+(cr$(Vs9LE# z0Eh#m(;1HB@{s`NLMd=l3Z)da7uEYf05Hbjc`mO%_Gxx9%%#H)XKp%DNa@dEf@UYf zg64HNtq;9mzz z0TxYQt&biCWrx3zi1`R0A}FO05k$Zj!CGNxeauK0j4VSdL!1G*==LU59h96xI zMByWh2N4hikuew;S(yNSb=sUhdyWrZ|BROccMMviB*Wb(P{I#8i on>e<%XnEP;xA*VLo2x9V1Hy;2%BQg6_y7O^07*qoM6N<$g5ayrIRF3v literal 0 HcmV?d00001 diff --git a/static/pics/overview.png b/static/pics/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..13d2c00d08a1a278e02348d3a66d11747e015892 GIT binary patch literal 528 zcmV+r0`L8aP)Ty^tSaylA~ zKBEC^ZTT$N2TO~u76?E+dh$@l?%Zp7X^iIO(hKtW{O#0A`gNcnnYh8$*R7x-eAsy3 zSbCmXNlyTT0K(x2U7g*5g2eS?d74rvrAWU`Jq5}FYb{!Pyvy#hnTv2D&Lp^eKOsHs z=36zvW-h|qnN^$Q^YtvvvK?HRUr{?(5ktW#HrmuE%R?a1=f7OggNl*6SO%c z1Q3y93#*$Li{tJY^14X%BWdk%wzI(QzQx_uAPNB(Ie>nsV%D0ZT S3$t$k00000@?kEP)R4zO-SE_shK7L#K^bOYL`2|4i#%^^+d&Zo zk&sSy=pcw4>ZZxwY}*bA=G8Pha*v|J@tT}?%W zfngW`WV2bOr`~d}ruX2F47l3KjU|BXjcOQ7B-6|9cn5Kyc>qH_^Pxk?Y5HvP6 z@NQ%F=Lf9UupKV(UBp_bq(9L5mb75XrJ44nVXyC#?3BIa!Q*#eDs))^YbWW z`NsDFJ}oTJ+SbPAhK=hul>Tj?q2ZJq>dv3z`|8@kUyxGLkZ9U&=waVr86b=ZH79G4 oQU(Hs$Hqtd{^O2@kK`J+0Q~)|WKOu)r~m)}07*qoM6N<$g3M6(EC2ui literal 0 HcmV?d00001 diff --git a/static/stylesheets/basic.css b/static/stylesheets/basic.css index 6529c8112..58de59d91 100644 --- a/static/stylesheets/basic.css +++ b/static/stylesheets/basic.css @@ -2,6 +2,7 @@ body { font-family: arial, helvetica, verdana, tahoma, sans-serif; font-size: small; background: #153F65; + margin: 0; } #first-container { @@ -342,3 +343,13 @@ body { margin: 20px; font-weight: bold; } + +/* nouveau style */ +#topmenubar { + width: 100%; + margin: 0 auto; + padding: 0; + background: #072342; + border-bottom: 2px solid #333; + height: 30px; +} diff --git a/static/stylesheets/default.css b/static/stylesheets/default.css new file mode 100644 index 000000000..84ef58b6c --- /dev/null +++ b/static/stylesheets/default.css @@ -0,0 +1,254 @@ +html, body { + font-family: arial, helvetica, verdana, tahoma, sans-serif; + font-size: small; + background: #153F65; + margin: 0; + padding: 0; + +} + +#topmenubar { + width: 100%; + margin: 0 auto; + padding: 0; + background: #072342; + border-bottom: 2px solid #333; + height: 30px; +} + +#menubar { + margin: 5px; + display: table; + width: 700px; + margin-top: 35px; + /*position: absolute;*/ +} + +#topmenubar a, #topmenubar a:visited { + color: white; + font-weight: bold; +} + +#topmenubar label { + color: white; + font-weight: bold; +} + +#menubar a, #menubar a:visited { + color: black; + font-weight: bold; +} + +#menubar label { + color: black; + font-weight: bold; +} + +#page { + border: 2px solid #333; + background: white; + min-height: 92%; + margin: 0 15px; +} + +#content { + overflow: auto; + padding: 5px; +} + +#header { + width: 100%; + padding: 5px 0; +} + +#header h1 { + font-size: medium; + width: 50%; + margin: 0 0 0 5px; +} + +#loginbox { + margin: 100px auto; + width: 610px; + padding-bottom: 15px; + border: 2px solid #333; + background: white; +} + +#loginbox h2 { + margin: 30px 10px 0 10px; + font-size: small; +} + +#loginbox h3 { + font-size: small; + text-align: center; +} + +#logo { + float: left; + text-align: center; + width: 300px; + margin-top: 10px; + border-right: 1px solid #ccc; +} + +#logo img { + height: 90px; +} + +#test { + float: right; + width: 300px; +} + +.menubar a, .menubar a:visited { + text-decoration: none; +} + +.menubar a:hover { + text-decoration: underline; +} + +.menubar ul { + margin: 0; + padding: 0; + list-style: none; + width: 100%; + color: #2a4069; +} + +.menubar li { + float: left; + display: inline; + margin: 5px 5px; +} + +.menubar li.selected { + background: #ffff77; +} + +.menubar img { + float: left; + margin: 0px 3px; + border: 0; +} + +.menubar .title { + color: white; +} + +.left { + text-align: left; +} + +.right { + float: right; +} + +.logo { + float: right; + /*margin: 5px 0 0 5px;*/ + height: 90px; +} + +.dropdown { + position: absolute; +} + +.dropdown ul { + display: block; + background: #ffffcc; + width: 150px; + height: 90px; + margin: 0px; + padding: 0px; + padding-top: 10px; +} + +.dropdown a, .dropdown a:visited { + color: black !important; +} + +.spacer { + clear: both; +} + +/* Mootools tooltips */ +.tip { + color: #fff; + width: 139px; + z-index: 13000; + border: 1px solid #cccccc; +} + +.tip-title { + font-weight: bold; + font-size: 11px; + margin: 0; + color: #9FD4FF; + padding: 8px 8px 4px; + background: #eee; +} + +.tip-text { + font-size: 11px; + padding: 4px 8px 8px; + background: #eee; + color: black; +} + +table { + margin: 0 auto; + width: 100%; + border-collapse: collapse; + /*border-spacing: 20px;*/ +} + +tr.odd { + background: white; +} + +tr.even { + background: #F4F6F8; +} + +th, td { + height: 20px; +} + +table th { + background: #CEDBEC; + text-align: left; + padding: 5px; +} + +table th.right { + text-align: right; +} + +table td { + border-bottom: 1px solid #C8D6E8; + /*padding: 5px;*/ +} + +input { + border: 1px solid #cccccc; + color: inherit; + /*background-color: #e9e9e9;*/ + background-color: #eee; + padding: 2px; +} + +input:hover { + color: inherit; + background-color: #ffffcc; +} + +.hr td.odd { + background: #F7F9Fb; +} + +.hr td.even { + +} \ No newline at end of file diff --git a/static/stylesheets/login.css b/static/stylesheets/login.css new file mode 100644 index 000000000..11060e5c0 --- /dev/null +++ b/static/stylesheets/login.css @@ -0,0 +1,70 @@ +html, body { + font-family: arial, helvetica, verdana, tahoma, sans-serif; + font-size: small; + background: #153F65; + margin: 0; + padding: 0; +} + +#loginbox { + margin: 100px auto; + width: 610px; + padding-bottom: 15px; + border: 5px solid #333; + background: white; +} + +#loginbox h2 { + margin: 30px 10px 0 10px; + font-size: small; +} + +#loginbox h3 { + font-size: small; + text-align: center; +} + +#logo { + float: left; + text-align: center; + width: 300px; + margin-top: 10px; + border-right: 1px solid #ccc; +} + +#logo img { + height: 90px; +} + +#test { + float: right; + width: 300px; +} + +.spacer { + clear: both; +} + +/* Mootools tooltips */ +.tip { + color: #fff; + width: 139px; + z-index: 13000; + border: 1px solid #cccccc; +} + +.tip-title { + font-weight: bold; + font-size: 11px; + margin: 0; + color: #9FD4FF; + padding: 8px 8px 4px; + background: #eee; +} + +.tip-text { + font-size: 11px; + padding: 4px 8px 8px; + background: #eee; + color: black; +} diff --git a/static/stylesheets/mailng.png b/static/stylesheets/mailng.png new file mode 100644 index 0000000000000000000000000000000000000000..f12629da5d3a6c387567b794b6f74321a696b058 GIT binary patch literal 13264 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 literal 0 HcmV?d00001 diff --git a/templates/admin/addpermission.html b/templates/admin/addpermission.html index dfbbee887..c5108058d 100644 --- a/templates/admin/addpermission.html +++ b/templates/admin/addpermission.html @@ -26,7 +26,7 @@ window.addEvent("domready", function() { {% if error %}
{{ error }}
{% endif %} {% url admin.views.addpermission as addperm_url %}
- {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/domain_header.html b/templates/admin/domain_header.html index 347ebf4ee..d2f8e20b7 100644 --- a/templates/admin/domain_header.html +++ b/templates/admin/domain_header.html @@ -1,4 +1,4 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% load i18n %} {% block html_head %} diff --git a/templates/admin/domains.html b/templates/admin/domains.html index 79c8c7e40..6e7c12414 100644 --- a/templates/admin/domains.html +++ b/templates/admin/domains.html @@ -1,9 +1,17 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% load i18n %} +{% load admin_extras %} {% block html_head %} + {% endblock %} {% block middletitle %}{% trans "Available domains" %}{% endblock %} +{% block menu %}{% domains_menu "domains" perms %}{% endblock %} {% block content %}
{% if not domains %} @@ -12,13 +20,13 @@ - - + + {% for dom in domains %} - + {% url admin.views.mailboxes dom.id as domurl %} - + diff --git a/templates/admin/editalias.html b/templates/admin/editalias.html index 5993301b5..4393fd8fc 100644 --- a/templates/admin/editalias.html +++ b/templates/admin/editalias.html @@ -8,7 +8,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.editalias alias.mbox.domain.id alias.id as edit_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/editdomain.html b/templates/admin/editdomain.html index c7e71c2cf..61666f565 100644 --- a/templates/admin/editdomain.html +++ b/templates/admin/editdomain.html @@ -9,7 +9,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.editdomain domain.id as dom_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/editmailbox.html b/templates/admin/editmailbox.html index 7233ebe1d..6cbf7d6e1 100644 --- a/templates/admin/editmailbox.html +++ b/templates/admin/editmailbox.html @@ -9,7 +9,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.editmailbox mbox.domain_id mbox.id as box_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/newalias.html b/templates/admin/newalias.html index 7062ec0e1..834529f4a 100644 --- a/templates/admin/newalias.html +++ b/templates/admin/newalias.html @@ -8,7 +8,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.newalias mbox.domain_id mbox.id as box_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/newdomain.html b/templates/admin/newdomain.html index 4a06830ae..16ddfef35 100644 --- a/templates/admin/newdomain.html +++ b/templates/admin/newdomain.html @@ -9,7 +9,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.newdomain as dom_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/newmailbox.html b/templates/admin/newmailbox.html index b640b9bf6..446b78585 100644 --- a/templates/admin/newmailbox.html +++ b/templates/admin/newmailbox.html @@ -9,7 +9,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.newmailbox domain.id as box_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/parameters.html b/templates/admin/parameters.html index 9a9c3711b..523fe20cd 100644 --- a/templates/admin/parameters.html +++ b/templates/admin/parameters.html @@ -1,8 +1,4 @@ {% extends "admin/settings_header.html" %} -{% block html_head %} - - -{% endblock %} {% block menu %} {% load admin_extras %} {% settings_menu "parameters" perms %} @@ -11,7 +7,7 @@ {% load i18n %} {% url admin.views.saveparameters as action %} -
{% trans "Actions" %}{% trans "Name" %}{% trans "Mailboxes" %}{% trans "Name" %}{% trans "Mailboxes" %} {% trans "Quota" %} {% trans "Enabled" %}
{% url admin.views.editdomain dom.id as edit_url %} {{ dom.name }}{{ dom.name }} {{ dom.mboxcounter }} {{ dom.quota }} {% trans "MB" %} {{ dom.enabled|yesno:_("yes,no") }}
+
diff --git a/templates/admin/settings_header.html b/templates/admin/settings_header.html index 9748e53fd..814bfa687 100644 --- a/templates/admin/settings_header.html +++ b/templates/admin/settings_header.html @@ -1,4 +1,4 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% load i18n %} {% block html_head %} diff --git a/templates/common/email_listing.html b/templates/common/email_listing.html new file mode 100644 index 000000000..3c0459666 --- /dev/null +++ b/templates/common/email_listing.html @@ -0,0 +1,57 @@ +{% extends "nlayout.html" %} +{% load i18n %} +{% block html_head %} + + +{% endblock %} +{% block content %} +{% block leftcol %}{% endblock %} + + + +
{{ listing }}
+ +
+ +{% endblock %} diff --git a/templates/admin/genericform.html b/templates/common/genericform.html similarity index 87% rename from templates/admin/genericform.html rename to templates/common/genericform.html index 5f993d07c..d07b5af55 100644 --- a/templates/admin/genericform.html +++ b/templates/common/genericform.html @@ -1,7 +1,8 @@ {% for field in form %}
{% if field.help_text %} - {% endif %} + + {% endif %} {{ field.label_tag }} {{ field }} {% ifequal noerrors False %}{{ field.errors }}{% endifequal %}
{% endfor %} diff --git a/templates/common/navbar.html b/templates/common/navbar.html new file mode 100644 index 000000000..9d6ed8b4e --- /dev/null +++ b/templates/common/navbar.html @@ -0,0 +1,19 @@ +{% load i18n %} + + +{% if page.previous_page %} + + + + + + +{% endif %} +{% if page.next_page %} + + + + + + +{% endif %} diff --git a/templates/common/tables.html b/templates/common/tables.html new file mode 100644 index 000000000..3906302ee --- /dev/null +++ b/templates/common/tables.html @@ -0,0 +1,18 @@ +
{% trans "Name" %} {% trans "Value" %}
+ + {% for c in table.columns %} + + {% endfor %} + + {% for row in table.rows %} + + {% for cell in row.cols %} + {% if cell.safe %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} +
{{ c|safe }}
{{ cell.value|safe }}{{ cell.value }}
diff --git a/templates/genericform.html b/templates/genericform.html deleted file mode 100644 index 728f1c91a..000000000 --- a/templates/genericform.html +++ /dev/null @@ -1,5 +0,0 @@ -{% for field in form %} -
- {{ field.label_tag }} {{ field }} {% ifequal noerrors False %}{{ field.errors }}{% endifequal %} -
-{% endfor %} diff --git a/templates/main/autoreply.html b/templates/main/autoreply.html index 512235191..029ff881f 100644 --- a/templates/main/autoreply.html +++ b/templates/main/autoreply.html @@ -1,8 +1,10 @@ {% extends "main/header.html" %} {% load i18n %} {% block html_head %} - + + + {% endblock %} {% block menu %} {% load main_extras %} @@ -10,11 +12,11 @@ {% endblock %} {% block subcontent %}
- {% include "genericform.html" %} + {% include "common/genericform.html" %}
- +
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/main/chpassword.html b/templates/main/chpassword.html index 93aeac38b..47656ab04 100644 --- a/templates/main/chpassword.html +++ b/templates/main/chpassword.html @@ -9,10 +9,10 @@ {% if error %}
{{ error }}
{% endif %} {% url main.views.changepassword as action_url %}
- {% include "genericform.html" %} + {% include "common/genericform.html" %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/main/header.html b/templates/main/header.html index 37d898d43..f915c2319 100644 --- a/templates/main/header.html +++ b/templates/main/header.html @@ -1,7 +1,9 @@ {% extends "layout.html" %} {% load i18n %} {% block html_head %} + + {% endblock %} {% block middletitle %}{% trans "General" %}{% endblock %} {% block content %} diff --git a/templates/main/menu.html b/templates/main/menu.html index 083ae8336..86111face 100644 --- a/templates/main/menu.html +++ b/templates/main/menu.html @@ -4,7 +4,7 @@ {% if entry.menu %}