diff --git a/auth/views.py b/auth/views.py index 75e730a90..52d127346 100755 --- a/auth/views.py +++ b/auth/views.py @@ -4,6 +4,7 @@ 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 _ +from mailng.lib import events import mailng.admin from forms import LoginForm @@ -17,7 +18,10 @@ def dologin(request): if user.is_active: login(request, user) - request.session["password"] = request.POST["password"] + events.raiseEvent("UserLogin", request=request, + username=request.POST["username"], + password=request.POST["password"]) + next = request.POST["next"] if next == "None": next = reverse(mailng.admin.views.domains) diff --git a/extensions/webmail/imap_listing.py b/extensions/webmail/imap_listing.py index c5977d064..2ee488a29 100644 --- a/extensions/webmail/imap_listing.py +++ b/extensions/webmail/imap_listing.py @@ -2,6 +2,7 @@ import re import time +import imaplib from datetime import datetime, timedelta from email.header import decode_header import email.utils @@ -10,34 +11,69 @@ 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("([^<]+)<([^>]+)>") + flags = tables.ImgColumn("flags") + + def parse(self, header, value): + try: + return getattr(IMAPheader, "parse_%s" % header)(value) + except AttributeError: + return value + +class IMAPheader(object): + addr_exp = re.compile("([^<\(]+)<|\(([^>\)]+)>|\)") name_exp = re.compile("\"(.+)\"") - def parse_from(self, value): - m = self.from_exp.match(value) + @staticmethod + def parse_address(value): + m = IMAPheader.addr_exp.match(value) if m: name = m.group(1) name = name.strip() - m2 = self.name_exp.match(name) + m2 = IMAPheader.name_exp.match(name) if m2: - return m2.group(1) - return m.group(1) + return IMAPheader.parse_subject(m2.group(1)) + return IMAPheader.parse_subject(name) return value - def parse_date(self, value): + @staticmethod + def parse_address_list(values): + lst = values.split(",") + result = "" + for addr in lst: + if result != "": + result += ", " + result += IMAPheader.parse_address(addr) + return result + + @staticmethod + def parse_from(value): + return IMAPheader.parse_address(value) + + @staticmethod + def parse_to(value): + return IMAPheader.parse_address_list(value) + + @staticmethod + def parse_cc(value): + return IMAPheader.parse_address_list(value) + + @staticmethod + def parse_date(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): + @staticmethod + def parse_subject(value): res = "" dcd = decode_header(value) for part in dcd: @@ -45,8 +81,7 @@ class WMtable(tables.Table): res += " " res += part[0].strip(" ") return decode(res) - - + class IMAPconnector(MBconnector): def login(self, user, passwd): import imaplib @@ -65,7 +100,9 @@ class IMAPconnector(MBconnector): def messages_count(self, folder=None): (status, data) = self.m.select(self._encodefolder(folder)) - return int(data[0]) + (status, data) = self.m.sort("(REVERSE DATE)", "UTF-8", "(NOT DELETED)") + self.messages = data[0].split() + return len(self.messages) def listfolders(self, topfolder='INBOX', md_folders=[]): (status, data) = self.m.list() @@ -86,34 +123,47 @@ class IMAPconnector(MBconnector): if not start and not stop: return [] result = [] - self.m.select(self._encodefolder(folder)) + self.m.select(self._encodefolder(folder), True) if start and stop: - range = "%d:%d" % (start, stop) + submessages = self.messages[start - 1:stop] + range = ",".join(submessages) else: + submessages = [start] range = start if not all: - query = '(BODY[HEADER.FIELDS (DATE FROM TO SUBJECT)])' + query = '(FLAGS BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])' else: query = '(RFC822)' typ, data = self.m.fetch(range, query) - imapid = int(start) + if not folder: + folder = "INBOX" + tmpdict = {} for response_part in data: if isinstance(response_part, tuple): + imapid = response_part[0].split()[0] + flags = imaplib.ParseFlags(response_part[0]) msg = email.message_from_string(response_part[1]) - msg["imapid"] = imapid - result += [msg] - imapid = imapid + 1 + msg["imapid"] = "%s/%s" % (folder, imapid) + if not "\\Seen" in flags: + msg["class"] = "unseen" + if "\\Answered" in flags: + msg["img_flags"] = "/static/pics/answered.png" + tmpdict[imapid] = msg + for id in submessages: + result += [tmpdict[id]] return result class ImapListing(EmailListing): tpl = "webmail/index.html" - def __init__(self, user, password, **kwargs): + def __init__(self, user, password, baseurl=None, **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) + if baseurl: + self.paginator.baseurl = baseurl def getfolders(self): md_folders = [{"name" : "INBOX", "icon" : "overview.png"}, diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py index cc2dc6381..444d05008 100644 --- a/extensions/webmail/main.py +++ b/extensions/webmail/main.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import time from django.http import HttpResponse +from django.template.loader import render_to_string from django.utils import simplejson from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse @@ -13,6 +14,7 @@ from imap_listing import * def init(): events.register("UserMenuDisplay", menu) + events.register("UserLogin", userlogin) parameters.register("webmail", "SERVER_ADDRESS", "string", "127.0.0.1", help=_("Mailboxes server address")) @@ -25,59 +27,67 @@ def menu(**kwargs): return [] return [ {"name" : _("Webmail"), - "url" : reverse(index), + "url" : reverse(folder), "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) +def userlogin(**kwargs): + kwargs["request"].session["password"] = kwargs["password"] @login_required -def folder(request, folder): +def folder(request, name=None): + if not name: + name = "INBOX" lst = ImapListing(request.user.username, request.session["password"], - 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") + reverse(folder, args=[name]), folder=name) + pageid = request.GET.has_key("page") and int(request.GET["page"]) or 1 + if "mode" in request.GET.keys() and request.GET["mode"] == "ajax": + 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, + "menu" : "", "current_page" : pageid} + if "menu" in request.GET.keys(): + from templatetags.webextras import listing_menu + ctx["menu"] = listing_menu("", request.user.get_all_permissions()) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + return lst.render(request, pageid=int(pageid)) -def fetchmail(request, mail_id, all=False): +def fetchmail(request, folder, mail_id, all=False): mbc = IMAPconnector(parameters.get("webmail", "SERVER_ADDRESS"), 143) mbc.login(request.user.username, request.session["password"]) - res = mbc.fetch(start=mail_id, all=all) + res = mbc.fetch(start=mail_id, folder=folder, all=all) if len(res): return res[0] return None @login_required -def viewmail(request, mail_id): - header = fetchmail(request, mail_id) +def viewmail(request, folder, mail_id=None): + header = fetchmail(request, folder, mail_id) pageid = request.GET.has_key("page") and request.GET["page"] or "1" if header: - return _render(request, "webmail/viewmail.html", { - "header" : header, "pageid" : pageid + from templatetags.webextras import viewm_menu + menu = viewm_menu("", folder, mail_id, pageid, + request.user.get_all_permissions()) + folder, imapid = header["imapid"].split("/") + mailcontent = render_to_string("webmail/viewmail.html", { + "header" : header, "folder" : folder, "imapid" : imapid }) + ctx = {"status" : "ok", "menu" : menu, "mailcontent" : mailcontent} + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") +# return _render(request, "webmail/viewmail.html", { +# "header" : header, "folder" : folder, "pageid" : pageid, +# "menu" : menu +# }) @login_required -def getmailcontent(request, mail_id): +def getmailcontent(request, folder, mail_id): from mailng.lib.email_listing import Email - msg = fetchmail(request, mail_id, True) + msg = fetchmail(request, folder, mail_id, True) email = Email(msg) return email.render(request) diff --git a/extensions/webmail/templates/headers.html b/extensions/webmail/templates/headers.html new file mode 100644 index 000000000..fa570da53 --- /dev/null +++ b/extensions/webmail/templates/headers.html @@ -0,0 +1,5 @@ + + {% for hdr in headers %} + + {% endfor %} +
{{ hdr.name }}{{ hdr.value }}
diff --git a/extensions/webmail/templates/index.html b/extensions/webmail/templates/index.html index eb15bc75d..68c7b95e7 100644 --- a/extensions/webmail/templates/index.html +++ b/extensions/webmail/templates/index.html @@ -46,27 +46,64 @@ } {% endblock %} {% block js %} - $$("a[name=loadfolder]").addEvent("click", function(event) { + {% url extensions.webmail.main.folder as view_url %} + loadMail = function(event) { event.stop(); + if (event.target.parentNode.id) { + var url = "{{ view_url }}" + event.target.parentNode.id + "?page=" + + $("pagenum").value; + event.target.parentNode.setStyle("background", "#ffffcc"); + new Request.JSON({url: url, onSuccess: function(resp) { + $("menubar").set("html", resp.menu); + $("listing").set("html", resp.mailcontent); + $("listing").setStyle("overflow", "hidden"); + $$("a[name=back]").addEvent("click", loadfolder); + setDivHeight("mailcontent", $("table-container").getSize().y, 0); + }}).get(); + } + } + updatelisting = function(resp) { + if (resp.menu != "") { + $("menubar").set("html", resp.menu); + } + $("pagenum").value = resp.current_page; + $("listing").set("html", resp.listing); + $("listing").setStyle("overflow", "auto"); + $$("tr").addEvent("click", loadMail); + $("navbar").set("html", resp.navbar); + $("navbar").getChildren("a").addEvent("click", navbarcallback); + } + navbarcallback = function(event) { + event.stop(); + new Request.JSON({url: event.target.getParent().get("href"), onSuccess: function(resp) { + updatelisting(resp); + }}).get({"mode" : "ajax"}); + } + select = function(obj) { $$("a[name=loadfolder]").each(function(item) { - item.setStyle("background", "white"); + item.getParent().setStyle("background", "white"); }); - event.target.setStyle("background", "#FFFFCC"); + obj.getParent().setStyle("background", "#FFFFCC"); + } + loadfolder = function(event) { + event.stop(); new Request.JSON({url: event.target.get("href"), onSuccess: function(resp) { - $("listing").set("html", resp.listing); - $("navbar").set("html", resp.navbar); - }}).get(); - }); - {% url extensions.webmail.main.viewmail as view_url %} - bindRows("{{ view_url }}", "{{ current_page }}"); + updatelisting(resp); + }}).get({"mode" : "ajax"}); + } + foldercallback = function(event) { + select(event.target); + loadfolder(event); + } + + $$("a[name=loadfolder]").addEvent("click", foldercallback); + $("navbar").getChildren("a").addEvent("click", navbarcallback); + $$("tr").addEvent("click", loadMail); {% endblock %} {% block middletitle %}{% trans "Webmail" %}{% endblock %} {% block menu %} - + {% load webextras %} + {% listing_menu "" perms %} {% endblock %} {% block leftcol %}
diff --git a/extensions/webmail/templates/viewmail.html b/extensions/webmail/templates/viewmail.html index b2cb5e310..97637a322 100644 --- a/extensions/webmail/templates/viewmail.html +++ b/extensions/webmail/templates/viewmail.html @@ -1,10 +1,10 @@ -{% extends "nlayout.html" %} {% load i18n %} -{% block html_head %} +{% load webextras %} - -{% endblock %} -{% block middletitle %}{% trans "Email content" %}{% endblock %} -{% block menu %} - {% load webextras %} - {% viewm_menu "" header.imapid pageid perms %} -{% endblock %} -{% block content %}
- - - - - -
{% trans "Subject" %}{{ header.Subject }}
{% trans "From" %}{{ header.From }}
{% trans "To" %}{{ header.To }}
{% trans "Date" %}{{ header.Date }}
+ {% render_headers header %}
-{% url extensions.webmail.main.getmailcontent header.imapid as geturl %} +{% url extensions.webmail.main.getmailcontent folder imapid as geturl %} -{% endblock %} diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 99bc4cc5c..d948db506 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -3,14 +3,16 @@ from django.template.loader import render_to_string from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from mailng.extensions import webmail +from mailng.extensions.webmail.imap_listing import IMAPheader register = template.Library() @register.simple_tag -def viewm_menu(selection, mail_id, page_id, perms): +def viewm_menu(selection, folder, mail_id, page_id, perms): entries = [ {"name" : "back", - "url" : reverse(webmail.main.index) + "?page=%s" % page_id, + "url" : reverse(webmail.main.folder, args=[folder]) \ + + "?page=%s" % page_id + "&menu=1", "img" : "/static/pics/back.png", "label" : _("Back to list")}, ] @@ -18,3 +20,32 @@ def viewm_menu(selection, mail_id, page_id, perms): return render_to_string('main/menu.html', {"selection" : selection, "entries" : entries, "perms" : perms}) + +@register.simple_tag +def listing_menu(selection, perms): + entries = [ + {"name" : "newmail", + "url" : "", + "img" : "/static/pics/edit.png", + "label" : _("New message")} + ] + + return render_to_string("main/menu.html", + {"selection" : selection, "entries" : entries, + "perms" : perms}) + +@register.simple_tag +def render_headers(headers): + fields = ["Subject", "From", "To", "Cc", "Date"] + result = [] + for f in fields: + if not f in headers.keys(): + continue + try: + result += [{"name" : f, "value" : getattr(IMAPheader, "parse_%s" % f.lower())(headers[f])}] + except AttributeError: + result += [{"name" : f, "value" : headers[f]}] + return render_to_string("webmail/headers.html", { + "headers" : result + }) + diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index 0203315fd..6997d4b7c 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -1,10 +1,8 @@ from django.conf.urls.defaults import * urlpatterns = patterns('mailng.extensions.webmail.main', - (r'^$', "index"), - url(r'^viewmail/(?P[\w\-\+]+)/$', 'viewmail', - name="viewmail_std"), - url(r'^viewmail/$', 'viewmail', name="viewmail_null"), - (r'^getmailcontent/(?P[\w\-\+]+)/$', 'getmailcontent'), -# (r'^(?P.+)/$', "folder"), + url(r'^$', "folder", name="index"), + (r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'), + (r'^(?P.+)/(?P[\w\-\+]+)/$', 'viewmail'), + url(r'^(?P.+)/$', "folder", name="folder"), ) diff --git a/lib/email_listing.py b/lib/email_listing.py index c5bdf5d6a..71ae04db3 100644 --- a/lib/email_listing.py +++ b/lib/email_listing.py @@ -14,7 +14,8 @@ class MBconnector(object): class Page(object): def __init__(self, pageid, id_start, id_stop, items, - items_per_page, has_previous, has_next): + items_per_page, has_previous, has_next, + baseurl=None): self.pageid = pageid self.id_start = id_start self.id_stop = id_stop @@ -22,6 +23,7 @@ class Page(object): self.items_per_page = items_per_page self.has_previous = has_previous self.has_next = has_next + self.baseurl = baseurl def previous_page(self): if not self.has_previous: @@ -45,6 +47,7 @@ class Paginator(object): def __init__(self, total, elems_per_page): self.total = total self.elems_per_page = elems_per_page + self.baseurl = None def _indexes(self, page): id_start = self.elems_per_page * page + 1 @@ -65,8 +68,11 @@ class Paginator(object): id_stop = self.total else: return None - return Page(page, id_start, id_stop, self.total, - self.elems_per_page, has_previous, has_next) + p = Page(page, id_start, id_stop, self.total, + self.elems_per_page, has_previous, has_next) + if self.baseurl: + p.baseurl = self.baseurl + return p class EmailListing: def __init__(self, folder=None, elems_per_page=40, **kwargs): diff --git a/lib/events.py b/lib/events.py index 9e04a9900..4e9dcf493 100644 --- a/lib/events.py +++ b/lib/events.py @@ -13,7 +13,9 @@ events = ["CreateDomain", "ModifyMailbox", "UserMenuDisplay", - "AdminMenuDisplay"] + "AdminMenuDisplay", + + "UserLogin"] callbacks = {} diff --git a/lib/tables.py b/lib/tables.py index a83b806a3..13a36eba1 100644 --- a/lib/tables.py +++ b/lib/tables.py @@ -24,6 +24,13 @@ class SelectionColumn(Column): return "" \ % (self.name, value) +class ImgColumn(Column): + def __str__(self): + return "" + + def render(self, value): + return "" % value + class Table(object): def __init__(self, rows={}): self.columns = [] @@ -41,16 +48,24 @@ class Table(object): if isinstance(c, SelectionColumn): nrow["cols"] += [{"value" : c.render(row[self.idkey]), "safe" : True}] continue + if isinstance(c, ImgColumn): + key = "img_%s" % c.name + if key in row.keys(): + nrow["cols"] += [{"value" : c.render(row[key]), "safe" : True}] + else: + nrow["cols"] += ["."] + 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]}] + if "class" in row.keys(): + cssclass = row["class"] + else: + cssclass = "" + + nrow["cols"] += [{"value" : self.parse(c.name, row[c.name]), + "class" : cssclass}] self.rows += [nrow] def render(self, request): return render_to_string("common/tables.html", { "table" : self }) - - diff --git a/static/js/common.js b/static/js/common.js index 17bcf706a..081fe2a54 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -71,13 +71,17 @@ 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; +setDivHeight = function(id, extrah, modulo) { + var contentsize = $(document.body).getSize().y + - $("topmenubar").getSize().y + - $("header").getSize().y + - ($("navbar").getSize().y) + - extrah; - contentsize -= $("navbar").getSize().y * 2; - contentsize -= contentsize % cellsize; - $("listing").setStyle("height", contentsize + "px"); + if (modulo) { + contentsize -= contentsize % modulo; + } + $(id).setStyle("height", contentsize + "px"); } bindRows = function(rooturl, page) { diff --git a/static/pics/answered.png b/static/pics/answered.png new file mode 100644 index 000000000..9d31e533f Binary files /dev/null and b/static/pics/answered.png differ diff --git a/static/stylesheets/default.css b/static/stylesheets/default.css index 07b9c14e0..0e72a938d 100644 --- a/static/stylesheets/default.css +++ b/static/stylesheets/default.css @@ -47,7 +47,6 @@ html, body { #page { border: 2px solid #333; background: white; - min-height: 92%; margin: 0 15px; } @@ -251,4 +250,8 @@ input:hover { .hr td.even { +} + +.unseen { + font-weight: bold; } \ No newline at end of file diff --git a/templates/common/email_listing.html b/templates/common/email_listing.html index 3c0459666..474df0eeb 100644 --- a/templates/common/email_listing.html +++ b/templates/common/email_listing.html @@ -40,8 +40,7 @@ window.addEvent("domready", function() { }); } {% block js %}{% endblock %} - setContentSize(20); - var scroller = new Scroller("listing", {area: 150, velocity: 1}); + setDivHeight("listing", 0, 25); }); {% endblock %} diff --git a/templates/common/navbar.html b/templates/common/navbar.html index 9d6ed8b4e..668152024 100644 --- a/templates/common/navbar.html +++ b/templates/common/navbar.html @@ -2,18 +2,18 @@ {% if page.previous_page %} - + - + {% endif %} {% if page.next_page %} - + - + {% endif %} diff --git a/templates/common/tables.html b/templates/common/tables.html index 3906302ee..1225475e3 100644 --- a/templates/common/tables.html +++ b/templates/common/tables.html @@ -10,7 +10,7 @@ {% if cell.safe %} {{ cell.value|safe }} {% else %} - {{ cell.value }} + {{ cell.value }} {% endif %} {% endfor %} diff --git a/templates/registration/login.html b/templates/registration/login.html index 3d9cc6b7b..f5b23a6bc 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -21,7 +21,7 @@ {% if error %}

{{ error }}

{% endif %} -

Antoine Nguyen - 2010 - Version 0.7-dev

+

2010 - Version 0.7-dev

{% trans "Welcome" %}