This commit is contained in:
tonio
2010-04-12 10:05:58 +02:00
100 changed files with 7068 additions and 614 deletions
+1
View File
@@ -1,3 +1,4 @@
.*~$
^[^\.]+\.pyc
static/tmp
static/ckeditor
+4
View File
@@ -115,3 +115,7 @@ class Alias(models.Model):
permissions = (
("view_aliases", "View aliases"),
)
class Parameter(models.Model):
name = models.CharField(max_length=255)
value = models.CharField(max_length=255)
+74 -3
View File
@@ -46,9 +46,40 @@ def domain_menu(domain_id, selection, perms):
"perms" : perms})
@register.simple_tag
def settings_menu(selection):
return render_to_string('admin/settings_menu.html',
{"selection" : selection})
def settings_menu(selection, perms):
entries = [
{"name" : "permissions",
"url" : reverse(admin.views.settings),
"label" : _("Permissions"),
"img" : "/static/pics/permissions.png"},
{"name" : "addperm",
"url" : reverse(admin.views.addpermission),
"img" : "/static/pics/add.png",
"label" : _("Add permission"),
"class" : "boxed",
"rel" : "{handler:'iframe',size:{x:320,y:210}}"},
{"name" : "parameters",
"url" : reverse(admin.views.viewparameters),
"img" : "/static/pics/domains.png",
"label" : _("Parameters")},
]
return render_to_string('main/menu.html',
{"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):
@@ -57,6 +88,46 @@ def loadadminextmenu(perms):
return render_to_string('main/menulist.html',
{"menu" : menu, "perms" : perms})
@register.simple_tag
def param(app, definition):
result = """<div class='row'>
<label>%s</label>""" % definition["name"]
name = "%s.%s" % (app, definition["name"])
if definition["type"] in ["string", "int"]:
value = definition.has_key("value") \
and definition["value"] or definition["default"]
result += """
<input type='text' name='%s' id='%s' value='%s' />""" % (name, name, value)
if definition["type"] in ["list", "list_yesno"]:
result += """
<select name='%s' id='%s'>""" % (name, name)
values = []
if definition["type"] == "list_yesno":
values = [("yes", _("Yes")), ("no", _("No"))]
else:
if definition.has_key("values"):
values = definition["values"]
value = definition.has_key("value") \
and definition["value"] or definition["default"]
for v in values:
selected = ""
if value == v[0]:
selected = " selected='selected'"
result += "<option value='%s'%s>%s</option>\n" % (v[0], selected, v[1])
result += """
</select>
"""
if definition.has_key("help"):
result += """
<a href='%s' onclick='return false;' class='Tips' title='%s'>
<img src='/static/pics/info.png' border='0' />
</a>""" % (definition["help"], _("Help"))
result += """
</div>
"""
return result
@register.filter
def gender(value, target):
if genders.has_key(value):
+3 -1
View File
@@ -18,5 +18,7 @@ urlpatterns = patterns('mailng.admin.views',
(r'^domains/(?P<dom_id>\d+)/delalias/(?P<alias_id>\d+)/$', 'delalias'),
(r'^settings/$', 'settings'),
(r'^settings/addperm/$', 'addpermission'),
(r'^settings/delperm/(?P<mbox_id>\d+)/(?P<group>\w+)/$', 'deletepermission')
(r'^settings/delperm/(?P<mbox_id>\d+)/(?P<group>\w+)/$', 'deletepermission'),
(r'^settings/parameters/$', 'viewparameters'),
(r'^settings/parameters/save/$', 'saveparameters'),
)
+33 -3
View File
@@ -12,11 +12,11 @@ from django.contrib.auth.decorators \
import login_required, permission_required, user_passes_test
from django.contrib.auth.models import User, Group
from mailng import admin, main
from mailng.admin.models import Domain, Mailbox, Alias
from mailng.admin.models import Domain, Mailbox, Alias, Parameter
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
from mailng.lib import events, parameters
import string
import copy
import pwd
@@ -51,7 +51,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:
@@ -408,3 +408,33 @@ def deletepermission(request, mbox_id, group):
mbox.user.groups.remove(grp)
mbox.user.save()
return HttpResponseRedirect(reverse(admin.views.settings))
@login_required
@user_passes_test(lambda u: u.is_superuser)
def viewparameters(request):
apps = sorted(parameters._params.keys())
gparams = []
for app in apps:
tmp = {"name" : app, "params" : []}
for p in sorted(parameters._params[app]):
tmp["params"] += [{"name" : p,
"value" : parameters.get(app, p),
"help" : parameters._params[app][p]["help"],
"default" : parameters._params[app][p]["default"],
"type" : parameters._params[app][p]["type"]}]
gparams += [tmp]
return _render(request, 'admin/parameters.html', {
"gparams" : gparams
})
@login_required
@user_passes_test(lambda u: u.is_superuser)
def saveparameters(request):
for pname, v in request.POST.iteritems():
if pname == "update":
continue
app, name = pname.split('.')
parameters.save(app, name, v)
request.user.message_set.create(message=_("Configuration saved."))
return HttpResponseRedirect(reverse(admin.views.viewparameters))
View File
+7
View File
@@ -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"))
Executable
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
Executable
+23
View File
@@ -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
"""}
Executable
+37
View File
@@ -0,0 +1,37 @@
# -*- 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 _
from mailng.lib import events
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)
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)
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
})
+105 -65
View File
@@ -8,6 +8,7 @@ import email
import re
import os
from django.http import HttpResponseRedirect, HttpResponse
from django.template import Template, Context
from django.utils import simplejson
from django.conf.urls.defaults import *
from django.utils.translation import ugettext as _, ungettext
@@ -15,10 +16,11 @@ from django.core.urlresolvers import reverse
from django.contrib.auth.decorators \
import login_required
from mailng.lib import events, parameters
from mailng.lib import _render, _ctx_ok, _ctx_ko, decode
from mailng.lib import _render, _ctx_ok, _ctx_ko, decode, getctx
from mailng.lib import db
from mailng.admin.models import Mailbox
from lib import AMrelease
from templatetags.amextras import *
from sql_listing import *
def init():
@@ -40,21 +42,71 @@ def menu(**kwargs):
return []
@login_required
def index(request, message=None):
start = time.time()
def _listing(request, internal=False, filter=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]
if filter is None:
filter = ["&maddr.email='%s'" % mb.full_address]
else:
filter += ["&maddr.email='%s'" % mb.full_address]
pageid = request.GET.has_key("page") and int(request.GET["page"]) or 1
request.session["page"] = pageid # ?? nécessaire
lst = SQLlisting(filter, baseurl="listing/", empty=internal)
if internal:
return lst.render(request, pageid=int(pageid))
page = lst.paginator.getpage(pageid)
if page:
content = lst.fetch(request, page.id_start, page.id_stop)
navbar = lst.render_navbar(page)
else:
filter = None
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)
content = _("Empty quarantine")
navbar = ""
ctx = getctx("ok", listing=content, navbar=navbar,
menu=quar_menu("", request.user.get_all_permissions()))
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
@login_required
def index(request, message=None):
return _listing(request, True)
# @login_required
# def getmailcontent(request, mail_id):
# from mailng.lib.email_listing import Email
# conn = db.getconnection("amavis_quarantine")
# status, cursor = db.execute(conn, """
# SELECT mail_text
# FROM quarantine
# WHERE quarantine.mail_id='%s'
# """ % mail_id)
# content = ""
# for part in cursor.fetchall():
# content += part[0]
# msg = email.message_from_string(content)
# links = request.GET.has_key("links") and request.GET["links"] or "0"
# mode = request.GET.has_key("mode") and request.GET["mode"] or "plain"
# mail = Email(msg, mode, links)
# return mail.render(request)
# @login_required
# def viewmail(request, mail_id):
# conn = db.getconnection("amavis_quarantine")
# status, cursor = db.execute(conn, """
# SELECT mail_text
# FROM quarantine
# WHERE quarantine.mail_id='%s'
# """ % mail_id)
# content = ""
# for part in cursor.fetchall():
# content += part[0]
# msg = email.message_from_string(content)
# ctx = getctx("ok", )
# return _render(request, 'amavis_quarantine/viewmail.html', {
# "headers" : msg, "alert" : msg.get("X-Amavis-Alert"),
# "mail_id" : mail_id, "page_id" : request.session["page"],
# "params" : request.GET
# })
@login_required
def getmailcontent(request, mail_id):
@@ -73,25 +125,25 @@ WHERE quarantine.mail_id='%s'
links = request.GET.has_key("links") and request.GET["links"] or "0"
mode = request.GET.has_key("mode") and request.GET["mode"] or "plain"
mail = Email(msg, mode, links)
return mail.render(request)
return _render(request, "common/viewmail.html", {
"headers" : "",
"mailbody" : mail.body,
"pre" : mail.pre
})
@login_required
def viewmail(request, mail_id):
conn = db.getconnection("amavis_quarantine")
status, cursor = db.execute(conn, """
SELECT mail_text
FROM quarantine
WHERE quarantine.mail_id='%s'
""" % mail_id)
content = ""
for part in cursor.fetchall():
content += part[0]
msg = email.message_from_string(content)
return _render(request, 'amavis_quarantine/viewmail.html', {
"headers" : msg, "alert" : msg.get("X-Amavis-Alert"),
"mail_id" : mail_id, "page_id" : request.GET["page"],
"params" : request.GET
})
args = ""
for kw in ["mode", "links"]:
if kw in request.GET.keys():
args += args != "" and "&" or "?"
args += "%s=%s" % (kw, request.GET[kw])
content = Template("""
<iframe width="100%" frameBorder="0" src="{{ url }}" id="mailcontent"></iframe>
""").render(Context({"url" : reverse(getmailcontent, args=[mail_id]) + args}))
menu = viewm_menu("", request.session, mail_id, request.user.get_all_permissions())
ctx = getctx("ok", menu=menu, listing=content)
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
@login_required
def viewheaders(request, mail_id):
@@ -129,7 +181,9 @@ def delete(request, mail_id, count=1):
count) % {"count" : count}
else:
message = error
return _redirect_to_index(request, message, count)
ctx = getctx("ok", url="?page=%s" % request.session["page"], message=message)
return HttpResponse(simplejson.dumps(ctx),
mimetype="application/json")
@login_required
def release(request, mail_id, count=1):
@@ -152,13 +206,6 @@ WHERE msgrcpt.mail_id=msgs.mail_id AND msgrcpt.rid=maddr.id AND msgs.mail_id IN
error = None
for k, values in emails.iteritems():
result = amr.sendreq(k, values["secret"], *values["rcpts"])
# cmd = "/tmp/amavisd-release "
# cmd += "%s %s" % (k, values["secret"])
# for rcpt in values["rcpts"]:
# cmd += " %s" % rcpt
# cmd += "\n"
# result = os.system(cmd)
# if re.search("250 [^\s]+ Ok", result):
if result:
count += 1
else:
@@ -170,7 +217,9 @@ WHERE msgrcpt.mail_id=msgs.mail_id AND msgrcpt.rid=maddr.id AND msgs.mail_id IN
count) % {"count" : count}
else:
message = error
return _redirect_to_index(request, message, count)
ctx = getctx("ok", url="?page=%s" % request.session["page"], message=message)
return HttpResponse(simplejson.dumps(ctx),
mimetype="application/json")
@login_required
def process(request):
@@ -182,19 +231,13 @@ def process(request):
ids += "'%s'" % id
if ids == "":
return HttpResponseRedirect(reverse(index))
print "Ok c pas vide"
if request.POST["action"] == "release":
release(request, ids, count)
return release(request, ids, count)
if request.POST["action"] == "delete":
delete(request, ids, count)
try:
pagenum = int(request.POST.get('pagenum', '1'))
except ValueError:
pagenum = 1
ctx = _ctx_ok(reverse(index) + "?page=%s" % pagenum)
return HttpResponse(simplejson.dumps(ctx),
mimetype="application/json")
return delete(request, ids, count)
@login_required
def search(request):
@@ -211,19 +254,16 @@ def search(request):
filter += " OR "
filter += "msgs.%s LIKE '%%%s%%'" % (c, pattern)
filter = "&(%s)" % filter
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")
return _listing(request, filter=filter)
# lst = SQLlisting([filter])
# page = lst.paginator.getpage(1)
# 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")
@@ -0,0 +1,92 @@
# -*- 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):
tableid = "emails"
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)
def parse(self, header, value):
try:
return getattr(self, "parse_%s" % header)(value)
except AttributeError:
return 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, **kwargs):
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"
tbltype = Qtable
deflocation = "listing/"
defcallback = "updatelisting"
def __init__(self, filter, **kwargs):
self.mbc = SQLconnector(filter)
EmailListing.__init__(self, **kwargs)
+115 -137
View File
@@ -1,49 +1,12 @@
{% extends "layout.html" %}
{% extends "common/email_listing.html" %}
{% load i18n %}
{% load amextras %}
{% block html_head %}
<link rel="stylesheet" type="text/css" href="/static/stylesheets/box.css" />
<style type="text/css">
form {
margin: 0 auto;
width: 98%;
}
#quarmenu {
float: left;
width: 100%;
margin: 5px 0;
}
#listing {
float: left;
width: 100%;
padding-bottom: 10px;
}
#listheader {
float: left;
width: 100%;
margin-bottom: 10px;
}
#navbar {
float: right;
}
#counter {
float: left;
font-weight: bold;
}
td {
font-size: smaller;
height: 40px;
}
{% block css %}
#searchbox {
float: right;
/*float: left;*/ /* Marche pas ici?! obligé de le faire en JS sous FF */
margin-right: 10px;
z-index: 2;
position: relative;
}
#searchbox img {
@@ -55,122 +18,137 @@ td {
#searchparams {
background: white;
padding: 5px;
width: 90%;
border-bottom: 5px solid #CCCCCC;
border-left: 5px solid #CCCCCC;
border-right: 5px solid #CCCCCC;
}
</style>
<script type="text/javascript">
window.addEvent("domready", function() {
thead tr {
position: relative;
z-index: 1;
}
{% endblock %}
{% block js %}
mysubmit = function(event, name) {
event.stop();
new Element('input', {
'type' : 'hidden', 'name' : "action", style: "display:none", 'value' : name
}).inject($("quarlistingform"), "top");
$("quarlistingform").set("send", {
}).inject($("listingform"), "top");
{% url extensions.amavis_quarantine.main.process as actionurl %}
$("listingform").set("action", "{{ actionurl }}");
$("listingform").set("send", {
onSuccess : function(res) {
response = JSON.decode(res);
location.href = response.url;
actioncallback(response);
}
});
$("quarlistingform").send();
$("listingform").send();
}
checkSelection = function(text) {
var getout = $$("input[name=selection]").every(function(item, index) {
return item.checked == false;
updatehash = function(evt) {
evt.stop();
current_anchor.parse_string(evt.target.get("href"), true).setparams(navparams);
current_anchor.update();
}
appendhash = function(evt) {
evt.stop();
location.hash += evt.target.get("href");
}
updateparams = function(evt) {
evt.stop();
current_anchor.updateparams(evt.target.get("href"));
current_anchor.update();
}
register_callback("_listing", function(resp) {
updatelisting(resp);
$$("a[name=release]").addEvent("click", function(event) {
if (!checkSelection("selection", "{% trans "Release this selection?" %}")) {
event.stop();
return;
}
mysubmit(event, "release");
});
if (getout) {
return false;
}
return confirm(text);
}
toggleSelection = function(value) {
$$("input[name=selection]").each(function(obj) {
obj.checked = value;
$$("a[name=delete]").addEvent("click", function(event) {
if (!checkSelection("selection", "{% trans "Delete this selection?" %}")) {
event.stop();
return;
}
mysubmit(event, "delete");
});
}
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;
});
var url = "search"
+ "?pattern=" + $("searchfield").value
+ "&criteria=" + criteria;
current_anchor.parse_string(url, true).setparams(navparams);
current_anchor.update();
$$("a[name=delete]").addEvent("click", function(event) {
if (!checkSelection("{% trans "Delete this selection?" %}")) {
event.stop();
return;
}
mysubmit(event, "delete");
});
$$("a[name=release]").addEvent("click", function(event) {
if (!checkSelection("{% trans "Release this selection?" %}")) {
event.stop();
return;
}
mysubmit(event, "release");
});
/*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();
});
if ($("selectall")) {
$("selectall").addEvent("click", function(event) {
if ($("selectall").checked) {
toggleSelection(true);
} else {
toggleSelection(false);
$("searchfield").addEvent("blur", function(event) {
if ($("searchfield").value == "") {
$("searchfield").set("value", "{% trans "Search..." %}");
}
});
$("searchbox").setStyle("float", "left");
$$("tr").addEvent("dblclick", viewmail);
});
actioncallback = function(resp) {
$("listing").set("html", "");
$("menubar").set("html", "");
$("navbar").set("html", "");
if (resp.status == "ok") {
infobox.show(resp.message, "green", 1);
} else {
infobox.show(resp.message, "red", 1);
}
current_anchor.parse_string(resp.url, true).setparams(navparams);
current_anchor.update(1);
}
{% url extensions.amavis_quarantine.main.search as s_url %}
$("searchfield").addEvent("change", function(event) {
var criteria = "";
$$("input[name=scriteria]:checked").each(function(obj) {
criteria = obj.value;
});
new Request.JSON({url: "{{ s_url }}", onSuccess: function(resp) {
$("listing").set("html", resp.content);
}}).get({"pattern" : $("searchfield").value, "criteria" : criteria});
});
actionclick = function(evt) {
evt.stop();
new Request.JSON({
url: evt.target.get("href"),
onSuccess: actioncallback
}).get();
}
$("searchfield").addEvent("click", function(event) {
$("searchfield").select();
register_callback("viewmail", function(resp) {
updatelisting(resp);
$$("a[name=back]").addEvent("click", updatehash);
$$("a[name=delete]").addEvent("click", actionclick);
$$("a[name=release]").addEvent("click", actionclick);
$$("a[name=viewmode]").addEvent("click", updateparams);
setDivHeight("mailcontent", 5, 0);
});
$("searchfield").addEvent("blur", function(event) {
if ($("searchfield").value == "") {
$("searchfield").set("value", "{% trans "Search..." %}");
}
});
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();
});
});
</script>
{% endblock %}
{% block middletitle %}{% trans "Quarantine" %}{% endblock %}
{% block menu %}
{% quar_menu "" perms %}
<div id="searchbox">
<img src="/static/pics/search.png" id="searchopts" />
<input type="text" name="searchfield" id="searchfield" value="{% trans "Search..." %}" />
<div id="searchparams">
<input type="radio" name="scriteria" value="from_addr" checked>{% trans "From address" %}<br/>
<input type="radio" name="scriteria" value="subject">{% trans "Subject" %}<br/>
<input type="radio" name="scriteria" value="from_addr,subject">{% trans "Both" %}<br/>
</div>
</div>
{% endblock %}
{% block content %}
<div id="form-container">
{% url extensions.amavis_quarantine.main.process as actionurl %}
<form method="POST" action="{{ actionurl }}" id="quarlistingform">
<input type="hidden" name="pagenum" id="pagenum" value="{{ current_page }}" />
<div id="listing">
{{ content }}
</div>
</form>
</div>
{% endblock %}
@@ -1,51 +0,0 @@
{% load i18n %}
{% if not count %}
<div id="notifybox">{% trans "No messages in quarantine" %}</div>
{% else %}
<div id="listheader">
<div id="counter">
Messages {{ emails.start_index }} à {{ emails.end_index }} sur {{ count }}
</div>
<div id="navbar">
{% if emails.has_previous %}
<a href="?page=1">
<img src="/static/pics/first.png" border="0" title="{% trans "First" %}" />
</a>
<a href="?page={{ emails.previous_page_number }}">
<img src="/static/pics/back.png" border="0" title="{% trans "Previous" %}" />
</a>
{% endif %}
{% if emails.has_next %}
<a href="?page={{ emails.next_page_number }}">
<img src="/static/pics/next.png" border="0" title="{% trans "Next" %}" />
</a>
<a href="?page={{ last_page }}">
<img src="/static/pics/last.png" border="0" title="{% trans "Last" %}" />
</a>
{% endif %}
</div>
</div>
<table>
<tr>
<th><input type="checkbox" name="selectall" id="selectall" /></th>
<th>{% trans "Type" %}</th>
<th width="150px">{% trans "From" %}</th>
<th>{% trans "Subject" %}</th>
<th width="100px">{% trans "Date" %}</th>
</tr>
{% for m in emails.object_list %}
<tr class="{% cycle 'odd' 'even' %}">
<td><input type="checkbox" name="selection" id="selection"
value="{{ m.mailid }}" /></td>
<td>{{ m.type }}</td>
<td>{{ m.from }}</td>
{% url extensions.amavis_quarantine.main.viewmail m.mailid as view_url %}
<td><a href="{{ view_url }}?page={{ current_page }}">{{ m.subject }}</a></td>
<td>{{ m.time }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
@@ -1,11 +1,11 @@
{% extends "layout.html" %}
{% extends "nlayout.html" %}
{% load i18n %}
{% block html_head %}
<link rel="stylesheet" type="text/css" href="/static/stylesheets/box.css" />
<style type="text/css">
td {
font-size: smaller;
font-size: 1em;
height: 20px;
padding-left: 5px;
}
td.left {
@@ -14,10 +14,23 @@ td.left {
#mailcontent {
width: 100%;
height: 400px;
border: 0;
}
</style>
<script type="text/javascript">
window.addEvent("domready", function() {
var spage = $("page").getSize();
var stable = $("table-container").getSize();
var slogo = $("header").getSize();
var iheight = spage.y - stable.y - slogo.y;
$("mailcontent").setStyle("height", iheight + "px");
parse_menubar("menubar");
});
</script>
{% endblock %}
{% block middletitle %}{% trans "Mail content" %}{% endblock %}
{% block menu %}
@@ -7,25 +7,22 @@ from mailng.extensions import amavis_quarantine
register = template.Library()
@register.simple_tag
def viewm_menu(selection, mail_id, page_id, perms):
def viewm_menu(selection, session, mail_id, perms):
options_menu = [
{"name" : "plain",
{"name" : "viewmode",
"label" : _("View as plain text"),
"url" : reverse(amavis_quarantine.main.viewmail, args=[mail_id])
+ "?page=%s&mode=plain" % page_id},
{"name" : "html",
"url" : "?mode=plain&links=0"},
{"name" : "viewmode",
"label" : _("View as HTML"),
"url" : reverse(amavis_quarantine.main.viewmail, args=[mail_id])
+ "?page=%s&mode=html" % page_id},
{"name" : "links",
"url" : "?mode=html&links=0"},
{"name" : "viewmode",
"label" : _("Activate links"),
"url" : reverse(amavis_quarantine.main.viewmail, args=[mail_id])
+ "?page=%s&mode=html&links=1" % page_id}
"url" : "?mode=html&links=1"}
]
entries = [
{"name" : "back",
"url" : reverse(amavis_quarantine.main.index) + "?page=%s" % page_id,
"url" : "?page=%s" % session["page"],
"img" : "/static/pics/back.png",
"label" : _("Back to list")},
{"name" : "headers",
@@ -35,20 +32,19 @@ def viewm_menu(selection, mail_id, page_id, perms):
"class" : "boxed",
"rel" : "{handler:'iframe',size:{x:600,y:500}}"},
{"name" : "release",
"url" : reverse(amavis_quarantine.main.release, args=[mail_id])
+ "?page=%s" % page_id,
"url" : reverse(amavis_quarantine.main.release, args=[mail_id]),
"img" : "/static/pics/release.png",
"label" : _("Release"),
"confirm" : _("Release this message?")},
{"name" : "delete",
"url" : reverse(amavis_quarantine.main.delete, args=[mail_id])
+ "?page=%s" % page_id,
"url" : reverse(amavis_quarantine.main.delete, args=[mail_id]),
"img" : "/static/pics/remove.png",
"label" : _("Delete"),
"confirm" : _("Delete this message?")},
{"name" : "options",
"label" : _("Options"),
"img" : "/static/pics/settings.png",
"class" : "menubardropdown",
"menu" : options_menu}
]
@@ -69,6 +65,8 @@ def quar_menu(selection, perms):
"label" : _("Delete")},
]
return render_to_string('main/menu.html',
menu = render_to_string('main/menu.html',
{"selection" : selection, "entries" : entries,
"perms" : perms})
searchbar = render_to_string('common/email_searchbar.html', {})
return menu + searchbar
+3 -2
View File
@@ -2,11 +2,12 @@ from django.conf.urls.defaults import *
urlpatterns = patterns('mailng.extensions.amavis_quarantine.main',
(r'^$', 'index'),
(r'^viewmail/(?P<mail_id>[\w\-\+]+)/$', 'viewmail'),
(r'^listing/$', '_listing'),
(r'^getmailcontent/(?P<mail_id>[\w\-\+]+)/$', 'getmailcontent'),
(r'^viewheaders/(?P<mail_id>[\w\-\+]+)/$', 'viewheaders'),
(r'^(?P<mail_id>[\w\-\+]+)/headers/$', 'viewheaders'),
(r'^process/$', 'process'),
(r'^delete/(?P<mail_id>[\w\-\+]+)/$', 'delete'),
(r'^release/(?P<mail_id>[\w\-\+]+)/$', 'release'),
(r'^search/', 'search'),
(r'^(?P<mail_id>[\w\-\+]+)/$', 'viewmail'),
)
+13 -3
View File
@@ -49,6 +49,10 @@ size_avg_template = {
}
}
tpl = {'traffic':traffic_avg_template,
'badtraffic':badtraffic_avg_template,
'size':size_avg_template}
class Grapher(object):
def __init__(self):
self.rrd_rootdir = getoption("RRD_ROOTDIR", "/tmp")
@@ -57,9 +61,10 @@ class Grapher(object):
def process(self, target, suffix, start, end, tpl=traffic_avg_template):
rrdfile = "%s/%s.rrd" % (self.rrd_rootdir, target)
if not os.path.exists(rrdfile):
print "[graph] no rrd file %s detected" %rrdfile
return
ext = "png"
path = "%s/%s_%s_%s_%s.%s" % (self.img_rootdir, tpl['name'],
path = "%s/%s_%s_%s_%s.%s" % (self.img_rootdir, tpl['name'],
target, tpl['cf'], suffix, ext)
start = str(start)
end = str(end)
@@ -69,10 +74,10 @@ class Grapher(object):
defs += ['DEF:%s=%s:%s:%s' % (v, rrdfile, v, tpl['cf']),
'CDEF:%spm=%s,60,*' % (v, v)]
type = d.has_key("type") and d["type"] or "LINE"
lines += ["%s:%spm%s:%s" % (type, v, d["color"],
lines += ["%s:%spm%s:%s" % (type, v, d["color"],
d["legend"].encode("utf-8"))]
params = defs + lines
rrdtool.graph(path,
rrdtool.graph(str(path),
"--imgformat", "PNG",
"--width", tpl["width"],
"--height", tpl["height"],
@@ -84,6 +89,11 @@ class Grapher(object):
"--units-exponent", "0",
*params)
if not os.path.exists(path):
print "[graph] Impossible to create %s graph" %path
else:
print "[graph] %s graph created" %path
def make_defaults(self, target, tpl=traffic_avg_template):
end = "%d" % int(time.mktime(time.localtime()))
self.process(target, "day", "-1day", end, tpl)
+86 -60
View File
@@ -5,7 +5,7 @@ import sys
import os
import re
import rrdtool
import string
import string, pdb
from optparse import OptionParser
from mailng.lib import getoption
from mailng.admin.models import Domain
@@ -35,6 +35,25 @@ points_per_sample = 3
variables = ["sent", "recv", "bounced", "reject", "spam", "virus",
"size_sent", "size_recv"]
def str2Time(y, M, d, h ="0", m ="0", s="0"):
"""str2Time
return epoch time from Year Month Day Hour:Minute:Second time format
"""
try:
local = time.strptime("%s %s %s %s:%s:%s" %(y, M, d, h, m, s), \
"%Y %b %d %H:%M:%S")
except:
# try with 01-12 month format
try:
local = time.strptime("%s %s %s %s:%s:%s" %(y, M, d, h, m, s), \
"%Y %m %d %H:%M:%S")
except:
print "[rrd] ERROR unrecognized %s time format" %(y, M, d, h, m, s)
return 0
return int(time.mktime(local))
class LogParser(object):
def __init__(self, logfile, workdir,
year=None, debug=False, verbose=False):
@@ -42,17 +61,20 @@ class LogParser(object):
try:
self.f = open(logfile)
except IOError, errno:
print "%s" % errno
if self.debug:
print "%s" % errno
sys.exit(1)
self.workdir = workdir
self.year = year
self.debug = debug
self.verbose = verbose
self.cfs = ['AVERAGE', 'MAX']
self.last_month = None
if not self.year:
self.year = time.localtime().tm_year
if self.debug:
print "[rrd] Dealing with year %s" %self.year
self.data = {}
domains = Domain.objects.all()
self.domains = []
@@ -65,19 +87,6 @@ class LogParser(object):
self.lupdates = {}
self.line_expr = re.compile("(\w+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\w+)\s+(\w+)/?\w*[[](\d+)[]]:\s+(.*)")
def str2Time(self, y, M, d, h, m, s):
"""str2Time
return epoch time from Year Month Day Hour:Minute:Second time format
"""
try:
local = time.strptime("%s %s %s %s:%s:%s" %(y, M, d, h, m, s), \
"%Y %b %d %H:%M:%S")
except:
print "[rrd] ERROR unrecognized %s time format" %(y, M, d, h, m, s)
return 0
return int(time.mktime(local))
def init_rrd(self, fname, m):
"""init_rrd
@@ -128,7 +137,8 @@ class LogParser(object):
m = t - (t % rrdstep)
if not os.path.exists(fname):
self.lupdates[fname] = self.init_rrd(fname, m)
print "[rrd] create new RRD file"
if self.debug:
print "[rrd] create new RRD file %s" %fname
else:
if not self.lupdates.has_key(fname):
self.lupdates[fname] = rrdtool.last(fname)
@@ -186,60 +196,76 @@ class LogParser(object):
self.data["global"][cur_t][counter] += val
def process(self):
id_expr = re.compile("(\w{9,18}): (.*)")
prev_se = -1
prev_mi = -1
prev_ho = -1
for line in self.f.readlines():
m = self.line_expr.match(line)
if not m:
continue
(mo, da, ho, mi, se, host, prog, pid, log) = m.groups()
se = int(int(se) / rrdstep) # rrd step is one-minute => se = 0
cur_t = self.str2Time(self.year, mo, da, ho, mi, se)
cur_t = cur_t - cur_t % rrdstep
m = re.search("(\w{10}): message-id=<([^>]*)>", log)
if prev_se != se or prev_mi != mi or prev_ho != ho:
cur_t = str2Time(self.year, mo, da, ho, mi, se)
cur_t = cur_t - cur_t % rrdstep
prev_mi = mi
prev_ho = ho
prev_se = se
m = id_expr.search(log)
if m:
self.workdict[m.group(1)] = {'from' : m.group(2), 'size' : 0}
continue
(line_id, line_log) = m.groups()
m = re.search("(\w{10}): from=<([^>]*)>, size=(\d+)", log)
if m:
self.workdict[m.group(1)] = {'from' : m.group(2),
'size' : string.atoi(m.group(3))}
continue
m = re.search("(\w{10}): to=<([^>]*)>.*status=(\S+)", log)
if m:
if not self.workdict.has_key(m.group(1)):
print "Inconsistent mail (%s: %s), skipping" % (m.group(1), m.group(2))
m = re.search("message-id=<([^>]*)>", line_log)
if m:
self.workdict[line_id] = {'from' : m.group(1), 'size' : 0}
continue
if not m.group(3) in variables:
print "Unsupported status %s, skipping" % m.group(3)
m = re.search("from=<([^>]*)>, size=(\d+)", line_log)
if m:
self.workdict[line_id] = {'from' : m.group(1),
'size' : string.atoi(m.group(2))}
continue
addrfrom = re.match("([^@]+)@(.+)", self.workdict[m.group(1)]['from'])
if addrfrom and addrfrom.group(2) in self.domains:
self.inc_counter(addrfrom.group(2), cur_t, 'sent')
self.inc_counter(addrfrom.group(2), cur_t, 'size_sent',
self.workdict[m.group(1)]['size'])
addrto = re.match("([^@]+)@(.+)", m.group(2))
if addrto.group(2) in self.domains:
if m.group(3) == "sent":
self.inc_counter(addrto.group(2), cur_t, 'recv')
self.inc_counter(addrto.group(2), cur_t, 'size_recv',
self.workdict[m.group(1)]['size'])
else:
self.inc_counter(addrto.group(2), cur_t, m.group(3))
continue
m = re.search("NOQUEUE: reject: .*from=<(.*)> to=<([^>]*)>", log)
if m:
addrto = re.match("([^@]+)@(.+)", m.group(2))
if addrto and addrto.group(2) in self.domains:
self.inc_counter(addrto.group(2), cur_t, 'reject')
continue
m = re.search("to=<([^>]*)>.*status=(\S+)", line_log)
if m:
if not self.workdict.has_key(line_id):
if self.debug:
print "Inconsistent mail (%s: %s), skipping" % (line_id, m.group(1))
continue
if not m.group(2) in variables:
if self.debug:
print "Unsupported status %s, skipping" % m.group(2)
continue
addrfrom = re.match("([^@]+)@(.+)", self.workdict[line_id]['from'])
if addrfrom and addrfrom.group(2) in self.domains:
self.inc_counter(addrfrom.group(2), cur_t, 'sent')
self.inc_counter(addrfrom.group(2), cur_t, 'size_sent',
self.workdict[line_id]['size'])
addrto = re.match("([^@]+)@(.+)", m.group(1))
if addrto.group(2) in self.domains:
if m.group(2) == "sent":
self.inc_counter(addrto.group(2), cur_t, 'recv')
self.inc_counter(addrto.group(2), cur_t, 'size_recv',
self.workdict[line_id]['size'])
else:
self.inc_counter(addrto.group(2), cur_t, m.group(2))
continue
else:
m = re.search("NOQUEUE: reject: .*from=<(.*)> to=<([^>]*)>", log)
if m:
addrto = re.match("([^@]+)@(.+)", m.group(2))
if addrto and addrto.group(2) in self.domains:
self.inc_counter(addrto.group(2), cur_t, 'reject')
continue
# Sort everything by time
G = grapher.Grapher()
for dom, data in self.data.iteritems():
if self.debug:
print "[rrd] dealing with domain %s" %dom
sortedData = {}
sortedData = [ (i, data[i]) for i in sorted(data.keys()) ]
for t, dict in sortedData:
@@ -256,12 +282,12 @@ if __name__ == "__main__":
parser = OptionParser()
parser.add_option("-l","--logFile", default=log_file,
help="postfix log in syslog format", metavar="FILE")
parser.add_option("-v","--verbose", default=False, action="store_true",
parser.add_option("-v","--verbose", default=False, action="store_true",
dest="verbose", help="set verbose mode")
parser.add_option("-d","--debug", default=False, action="store_true",
parser.add_option("-d","--debug", default=False, action="store_true",
dest="debug", help="set debug mode")
(options, args) = parser.parse_args()
P = LogParser(options.logFile, rrd_rootdir,
debug=options.debug, verbose=options.verbose)
P.process()
+89 -16
View File
@@ -7,19 +7,33 @@ This module provides rrdtool support to retreive statistics
from postfix log : sent, received, bounced, rejected
"""
import calendar
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.conf.urls.defaults import *
from mailng.lib import events, _render
from mailng.lib import events, parameters, _render
from mailng.admin.views import good_domain
from mailng.admin.models import Domain, Mailbox
from django.contrib.auth.decorators \
import login_required, user_passes_test, permission_required
from extensions.stats.grapher import Grapher, tpl
from extensions.stats.logparser import str2Time
from mailng.lib import getoption
graph_types = ['AVERAGE', 'MAX']
graph_list = [{"name" : "traffic", "label" : _("Average normal traffic")},
{"name" : "badtraffic", "label" : _("Average bad traffic")},
{"name" : "size", "label" : _("Average normal traffic size")}]
def init():
events.register("AdminMenuDisplay", menu)
parameters.register("stats", "LOGFILE", "string", "/var/log/mail.log",
help=_("Path to log file used to collect statistics"))
parameters.register("stats", "RRD_ROOTDIR", "string", "/tmp/mailng",
help=_("Path to directory where RRD files are stored"))
parameters.register("stats", "IMG_ROOTDIR", "string", "/tmp/mailng",
help=_("Path to directory where PNG files are stored"))
def urls():
return (r'^mailng/stats/',
@@ -77,26 +91,87 @@ def graph_display(request,dom_id,graph_t=graph_types):
return _render(request, 'stats/index.html', {
"page" : "Domain statistics", "graph" : graph_nature,
"domain":domain,"domains" : domains, "messages" : errors ,
"types" : graph_type, "tmp_path" : tmp_path})
"domain": domain, "domains" : domains, "messages" : errors,
"types" : graph_type, "tmp_path" : tmp_path,
"img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")})
@login_required
@user_passes_test(lambda u: u.is_superuser)
def adminindex(request):
CH = calendar.HTMLCalendar(calendar.MONDAY)
CH_print = CH.formatmonth(2009,11)
period = request.GET.has_key("period") and request.GET["period"] or "day"
domain = request.GET.has_key("domain") and request.GET["domain"] or "global"
if domain != "global":
domain = Domain.objects.get(pk=domain)
domain = domain.name
domains = Domain.objects.all()
G = Grapher()
for name, t in tpl.iteritems():
G.make_defaults(domain, t)
return _render(request, 'stats/adminindex.html', {
"domains" : domains, "domain" : domain,
"graphs" : graph_list,
"periods" : ["day", "week", "month", "year","Custom"],
"period" : period, "cal" : CH_print,
"img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")})
@login_required
@user_passes_test(lambda u: u.is_superuser)
def custom_period(request):
start = None
end = None
start_ref = None
end_ref = None
domains = Domain.objects.all()
domain = request.GET.has_key("domain") and request.GET["domain"] or "global"
period = request.GET.has_key("period") and request.GET["period"] or "Custom"
if domain != "global":
domain = Domain.objects.get(pk=domain)
domain = domain.name
try:
start_ref = str(request.POST.get('start'))
start = start_ref.split(', ')[1]
end_ref = str(request.POST.get('end'))
end = end_ref.split(', ')[1]
except:
pass
if not end or not start:
return _render(request, 'stats/adminindex.html', {
"domains" : domains, "domain" : domain,
"graphs" : [{"name" : "traffic", "label" : _("Average normal traffic")},
{"name" : "badtraffic", "label" : _("Average bad traffic")},
{"name" : "size", "label" : _("Average normal traffic size")}],
"periods" : ["day", "week", "month", "year"],
"period" : period
})
"graphs" : graph_list,
"period" : period,
"periods" : ["day", "week", "month", "year","Custom"],
"period_name":None,
"start" : start_ref,
"end" : end_ref,
"messages" : ["Custom period not selected"],
"img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")})
period_name = "%s_%s" %(start.replace('/',''),end.replace('/',''))
G = Grapher()
for tpl_name in graph_list:
G.process(domain,
period_name,
str2Time(*start.split('/')),
str2Time(*end.split('/')),
tpl[tpl_name['name']])
print "[stats] ", period
return _render(request, 'stats/adminindex.html', {
"domains" : domains, "domain" : domain,
"graphs" : graph_list,
"period" : period,
"periods" : ["day", "week", "month", "year","Custom"],
"period_name": period_name,
"start" : start_ref,
"end" : end_ref,
"messages" : None,
"img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")})
@login_required
@good_domain
@@ -108,10 +183,8 @@ def index(request, dom_id=None):
domain = Domain.objects.get(pk=dom_id)
return _render(request, 'stats/index.html', {
"domain" : domain,
"graphs" : [{"name" : "traffic", "label" : _("Average normal traffic")},
{"name" : "badtraffic", "label" : _("Average bad traffic")},
{"name" : "size", "label" : _("Average normal traffic size")}],
"periods" : ["day", "week", "month", "year"],
"period" : period
})
"domain" : domain,
"graphs" : graph_list,
"periods" : ["day", "week", "month", "year","Custom"],
"period" : period,
"img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")})
+12 -3
View File
@@ -1,9 +1,9 @@
{% extends "layout.html" %}
{% extends "nlayout.html" %}
{% load i18n %}
{% load stats_extra %}
{% block html_head %}
<script src="/static/js/mootools-more.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="/static/stylesheets/box.css" />
<link rel="stylesheet" type="text/css" href="/static/stylesheets/calendar.css" />
<style type="text/css">
#graphcontent {
margin: 25px auto 10px;
@@ -11,7 +11,7 @@
}
#selectors {
float: right;
float: none;
}
</style>
@@ -27,6 +27,7 @@ window.addEvent("domready", function() {
});
});
</script>
<script type="text/javascript" src="/static/js/calendar.js"></script>
{% endblock %}
{% block middletitle %}{% trans "Global statistics" %}{% endblock %}
{% block content %}
@@ -46,7 +47,15 @@ window.addEvent("domready", function() {
<option value="{{ p }}" {% ifequal p period %}selected{% endifequal %}>{% getLabel p %}</option>
{% endfor %}
</select>
{% ifequal period "Custom" %}
{% include "stats/calendar.html" %}
{% endifequal %}
</div>
{% if start or end %}
<h3>{% trans "Custom period : from" %} {{ start }} {% trans "to" %} {{ end }}</h3>
{% endif %}
<div style="float:None;">
{% include "stats/graphs.html" %}
</div>
</div>
{% endblock %}
+29
View File
@@ -0,0 +1,29 @@
{% load i18n %}
<div style="border:1px solid #dddddd;float:right;padding:3px;margin-top:-3px;">
{% url extensions.stats.main.custom_period as c_stats_url %}
<form class="calendarForm" action="{{ c_stats_url }}" method="POST">
{% trans "Start" %}: <input id="dateTravelStart" name="start" value="{{ start }}" type="text"/>
{% trans "End" %}: <input id="dateTravelEnd" name="end" value="{{ end }}" type="text" />
<input TYPE=SUBMIT VALUE="ok">
</form>
</div>
<script type="text/javascript">
myCal = new Calendar(
{
dateTravelStart: 'D, Y/m/d',
dateTravelEnd : 'D, Y/m/d'
},
{
direction: 0, // -1 past, 0 past + future, 1 future
pad:1,
offset: 1, // first day of the week: 0 = sunday, 1 = monday, etc..
tweak: {x: -147, y: 18} // tweak calendar positioning
});
</script>
<!--
<form name="calendar">
<textarea type="text" rows="0" cols="5">{{ sdate }}</textarea>
<textarea type="text" rows="0" cols="5">{{ edate }}</textarea>
</select>
-->
+1 -1
View File
@@ -1,6 +1,6 @@
{% load i18n %}
{% for elems in graphs %}
<h3>{{ elems.label }}</h3>
<p><img src="/static/graphs/{{ elems.name }}_{{ domain }}_AVERAGE_{{ period }}.png"
<p><img src="/img/stats/{{ elems.name }}_{{ domain }}_AVERAGE_{% ifequal period 'Custom' %}{{ period_name }}{% else %}{{ period }}{% endifequal %}.png"
alt="{% trans 'No statistics available' %}" /></p>
{% endfor %}
+11 -2
View File
@@ -2,10 +2,10 @@
{% load i18n %}
{% block html_head %}
<script src="/static/js/mootools-more.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="/static/stylesheets/box.css" />
<link rel="stylesheet" type="text/css" href="/static/stylesheets/calendar.css" />
<style type="text/css">
#selectors {
float: right;
float: none;
}
</style>
@@ -17,6 +17,7 @@ window.addEvent("domready", function() {
});
});
</script>
<script type="text/javascript" src="/static/js/calendar.js"></script>
{% endblock %}
{% block menu %}
@@ -33,7 +34,15 @@ window.addEvent("domready", function() {
<option value="{{ p }}" {% ifequal p period %}selected{% endifequal %}>{{ p }}</option>
{% endfor %}
</select>
{% ifequal period "Custom" %}
{% include "stats/calendar.html" %}
{% endifequal %}
</div>
{% if start or end %}
<h3>{% trans "Custom period : from" %} {{ start }} {% trans "to" %} {{ end }}</h3>
{% endif %}
<div style="float:None;">
{% include "stats/graphs.html" %}
</div>
</div>
{% endblock %}
+1
View File
@@ -2,6 +2,7 @@ from django.conf.urls.defaults import *
urlpatterns = patterns('mailng.extensions.stats.main',
url(r'^$', 'adminindex', name='fullindex'),
url(r'^custom_period$', 'custom_period'),
url(r'^(?P<dom_id>\d+)/$', 'index', name='domindex'),
(r'^display/(?P<dom_id>\d+)/(?P<graph_t>\w+)/$', 'graph_display'),
(r'^domain/(?P<dom_id>\d+)/$', 'domain'),
View File
+9
View File
@@ -0,0 +1,9 @@
from django import forms
from django.utils.translation import ugettext as _
class ComposeMailForm(forms.Form):
from_ = forms.CharField(label=_("From"))
to = forms.CharField(label=_("To"), widget=forms.Textarea())
cc = forms.CharField(label=_("Cc"), widget=forms.Textarea(), required=False)
subject = forms.CharField(label=_("Subject"), max_length=255)
+291
View File
@@ -0,0 +1,291 @@
# -*- coding: utf-8 -*-
import re
import time
import imaplib
import ssl
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, Singleton
from mailng.lib.email_listing import MBconnector, EmailListing, Email
from mailng.lib import tables, imap_utf7, parameters
class WMtable(tables.Table):
tableid = "emails"
idkey = "imapid"
# selection = tables.SelectionColumn("selection", width="20px", first=True)
_1_flags = tables.ImgColumn("flags", width="4%",
header="<input type='checkbox' name='toggleselect' id='toggleselect' />")
_2_subject = tables.Column("subject", label=_("Subject"),
cssclass="draggable", width="50%")
_3_from_ = tables.Column("from", width="20%", label=_("From"))
_4_date = tables.Column("date", width="20%", label=_("Date"))
def parse(self, header, value):
try:
return getattr(IMAPheader, "parse_%s" % header)(value)
except AttributeError:
return value
class EmailAddress(object):
addr_exp = re.compile("([^<\(]+)[<(]([^>)]+)[>)]")
name_exp = re.compile('"([^"]+)"')
def __init__(self, address):
self.fulladdress = address
self.name = self.address = None
m = EmailAddress.addr_exp.match(self.fulladdress)
if m is None:
return
name = m.group(1).strip()
self.address = m.group(2).strip()
m2 = EmailAddress.name_exp.match(name)
if m2:
name = m2.group(1)
self.name = ""
for part in decode_header(name):
if self.name != "":
self.name += " "
if part[1] is None:
self.name += part[0]
else:
self.name += unicode(part[0], part[1])
self.fulladdress = "%s <%s>" % (self.name, self.address)
def __str__(self):
return self.fulladdress
class IMAPheader(object):
@staticmethod
def parse_address(value, **kwargs):
addr = EmailAddress(value)
if "full" in kwargs.keys() and kwargs["full"]:
return addr.fulladdress
return addr.name and addr.name or value
@staticmethod
def parse_address_list(values, **kwargs):
lst = values.split(",")
result = ""
for addr in lst:
if result != "":
result += ", "
result += IMAPheader.parse_address(addr, **kwargs)
return result
@staticmethod
def parse_from(value, **kwargs):
return IMAPheader.parse_address(value, **kwargs)
@staticmethod
def parse_to(value, **kwargs):
return IMAPheader.parse_address_list(value, **kwargs)
@staticmethod
def parse_cc(value, **kwargs):
return IMAPheader.parse_address_list(value, **kwargs)
@staticmethod
def parse_reply_to(value, **kwargs):
return IMAPheader.parse_address_list(value, **kwargs)
@staticmethod
def parse_date(value, **kwargs):
tmp = email.utils.parsedate_tz(value)
if not tmp:
return value
ndate = datetime(*(tmp)[:7])
now = datetime.now()
if now - ndate > timedelta(7):
return ndate.strftime("%d.%m.%Y %H:%M")
return ndate.strftime("%a %H:%M")
@staticmethod
def parse_subject(value, **kwargs):
res = ""
dcd = decode_header(value)
for part in dcd:
if res != "":
res += " "
res += part[0].strip(" ")
return decode(res)
class IMAPconnector(object):
__metaclass__ = Singleton
def __init__(self, request=None, user=None, password=None):
self.address = parameters.get("webmail", "IMAP_SERVER")
self.port = int(parameters.get("webmail", "IMAP_PORT"))
if request:
status, msg = self.login(request.user.username,
request.session["password"])
else:
status, msg = self.login(user, password)
if not status:
raise Exception(msg)
def login(self, user, passwd):
import imaplib
try:
secured = parameters.get("webmail", "IMAP_SECURED")
if secured == "yes":
self.m = imaplib.IMAP4_SSL(self.address, self.port)
else:
self.m = imaplib.IMAP4(self.address, self.port)
except (imaplib.IMAP4.error, ssl.SSLError), error:
return False, _("Connection to IMAP server failed, check your configuration")
try:
self.m.login(user, passwd)
except (imaplib.IMAP4.error, ssl.SSLError), error:
return False, _("Authentication failed, check your configuration")
return True, None
def _encodefolder(self, folder):
if not folder:
return "INBOX"
return folder.encode("imap4-utf-7")
def messages_count(self, **kwargs):
"""An enhanced version of messages_count
With IMAP, to know how many messages a mailbox contains, we
have to make a request to the server. To avoid requests
multiplications, we sort messages in the same time. This will
be usefull for other methods.
:param order: sorting order
:param folder: mailbox to scan
"""
if "order" in kwargs.keys() and kwargs["order"]:
sign = kwargs["order"][:1]
criterion = kwargs["order"][1:].upper()
if sign == '-':
criterion = "REVERSE %s" % criterion
else:
criterion = "REVERSE DATE"
print criterion
folder = kwargs.has_key("folder") and kwargs["folder"] or None
(status, data) = self.m.select(self._encodefolder(folder))
(status, data) = self.m.sort("(%s)" % criterion, "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()
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 msgseen(self, imapid):
folder, id = imapid.split("/")
self.m.store(id, "+FLAGS", "\\Seen")
def msgforwarded(self, folder, imapid):
self.m.select(self._encodefolder(folder), True)
self.m.store(imapid, "+FLAGS", "$Forwarded")
def move(self, msgset, oldfolder, newfolder):
self.m.select(self._encodefolder(oldfolder), True)
self.m.copy(msgset, newfolder)
self.m.store(msgset, "+FLAGS", "\\Deleted")
def empty(self, folder):
self.m.select(self._encodefolder(folder))
typ, data = self.m.search(None, 'ALL')
for num in data[0].split():
self.m.store(num, "+FLAGS", "\\Deleted")
self.m.expunge()
def fetch(self, start=None, stop=None, folder=None, all=False):
if not start and not stop:
return []
result = []
self.m.select(self._encodefolder(folder), True)
if start and stop:
submessages = self.messages[start - 1:stop]
range = ",".join(submessages)
else:
submessages = [start]
range = start
if not all:
query = '(FLAGS BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])'
else:
query = '(RFC822)'
typ, data = self.m.fetch(range, query)
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"] = "%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"
tbltype = WMtable
deflocation = "INBOX/"
defcallback = "wm_updatelisting"
def __init__(self, user, password, **kwargs):
self.mbc = IMAPconnector(user=user, password=password)
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
class ImapEmail(Email):
def __init__(self, msg, addrfull=False, **kwargs):
Email.__init__(self, msg, **kwargs)
fields = ["Subject", "From", "To", "Reply-To", "Cc", "Date"]
for f in fields:
label = f
if not f in msg.keys():
f = f.upper()
if not f in msg.keys():
continue
try:
key = re.sub("-", "_", f).lower()
value = getattr(IMAPheader, "parse_%s" % key)(msg[f], full=addrfull)
self.headers += [{"name" : label, "value" : value}]
except AttributeError:
self.headers += [{"name" : label, "value" : msg[f]}]
try:
label = re.sub("-", "_", label)
setattr(self, label, value)
except:
pass
+296
View File
@@ -0,0 +1,296 @@
# -*- coding: utf-8 -*-
import time
import sys
from django.http import HttpResponse, Http404
from django.template import Template, Context
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
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, _render, _render_error, getctx
from imap_listing import *
from forms import *
from templatetags.webextras import *
from imap_listing import ImapEmail
def init():
events.register("UserMenuDisplay", menu)
events.register("UserLogin", userlogin)
parameters.register("webmail", "IMAP_SERVER", "string", "127.0.0.1",
help=_("Address of your IMAP server"))
parameters.register("webmail", "IMAP_SECURED", "list_yesno", "no",
help=_("Use a secured connection to access IMAP server"))
parameters.register("webmail", "IMAP_PORT", "int", "143",
help=_("Listening port of your IMAP server"))
parameters.register("webmail", "SMTP_SERVER", "string", "127.0.0.1",
help=_("Address of your SMTP server"))
parameters.register("webmail", "SMTP_PORT", "int", "25",
help=_("Listening port of your SMTP server"))
parameters.register("webmail", "SMTP_AUTHENTICATION", "list_yesno", "no",
help=_("Server needs authentication"))
parameters.register("webmail", "SMTP_SECURED", "list_yesno", "no",
help=_("Use a secured connection to access SMTP server"))
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"}
]
def userlogin(**kwargs):
kwargs["request"].session["password"] = kwargs["password"]
@login_required
def folder(request, name, updatenav=True):
if not name:
name = "INBOX"
order = request.GET.has_key("order") and request.GET["order"] or "-date"
if updatenav:
pageid = request.GET.has_key("page") and int(request.GET["page"]) or 1
if not "navparams" in request.session.keys():
request.session["navparams"] = {}
request.session["folder"] = name
request.session["page"] = pageid
if order:
request.session["navparams"]["order"] = order
else:
# Internal usage
order = request.session["navparams"]["order"]
lst = ImapListing(request.user.username, request.session["password"],
baseurl=name, folder=name, order=order)
page = lst.paginator.getpage(request.session["page"])
if page:
content = lst.fetch(request, page.id_start, page.id_stop)
navbar = lst.render_navbar(page)
else:
content = "Empty folder"
navbar = ""
ctx = getctx("ok", listing=content, navbar=navbar,
menu=listing_menu("", name, request.user.get_all_permissions()))
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
@login_required
def index(request):
try:
navp = request.session.has_key("navparams") \
and request.session["navparams"] or {}
lst = ImapListing(request.user.username, request.session["password"],
baseurl="INBOX", navparams=navp, folder="INBOX",
empty=True)
except Exception as exp:
return _render_error(request, {"error" : exp})
return lst.render(request)
def fetchmail(request, folder, mail_id, all=False):
res = IMAPconnector(request).fetch(start=mail_id, folder=folder, all=all)
if len(res):
return res[0]
return None
@login_required
def viewmail(request, folder, mail_id=None):
from templatetags.webextras import viewm_menu
content = Template("""
<iframe width="100%" frameBorder="0" src="{{ url }}" id="mailcontent"></iframe>
""").render(Context({"url" : reverse(getmailcontent, args=[folder, mail_id])}))
menu = viewm_menu("", request.session, mail_id,
request.user.get_all_permissions())
ctx = getctx("ok", menu=menu, listing=content)
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
@login_required
def getmailcontent(request, folder, mail_id):
msg = fetchmail(request, folder, mail_id, True)
if "class" in msg.keys() and msg["class"] == "unseen":
IMAPconnector(request).msgseen(msg["imapid"])
email = ImapEmail(msg, mode="html", links="1")
try:
pageid = request.session["page"]
except KeyError:
pageid = "1"
folder, imapid = msg["imapid"].split("/")
return _render(request, "common/viewmail.html", {
"headers" : email.render_headers(folder, mail_id),
"folder" : folder, "imapid" : imapid, "mailbody" : email.body,
"pre" : email.pre
})
@login_required
def getattachment(request, folder, mail_id):
msg = fetchmail(request, folder, mail_id, True)
for part in msg.walk():
fname = part.get_filename()
if fname is None:
continue
decoded = decode_header(fname)
if decoded[0][1] is None:
fname = unicode(decoded[0][0])
else:
fname = unicode(decoded[0][0], decoded[0][1])
if fname == request.GET["fname"]:
resp = HttpResponse(part.get_payload(decode=1))
for hdr in ["Content-Disposition", "Content-Type"]:
resp[hdr] = re.sub("\s", "", part[hdr])
resp["Content-Length"] = len(resp.content)
return resp
raise Http404
@login_required
def move(request):
for arg in ["msgset", "to"]:
if not request.GET.has_key(arg):
return
msgset = []
fdname = ""
for item in request.GET["msgset"].split(","):
fdname, id = item.split("/")
msgset += [id]
mbc = IMAPconnector(request)
mbc.move(",".join(msgset), fdname, request.GET["to"])
return folder(request, fdname, False)
@login_required
def delete(request, fdname, mail_id):
mbc = IMAPconnector(request)
mbc.move(mail_id, fdname, "Trash")
return folder(request, fdname, False)
@login_required
def empty(request, name):
mbc = IMAPconnector(request)
mbc.empty(name)
return folder(request, name, False)
def render_compose(request, form, posturl, bodyheader=None, body=None):
menu = compose_menu("", request.session, request.user.get_all_permissions())
content = render_to_string("webmail/compose.html", {
"form" : form, "bodyheader" : bodyheader, "body" : body,
"posturl" : posturl
})
ctx = getctx("ok", level=2, menu=menu, listing=content)
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
def send_mail(request, withctx=False):
form = ComposeMailForm(request.POST)
if form.is_valid():
from email.mime.text import MIMEText
import smtplib
msg = MIMEText(request.POST["id_body"].encode("utf-8"),
_charset="utf-8")
msg["Subject"] = request.POST["subject"]
msg["From"] = request.POST["from_"]
msg["To"] = request.POST["to"]
rcpts = msg['To'].split(',')
if "cc" in request.POST.keys():
msg["Cc"] = request.POST["cc"]
rcpts += msg["Cc"].split(",")
try:
s = smtplib.SMTP(parameters.get("webmail", "SMTP_SERVER"))
if parameters.get("webmail", "SMTP_SECURED") == "yes":
s.starttls()
except (smtplib.SMTPException, ssl.SSLError), error:
print error
# Prévoir la remontée de cette erreur au niveau du client!!
if parameters.get("webmail", "SMTP_AUTHENTICATION") == "yes":
s.login(request.user.username, request.session["password"])
s.sendmail(msg['From'], rcpts, msg.as_string())
s.quit()
ctx = getctx("ok", url="%s?page=%s" % (request.session["folder"],
request.session["page"]))
else:
ctx = getctx("ko", level=2, listing=render_to_string("webmail/compose.html",
{"form" : form}))
if not withctx:
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
return ctx, HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
@login_required
def compose(request):
if request.method == "POST":
return send_mail(request)
form = ComposeMailForm()
form.fields["from_"].initial = request.user.username
return render_compose(request, form, reverse(compose))
@login_required
def reply(request, folder, mail_id):
if request.method == "POST":
return send_mail(request)
msg = fetchmail(request, folder, mail_id, True)
email = ImapEmail(msg, True)
lines = email.body.split('\n')
body = ""
for l in lines:
if body != "":
body += "\n"
body += ">%s" % l
form = ComposeMailForm()
form.fields["from_"].initial = request.user.username
if not "Reply-To" in msg.keys():
form.fields["to"].initial = email.From
else:
form.fields["to"].initial = email.Reply_To
if request.GET.has_key("all"):
form.fields["cc"].initial = ""
toparse = msg["To"].split(",")
if "Cc" in msg.keys():
toparse += msg["Cc"].split(",")
for addr in toparse:
tmp = EmailAddress(addr)
if tmp.address and tmp.address == request.user.username:
continue
if form.fields["cc"].initial != "":
form.fields["cc"].initial += ", "
form.fields["cc"].initial += tmp.fulladdress
m = re.match("re\s*:\s*.+", email.Subject.lower())
if m:
form.fields["subject"].initial = email.Subject
else:
form.fields["subject"].initial = "Re: %s" % email.Subject
textheader = "%s %s" % (email.From, _("wrote:"))
return render_compose(request, form, reverse(reply, args=[folder, mail_id]),
textheader, body)
@login_required
def forward(request, folder, mail_id):
if request.method == "POST":
ctx, response = send_mail(request, True)
if ctx["status"] == "ok":
IMAPconnector(request).msgforwarded(folder, mail_id)
return response
msg = fetchmail(request, folder, mail_id, True)
email = ImapEmail(msg, True)
textheader = "----- %s -----" % _("Original message")
textheader += "\n%s: %s" % (_("Subject"), email.Subject)
textheader += "\n%s: %s" % (_("Date"), msg["Date"])
for hdr in ["From", "To", "Reply-To"]:
try:
key = re.sub("-", "_", hdr)
value = getattr(email, key)
textheader += "\n%s: %s" % (_(hdr), value)
except:
pass
textheader += "\n"
form = ComposeMailForm()
form.fields["from_"].initial = request.user.username
form.fields["subject"].initial = "Fwd: %s" % email.Subject
return render_compose(request, form, reverse(forward, args=[folder, mail_id]),
textheader, email.body)
+54
View File
@@ -0,0 +1,54 @@
{% load i18n %}
<style type="text/css">
#compose-container {
margin: 0 auto;
width: 100%;
}
div.row {
margin: 3px;
clear: both;
}
div.row label {
float: left;
width: 120px;
font-size: small;
padding: 3px 0;
}
textarea {
border: 1px solid #cccccc;
background-color: #eee;
width: 80%;
height: 30px;
font-size: medium;
}
textarea:hover {
background-color: #ffffcc;
}
#id_body {
background: white;
width: 98%;
}
input {
width: 80%;
padding: 0;
}
#mailheader {
padding-bottom: 5px;
}
</style>
<form method="POST" action="{{ posturl }}" id="composemail">
<div id="compose-container">
<div id="mailheader">
{% include "common/genericform.html" %}
</div>
{% if body %}
<textarea id="id_body" name="id_body">
{% if bodyheader %}{{ bodyheader }}{% endif %}
{{ body }}
</textarea>
{% else %}
<textarea id="id_body" name="id_body"></textarea>
{% endif %}
</div>
</form>
+17
View File
@@ -0,0 +1,17 @@
{% load i18n %}
<table id="emailheaders">
{% for hdr in headers %}
<tr><th width="130px">{{ hdr.name }}</th><td>{{ hdr.value }}</td></tr>
{% endfor %}
{% if attachments %}
<tr>
<th>{% trans "Attachments" %}</th>
<td>
{% url extensions.webmail.main.getattachment folder mail_id as test %}
{% for attachment in attachments %}
<a href="{{ test }}?fname={{ attachment }}">{{ attachment }}</a>
{% endfor %}
</td>
</tr>
{% endif %}
</table>
+363
View File
@@ -0,0 +1,363 @@
{% extends "common/email_listing.html" %}
{% load i18n %}
{% block css %}
#listing {
position: absolute;
left: 155px;
right: 0px;
top: 130px;
height: expression(document.body.clientHeight - 35 + "px");
overflow: auto;
}
html > body #listing {
position: fixed;
bottom: 35px;
}
#leftcol {
float: left;
width: 150px;
}
thead tr {
position: relative;
top: expression(offsetParent.scrollTop); /*IE5+ only*/
z-index: 1;
}
table#emails > tbody {
position: fixed;
bottom: 35px;
top: 155px;
left: 155px;
right: 0px;
overflow-x: hidden;
overflow-y: auto;
}
#emails th {
/*text-align: center;*/
}
#emails tr {
width: 100%;
}
.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;
}
th {
/*-moz-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset;
-webkit-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset;*/
}
th.selected {
background: #EEEEEE;
}
.draggable {2
cursor: move;
}
.dragging {
background: #6AB0C5;
}
tr {
position: relative;
border-bottom: 1px solid #C8D6E8;
}
tr.selected {
background: #ffffcc;
}
{% endblock %}
{% block js %}
wm_updatelisting = function(resp) {
updatelisting(resp);
$$("a[name=compose]").addEvent("click", loadCompose);
$$("a[name=empty]").addEvent("click", empty);
if (!checkSelection()) {
select($$("a[href=INBOX]"));
}
}
empty = function(event) {
event.stop();
new Request.JSON({url : event.target.get("href"), onSuccess : function(resp) {
wm_updatelisting(resp);
}}).get();
}
checkSelection = function() {
var selection = false;
$$("a[name=loadfolder]").each(function(item) {
if (!selection) {
selection = item.getParent().hasClass("selected");
}
});
return selection;
}
select = function(obj) {
$$("a[name=loadfolder]").each(function(item) {
item.getParent().removeClass("selected");
item.getParent().addClass("droppable");
});
obj.getParent().removeClass("droppable");
obj.getParent().addClass("selected");
}
register_callback("viewmail", function(resp) {
wm_updatelisting(resp, "hidden");
$$("a[name=back]").addEvent("click", loadFolder);
$$("a[name=reply]").addEvent("click", reply);
$$("a[name=replyall]").addEvent("click", replyall);
$$("a[name=forward]").addEvent("click", forward);
$$("a[name=delete]").addEvent("click", function(event) {
var lnk = event.target;
event.stop();
new Request.JSON({url: lnk.get("href"),
onSuccess: get_callback("folder")}).get();
});
setDivHeight("mailcontent", 5, 0);
});
compose_callback = function(resp) {
wm_updatelisting(resp);
window.addEvent("resize", function(evt) {
setDivHeight("id_body", $("mailheader").getSize().y, 0);
});
$$("a[name=back]").addEvent("click", loadFolder);
$$("a[name=sendmail]").addEvent("click", function(evt) {
evt.stop();
$("composemail").set("send", {
onSuccess: function(resp) {
resp = JSON.decode(resp);
if (resp.status == "ko") {
wm_updatelisting(resp);
window.fireEvent("resize");
return;
}
current_anchor.parse_string(resp.url, true).setparams(navparams);
current_anchor.update();
}
});
$("composemail").send();
});
window.fireEvent("resize");
}
loadCompose = function(event) {
event.stop();
current_anchor.baseurl("compose/").update();
}
register_callback("compose", compose_callback);
reply = function(event) {
event.stop();
location.hash += event.target.get("href");
}
register_callback("reply", compose_callback);
replyall = function(event) {
event.stop();
location.hash += event.target.get("href") + "?all=1";
}
register_callback("replyall", compose_callback);
forward = function(event) {
event.stop();
location.hash += event.target.get("href");
}
register_callback("forward", compose_callback);
loadFolder = function(event, obj) {
if (!$defined(obj)) {
obj = event.target;
}
event.stop();
current_anchor.parse_string(obj.get("href"), true).setparams(navparams);
current_anchor.update();
}
register_callback("folder", function(resp) {
wm_updatelisting(resp);
if (navparams.has("order")) {
src = navparams.get("order").charAt(0) == "-" ? "down_arrow.png" : "up_arrow.png";
th = $$("th[id=" + navparams.get("order").substring(1) + "]");
th.getElement("img").set("src", "/static/pics/" + src);
th.addClass("selected");
}
var selectedRows = new Array();
$$("tr").addEvent("click", function(event) {
var tr = event.target.getParent();
if (!tr.hasClass("selected")) {
tr.addClass("selected");
selectedRows.include(tr);
} else {
tr.removeClass("selected");
selectedRows.erase(tr);
}
});
$$("tr").each(function(item) {
new Drag.Move(item, {
droppables: ".droppable",
}).addEvents({
"start" : function(item) {
if (!item.hasClass("selected")) {
selectedRows.each(function(item, index) {
item.removeClass("selected");
});
selectedRows.empty();
item.addClass("selected");
selectedRows.include(item);
}
var block = new Element("div", {id: "draggeditems"})
.setStyles({"position" : "absolute",
"opacity" : 0.8,
"width" : "200px",
"padding" : "5px",
"text-align" : "center",
"height" : "25px",
"border" : "1px solid #333",
"background" : "white"})
.setStyles(item.getPosition())
.inject(document.body);
block.set("html", "Moving " + selectedRows.length + " messages");
this.oldelement = this.element;
this.element = block;
},
"cancel" : function(item) {
if ($defined(this.oldelement)) {
item.dispose();
delete(item);
this.element = this.oldelement;
}
},
"enter" : function(item, droppable) {
droppable.addClass("dragging");
},
"leave" : function(item, droppable) {
droppable.removeClass("dragging");
},
"drop" : function(item, droppable, event) {
item.dispose();
delete(item);
if (!droppable) {
this.element = this.oldelement;
return;
}
droppable.removeClass("dragging");
dest = droppable.getFirst("a").get("href");
msgset = new Array();
selectedRows.each(function(item, index) {
msgset.include(item.get("id"));
});
{% url extensions.webmail.main.move as moveurl %}
new Request.JSON({url: "{{ moveurl }}",
onSuccess: get_callback("folder")}
).get({msgset : msgset.join(), to : dest});
},
});
});
$("toggleselect").addEvent("click", function(evt) {
var checked = evt.target.get("checked");
if (checked) {
$$("tbody>tr").each(function(item) {
item.addClass("selected");
selectedRows.include(item);
});
} else {
$$("tr").each(function(item) { item.removeClass("selected"); });
selectedRows.empty();
}
});
$$("tr").addEvent("dblclick", viewmail);
$$("th[class*=sortable]").addEvent("click", function(evt) {
var tmp = "?";
var order = (navparams.has("order")) ? navparams.get("order") : "";
if (order == "") {
order = current_anchor.getparam("order");
}
if (order.substring(1) == evt.target.id) {
var sign = (order.charAt(0) == '+') ? '-' : '+';
order = sign + evt.target.id;
} else {
order = "-" + evt.target.id;
}
navparams.set("order", order);
current_anchor.setparams(navparams).update();
});
});
foldercallback = function(event) {
select(event.target);
loadFolder(event, event.target);
}
$$("a[name=loadfolder]").addEvent("click", foldercallback);
{% endblock %}
{% block middletitle %}{% trans "Webmail" %}{% endblock %}
{% block leftcol %}
<div id="leftcol" class="box">
<h3>{% trans "Folders" %}</h3>
<ul>
{% for fd in folders %}
{% url extensions.webmail.main.folder fd.name as fdurl %}
<li class="droppable">
<a href="{{ fd.name }}" name="loadfolder">
{% if fd.icon %}<img src="/static/pics/{{ fd.icon }}" />
{% else %}<img src="/static/pics/folder.png" />
{% endif %}
{{ fd.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
{% block menu %}
{% endblock %}
@@ -0,0 +1,100 @@
from django import template
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()
def backurl(session, **kwargs):
res = session["folder"]
params = ""
for key in ["page"]:
if key in session.keys():
if params != "":
params += "&"
params += "%s=%s" % (key, session[key])
for k, v in kwargs.iteritems():
if params != "":
params += "&"
params += "%s=%s" % (k, v)
if params != "":
res += "?%s" % params
return res
@register.simple_tag
def viewm_menu(selection, session, mail_id, perms):
entries = [
{"name" : "back",
"url" : "%s?page=%s" % (session["folder"], session["page"]),
"img" : "/static/pics/back.png",
"label" : _("Back")},
{"name" : "reply",
"url" : "reply/",
"img" : "/static/pics/reply.png",
"label" : _("Reply")},
{"name" : "replyall",
"url" : "reply/",
"img" : "/static/pics/reply-all.png",
"label" : _("Reply all")},
{"name" : "forward",
"url" : "forward/",
"img" : "/static/pics/alias.png",
"label" : _("Forward")},
{"name" : "delete",
"img" : "/static/pics/remove.png",
"url" : reverse(webmail.main.delete, args=[session["folder"], mail_id]),
"label" : _("Delete")},
]
return render_to_string('main/menu.html',
{"selection" : selection, "entries" : entries,
"perms" : perms})
@register.simple_tag
def compose_menu(selection, session, perms):
entries = [
{"name" : "back",
"url" : "%s?page=%s" % (session["folder"], session["page"]),
"img" : "/static/pics/back.png",
"label" : _("Back")},
{"name" : "sendmail",
"url" : "",
"img" : "/static/pics/send-receive.png",
"label" : _("Send")},
]
return render_to_string('main/menu.html',
{"selection" : selection, "entries" : entries,
"perms" : perms})
@register.simple_tag
def listing_menu(selection, folder, perms):
entries = [
{"name" : "compose",
"url" : reverse(webmail.main.compose),
"img" : "/static/pics/edit.png",
"label" : _("New message")},
{"name" : "actions",
"img" : "/static/pics/domains.png",
"label" : _("Mark messages"),
"class" : "menubardropdown",
"menu" : [
{"name" : "mark-read",
"label" : _("As read")},
{"name" : "mark-unread",
"label" : _("As unread")}
]
}
]
if folder in ["Trash"]:
entries += [
{"name" : "empty",
"url" : reverse(webmail.main.empty, args=[folder]),
"img" : "/static/pics/clear.png",
"label" : _("Empty trash")}
]
return render_to_string("main/menu.html",
{"selection" : selection, "entries" : entries,
"perms" : perms})
+15
View File
@@ -0,0 +1,15 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('mailng.extensions.webmail.main',
(r'^$', "index"),
(r'^compose/$', "compose"),
(r'^move/$', "move"),
(r'^empty/(?P<name>.+)/', "empty"),
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/part/$', 'getattachment'),
(r'^(?P<fdname>.+)/(?P<mail_id>[\w\-\+]+)/delete/$', 'delete'),
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/content/$', 'getmailcontent'),
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/forward/$', 'forward'),
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/reply/$', 'reply'),
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/$', 'viewmail'),
(r'^(?P<name>.+)/$', "folder"),
)
+27 -1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import popen2
import os
import sys
import time
from django.conf import settings
from django.template import RequestContext
@@ -41,6 +42,10 @@ def _render(request, tpl, user_context):
return render_to_response(tpl, user_context,
context_instance=RequestContext(request))
def _render_error(request, user_context):
return render_to_response("common/error.html", user_context,
context_instance=RequestContext(request))
def exec_as_vuser(cmd):
code, output = exec_pipe("sudo -u %s %s" % (settings.VIRTUAL_UID, cmd))
if code:
@@ -54,7 +59,17 @@ 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 getctx(status, level=1, callback=None, **kwargs):
if not callback:
callername = sys._getframe(level).f_code.co_name
else:
callername = callback
ctx = {"status" : status, "callback" : callername}
for kw, v in kwargs.iteritems():
ctx[kw] = v
return ctx
def decode(s, encodings=('utf8', 'latin1', 'windows-1252', 'ascii')):
for encoding in encodings:
try:
return s.decode(encoding)
@@ -69,3 +84,14 @@ def getoption(name, default=None):
except AttributeError:
res = default
return res
class Singleton(type):
def __init__(cls, name, bases, dict):
super(Singleton, cls).__init__(name, bases, dict)
cls.instance = None
def __call__(cls, *args, **kwargs):
if cls.instance is None:
cls.instance = super(Singleton, cls).__call__(*args, **kwargs)
return cls.instance
+228
View File
@@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
import time
import re
from email.header import decode_header
import lxml
import lxml.html
from lxml import etree
from django.template.loader import render_to_string
from django.template import Template, Context
from django.utils.translation import ugettext as _
from mailng.lib import _render, decode
attached_map = {}
class MBconnector(object):
def __init__(self, address, port):
self.address = address
self.port = port
def messages_count(self, **kwargs):
pass
class Page(object):
def __init__(self, pageid, id_start, id_stop, items,
items_per_page, has_previous, has_next,
baseurl=None):
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
self.baseurl = baseurl
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
self.baseurl = None
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
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, baseurl=None, folder=None, elems_per_page=40,
navparams={}, **kwargs):
self.folder = folder
self.elems_per_page = elems_per_page
self.navparams = navparams
order = "order" in kwargs.keys() and kwargs["order"] or None
self.empty = "empty" in kwargs.keys() and kwargs["empty"] or False
if not self.empty:
self.paginator = Paginator(self.mbc.messages_count(folder=self.folder,
order=order),
elems_per_page)
if baseurl:
self.paginator.baseurl = baseurl
def render_navbar(self, page):
if page is None:
return ""
return render_to_string("common/navbar.html", {
"page" : page
})
def getfolders(self):
return None
def fetch(self, request, id_start, id_stop):
table = self.tbltype(self.mbc.fetch(start=id_start, stop=id_stop,
folder=self.folder))
tpl = Template("""
<form method="POST" id="listingform">
{{ table }}
</form>""")
return tpl.render(Context({"table" : table.render(request)}))
def render(self, request, pageid=1, **kwargs):
listing = ""
page = None
if not self.empty:
page = self.paginator.getpage(pageid)
if not page:
listing = "Empty folder"
else:
listing = self.fetch(request, page.id_start, page.id_stop)
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,
"navparams" : self.navparams,
"deflocation" : self.deflocation, "defcallback" : self.defcallback
})
class Email(object):
def __init__(self, msg, mode="plain", links="0"):
self.attached_map = {}
contents = {"html" : "", "plain" : ""}
self.headers = []
self.attachments = []
for part in msg.walk():
if part.get_content_maintype() == 'multipart':
continue
if part.has_key("Content-Disposition"):
if re.match("^attachment", part["Content-Disposition"]):
self.attachments += [part]
if part.get_content_type() in ("text/html", "text/plain"):
contents[part.get_content_subtype()] += part.get_payload(decode=True)
if mode != "html" or links == "0":
continue
if part.get_content_maintype() == "image":
if part.has_key("Content-Location"):
fname = part["Content-Location"]
if re.match("^http:", fname):
path = fname
else:
path = "/%s" % os.path.join("static/tmp", fname)
fp = open(path, "wb")
fp.write(part.get_payload(decode=True))
fp.close()
m = re.match("<(.+)>", part["Content-ID"])
if m:
self.attached_map[m.group(1)] = path
else:
self.attached_map[part["Content-ID"]] = path
if not contents.has_key(mode) or contents[mode] == "":
if mode == "html":
mode = "plain"
else:
mode = "html"
self.pre, self.body = \
getattr(self, "viewmail_%s" % mode)(contents[mode], links=links)
def map_cid(self, url):
import re
map = globals()["attached_map"]
m = re.match(".*cid:([^\"]+)", url)
if m:
if map.has_key(m.group(1)):
return map[m.group(1)]
return url
def render_headers(self, folder, mail_id):
attachments = []
for part in self.attachments:
decoded = decode_header(part.get_filename())
if decoded[0][1] is None:
attachments += [decoded[0][0]]
else:
attachments += [unicode(decoded[0][0], decoded[0][1])]
return render_to_string("webmail/headers.html", {
"headers" : self.headers,
"folder" : folder, "mail_id" : mail_id,
"attachments" : attachments != [] and attachments or None
})
def render(self, request):
return _render(request, 'common/getmailcontent.html', {
"body" : self.body, "pre" : self.pre
})
def viewmail_plain(self, content, **kwargs):
body = decode(content)
return (True, body)
def viewmail_html(self, content, **kwargs):
links = kwargs.has_key("links") and kwargs["links"] or "0"
html = lxml.html.fromstring(content)
if links == "0":
html.rewrite_links(lambda x: None)
else:
html.rewrite_links(self.map_cid)
body = html.find("body")
if body is None:
body = lxml.html.tostring(html)
else:
body = lxml.html.tostring(body)
body = re.sub("<(/?)body", lambda m: "<%sdiv" % m.group(1), body)
body = Template(decode(body)).render({})
return (False, body)
+3 -1
View File
@@ -13,7 +13,9 @@ events = ["CreateDomain",
"ModifyMailbox",
"UserMenuDisplay",
"AdminMenuDisplay"]
"AdminMenuDisplay",
"UserLogin"]
callbacks = {}
+210
View File
@@ -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')
+57
View File
@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
import string
from mailng.admin.models import Parameter
"""
This interface provides a simple way to declare and store parameters
in MailNG's database.
Core components or extensions can register their own parameters, which
will be available and modifiable directly from the web interface.
Only super users will be able to acces this part of the web interface.
"""
_params = {}
class NotDefined(Exception):
def __init__(self, app, name):
self.app = app
self.name = name
def __str__(self):
return "Application '%s' and/or parameter '%s' not defined" % (self.app,
self.name)
def register(app, name, type="string", deflt=None, help=None, **kwargs):
"""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" : deflt, "help" : help}
def save(app, name, value):
if not app in _params.keys() or not name in _params[app].keys():
raise NotDefined(app, name)
fullname = "%s.%s" % (app, name)
try:
p = Parameter.objects.get(name=fullname)
except Parameter.DoesNotExist:
p = Parameter()
p.name = fullname
if p.value != value:
p.value = value
p.save()
return True
def get(app, name):
if not app in _params.keys() or not name in _params[app].keys():
raise NotDefined(app, name)
try:
p = Parameter.objects.get(name="%s.%s" % (app, name))
except Parameter.DoesNotExist:
return _params[app][name]["default"]
return p.value
+115
View File
@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
import inspect
import lxml
from django.template import Template, Context
from django.template.loader import render_to_string
class Column:
sortable = True
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 "<input type='checkbox' name='selectall' id='selectall' />"
def render(self, value):
return "<input type='checkbox' name='%s' value='%s' />" \
% (self.name, value)
class ImgColumn(Column):
sortable = False
def __str__(self):
try:
return getattr(self, "header")
except AttributeError:
return ""
def render(self, value):
return "<img src='%s' />" % value
class Table(object):
tableid = ""
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:
newcol = {}
try:
newcol["width"] = getattr(c, "width")
except AttributeError:
pass
if isinstance(c, SelectionColumn):
newcol["value"] = c.render(row[self.idkey])
newcol["safe"] = True
if isinstance(c, ImgColumn):
key = "img_%s" % c.name
if key in row.keys():
newcol["value"] = c.render(row[key])
newcol["safe"] = True
if row.has_key(c.name):
try:
cssclass = getattr(c, "cssclass")
except AttributeError:
cssclass = ""
if "class" in row.keys():
cssclass += (cssclass != "") and ", %s" % row["class"] or row["class"]
newcol["value"] = self.parse(c.name, row[c.name])
newcol["class"] = cssclass
nrow["cols"] += [newcol]
self.rows += [nrow]
def render(self, request):
t = Template("""
<table id="{{ tableid }}">
{% include "common/table_head.html" %}
{% include "common/table_body.html" %}
</table>
""")
return t.render(Context({
"table" : self, "tableid" : self.tableid
}))
def render_head(self, request):
t = Template("""
<table id="{{ tableid }}">
{% include "common/table_head.html" %}
</table>
""")
return t.render(Context({
"table" : self, "tableid" : "%s_head" % self.tableid
}))
def render_body(self, request):
t = Template("""
<table id="{{ tableid }}">
{% include "common/table_body.html" %}
</table>
""")
return t.render(Context({
"table" : self, "tableid" : "%s_body" % self.tableid
}))
+1 -1
View File
@@ -478,7 +478,7 @@ msgstr "Domaines"
#: templates/layout.html:87 templates/admin/settings_header.html:6
msgid "Settings"
msgstr "Paramètres"
msgstr "Configuration"
#: templates/admin/addpermission.html:24 templates/admin/settings_menu.html:15
msgid "Add permission"
+1 -1
View File
@@ -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)
+1
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+219 -7
View File
@@ -1,8 +1,181 @@
window.addEvent('domready', function(){
if ($('menubar')) {
$('menubar').getElements('li.dropdown').each( function( elem ){
parse_menubar('topmenubar');
SqueezeBox.assign($$('a.boxed'), {
parse: 'rel',
});
infobox = new InfoBox({
parent : $("menubar"),
message : "Loading"
});
});
callbacks = {};
register_callback = function(name, callback) {
callbacks[name] = callback;
}
get_callback = function(name) {
return callbacks[name];
}
current_anchor = null;
check_anchor = function() {
if (current_anchor.serialized == location.hash
&& !$defined(current_anchor.force)) {
return;
}
delete(current_anchor.force);
current_anchor.from_string(location.hash);
if (!current_anchor.serialized) {
query = current_anchor.deflocation;
} else {
query = current_anchor.serialized.substring(1);
}
infobox.show("Loading...", "gray");
new Request.JSON({url: query, onSuccess: function(resp) {
callback = ($defined(resp.callback)) ? resp.callback : "default";
callbacks[callback](resp);
infobox.notice("Done");
infobox.hide(1);
}}).get();
}
ajaxListener = function(deflocation, defcallback) {
current_anchor = new HashWrapper(deflocation);
register_callback("default", defcallback);
check_anchor.periodical(300);
}
/*
*
*/
function HashWrapper(deflocation) {
this.deflocation = deflocation;
this.params = $H();
this.base = '';
this.serialized = null;
this.reset = function() {
this.base = null;
this.params.empty();
return this;
};
this.parse_string = function(value, reset) {
var splits = (value.charAt(0) == '#')
? value.substring(1).split('?') : value.split('?');
if (splits.length == 0) {
return this;
}
if (!$defined(reset)) {
reset = false;
}
if (reset) {
this.reset();
}
this.base = (splits[0] == "") ? this.deflocation : splits[0];
if (this.base[this.base.length - 1] != '/') {
this.base += '/';
}
if (splits.length > 1) {
params = splits[1].split('&');
for (var i = 0; i < params.length; i++) {
tmp = params[i].split('=');
this.setparam(tmp[0], tmp[1]);
}
}
return this;
};
this.from_string = function(value, reset) {
this.parse_string(value);
this.serialized = value;
return this;
};
this.serialize = function() {
var res = this.base;
if (this.params.getLength() != 0) {
res += "?" + this.params.toQueryString();
}
return res;
};
this.update = function(force) {
location.hash = this.serialize();
if ($defined(force)) {
this.force = force;
}
};
this.updateparams = function(str) {
if (str.charAt(0) == '?') {
str = str.substring(1);
}
var elems = str.split('&');
for (var i = 0; i < elems.length; i++) {
this.setparamfromstring(elems[i]);
}
};
this.setparamfromstring = function(str) {
var def = str.split('=');
this.params.set(def[0], def[1]);
};
this.setparam = function(name, value) {
this.params.set(name, value);
return this;
};
this.setparams = function(params) {
this.params.extend(params);
return this;
};
this.getparam = function(name) {
if (!this.params.has(name)) {
return "";
}
return this.params.get(name);
}
this.baseurl = function(value) {
this.reset();
this.base = value;
if (this.base[this.base.length - 1] != '/') {
this.base += '/';
}
return this;
};
this.delparam = function(name) {
if (!this.params.has(name)) {
return false;
}
this.params.erase(name);
return this;
};
};
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();
var myFx = new Fx.Slide(list, {
duration: 200
}).hide();
myFx.addEvent("start", function() {
this.wrapper.setStyle("clear", "both");
});
elem.addEvents({
'mouseenter' : function(){
myFx.cancel();
@@ -14,11 +187,11 @@ window.addEvent('domready', function(){
}
});
});
SqueezeBox.assign($$('a.boxed'), {
parse: 'rel',
});
}
SqueezeBox.assign($$('a.boxed'), {
parse: 'rel',
});
});
}
get_iframe_body = function(id) {
var saf = navigator.userAgent.match(/Safari/i);
@@ -65,3 +238,42 @@ confirmation = function(question, action, callback) {
});
SqueezeBox.open("/mailng/main/confirm/?question=" + question);
}
setDivHeight = function(id, extrah, modulo) {
var contentsize = $(document.body).getSize().y
- $("topmenubar").getSize().y
- $("header").getSize().y
- $("footer").getSize().y
- extrah;
if (modulo) {
contentsize -= contentsize % modulo;
}
$(id).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);
}
+3 -2
View File
@@ -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');
@@ -66,6 +66,7 @@ window.addEvent('domready', function() {
}
}
});
setDivHeight("content", 20, 0);
});
newdom = function() {
+105
View File
@@ -0,0 +1,105 @@
var InfoBox = new Class({
Implements: [Options, Events],
options: {
parent : null,
message : "This is my info box!",
width : 200,
},
profiles: {
gray : {"background" : "#EEEEEE", "border" : "#CCCCCC"},
green : {"background" : "#DAF8DA", "border" : "#166D14"},
red : {"background" : "#F9D2D2", "border" : "#FF2A00"},
yellow : {"background" : "#FDE6B5", "border" : "#FF7F00"}
},
initialize: function(options) {
this.setOptions(options);
var boxleft = document.body.getSize().x / 2 - this.options.width / 2;
this.box = new Element("div", {"id" : "infobox"});
this.box.setStyles({
"font-size" : "normal",
"font-weight" : "bold",
"text-align" : "left",
"width" : this.options. width + "px",
"padding" : "10px",
"position" : "absolute",
"top" : "0px",
"left" : boxleft + "px",
"display" : "none"
});
this.setProfile("gray");
this.spinner = new Element("img", {
"src" : "/static/stylesheets/spinner.gif"
});
this.spinner.setStyle("float", "left");
this.spinner.inject(this.box);
this.label = new Element("label").set("html", this.options.message);
this.label.setStyles({
"margin-left" : "10px"
});
this.label.inject(this.box);
this.box.inject(document.body);
},
setMessage: function(text) {
this.label.set("html", text);
},
setProfile: function(color) {
this.box.setStyles({
"background" : this.profiles[color].background,
"border" : "2px solid " + this.profiles[color].border
});
},
notice: function(message) {
this.setProfile("green");
this.setMessage(message);
},
error: function(message) {
this.setProfile("red");
this.setMessage(message);
},
warning: function(message) {
this.setProfile("yellow");
this.setMessage(message);
},
show: function(message, prof, delay) {
if ($defined(this.start)) {
if ($time() - this.start > this.delay) {
$clear(this.showtimer);
delete(this.start);
delete(this.delay);
} else {
this.showtimer = this.show.delay(100, this, [message, prof]);
return;
}
}
if ($defined(message)) {
this.setMessage(message);
}
if ($defined(prof)) {
this.setProfile(prof);
}
if ($defined(delay)) {
this.start = $time();
this.delay = delay * 1000;
}
this.box.setStyle("display", "block");
},
hide: function(timer) {
if ($defined(timer)) {
this.timer = this.hide.periodical(timer * 1000, this);
return;
}
$clear(this.timer);
this.box.setStyle("display", "none");
},
});
+5
View File
@@ -2,4 +2,9 @@ window.addEvent('domready', function() {
SqueezeBox.assign($$('a.boxed'), {
parse: 'rel'
});
new DatePicker("#untildate", {
pickerClass: "datepicker_vista",
allowEmpty: true
});
});
+1839 -154
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -3,10 +3,25 @@ window.addEvent('domready', function() {
parse: 'rel',
});
setDivHeight("content", 20, 0);
if ($$("a[name=deletePerm]"))
$$("a[name=deletePerm]").addEvent('click', function(event) {
if (!confirm(gettext("Remove this permission?")))
event.stop();
});
var tip = new Tips($$(".Tips"));
var accordion = new Accordion('h3.atStart', 'div.atStart', {
opacity: false,
onActive: function(toggler, element){
toggler.setStyle('color', '#ff3300');
},
onBackground: function(toggler, element){
toggler.setStyle('color', '#222');
}
}, $('accordion'));
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 B

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

+11
View File
@@ -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;
}
+8
View File
@@ -31,6 +31,14 @@ input:hover {
background-color: #ffffcc;
}
td.odd {
background: #F7F9Fb;
}
td.even {
}
.title {
float: left;
width: 100%;
+91
View File
@@ -0,0 +1,91 @@
/*
Document : calendar.css
Created on : 29.07.2009
Author : jens klose
Description:
example styles for calendar
*/
.calendarForm{
float:right;
}
.calendar{
font-family:verdana,sans-serif;
font-size:10px;
float:None;
}
button.calendar{
position:relative;
top:-3px;
left:1px;
background-color:transparent;
background-image:url(../pics/calendar.gif);
border:none;
margin: 0px;
padding:0px;
margin-right:3px;
cursor:pointer;
width:16px;
height:16px;
}
input.calendar{
cursor:pointer;
border: 1px solid #87847F;
height:12px;
width:100px;
color: #87847F;
}
div.calendar{
background:white;
border: 1px solid #87847F;
width:180px
}
a.footer{
color:#474747;
padding:2px;
}
.calendar table{
width:100%;
}
.calendar th,
.calendar td{
text-align:center;
font-size:10px;
padding: 0.1em 0.25em 0.1em 0.3em;
}
.calendar tbody td,
.calendar .invalid{
color:silver;
}
.calendar .valid{
color:blue;
font-weight:bold;
cursor:pointer;
}
.calendar .valid.hover{
background:#FFFF77;
font-weight:bold;
}
.calendar caption a,
.calendar .next{
color:blue;
font-weight:bold;
padding:2px;
cursor:pointer;
}
.calendar caption a,
.calendar .prev{
color:blue;
font-weight:bold;
padding:2px;
}
.calendar .active,
.calendar .hilite,
.calendar .inactive{
color:white;
background-color:#BE3916;
font-weight:bold;
}
.calendar .active,
.calendar .inactive{
background-color:red;
}
+361
View File
@@ -0,0 +1,361 @@
html, body {
font-family: arial, helvetica, verdana, tahoma, sans-serif;
font-size: small;
background: #153F65;
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
div#container {
height: auto !important;
height: 100%;
min-height: 100%;
position: relative;
background: white;
}
@media screen {
body > div#footer {
position: fixed;
}
}
div#topmenubar {
position: fixed;
border-bottom: 5px solid #333;
height: 30px;
bottom: auto;
}
div#footer {
position: absolute;
height: 30px;
bottom: 0px;
border-top: 5px solid #333;
}
#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 {
width: 100%;
top: 35px;
bottom: 35px;
position: absolute;
}
#content {
position: fixed;
overflow: auto;
padding: 5px;
top: 130px;
bottom: 35px;
width: 100%;
}
#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;
}
.uibar {
background: #153F65;
padding: 0;
margin: 0 auto;
width: 100%;
}
.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;
}
.menubardropdown ul {
background: white;
width: 120px;
border-bottom: 5px solid #ccc;
border-right: 5px solid #ccc;
border-left: 5px solid #ccc;
}
div.left {
float: left;
}
div.right {
float: right;
}
.logo {
float: right;
/*margin: 5px 0 0 5px;*/
height: 90px;
}
.dropdown {
position: absolute;
clear: both;
z-index: 2;
width: 250px;
}
.dropdown li {
padding: 5px;
width: 170px;
cursor: pointer;
}
.dropdown ul {
list-style-type: none;
}
.topdropdown ul {
background: #153F65;
border-bottom: 5px solid #333;
border-right: 5px solid #333;
border-left: 5px solid #333;
width: 150px;
}
.dropdown ul li {
display: block;
float: none;
}
.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;
}
tr.odd {
background: white;
}
tr.even {
background: #F4F6F8;
}
th, td {
height: 20px;
}
th {
background: #CEDBEC;
text-align: left;
padding: 5px;
}
th.right {
text-align: right;
}
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;
}
table.hr {
width: 80%;
margin: 10px auto;
}
.hr td.odd {
background: #F7F9Fb;
}
.hr td.even {
}
.unseen {
font-weight: bold;
}
.toggler {
margin: 5px auto;
padding: 2px 5px;
background: #CEDBEC;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
/*border: 1px solid #CCC;*/
font-size: 1.1em;
}
.element {
padding-left: 10px;
}
div.row {
margin: 10px;
}
div.row label {
float: left;
width: 200px;
padding: 3px 0;
}
.error {
margin: 0 auto;
padding: 5px;
background: red;
width: 500px;
text-align: center;
background-color: #F9D2D2;
border: 3px solid #FF2A2A;
color: #FF2A00;
}
.errorlist ul {
float: left;
list-style: none;
}
.errorlist li {
margin-left: 10px;
float: left;
display: inline;
color: red;
}
+75
View File
@@ -0,0 +1,75 @@
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;
background: white;
border: 5px solid #333;
/*-moz-box-shadow: 0 10px 30px #222;*/
-webkit-box-shadow: 0 10px 30px #222;
-moz-border-radius: 10px;
-webkit-border-radius: 5px;
}
#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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1 -1
View File
@@ -26,7 +26,7 @@ window.addEvent("domready", function() {
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% url admin.views.addpermission as addperm_url %}
<form action="{{ addperm_url }}" method="POST" id="permform" name="permform">
{% include "admin/genericform.html" %}
{% include "common/genericform.html" %}
<div class="spacer"></div>
<div class="lastrow" align="center">
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "layout.html" %}
{% extends "nlayout.html" %}
{% load i18n %}
{% block html_head %}
<script src="/static/js/domains.js" type="text/javascript"></script>
+13 -5
View File
@@ -1,9 +1,17 @@
{% extends "layout.html" %}
{% extends "nlayout.html" %}
{% load i18n %}
{% load admin_extras %}
{% block html_head %}
<script src="/static/js/domains.js" type="text/javascript"></script>
<script type="text/javascript">
window.addEvent('domready', function() {
{% url admin.views.domains as view_url %}
bindRows("{{ view_url }}", "{{ current_page }}");
});
</script>
{% endblock %}
{% block middletitle %}{% trans "Available domains" %}{% endblock %}
{% block menu %}{% domains_menu "domains" perms %}{% endblock %}
{% block content %}
<div id="domcontent">
{% if not domains %}
@@ -12,13 +20,13 @@
<table>
<tr>
<th width="50px">{% trans "Actions" %}</th>
<th width="80px">{% trans "Name" %}</th>
<th width="50px">{% trans "Mailboxes" %}</th>
<th>{% trans "Name" %}</th>
<th width="150px">{% trans "Mailboxes" %}</th>
<th width="50px">{% trans "Quota" %}</th>
<th width="50px">{% trans "Enabled" %}</th>
</tr>
{% for dom in domains %}
<tr class="{% cycle 'odd' 'even' %}">
<tr class="{% cycle 'odd' 'even' %}" id="{{ dom.id }}">
<td>
{% url admin.views.editdomain dom.id as edit_url %}
<a href="{{ edit_url }}" class="boxed"
@@ -31,7 +39,7 @@
</a>
</td>
{% url admin.views.mailboxes dom.id as domurl %}
<td><a name="loadDom" href="{{ domurl }}">{{ dom.name }}</a></td>
<td>{{ dom.name }}</td>
<td>{{ dom.mboxcounter }}</td>
<td>{{ dom.quota }} {% trans "MB" %}</td>
<td>{{ dom.enabled|yesno:_("yes,no") }}</td>
+1 -1
View File
@@ -8,7 +8,7 @@
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% url admin.views.editalias alias.mbox.domain.id alias.id as edit_url %}
<form action="{{ edit_url }}" method="POST" id="aliasform" name="aliasform">
{% include "admin/genericform.html" %}
{% include "common/genericform.html" %}
<div class="spacer"></div>
<div class="lastrow" align="center">
<input type="submit" name="submit" value="{% trans 'Update' %}" />
+1 -1
View File
@@ -9,7 +9,7 @@
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% url admin.views.editdomain domain.id as dom_url %}
<form action="{{ dom_url }}" method="post" name="domform" id="domform">
{% include "admin/genericform.html" %}
{% include "common/genericform.html" %}
<div class="lastrow" align="center">
<input type="submit" class="button" name="submit" value="{% trans 'Update' %}" />
+1 -1
View File
@@ -9,7 +9,7 @@
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% url admin.views.editmailbox mbox.domain_id mbox.id as box_url %}
<form action="{{ box_url }}" method="POST" id="mboxform" name="mboxform">
{% include "admin/genericform.html" %}
{% include "common/genericform.html" %}
<div class="spacer"></div>
<div class="lastrow" align="center">
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "layout.html" %}
{% extends "nlayout.html" %}
{% block content %}
{% load i18n %}
<div id="body">
+1 -1
View File
@@ -8,7 +8,7 @@
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% url admin.views.newalias mbox.domain_id mbox.id as box_url %}
<form action="{{ box_url }}" method="POST" id="aliasform" name="aliasform">
{% include "admin/genericform.html" %}
{% include "common/genericform.html" %}
<div class="spacer"></div>
<div class="lastrow" align="center">
<input type="submit" name="submit" value="{% trans 'Create' %}" />
+1 -1
View File
@@ -9,7 +9,7 @@
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% url admin.views.newdomain as dom_url %}
<form action="{{ dom_url }}" method="post" name="domform" id="domform">
{% include "admin/genericform.html" %}
{% include "common/genericform.html" %}
<div class="lastrow" align="center">
<input type="submit" class="button" name="submit" value="{% trans 'Create' %}" />
+1 -1
View File
@@ -9,7 +9,7 @@
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% url admin.views.newmailbox domain.id as box_url %}
<form action="{{ box_url }}" method="POST" id="mboxform" name="mboxform">
{% include "admin/genericform.html" %}
{% include "common/genericform.html" %}
<div class="spacer"></div>
<div class="lastrow" align="center">
+22
View File
@@ -0,0 +1,22 @@
{% extends "admin/settings_header.html" %}
{% block menu %}
{% load admin_extras %}
{% settings_menu "parameters" perms %}
{% endblock %}
{% block setcontent %}
{% load i18n %}
{% url admin.views.saveparameters as action %}
<form method="POST" action="{{ action }}">
<div id="accordion">
{% for app in gparams %}
<h3 class="toggler atStart">{{ app.name }}</h3>
<div class="element atStart">
{% for p in app.params %}
{% param app.name p %}
{% endfor %}
</div>
{% endfor %}
</div>
<p><input type="submit" name="update" value="{% trans 'Save' %}" /></p>
</form>
{% endblock %}
+1 -1
View File
@@ -1,7 +1,7 @@
{% extends "admin/settings_header.html" %}
{% block menu %}
{% load admin_extras %}
{% settings_menu "permissions" %}
{% settings_menu "permissions" perms %}
{% endblock %}
{% block setcontent %}
{% load i18n %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "layout.html" %}
{% extends "nlayout.html" %}
{% load i18n %}
{% block html_head %}
<script src="/static/js/settings.js" type="text/javascript"></script>
-18
View File
@@ -1,18 +0,0 @@
{% load i18n %}
<ul>
{% url admin.views.permissions as perm_url %}
<li {% ifequal selection "permissions" %}class="selected"{% endifequal %}>
<a href="{{ perm_url }}">
<img src="/static/pics/permissions.png" border="0" />
{% trans "Permissions" %}
</a>
</li>
{% url admin.views.addpermission as addperm_url %}
<li>
<a href="{{ addperm_url }}" class="boxed"
rel="{handler:'iframe',size:{x:320,y:210}}">
<img src="/static/pics/add.png" border="0" />
{% trans "Add permission" %}
</a>
</li>
</ul>
+79
View File
@@ -0,0 +1,79 @@
{% extends "nlayout.html" %}
{% load i18n %}
{% block html_head %}
<style type="text/css">
#navbar {
float: right;
padding: 0;
margin: 0;
text-align: right;
}
#navbar label {
color: white;
margin-right: 5px;
font-weight: bold;
}
#navbar img{
margin-top: 5px;
margin-right: 5px;
}
{% block css %}{% endblock %}
</style>
<script type="text/javascript">
window.addEvent("domready", function() {
navparams = new Hash({
{% for k, v in navparams.items %}
{{ k }} : "{{ v }}",
{% endfor %}
});
navbarcallback = function(event) {
event.stop();
current_anchor.setparams(navparams);
current_anchor.parse_string(event.target.getParent().get("href")).update();
}
viewmail = function(event) {
event.stop();
if (event.target.parentNode.id) {
var url = event.target.parentNode.id + "/";
event.target.parentNode.setStyle("background", "#ffffcc");
current_anchor.baseurl(url).update();
}
}
updatelisting = function(resp) {
if ($defined(resp.menu)) {
$("menubar").set("html", resp.menu);
parse_menubar("menubar");
}
$("listing").set("html", resp.listing);
if ($defined(resp.navbar)) {
$("navbar").set("html", resp.navbar);
$("navbar").getChildren("a").addEvent("click", navbarcallback);
}
}
if ($("selectall")) {
$("selectall").addEvent("click", function(event) {
if ($("selectall").checked) {
toggleSelection("selection", true);
} else {
toggleSelection("selection", false);
}
});
}
{% block js %}{% endblock %}
ajaxListener("{{ deflocation }}", {{ defcallback }});
});
</script>
{% endblock %}
{% block content %}
{% block leftcol %}{% endblock %}
<div id="listing">{{ listing }}</div>
<div class="spacer"></div>
{% endblock %}
{% block footer %}<div id="navbar">{{ navbar }}</div>{% endblock %}
+10
View File
@@ -0,0 +1,10 @@
{% load i18n %}
<div id="searchbox">
<img src="/static/pics/search.png" id="searchopts" />
<input type="text" name="searchfield" id="searchfield" value="{% trans "Search..." %}" />
<div id="searchparams">
<input type="radio" name="scriteria" value="from_addr" checked>{% trans "From address" %}<br/>
<input type="radio" name="scriteria" value="subject">{% trans "Subject" %}<br/>
<input type="radio" name="scriteria" value="from_addr,subject">{% trans "Both" %}<br/>
</div>
</div>
+6
View File
@@ -0,0 +1,6 @@
{% extends "nlayout.html" %}
{% load i18n %}
{% block middletitle %}{% trans "Error" %}{% endblock %}
{% block content %}
<div class="error"><p>{{ error }}</p></div>
{% endblock %}
@@ -1,7 +1,8 @@
{% for field in form %}
<div class="row">
{% if field.help_text %}
<div id="{{ field.name }}_helptext" style="display:none">{{ field.help_text}}</div>{% endif %}
<div id="{{ field.name }}_helptext" style="display:none">{{ field.help_text}}</div>
{% endif %}
{{ field.label_tag }} {{ field }} {% ifequal noerrors False %}{{ field.errors }}{% endifequal %}
</div>
{% endfor %}
+1
View File
@@ -0,0 +1 @@
{% if pre %}<pre>{% endif %}{{ body }}{% if pre %}</pre>{% endif %}
+19
View File
@@ -0,0 +1,19 @@
{% load i18n %}
<label>Messages {{ page.id_start }} {% trans "to" %} {{ page.id_stop }} {% trans "of" %} {{ page.items }}</label>
{% if page.previous_page %}
<a href="{{ page.baseurl }}?page=1">
<img src="/static/pics/first.png" border="0" title="{% trans "First" %}" />
</a>
<a href="{{ page.baseurl }}?page={{ page.previous_page }}">
<img src="/static/pics/back.png" border="0" title="{% trans "Previous" %}" />
</a>
{% endif %}
{% if page.next_page %}
<a href="{{ page.baseurl }}?page={{ page.next_page }}">
<img src="/static/pics/next.png" border="0" title="{% trans "Next" %}" />
</a>
<a href="{{ page.baseurl }}?page={{ page.last_page }}">
<img src="/static/pics/last.png" border="0" title="{% trans "Last" %}" />
</a>
{% endif %}
+13
View File
@@ -0,0 +1,13 @@
<tbody>
{% for row in table.rows %}
<tr id="{{ row.id }}" class="{% cycle 'odd' 'even' %}">
{% for cell in row.cols %}
{% if cell.safe %}
<td width="{{ cell.width }}">{{ cell.value|safe }}</td>
{% else %}
<td width="{{ cell.width }}" class="{{ cell.class }}">{{ cell.value }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
+14
View File
@@ -0,0 +1,14 @@
<thead>
<tr>
{% for c in table.columns %}
{% if c.sortable %}
<th id="{{ c.name }}" class="sortable" width="{{ c.width }}">
{% else %}
<th id="{{ c.name }}" width="{{ c.width }}">
{% endif %}
{{ c|safe }}
<span style="float: right"><img src=""/></span>
</th>
{% endfor %}
</tr>
</thead>
+26
View File
@@ -0,0 +1,26 @@
<table id="{{ tableid }}">
<thead>
<tr>
{% for c in table.columns %}
<th id="{{ c.name }}" class="sortable" width="{{ c.width }}">
{{ c|safe }}
<span style="float: right"><img src="" /></span>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in table.rows %}
<tr id="{{ row.id }}" class="{% cycle 'odd' 'even' %}">
{% for cell in row.cols %}
{% if cell.safe %}
<td width="{{ c.width }}">{{ cell.value|safe }}</td>
{% else %}
<td width="{{ c.width }}" class="{{ cell.class }}">{{ cell.value }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
+54
View File
@@ -0,0 +1,54 @@
{% load i18n %}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head profile="http://www.w3.org/2005/10/profile">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="/static/stylesheets/box.css" />
<style type="text/css">
body {
margin: 5px;
}
#mailcontent {
width: 100%;
border: 0px transparent none;
margin: 0;
}
table#emailheaders {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
#emailheaders td,
#emailheaders th {
height: 22px;
text-align: left;
padding-left: 10px;
border: 1px solid #ccc;
}
#emailheaders td {
font-size: 1em;
}
#emailheaders th {
background: #F7F9Fb;
}
#emailheaders a, #emailheaders a:visited {
color: blue;
text-decoration: none;
}
#emailheaders a:hover {
text-decoration: underline;
}
pre {
font-size: medium;
}
</style>
</head>
<body>
<div id="table-container">
{{ headers }}
</div>
{% if pre %}<pre>{% endif %}{{ mailbody }}{% if pre %}</pre>{% endif %}
</body>
</html>
-5
View File
@@ -1,5 +0,0 @@
{% for field in form %}
<div class="row">
{{ field.label_tag }} {{ field }} {% ifequal noerrors False %}{{ field.errors }}{% endifequal %}
</div>
{% endfor %}
+6 -4
View File
@@ -1,8 +1,10 @@
{% extends "main/header.html" %}
{% load i18n %}
{% block html_head %}
<script src="/static/js/main.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="/static/stylesheets/box.css" />
<link rel="stylesheet" type="text/css" href="/static/stylesheets/datepicker/vista.css" />
<script src="/static/js/datepicker.js" type="text/javascript"></script>
<script src="/static/js/main.js" type="text/javascript"></script>
{% endblock %}
{% block menu %}
{% load main_extras %}
@@ -10,11 +12,11 @@
{% endblock %}
{% block subcontent %}
<form action="" method="POST" id="armform" name="armform" class="simple">
{% include "genericform.html" %}
{% include "common/genericform.html" %}
<div class="spacer"></div>
<div class="lastrow" align="center">
<input type="submit" name="submit" value="{% trans 'Update' %}" />
</div>
</form>
{% endblock %}
{% endblock %}
+2 -2
View File
@@ -9,10 +9,10 @@
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% url main.views.changepassword as action_url %}
<form action="{{ action_url }}" method="post" name="chpwdform" id="chpwdform">
{% include "genericform.html" %}
{% include "common/genericform.html" %}
<div class="lastrow" align="center">
<input type="submit" class="button" name="submit" value="{% trans 'Update' %}" />
</div>
</form>
{% endblock %}
{% endblock %}
+3 -1
View File
@@ -1,7 +1,9 @@
{% extends "layout.html" %}
{% extends "nlayout.html" %}
{% load i18n %}
{% block html_head %}
<script src="/static/js/calendar.js" type="text/javascript"></script>
<script src="/static/js/main.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="/static/stylesheets/calendar.css" />
{% endblock %}
{% block middletitle %}{% trans "General" %}{% endblock %}
{% block content %}
+6 -6
View File
@@ -2,14 +2,14 @@
<ul>
{% for entry in entries %}
{% if entry.menu %}
<li class="dropdown">
<li class="dropdown {{ entry.class }}">
<img src="{{ entry.img }}" border="0" />
{{ entry.label }}
<label>{{ entry.label }}</label>
<ul class="links">
{% for sentry in entry.menu %}
<a href="{{ sentry.url }}" name="{{ sentry.name }}">
<li>{{ sentry.label }}</li>
</a>
<li>
<a href="{{ sentry.url }}" name="{{ sentry.name }}">{{ sentry.label }}</a>
</li>
{% endfor %}
</ul>
</li>
@@ -26,4 +26,4 @@
</li>
{% endif %}
{% endfor %}
</ul>
</ul>
+94
View File
@@ -0,0 +1,94 @@
{% load i18n %}
{% load main_extras %}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head profile="http://www.w3.org/2005/10/profile">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>MailNG - {%block pagetitle %}{% endblock %}</title>
<link rel="icon" type="image/png" href="/static/stylesheets/logo-icon.png" />
<link rel="stylesheet" type="text/css" href="/static/stylesheets/default.css" />
<link rel="stylesheet" type="text/css" href="/static/stylesheets/SqueezeBox.css" />
<script src="/static/js/mootools.js" type="text/javascript"></script>
<script src="/static/js/mootools-more.js" type="text/javascript"></script>
<script src="/static/js/drag.group.js" type="text/javascript"></script>
<script src="/static/js/SqueezeBox.js" type="text/javascript"></script>
<script src="/static/js/infobox.js" type="text/javascript"></script>
<script src="/static/js/common.js" type="text/javascript"></script>
{% url django.views.i18n.javascript_catalog as catalog_url %}
<script type="text/javascript" src="{{ catalog_url }}"></script>
{%block html_head %}{% endblock %}
</head>
<body>
<div id="container">
<div id="topmenubar" class="uibar menubar">
<div class="left" style="width: 50%">
<ul>
<li>
{% url main.views.index as index_url %}
<a href="{{ index_url }}">
<img src="/static/pics/user.png" border="0" />{% trans "Preferences" %}</a>
</li>
{% loadextmenu perms %}
{% if user.is_superuser or perms.admin.view_domains %}
<li class="dropdown topdropdown">
<img src="/static/pics/admin.png" border="0" />
<label>{% trans "Admin" %}</label>
<ul class="links">
<li>
<a href="{% url admin.views.domains %}">
<img src="/static/pics/domains.png" />{% trans "Domains" %}
</a>
</li>
<li>
<a href="{% url admin.views.settings %}">
<img src="/static/pics/settings.png" border="0"/>
{% trans "Settings" %}
</a>
</li>
</ul>
</li>
{% endif %}
</ul>
</div>
<div class="right">
<ul>
<li class="title">
{% if user.first_name %}
{{ user.first_name }} {{ user.last_name }}
{% else %}
{{ user.username }}
{% endif %}
/
</li>
<li>
{% url django.contrib.auth.views.logout_then_login as logout_url %}
<a href="{{ logout_url }}">
<img src="/static/pics/logout.png" border="0" />{% trans "Logout" %}</a>
</li>
</ul>
</div>
</div>
<div id="page">
<div id="header">
<img class="logo" src="/static/stylesheets/mailng.png" />
<h1>{% block middletitle %}{% endblock %}</h1>
<div id="menubar" class="menubar">
{% block menu %}{% endblock %}
</div>
</div>
<div id="content">
{% block content %}{% endblock %}
</div>
</div>
<div id="footer" class="uibar">
&nbsp;
{% block footer %}{% endblock %}
</div>
</div>
</body>
</html>
+38 -24
View File
@@ -1,26 +1,40 @@
{% extends "layout.html" %}
{% load i18n %}
{% block html_head %}
<link rel="stylesheet" type="text/css" href="/static/stylesheets/box.css" />
{% endblock %}
{% block content %}
<div id="loginbox">
<h3>{% trans "Welcome" %}</h3>
{% if form.errors %}
<div class="error">
<p>{% trans "Your username and password didn't match. Please try again." %}</p>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head profile="http://www.w3.org/2005/10/profile">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% trans "Welcome to MailNG" %}</title>
<link rel="icon" type="image/png" href="/static/stylesheets/logo-icon.png" />
<link rel="stylesheet" type="text/css" href="/static/stylesheets/login.css" />
<link rel="stylesheet" type="text/css" href="/static/stylesheets/box.css" />
<script src="/static/js/mootools.js" type="text/javascript"></script>
<script src="/static/js/mootools-more.js" type="text/javascript"></script>
{% url django.views.i18n.javascript_catalog as catalog_url %}
<script type="text/javascript" src="{{ catalog_url }}"></script>
</head>
<body>
<div id="loginbox">
<div id="logo">
<img src="/static/stylesheets/mailng.png" />
{% if error %}
<div class="error"><p>{{ error }}</p></div>
{% endif %}
<h2>2010 - Version 0.7-dev</h2>
</div>
<div id="test">
<h3>{% trans "Welcome" %}</h3>
<form method="post" action="">
{% include "common/genericform.html" %}
<div class="lastrow" align="center">
<input type="submit" value="login" />
</div>
<input type="hidden" name="next" value="{{ next }}" />
</form>
</div>
<div class="spacer"></div>
</div>
{% endif %}
<form method="post" action=".">
{% for field in form %}
<div class="row">
{{ field.label_tag }}{{ field }}
</div>
{% endfor %}
<div class="lastrow" align="center">
<input type="submit" value="login" />
</div>
<input type="hidden" name="next" value="{{ next }}" />
</form>
</div>
{% endblock %}
</body>
</html>
+12 -3
View File
@@ -21,7 +21,7 @@ urlpatterns = patterns('',
(r'^mailng/admin/', include('mailng.admin.urls')),
(r'^mailng/main/', include('mailng.main.urls')),
(r'^accounts/login/$', 'django.contrib.auth.views.login'),
(r'^accounts/login/$', 'mailng.auth.views.dologin'),
(r'^accounts/logout/$', 'django.contrib.auth.views.logout_then_login'),
(r'^jsi18n/$', 'django.views.i18n.javascript_catalog',
{'packages': ('mailng',),})
@@ -33,6 +33,15 @@ if menus != ():
if settings.DEBUG:
urlpatterns += patterns(
'',
(r'^static/(?P<path>.*)$', 'django.views.static.serve', {'document_root': '/home/tonio/projets/mailng/static'})
(r'^static/(?P<path>.*)$', 'django.views.static.serve',
{'document_root': settings.STATIC_ROOTDIR})
)
# Stats
if 'mailng.extensions.stats' in settings.INSTALLED_APPS:
urlpatterns += patterns(
'',
(r'^img/stats/(?P<path>.*)/$', 'django.views.static.serve',
{'document_root': settings.IMG_ROOTDIR})
)